Skip to content

Commit

Permalink
fix(demo): custom bottom bar component (#962)
Browse files Browse the repository at this point in the history
Fix double rendering of the custom bottom bar component by refactoring
the following pieces:
- context: switch to using the reducer pattern, and expose 3 methods:
`getElementProps`, `setElementProps`, `setElement`.
- core component: render any child element of the custom "non-render"
element, not just the assumed single-child
- custom element: set the `registered` attribute on the element upon
setting the props in the context, to avoid re-render loops when the
screen holding the custom element re-renders. This does not prevent
server side updates of the tab bar.
- update the demo app to factorize the markup for the tab bar as well as
to show case the server updates of the tab bar.

Special thanks to @ehenighan for the thorough investigation and help
solving #956.



https://github.com/user-attachments/assets/756d8585-d563-4a23-b845-3b42a843c4e7



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
  - Introduced a notification badge design for the bottom tab bar.
- Added a new tab bar component to manage notifications within the
second tab.

- **Enhancements**
- Improved the BottomTabBar's context management with a reducer pattern
for better state handling.
- Streamlined the BottomTabBar component's logic and rendering process.
- Enhanced tab navigation with a modular approach for dynamic rendering.

- **Bug Fixes**
  - Resolved rendering loops in the BottomTabBar component.

- **Documentation**
- Updated context exports to prioritize the new custom hook for easier
access.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: flochtililoch <[email protected]>
  • Loading branch information
flochtililoch and flochtililoch authored Oct 15, 2024
1 parent 68067f6 commit a3c783a
Show file tree
Hide file tree
Showing 12 changed files with 233 additions and 137 deletions.
17 changes: 17 additions & 0 deletions demo/backend/_includes/macros/tabbar-bottom-tab/styles.xml.njk
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,20 @@
<style display="flex" />
</modifier>
</style>
<style
id="tabbar-bottom-tab-content"
alignItems="center"
position="relative"
/>
<style
id="tabbar-bottom-tab-notify"
backgroundColor="#d92b2b"
position="absolute"
top="0"
right="0"
height="6"
width="6"
borderWidth="2"
borderColor="#d92b2b"
borderRadius="6"
/>
2 changes: 1 addition & 1 deletion demo/backend/_includes/macros/tabbar-bottom/index.xml.njk
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
xmlns:navigation="https://hyperview.org/navigation"
navigation:navigator="{{ target }}"
>
<select-single style="tabbar-bottom" name="tabbar-bottom">
<select-single style="tabbar-bottom" name="tabbar-bottom" key="tabbar-bottom">
{% if caller %}
{{ caller() }}
{% endif %}
Expand Down
2 changes: 0 additions & 2 deletions demo/backend/_includes/macros/tabbar-bottom/styles.xml.njk
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@
alignItems="stretch"
borderTopColor="#eee"
borderTopWidth="1"
bottom="0"
flexDirection="row"
flexShrink="1"
flexGrow="0"
height="48"
justifyContent="space-around"
backgroundColor="white"
position="absolute"
width="100%"
/>
40 changes: 5 additions & 35 deletions demo/backend/navigation/navigator/bottom-tabs/tab-1.xml.njk
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,9 @@ hv_button_behavior: "back"
{% extends 'templates/base.xml.njk' %}

{% block container %}
{{ description('This is the content for tab 1.') }}
<navigation:bottom-tab-bar
xmlns:navigation="https://hyperview.org/navigation"
navigation:navigator="custom-bottom-tabs-navigator"
>
<select-single style="tabbar-bottom" name="custom-tab-bar">
<option
selected="true"
style="tabbar-bottom-tab"
value="custom-tab-1"
>
<behavior trigger="select" action="navigate" target="custom-bottom-tabs-tab-1" />
<view style="tabbar-bottom-tab-icon-selected">
{% include 'icons/behaviors-selected.svg' %}
</view>
<view style="tabbar-bottom-tab-icon">
{% include 'icons/behaviors.svg' %}
</view>
<text style="tabbar-bottom-tab-label">Tab 1</text>
</option>
<option
style="tabbar-bottom-tab"
value="custom-tab-2"
>
<behavior trigger="select" action="navigate" target="custom-bottom-tabs-tab-2" />
<view style="tabbar-bottom-tab-icon-selected">
{% include 'icons/advanced-selected.svg' %}
</view>
<view style="tabbar-bottom-tab-icon">
{% include 'icons/advanced.svg' %}
</view>
<text style="tabbar-bottom-tab-label">Tab 2</text>
</option>
</select-single>
</navigation:bottom-tab-bar>
<view scroll="true">
{{ description('This is the content for tab 2.') }}
</view>
{% set selected_tab = 1 %}
{% include "./tab-bar.xml.njk" %}
{% endblock %}
49 changes: 14 additions & 35 deletions demo/backend/navigation/navigator/bottom-tabs/tab-2.xml.njk
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,21 @@ hv_title: "Tab Navigator - Tab 2"
hv_button_behavior: "back"
---
{% from 'macros/description/index.xml.njk' import description %}
{% from 'macros/button/index.xml.njk' import button %}
{% extends 'templates/base.xml.njk' %}

{% block container %}
{{ description('This is the content for tab 2.') }}
<navigation:bottom-tab-bar
xmlns:navigation="https://hyperview.org/navigation"
navigation:navigator="custom-bottom-tabs-navigator"
>
<select-single style="tabbar-bottom" name="custom-tab-bar">
<option
style="tabbar-bottom-tab"
value="custom-tab-1"
>
<behavior trigger="select" action="navigate" target="custom-bottom-tabs-tab-1" />
<view style="tabbar-bottom-tab-icon-selected">
{% include 'icons/behaviors-selected.svg' %}
</view>
<view style="tabbar-bottom-tab-icon">
{% include 'icons/behaviors.svg' %}
</view>
<text style="tabbar-bottom-tab-label">Tab 1</text>
</option>
<option
selected="true"
style="tabbar-bottom-tab"
value="custom-tab-2"
>
<behavior trigger="select" action="navigate" target="custom-bottom-tabs-tab-2" />
<view style="tabbar-bottom-tab-icon-selected">
{% include 'icons/advanced-selected.svg' %}
</view>
<view style="tabbar-bottom-tab-icon">
{% include 'icons/advanced.svg' %}
</view>
<text style="tabbar-bottom-tab-label">Tab 2</text>
</option>
</select-single>
</navigation:bottom-tab-bar>
<view scroll="true">
{{ description('This is the content for tab 2.') }}
{% call button("Add notification to tab 2", attributes={id:"add-notification"}) -%}
<behavior action="hide" target="add-notification" />
<behavior action="show" target="remove-notification" />
<behavior action="replace" target="bottom-tab-bar" href="/hyperview/public/navigation/navigator/bottom-tabs/tab-bar-notify-tab-2.xml" />
{%- endcall %}
{% call button("Remove notification from tab 2", attributes={id:"remove-notification", hide:"true"}) -%}
<behavior action="reload" href="#" />
{%- endcall %}
</view>
{% set selected_tab = 2 %}
{% include "./tab-bar.xml.njk" %}
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
permalink: "/navigation/navigator/bottom-tabs/tab-bar-notify-tab-2.xml"

---
{% set selected_tab = 2 %}
{% set notify_tab = 2 %}
{% include "./tab-bar.xml.njk" %}
51 changes: 51 additions & 0 deletions demo/backend/navigation/navigator/bottom-tabs/tab-bar.xml.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<navigation:bottom-tab-bar
xmlns="https://hyperview.org/hyperview"
xmlns:navigation="https://hyperview.org/navigation"
navigation:navigator="custom-bottom-tabs-navigator"
id="bottom-tab-bar"
>
<select-single style="tabbar-bottom" name="custom-tab-bar" key="custom-tab-bar">
<option
{% if selected_tab == 1 %}
selected="true"
{% endif %}
style="tabbar-bottom-tab"
value="custom-tab-1"
>
<behavior trigger="select" action="navigate" target="custom-bottom-tabs-tab-1" />
<view style="tabbar-bottom-tab-content">
<view style="tabbar-bottom-tab-icon-selected">
{% include 'icons/behaviors-selected.svg' %}
</view>
<view style="tabbar-bottom-tab-icon">
{% include 'icons/behaviors.svg' %}
</view>
<text style="tabbar-bottom-tab-label">Tab 1</text>
{% if notify_tab == 1 %}
<view style="tabbar-bottom-tab-notify" />
{% endif %}
</view>
</option>
<option
{% if selected_tab == 2 %}
selected="true"
{% endif %}
style="tabbar-bottom-tab"
value="custom-tab-2"
>
<behavior trigger="select" action="navigate" target="custom-bottom-tabs-tab-2" />
<view style="tabbar-bottom-tab-content">
<view style="tabbar-bottom-tab-icon-selected">
{% include 'icons/advanced-selected.svg' %}
</view>
<view style="tabbar-bottom-tab-icon">
{% include 'icons/advanced.svg' %}
</view>
<text style="tabbar-bottom-tab-label">Tab 2</text>
{% if notify_tab == 2 %}
<view style="tabbar-bottom-tab-notify" />
{% endif %}
</view>
</option>
</select-single>
</navigation:bottom-tab-bar>
3 changes: 2 additions & 1 deletion demo/schema/navigation.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
<xs:element name="bottom-tab-bar">
<xs:complexType>
<xs:sequence>
<xs:any minOccurs="1" maxOccurs="1" />
<xs:any minOccurs="0" maxOccurs="unbounded" />
</xs:sequence>
<xs:attribute name="id" type="hv:ID" form="unqualified" />
<xs:attribute name="navigator" type="xs:string" />
</xs:complexType>
</xs:element>
Expand Down
15 changes: 9 additions & 6 deletions demo/src/Components/BottomTabBar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { HvComponentProps, LocalName } from 'hyperview';
import { useContext, useEffect } from 'react';
import { BottomTabBarContext } from '../Contexts';
import { useBottomTabBarContext } from '../Contexts';
import { useEffect } from 'react';

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

Expand All @@ -19,7 +19,7 @@ const namespaceURI = 'https://hyperview.org/navigation';
* </navigation:bottom-tab-bar>
*/
const BottomTabBar = (props: HvComponentProps) => {
const ctx = useContext(BottomTabBarContext);
const { setElementProps } = useBottomTabBarContext();
const navigator = props.element.getAttributeNS(namespaceURI, 'navigator');
useEffect(() => {
if (!navigator) {
Expand All @@ -28,9 +28,12 @@ const BottomTabBar = (props: HvComponentProps) => {
);
return;
}
ctx.setElementProps?.(navigator, props);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [navigator, props]); // Exclude ctx from dependencies to avoid infinite loop
// To avoid rendering loops, we only set the element props once
if (props.element.getAttribute('registered') !== 'true') {
props.element.setAttribute('registered', 'true');
setElementProps?.(navigator, props);
}
}, [navigator, props, setElementProps]);
return null;
};

Expand Down
118 changes: 98 additions & 20 deletions demo/src/Contexts/BottomTabBar.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,113 @@
import React, { createContext, useState } from 'react';
import React, {
createContext,
useCallback,
useContext,
useReducer,
} from 'react';
import type { HvComponentProps } from 'hyperview';

type ElementProps = Record<string, HvComponentProps>;
type State = Record<string, HvComponentProps>;

type Action = {
type: string;
payload: {
navigator: string;
} & Partial<{
props: HvComponentProps;
element: Element;
}>;
};

/**
* React context that provides the Hyperview demo app with a state
* holding the navigation elements rendered by each screens that
* React navigation navigators use to drive navigation.
*/
export const BottomTabBarContext = createContext<{
elementsProps: ElementProps | undefined;
setElementProps: ((id: string, props: HvComponentProps) => void) | undefined;
const Context = createContext<{
getElementProps:
| ((navigator: string) => HvComponentProps | undefined)
| undefined;
setElement: ((navigator: string, element: Element) => void) | undefined;
setElementProps:
| ((navigator: string, props: HvComponentProps) => void)
| undefined;
}>({
elementsProps: undefined,
getElementProps: undefined,
setElement: undefined,
setElementProps: undefined,
});

export function BottomTabBarContextProvider(props: {
children: React.ReactNode;
}) {
const [elementsProps, setElementsProps] = useState<ElementProps>({});
return (
<BottomTabBarContext.Provider
value={{
elementsProps,
setElementProps: (id: string, p: HvComponentProps) => {
setElementsProps({ ...elementsProps, [id]: p });
const initialState: State = {};

type Reducer<S, A> = (prevState: S, action: A) => S;

const reducer: Reducer<State, Action> = (
state: State = initialState,
action: Action,
) => {
const { element, navigator, props } = action.payload;
switch (action.type) {
case 'SET_ELEMENT_PROPS':
if (!props) {
return state;
}
return {
...state,
[navigator]: props,
};
case 'SET_ELEMENT':
if (!element) {
return state;
}
return {
...state,
[navigator]: {
...(state[navigator] || {}),
element,
},
};
default:
return state;
}
};

export function BottomTabBarContextProvider(p: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(reducer, initialState);
const getElementProps = useCallback(
(navigator: string) => {
return state[navigator];
},
[state],
);
const setElement = useCallback(
(navigator: string, element: Element) => {
dispatch({
payload: {
element,
navigator,
},
type: 'SET_ELEMENT',
});
},
[dispatch],
);
const setElementProps = useCallback(
(navigator: string, props: HvComponentProps) => {
dispatch({
payload: {
navigator,
props,
},
}}
>
{props.children}
</BottomTabBarContext.Provider>
type: 'SET_ELEMENT_PROPS',
});
},
[dispatch],
);
return (
<Context.Provider value={{ getElementProps, setElement, setElementProps }}>
{p.children}
</Context.Provider>
);
}

export const useBottomTabBarContext = () => useContext(Context);
2 changes: 1 addition & 1 deletion demo/src/Contexts/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export {
BottomTabBarContext,
useBottomTabBarContext,
BottomTabBarContextProvider,
} from './BottomTabBar';
Loading

0 comments on commit a3c783a

Please sign in to comment.