Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(demo): nav:back element #1110

Merged
merged 5 commits into from
Mar 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<style
id="text"
marginHorizontal="24"
marginBottom="16"
/>
26 changes: 26 additions & 0 deletions demo/backend/advanced/community/elements/nav-back/index.xml.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
permalink: "/backend/advanced/community/nav-back/index.xml"
tags: "Advanced/Community/Elements"
hv_button_behavior: "back"
hv_title: "Nav Back"
---

{% from 'macros/button/index.xml.njk' import button %}
{% from 'macros/description/index.xml.njk' import description %}
{% extends 'templates/scrollview.xml.njk' %}

{% block content %}
{{ description('Tapping one of the buttons will render the same screen with a different navigation style. The control to navigate back will be dynamic, based on the navigation context.') }}
{% call button('Push') -%}
<behavior
action="push"
href="/hyperview/public/advanced/community/elements/nav-back/screen.xml"
/>
{%- endcall %}
{% call button('New') -%}
<behavior
action="new"
href="/hyperview/public/advanced/community/elements/nav-back/screen.xml"
/>
{%- endcall %}
{% endblock %}
44 changes: 44 additions & 0 deletions demo/backend/advanced/community/elements/nav-back/screen.xml.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
permalink: "/backend/advanced/community/nav-back/screen.xml"
hv_title: "Nav Back"
---

{% from 'macros/button/index.xml.njk' import button %}
{% from 'macros/description/index.xml.njk' import description %}
{% extends 'templates/base.xml.njk' %}

{% block styles %}
{% include './_styles.xml.njk' %}
{% endblock %}

{% block body %}
<header style="header">
<nav:back xmlns:nav="https://hyperview.org/navigation">
<view
nav:role="close"
action="close"
href="#"
style="header-btn"
>
{% include 'icons/close.svg' %}
</view>
<view
nav:role="back"
action="back"
href="#"
style="header-btn"
>
{% include 'icons/back.svg' %}
</view>
</nav:back>
<text style="header-title">{{ hv_title }}</text>
</header>
<text style="text">
The button to navigate back to the previous screen will be rendered dynamically,
based on which kind of navigator the screen is currently loaded on. For regular screens
(added to the stack via "push" action), a back-arrow button will be rendered. For
modal screens (added to the stack via "new" action), a close button will be rendered.
Also (not demo'd here), if the screen is the first screen of the stack and there is no
place to navigate back to, no button will be rendered.
</text>
{% endblock %}
2 changes: 2 additions & 0 deletions demo/schema/hyperview.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
xmlns:hv="https://hyperview.org/hyperview"
xmlns:alert="https://hyperview.org/hyperview-alert"
xmlns:filter="https://hyperview.org/filter"
xmlns:nav="https://hyperview.org/navigation"
xmlns:scroll="https://hyperview.org/hyperview-scroll"
xmlns:share="https://hyperview.org/share"
>
Expand Down Expand Up @@ -85,6 +86,7 @@

<!-- Hyperview demo extensions -->
<xs:attributeGroup ref="filter:filterAttributes" />
<xs:attributeGroup ref="nav:navAttributes" />
<xs:attributeGroup ref="share:shareAttributes" />
</xs:attributeGroup>

Expand Down
17 changes: 17 additions & 0 deletions demo/schema/navigation.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
attributeFormDefault="qualified"
elementFormDefault="qualified"
targetNamespace="https://hyperview.org/navigation"
xmlns:nav="https://hyperview.org/navigation"
xmlns:hv="https://hyperview.org/hyperview"
>
<xs:import
Expand All @@ -28,4 +29,20 @@
<xs:attribute name="key" type="hv:KEY" form="unqualified" />
</xs:complexType>
</xs:element>
<xs:simpleType name="role">
<xs:restriction base="xs:string">
<xs:enumeration value="back" />
<xs:enumeration value="close" />
</xs:restriction>
</xs:simpleType>
<xs:element name="back">
<xs:complexType>
<xs:sequence>
<xs:any minOccurs="0" maxOccurs="unbounded" />
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:attributeGroup name="navAttributes">
<xs:attribute name="role" type="nav:role" />
</xs:attributeGroup>
</xs:schema>
66 changes: 20 additions & 46 deletions demo/src/Components/Filter/Filter.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import * as Logging from 'hyperview/src/services/logging';
import type { HvComponentProps, LocalName } from 'hyperview';
import Hyperview, {
Events,
LOCAL_NAME,
NODE_TYPE,
Namespaces,
} from 'hyperview';
import Hyperview, { Events, LOCAL_NAME, Namespaces } from 'hyperview';
import { findElements } from '../../Helpers';
import { useEffect } from 'react';

type FormDataPart = {
Expand All @@ -18,30 +14,6 @@ declare class FormData {
}
const FILTER_NS = 'https://hyperview.org/filter';

export const findElements = (node: Element, attributeNames: string[]) => {
if (node.nodeType !== NODE_TYPE.ELEMENT_NODE) {
return [];
}

if (
attributeNames.reduce(
(found, name) => found || !!node.getAttributeNS(FILTER_NS, name),
false,
)
) {
return [node];
}

return (Array.from(node.childNodes) as Element[])
.filter((child: Node | null) => {
return child !== null && child.nodeType === NODE_TYPE.ELEMENT_NODE;
})
.reduce((elements: Element[], child: Element) => {
elements.push(...findElements(child, attributeNames));
return elements;
}, []);
};

const Filter = (props: HvComponentProps) => {
const onEventDispatch = (eventName: string) => {
const filterEvent =
Expand Down Expand Up @@ -74,7 +46,7 @@ const Filter = (props: HvComponentProps) => {
: filterTerm;

// Hide/show each element with filter terms or matching given regex. Modify attributes in-place
const filterElements: Element[] = findElements(props.element, [
const filterElements: Element[] = findElements(FILTER_NS, props.element, [
'terms',
'regex',
]);
Expand All @@ -101,22 +73,24 @@ const Filter = (props: HvComponentProps) => {
props.onUpdate(null, 'swap', props.element, { newElement });

// Set elements that need to render the filter term
findElements(newElement, ['role']).forEach((element: Element) => {
if (element.getAttributeNS(FILTER_NS, 'role') === 'filter-terms') {
if (
element.namespaceURI === Namespaces.HYPERVIEW &&
element.localName !== LOCAL_NAME.TEXT
) {
Logging.error(
'Element with attribute `role="filter-terms"` should be a <text> element or a custom element',
);
return;
findElements(FILTER_NS, newElement, ['role']).forEach(
(element: Element) => {
if (element.getAttributeNS(FILTER_NS, 'role') === 'filter-terms') {
if (
element.namespaceURI === Namespaces.HYPERVIEW &&
element.localName !== LOCAL_NAME.TEXT
) {
Logging.error(
'Element with attribute `role="filter-terms"` should be a <text> element or a custom element',
);
return;
}
const newRoleElement = element.cloneNode(true) as Element;
newRoleElement.textContent = filterTerm;
props.onUpdate(null, 'swap', element, { newElement: newRoleElement });
}
const newRoleElement = element.cloneNode(true) as Element;
newRoleElement.textContent = filterTerm;
props.onUpdate(null, 'swap', element, { newElement: newRoleElement });
}
});
},
);
};

useEffect(() => {
Expand Down
40 changes: 40 additions & 0 deletions demo/src/Components/NavBack/NavBack.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { HvComponentProps, LocalName } from 'hyperview';
import Hyperview from 'hyperview';
import { NavigationContext } from '@react-navigation/native';
import { findElements } from '../../Helpers';
import { useContext } from 'react';

export const namespaceURI = 'https://hyperview.org/navigation';

const NavBack = (props: HvComponentProps) => {
const ctx = useContext(NavigationContext);
const state = ctx?.getState();
const route = state?.routes[state.index];

// Screen is first on the stack? Don't render anything
if (state?.index === 0) {
return null;
}

const role = route?.name === 'modal' ? 'close' : 'back';
const [element] = findElements(namespaceURI, props.element, ['role']).filter(
el => {
return el.getAttributeNS(namespaceURI, 'role') === role;
},
);
if (!element) {
return null;
}
return (Hyperview.renderElement(
element,
props.stylesheets,
props.onUpdate,
props.options,
) as unknown) as JSX.Element;
};

NavBack.namespaceURI = namespaceURI;
NavBack.localName = 'back' as LocalName;
NavBack.localNameAliases = [] as LocalName[];

export { NavBack };
1 change: 1 addition & 0 deletions demo/src/Components/NavBack/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { NavBack } from './NavBack';
2 changes: 2 additions & 0 deletions demo/src/Components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
import { BottomTabBar, BottomTabBarItem } from './BottomTabBar';
import { Map, MapMarker } from './Map';
import { Filter } from './Filter';
import { NavBack } from './NavBack';
import { ProgressBar } from './ProgressBar';
import { ScrollOpacity } from './ScrollOpacity';
import { Svg } from './Svg';
Expand All @@ -19,6 +20,7 @@ export default [
Filter,
Map,
MapMarker,
NavBack,
ProgressBar,
ScrollOpacity,
Svg,
Expand Down
29 changes: 29 additions & 0 deletions demo/src/Helpers/misc.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { NODE_TYPE } from 'hyperview';
import moment from 'moment';

export const formatDate = (
Expand All @@ -21,3 +22,31 @@ export const fetchWrapper = (
mode: 'cors',
});
};

export const findElements = (
namespace: string,
node: Element,
attributeNames: string[],
) => {
if (node.nodeType !== NODE_TYPE.ELEMENT_NODE) {
return [];
}

if (
attributeNames.reduce(
(found, name) => found || !!node.getAttributeNS(namespace, name),
false,
)
) {
return [node];
}

return (Array.from(node.childNodes) as Element[])
.filter((child: Node | null) => {
return child !== null && child.nodeType === NODE_TYPE.ELEMENT_NODE;
})
.reduce((elements: Element[], child: Element) => {
elements.push(...findElements(namespace, child, attributeNames));
return elements;
}, []);
};