From 355b6f055303531944b678949b44d4396b441d60 Mon Sep 17 00:00:00 2001 From: Hardin Gray Date: Thu, 22 May 2025 14:45:21 -0400 Subject: [PATCH 1/7] chore(hv-element): create component for element rendering (#1169) Improving performance by creating a new `hv-element` component as a replacement for Render.renderElement. This allows memoizing the options and props of the component to reduce unneeded re-renders. [Asana](https://app.asana.com/1/47184964732898/project/1204008699308084/task/1210344460236268?focus=true) --- src/components/hv-list/index.tsx | 15 ++- src/components/hv-section-list/index.tsx | 34 ++--- src/core/components/hv-element/index.tsx | 165 +++++++++++++++++++++++ src/core/components/hv-route/index.tsx | 23 ++-- src/core/components/hv-screen/index.js | 23 ++-- src/core/hyper-ref/index.tsx | 18 ++- 6 files changed, 225 insertions(+), 53 deletions(-) create mode 100644 src/core/components/hv-element/index.tsx diff --git a/src/components/hv-list/index.tsx b/src/components/hv-list/index.tsx index 81ab96398..e58f7a794 100644 --- a/src/components/hv-list/index.tsx +++ b/src/components/hv-list/index.tsx @@ -3,7 +3,6 @@ import * as Dom from 'hyperview/src/services/dom'; import * as Keyboard from 'hyperview/src/services/keyboard'; import * as Logging from 'hyperview/src/services/logging'; import * as Namespaces from 'hyperview/src/services/namespaces'; -import * as Render from 'hyperview/src/services/render'; import type { DOMString, HvComponentOnUpdate, @@ -20,6 +19,7 @@ import { createTestProps, getAncestorByTagName } from 'hyperview/src/services'; import { DOMParser } from '@instawork/xmldom'; import type { ElementRef } from 'react'; import { FlatList } from 'hyperview/src/core/components/scroll'; +import HvElement from 'hyperview/src/core/components/hv-element'; import { LOCAL_NAME } from 'hyperview/src/types'; export default class HvList extends PureComponent { @@ -265,12 +265,13 @@ export default class HvList extends PureComponent { removeClippedSubviews={false} // eslint-disable-next-line @typescript-eslint/no-explicit-any renderItem={({ item }: any) => - item && - Render.renderElement( - item, - this.props.stylesheets, - this.onUpdate, - this.props.options, + item && ( + ) } scrollIndicatorInsets={scrollIndicatorInsets} diff --git a/src/components/hv-section-list/index.tsx b/src/components/hv-section-list/index.tsx index d1323e330..9a6edb935 100644 --- a/src/components/hv-section-list/index.tsx +++ b/src/components/hv-section-list/index.tsx @@ -3,7 +3,6 @@ import * as Dom from 'hyperview/src/services/dom'; import * as Keyboard from 'hyperview/src/services/keyboard'; import * as Logging from 'hyperview/src/services/logging'; import * as Namespaces from 'hyperview/src/services/namespaces'; -import * as Render from 'hyperview/src/services/render'; import type { DOMString, HvComponentOnUpdate, @@ -20,6 +19,7 @@ import type { ScrollParams, State } from './types'; import { createTestProps, getAncestorByTagName } from 'hyperview/src/services'; import { DOMParser } from '@instawork/xmldom'; import type { ElementRef } from 'react'; +import HvElement from 'hyperview/src/core/components/hv-element'; import { SectionList } from 'hyperview/src/core/components/scroll'; const getSectionIndex = ( @@ -376,23 +376,23 @@ export default class HvSectionList extends PureComponent< } removeClippedSubviews={false} // eslint-disable-next-line @typescript-eslint/no-explicit-any - renderItem={({ item }: any): any => - Render.renderElement( - item, - this.props.stylesheets, - this.onUpdate, - this.props.options, - ) - } + renderItem={({ item }: any): any => ( + + )} // eslint-disable-next-line @typescript-eslint/no-explicit-any - renderSectionHeader={({ section: { title } }: any): any => - Render.renderElement( - title, - this.props.stylesheets, - this.onUpdate, - this.props.options, - ) - } + renderSectionHeader={({ section: { title } }: any): any => ( + + )} scrollIndicatorInsets={scrollIndicatorInsets} sections={sections} stickySectionHeadersEnabled={this.getStickySectionHeadersEnabled()} diff --git a/src/core/components/hv-element/index.tsx b/src/core/components/hv-element/index.tsx new file mode 100644 index 000000000..c542c8241 --- /dev/null +++ b/src/core/components/hv-element/index.tsx @@ -0,0 +1,165 @@ +import * as InlineContext from 'hyperview/src/services/inline-context'; +import * as Logging from 'hyperview/src/services/logging'; +import * as Namespaces from 'hyperview/src/services/namespaces'; +import { LOCAL_NAME, NODE_TYPE } from 'hyperview/src/types'; +import React, { useMemo } from 'react'; +import type { HvComponentProps } from 'hyperview/src/types'; + +export default (props: HvComponentProps): JSX.Element | null | string => { + if (!props.element) { + return null; + } + + if (props.element.nodeType === NODE_TYPE.ELEMENT_NODE) { + // Hidden elements don't get rendered + if (props.element.getAttribute('hide') === 'true') { + return null; + } + } + + if (props.element.nodeType === NODE_TYPE.COMMENT_NODE) { + // XML comments don't get rendered. + return null; + } + + if ( + props.element.nodeType === NODE_TYPE.ELEMENT_NODE && + props.element.namespaceURI === Namespaces.HYPERVIEW + ) { + switch (props.element.localName) { + case LOCAL_NAME.BEHAVIOR: + case LOCAL_NAME.MODIFIER: + case LOCAL_NAME.STYLES: + case LOCAL_NAME.STYLE: + // Non-UI elements don't get rendered + return null; + default: + break; + } + } + + const nodeType = useMemo(() => { + return props.element.nodeType; + }, [props.element]); + + const localName = useMemo(() => { + return props.element.localName; + }, [props.element]); + + const namespaceURI = useMemo(() => { + return props.element.namespaceURI; + }, [props.element]); + + const formattingContext = useMemo(() => { + let { inlineFormattingContext } = props.options; + if ( + !props.options.preformatted && + !inlineFormattingContext && + nodeType === NODE_TYPE.ELEMENT_NODE && + localName === LOCAL_NAME.TEXT + ) { + inlineFormattingContext = InlineContext.formatter(props.element); + } + return inlineFormattingContext; + }, [localName, nodeType, props.element, props.options]); + + const componentProps = useMemo(() => { + return { + element: props.element, + onUpdate: props.onUpdate, + options: { + ...props.options, + inlineFormattingContext: formattingContext, + }, + stylesheets: props.stylesheets, + }; + }, [ + formattingContext, + props.element, + props.onUpdate, + props.options, + props.stylesheets, + ]); + + const Component = useMemo(() => { + if (nodeType === NODE_TYPE.ELEMENT_NODE && namespaceURI && localName) { + return props.options.componentRegistry?.getComponent( + namespaceURI, + localName, + ); + } + return undefined; + }, [localName, namespaceURI, nodeType, props.options.componentRegistry]); + + if (nodeType === NODE_TYPE.ELEMENT_NODE) { + if (!namespaceURI) { + Logging.warn( + '`namespaceURI` missing for node:', + props.element.toString(), + ); + return null; + } + if (!localName) { + Logging.warn('`localName` missing for node:', props.element.toString()); + return null; + } + + if (Component) { + // Prepare props for the component + + // Conditionally render the component with a key if it exists, to avoid + // warnings with current React versions, when the key attribute is set + // using the spread operator. + const key = props.element.getAttribute('key'); + + if (key) { + // eslint-disable-next-line react/jsx-props-no-spreading + return ; + } + return ; // eslint-disable-line react/jsx-props-no-spreading + } + + // No component registered for the namespace/local name. + // Warn in case this was an unintended mistake. + Logging.warn( + `No component registered for tag <${localName}> (namespace: ${namespaceURI})`, + ); + } + + if (nodeType === NODE_TYPE.TEXT_NODE) { + // Render non-empty text nodes, when wrapped inside a element + if (props.element.nodeValue) { + if ( + ((props.element.parentNode as Element)?.namespaceURI === + Namespaces.HYPERVIEW && + (props.element.parentNode as Element)?.localName === + LOCAL_NAME.TEXT) || + (props.element.parentNode as Element)?.namespaceURI !== + Namespaces.HYPERVIEW + ) { + if (props.options.preformatted) { + return props.element.nodeValue; + } + // When inline formatting context exists, lookup formatted value using node's index. + if (formattingContext) { + const index = formattingContext[0].indexOf(props.element); + return formattingContext[1][index]; + } + + // Other strings might be whitespaces in non text elements, which we ignore + // However we raise a warning when the string isn't just composed of whitespaces. + const trimmedValue = props.element.nodeValue.trim(); + if (trimmedValue.length > 0) { + Logging.warn( + `Text string "${trimmedValue}" must be rendered within a element`, + ); + } + } + } + } + + if (nodeType === NODE_TYPE.CDATA_SECTION_NODE) { + return props.element.nodeValue; + } + return null; +}; diff --git a/src/core/components/hv-route/index.tsx b/src/core/components/hv-route/index.tsx index 447caf25d..87a210f94 100644 --- a/src/core/components/hv-route/index.tsx +++ b/src/core/components/hv-route/index.tsx @@ -4,7 +4,6 @@ import * as Helpers from 'hyperview/src/services/dom/helpers'; import * as Namespaces from 'hyperview/src/services/namespaces'; import * as NavigationContext from 'hyperview/src/contexts/navigation'; import * as NavigatorService from 'hyperview/src/services/navigator'; -import * as Render from 'hyperview/src/services/render'; import * as Stylesheets from 'hyperview/src/services/stylesheets'; import * as Types from './types'; import * as UrlService from 'hyperview/src/services/url'; @@ -13,12 +12,8 @@ import { BackBehaviorProvider, } from 'hyperview/src/contexts/back-behaviors'; import HvDoc, { StateContext } from 'hyperview/src/core/components/hv-doc'; -import React, { - JSXElementConstructor, - PureComponent, - useContext, - useMemo, -} from 'react'; +import React, { PureComponent, useContext, useMemo } from 'react'; +import HvElement from 'hyperview/src/core/components/hv-element'; import HvNavigator from 'hyperview/src/core/components/hv-navigator'; import HvScreen from 'hyperview/src/core/components/hv-screen'; import { LOCAL_NAME } from 'hyperview/src/types'; @@ -102,12 +97,14 @@ class HvRouteInner extends PureComponent { const styleSheet = Stylesheets.createStylesheets( (preloadElement as unknown) as Document, ); - const component: - | string - | React.ReactElement> - | null = Render.renderElement(body, styleSheet, () => noop, { - componentRegistry: this.componentRegistry, - }); + const component = ( + + ); if (component) { return ( ); } if (!screenElement) { diff --git a/src/core/hyper-ref/index.tsx b/src/core/hyper-ref/index.tsx index a272b1996..c45a80855 100644 --- a/src/core/hyper-ref/index.tsx +++ b/src/core/hyper-ref/index.tsx @@ -22,6 +22,7 @@ import type { PressHandlers, PressPropName, Props, State } from './types'; import React, { PureComponent } from 'react'; import { RefreshControl, Text, TouchableOpacity } from 'react-native'; import { BackBehaviorContext } from 'hyperview/src/contexts/back-behaviors'; +import HvElement from 'hyperview/src/core/components/hv-element'; import { PRESS_TRIGGERS_PROP_NAMES } from './types'; import { ScrollView } from 'hyperview/src/core/components/scroll'; import VisibilityDetectingView from 'hyperview/src/VisibilityDetectingView'; @@ -422,13 +423,18 @@ export default class HyperRef extends PureComponent { render() { // Render the component based on the XML element. Depending on the applied behaviors, // this component will be wrapped with others to provide the necessary interaction. - const children = Render.renderElement( - this.props.element, - this.props.stylesheets, - this.props.onUpdate, - { ...this.props.options, pressed: this.state.pressed, skipHref: true }, + const children = ( + ); - const { ScrollableView, TouchableView, VisibilityView } = this; return ( From d85b564cea77507a53bac63674a38cddf07a1a75 Mon Sep 17 00:00:00 2001 From: Hardin Gray Date: Wed, 28 May 2025 16:00:50 -0400 Subject: [PATCH 2/7] chore(render): simplify render.renderElement (#1171) Consolidate the logic between `src/services/render` and the new `hv-element` component. - Created a new utility to abstract the `null` render logic and logging - Update render service to use logic and then return the component - Update `hv-element` to use utility for logic and logging - Export `hv-element` for access outside Hyperview - Confirmed external use of `Hyperview.renderElement` with new logic still working (using existing nav-back demo) - Update `nav-back` demo to use component instead of function [Asana](https://app.asana.com/1/47184964732898/project/1204008699308084/task/1210368849843731?focus=true) --- demo/src/Components/NavBack/NavBack.tsx | 17 +-- src/core/components/hv-element/index.tsx | 123 ++++++--------------- src/core/components/hv-root/index.tsx | 3 + src/core/utils.ts | 99 +++++++++++++++++ src/services/render/index.tsx | 132 +++-------------------- 5 files changed, 159 insertions(+), 215 deletions(-) diff --git a/demo/src/Components/NavBack/NavBack.tsx b/demo/src/Components/NavBack/NavBack.tsx index 1b251e797..915063c40 100644 --- a/demo/src/Components/NavBack/NavBack.tsx +++ b/demo/src/Components/NavBack/NavBack.tsx @@ -1,8 +1,8 @@ +import React, { useContext } from 'react'; import type { HvComponentProps } 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'; @@ -25,12 +25,15 @@ const NavBack = (props: HvComponentProps) => { if (!element) { return null; } - return (Hyperview.renderElement( - element, - props.stylesheets, - props.onUpdate, - props.options, - ) as unknown) as JSX.Element; + + return ( + + ); }; NavBack.namespaceURI = namespaceURI; diff --git a/src/core/components/hv-element/index.tsx b/src/core/components/hv-element/index.tsx index c542c8241..55d5497de 100644 --- a/src/core/components/hv-element/index.tsx +++ b/src/core/components/hv-element/index.tsx @@ -4,113 +4,58 @@ import * as Namespaces from 'hyperview/src/services/namespaces'; import { LOCAL_NAME, NODE_TYPE } from 'hyperview/src/types'; import React, { useMemo } from 'react'; import type { HvComponentProps } from 'hyperview/src/types'; +import { isRenderableElement } from 'hyperview/src/core/utils'; export default (props: HvComponentProps): JSX.Element | null | string => { - if (!props.element) { - return null; - } - - if (props.element.nodeType === NODE_TYPE.ELEMENT_NODE) { - // Hidden elements don't get rendered - if (props.element.getAttribute('hide') === 'true') { - return null; - } - } - - if (props.element.nodeType === NODE_TYPE.COMMENT_NODE) { - // XML comments don't get rendered. - return null; - } - - if ( - props.element.nodeType === NODE_TYPE.ELEMENT_NODE && - props.element.namespaceURI === Namespaces.HYPERVIEW - ) { - switch (props.element.localName) { - case LOCAL_NAME.BEHAVIOR: - case LOCAL_NAME.MODIFIER: - case LOCAL_NAME.STYLES: - case LOCAL_NAME.STYLE: - // Non-UI elements don't get rendered - return null; - default: - break; - } - } - - const nodeType = useMemo(() => { - return props.element.nodeType; - }, [props.element]); - - const localName = useMemo(() => { - return props.element.localName; - }, [props.element]); - - const namespaceURI = useMemo(() => { - return props.element.namespaceURI; - }, [props.element]); + // eslint-disable-next-line react/destructuring-assignment + const { element, onUpdate, options, stylesheets } = props; + const { localName, namespaceURI, nodeType } = element; + const { componentRegistry, inlineFormattingContext, preformatted } = options; const formattingContext = useMemo(() => { - let { inlineFormattingContext } = props.options; if ( - !props.options.preformatted && + !preformatted && !inlineFormattingContext && nodeType === NODE_TYPE.ELEMENT_NODE && localName === LOCAL_NAME.TEXT ) { - inlineFormattingContext = InlineContext.formatter(props.element); + return InlineContext.formatter(element); } return inlineFormattingContext; - }, [localName, nodeType, props.element, props.options]); + }, [element, inlineFormattingContext, localName, nodeType, preformatted]); const componentProps = useMemo(() => { return { - element: props.element, - onUpdate: props.onUpdate, + element, + onUpdate, options: { - ...props.options, + ...options, inlineFormattingContext: formattingContext, }, - stylesheets: props.stylesheets, + stylesheets, }; - }, [ - formattingContext, - props.element, - props.onUpdate, - props.options, - props.stylesheets, - ]); + }, [element, formattingContext, onUpdate, options, stylesheets]); const Component = useMemo(() => { if (nodeType === NODE_TYPE.ELEMENT_NODE && namespaceURI && localName) { - return props.options.componentRegistry?.getComponent( - namespaceURI, - localName, - ); + return componentRegistry?.getComponent(namespaceURI, localName); } return undefined; - }, [localName, namespaceURI, nodeType, props.options.componentRegistry]); + }, [localName, namespaceURI, nodeType, componentRegistry]); - if (nodeType === NODE_TYPE.ELEMENT_NODE) { - if (!namespaceURI) { - Logging.warn( - '`namespaceURI` missing for node:', - props.element.toString(), - ); - return null; - } - if (!localName) { - Logging.warn('`localName` missing for node:', props.element.toString()); - return null; - } + // Check if the element is renderable before rendering the component + if (!isRenderableElement(element, options, formattingContext)) { + return null; + } + if (nodeType === NODE_TYPE.ELEMENT_NODE) { if (Component) { // Prepare props for the component // Conditionally render the component with a key if it exists, to avoid // warnings with current React versions, when the key attribute is set // using the spread operator. - const key = props.element.getAttribute('key'); + const key = element.getAttribute('key'); if (key) { // eslint-disable-next-line react/jsx-props-no-spreading @@ -118,37 +63,29 @@ export default (props: HvComponentProps): JSX.Element | null | string => { } return ; // eslint-disable-line react/jsx-props-no-spreading } - - // No component registered for the namespace/local name. - // Warn in case this was an unintended mistake. - Logging.warn( - `No component registered for tag <${localName}> (namespace: ${namespaceURI})`, - ); } if (nodeType === NODE_TYPE.TEXT_NODE) { // Render non-empty text nodes, when wrapped inside a element - if (props.element.nodeValue) { + if (element.nodeValue) { if ( - ((props.element.parentNode as Element)?.namespaceURI === + ((element.parentNode as Element)?.namespaceURI === Namespaces.HYPERVIEW && - (props.element.parentNode as Element)?.localName === - LOCAL_NAME.TEXT) || - (props.element.parentNode as Element)?.namespaceURI !== - Namespaces.HYPERVIEW + (element.parentNode as Element)?.localName === LOCAL_NAME.TEXT) || + (element.parentNode as Element)?.namespaceURI !== Namespaces.HYPERVIEW ) { - if (props.options.preformatted) { - return props.element.nodeValue; + if (preformatted) { + return element.nodeValue; } // When inline formatting context exists, lookup formatted value using node's index. if (formattingContext) { - const index = formattingContext[0].indexOf(props.element); + const index = formattingContext[0].indexOf(element); return formattingContext[1][index]; } // Other strings might be whitespaces in non text elements, which we ignore // However we raise a warning when the string isn't just composed of whitespaces. - const trimmedValue = props.element.nodeValue.trim(); + const trimmedValue = element.nodeValue.trim(); if (trimmedValue.length > 0) { Logging.warn( `Text string "${trimmedValue}" must be rendered within a element`, @@ -159,7 +96,7 @@ export default (props: HvComponentProps): JSX.Element | null | string => { } if (nodeType === NODE_TYPE.CDATA_SECTION_NODE) { - return props.element.nodeValue; + return element.nodeValue; } return null; }; diff --git a/src/core/components/hv-root/index.tsx b/src/core/components/hv-root/index.tsx index bef922895..c5c43eec3 100644 --- a/src/core/components/hv-root/index.tsx +++ b/src/core/components/hv-root/index.tsx @@ -25,6 +25,7 @@ import { UpdateAction, } from 'hyperview/src/types'; import React, { PureComponent } from 'react'; +import HvElement from 'hyperview/src/core/components/hv-element'; import HvRoute from 'hyperview/src/core/components/hv-route'; import { Linking } from 'react-native'; import { XNetworkRetryAction } from 'hyperview/src/services/dom/types'; @@ -43,6 +44,8 @@ export default class Hyperview extends PureComponent { static renderElement = Render.renderElement; + static HvElement = HvElement; + behaviorRegistry: BehaviorRegistry; componentRegistry: Components.Registry; diff --git a/src/core/utils.ts b/src/core/utils.ts index 3dd007333..c05a1549f 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -1,3 +1,7 @@ +import * as Logging from 'hyperview/src/services/logging'; +import * as Namespaces from 'hyperview/src/services/namespaces'; +import { HvComponentOptions, LOCAL_NAME, NODE_TYPE } from 'hyperview/src/types'; + /** * Provides a random UUID string. * @returns {string} @@ -17,3 +21,98 @@ export const uuid = (): string => { export const uuidNumber = (): number => { return parseInt(uuid().replace(/-/g, ''), 16); }; + +/** + * Checks if an element should be rendered based on the element type and options + * Logging warnings for unexpected conditions + * @param {Element} element + * @param {HvComponentOptions} options + * @param {InlineContext} inlineFormattingContext + * @returns {boolean} + */ +export const isRenderableElement = ( + element: Element, + options: HvComponentOptions, + inlineFormattingContext: [Node[], string[]] | null | undefined, +): boolean => { + if (!element) { + return false; + } + if ( + element.nodeType === NODE_TYPE.ELEMENT_NODE && + element.getAttribute('hide') === 'true' + ) { + // Hidden elements don't get rendered + return false; + } + if (element.nodeType === NODE_TYPE.COMMENT_NODE) { + // XML comments don't get rendered. + return false; + } + if ( + element.nodeType === NODE_TYPE.ELEMENT_NODE && + element.namespaceURI === Namespaces.HYPERVIEW + ) { + switch (element.localName) { + case LOCAL_NAME.BEHAVIOR: + case LOCAL_NAME.MODIFIER: + case LOCAL_NAME.STYLES: + case LOCAL_NAME.STYLE: + // Non-UI elements don't get rendered + return false; + default: + break; + } + } + + if (element.nodeType === NODE_TYPE.ELEMENT_NODE) { + if (!element.namespaceURI) { + Logging.warn('`namespaceURI` missing for node:', element.toString()); + return false; + } + if (!element.localName) { + Logging.warn('`localName` missing for node:', element.toString()); + return false; + } + + if ( + options.componentRegistry?.getComponent( + element.namespaceURI, + element.localName, + ) + ) { + // Has a component registered for the namespace/local name. + return true; + } + // No component registered for the namespace/local name. + // Warn in case this was an unintended mistake. + Logging.warn( + `No component registered for tag <${element.localName}> (namespace: ${element.namespaceURI})`, + ); + } + + if (element.nodeType === NODE_TYPE.TEXT_NODE) { + // Render non-empty text nodes, when wrapped inside a element + if (element.nodeValue) { + if ( + ((element.parentNode as Element)?.namespaceURI === + Namespaces.HYPERVIEW && + (element.parentNode as Element)?.localName === LOCAL_NAME.TEXT) || + (element.parentNode as Element)?.namespaceURI !== Namespaces.HYPERVIEW + ) { + if (options.preformatted) { + return true; + } + // When inline formatting context exists, lookup formatted value using node's index. + if (inlineFormattingContext) { + return true; + } + } + } + } + + if (element.nodeType === NODE_TYPE.CDATA_SECTION_NODE) { + return true; + } + return false; +}; diff --git a/src/services/render/index.tsx b/src/services/render/index.tsx index 8d1729128..4dc5d51a6 100644 --- a/src/services/render/index.tsx +++ b/src/services/render/index.tsx @@ -1,15 +1,14 @@ import * as InlineContext from 'hyperview/src/services/inline-context'; -import * as Logging from 'hyperview/src/services/logging'; -import * as Namespaces from 'hyperview/src/services/namespaces'; import type { - HvComponent, HvComponentOnUpdate, HvComponentOptions, HvComponentProps, StyleSheets, } from 'hyperview/src/types'; import { LOCAL_NAME, NODE_TYPE } from 'hyperview/src/types'; +import HvElement from 'hyperview/src/core/components/hv-element'; import React from 'react'; +import { isRenderableElement } from 'hyperview/src/core/utils'; export const renderElement = ( element: Element | null | undefined, @@ -20,125 +19,28 @@ export const renderElement = ( if (!element) { return null; } - if (element.nodeType === NODE_TYPE.ELEMENT_NODE) { - // Hidden elements don't get rendered - if (element.getAttribute('hide') === 'true') { - return null; - } - } - if (element.nodeType === NODE_TYPE.COMMENT_NODE) { - // XML comments don't get rendered. - return null; - } - if ( - element.nodeType === NODE_TYPE.ELEMENT_NODE && - element.namespaceURI === Namespaces.HYPERVIEW - ) { - switch (element.localName) { - case LOCAL_NAME.BEHAVIOR: - case LOCAL_NAME.MODIFIER: - case LOCAL_NAME.STYLES: - case LOCAL_NAME.STYLE: - // Non-UI elements don't get rendered - return null; - default: - break; - } - } - // Initialize inline formatting context for elements when not already defined - let { inlineFormattingContext } = options; - if ( + const inlineFormattingContext = !options.preformatted && - !inlineFormattingContext && + !options.inlineFormattingContext && element.nodeType === NODE_TYPE.ELEMENT_NODE && element.localName === LOCAL_NAME.TEXT - ) { - inlineFormattingContext = InlineContext.formatter(element); - } - - if (element.nodeType === NODE_TYPE.ELEMENT_NODE) { - if (!element.namespaceURI) { - Logging.warn('`namespaceURI` missing for node:', element.toString()); - return null; - } - if (!element.localName) { - Logging.warn('`localName` missing for node:', element.toString()); - return null; - } - - const Component: - | HvComponent - | undefined = options.componentRegistry?.getComponent( - element.namespaceURI, - element.localName, - ); - - if (Component) { - // Prepare props for the component - const props = { - element, - onUpdate, - options: { - ...options, - inlineFormattingContext, - }, - stylesheets, - }; - - // Conditionally render the component with a key if it exists, to avoid - // warnings with current React versions, when the key attribute is set - // using the spread operator. - const key = element.getAttribute('key'); - - if (key) { - // eslint-disable-next-line react/jsx-props-no-spreading - return ; - } - return ; // eslint-disable-line react/jsx-props-no-spreading - } - - // No component registered for the namespace/local name. - // Warn in case this was an unintended mistake. - Logging.warn( - `No component registered for tag <${element.localName}> (namespace: ${element.namespaceURI})`, - ); - } - - if (element.nodeType === NODE_TYPE.TEXT_NODE) { - // Render non-empty text nodes, when wrapped inside a element - if (element.nodeValue) { - if ( - ((element.parentNode as Element)?.namespaceURI === - Namespaces.HYPERVIEW && - (element.parentNode as Element)?.localName === LOCAL_NAME.TEXT) || - (element.parentNode as Element)?.namespaceURI !== Namespaces.HYPERVIEW - ) { - if (options.preformatted) { - return element.nodeValue; - } - // When inline formatting context exists, lookup formatted value using node's index. - if (inlineFormattingContext) { - const index = inlineFormattingContext[0].indexOf(element); - return inlineFormattingContext[1][index]; - } + ? InlineContext.formatter(element) + : options.inlineFormattingContext; - // Other strings might be whitespaces in non text elements, which we ignore - // However we raise a warning when the string isn't just composed of whitespaces. - const trimmedValue = element.nodeValue.trim(); - if (trimmedValue.length > 0) { - Logging.warn( - `Text string "${trimmedValue}" must be rendered within a element`, - ); - } - } - } + // Check if the element is renderable before rendering the component + if (!isRenderableElement(element, options, inlineFormattingContext)) { + return null; } - if (element.nodeType === NODE_TYPE.CDATA_SECTION_NODE) { - return element.nodeValue; - } - return null; + return ( + + ); }; export const renderChildren = ( From 688517ffb4973d3d16659edd5022ebc9f590873c Mon Sep 17 00:00:00 2001 From: Hardin Gray Date: Wed, 28 May 2025 16:54:15 -0400 Subject: [PATCH 3/7] chore(render): Replace renderChildren with component (#1173) Creating a component which can replace the use of the render service `renderChildren`. Provides a mapping of child nodes to `` components. - [Created the new component](https://github.com/Instawork/hyperview/commit/0ece9f6502dba35eade51d8097aee9df1ac439b7) - [Switched four element types from ts to tsx](https://github.com/Instawork/hyperview/commit/dd518da6aa03fe22a3b71d587d7c5971015ed108) - Replaced `React.createElement` implementations with declarative instances [using the new component to render children](https://github.com/Instawork/hyperview/commit/7c73c9bd2f3f45f3ee83ab77c3a9730d4f60928b) - Updated the sticky header logic to use the rendered children and [added a verification of a valid `getAttribute`](https://github.com/Instawork/hyperview/commit/65d8b20ece3b2dba561752ab8911eb666faa8046) [Asana](https://app.asana.com/1/47184964732898/project/1204008699308084/task/1210344460236290?focus=true) --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1210344460236290 --- .../hv-option/{index.ts => index.tsx} | 39 ++++++----- .../{index.ts => index.tsx} | 37 ++++++----- .../hv-select-single/{index.ts => index.tsx} | 37 ++++++----- .../hv-text/{index.ts => index.tsx} | 57 +++++++++------- src/components/hv-view/index.tsx | 65 ++++++++++--------- src/core/components/hv-children/index.tsx | 18 +++++ .../keyboard-aware-scroll-view/types.ts | 1 + src/core/hyper-ref/index.tsx | 12 ++-- src/services/render/index.tsx | 23 +++++++ 9 files changed, 176 insertions(+), 113 deletions(-) rename src/components/hv-option/{index.ts => index.tsx} (80%) rename src/components/hv-select-multiple/{index.ts => index.tsx} (84%) rename src/components/hv-select-single/{index.ts => index.tsx} (84%) rename src/components/hv-text/{index.ts => index.tsx} (52%) create mode 100644 src/core/components/hv-children/index.tsx diff --git a/src/components/hv-option/index.ts b/src/components/hv-option/index.tsx similarity index 80% rename from src/components/hv-option/index.ts rename to src/components/hv-option/index.tsx index efa64c0fb..60efb8ee9 100644 --- a/src/components/hv-option/index.ts +++ b/src/components/hv-option/index.tsx @@ -1,12 +1,9 @@ import * as Behaviors from 'hyperview/src/services/behaviors'; import * as Namespaces from 'hyperview/src/services/namespaces'; -import * as Render from 'hyperview/src/services/render'; -import type { - HvComponentOnUpdate, - HvComponentProps, -} from 'hyperview/src/types'; import React, { PureComponent } from 'react'; import { TouchableWithoutFeedback, View } from 'react-native'; +import HvChildren from 'hyperview/src/core/components/hv-children'; +import type { HvComponentProps } from 'hyperview/src/types'; import { LOCAL_NAME } from 'hyperview/src/types'; import type { State } from './types'; import { createEventHandler } from 'hyperview/src/core/hyper-ref'; @@ -56,6 +53,7 @@ export default class HvOption extends PureComponent { this.props.stylesheets, newOptions, ); + const { key, ...otherProps } = props; // Option renders as an outer TouchableWithoutFeedback view and inner view. // The outer view handles presses, the inner view handles styling. @@ -82,19 +80,24 @@ export default class HvOption extends PureComponent { outerProps.style = { flex: props.style.flex }; } - return React.createElement( - TouchableWithoutFeedback, - outerProps, - React.createElement( - View, - props, - ...Render.renderChildren( - this.props.element, - this.props.stylesheets, - this.props.onUpdate as HvComponentOnUpdate, - newOptions, - ), - ), + return ( + + + + + ); } } diff --git a/src/components/hv-select-multiple/index.ts b/src/components/hv-select-multiple/index.tsx similarity index 84% rename from src/components/hv-select-multiple/index.ts rename to src/components/hv-select-multiple/index.tsx index 27243e924..0b19269f5 100644 --- a/src/components/hv-select-multiple/index.ts +++ b/src/components/hv-select-multiple/index.tsx @@ -1,11 +1,7 @@ import * as Namespaces from 'hyperview/src/services/namespaces'; -import * as Render from 'hyperview/src/services/render'; -import type { - DOMString, - HvComponentOnUpdate, - HvComponentProps, -} from 'hyperview/src/types'; +import type { DOMString, HvComponentProps } from 'hyperview/src/types'; import React, { PureComponent } from 'react'; +import HvChildren from 'hyperview/src/core/components/hv-children'; import { LOCAL_NAME } from 'hyperview/src/types'; import { View } from 'react-native'; import { createProps } from 'hyperview/src/services'; @@ -98,18 +94,23 @@ export default class HvSelectMultiple extends PureComponent { const props = createProps(this.props.element, this.props.stylesheets, { ...this.props.options, }); - return React.createElement( - View, - props, - ...Render.renderChildren( - this.props.element, - this.props.stylesheets, - this.props.onUpdate as HvComponentOnUpdate, - { - ...this.props.options, - onToggle: this.onToggle, - }, - ), + const { key, ...otherProps } = props; + return ( + + + ); } } diff --git a/src/components/hv-select-single/index.ts b/src/components/hv-select-single/index.tsx similarity index 84% rename from src/components/hv-select-single/index.ts rename to src/components/hv-select-single/index.tsx index 19c7bc4b1..51f043216 100644 --- a/src/components/hv-select-single/index.ts +++ b/src/components/hv-select-single/index.tsx @@ -1,11 +1,7 @@ import * as Namespaces from 'hyperview/src/services/namespaces'; -import * as Render from 'hyperview/src/services/render'; -import type { - DOMString, - HvComponentOnUpdate, - HvComponentProps, -} from 'hyperview/src/types'; +import type { DOMString, HvComponentProps } from 'hyperview/src/types'; import React, { PureComponent } from 'react'; +import HvChildren from 'hyperview/src/core/components/hv-children'; import { LOCAL_NAME } from 'hyperview/src/types'; import { View } from 'react-native'; import { createProps } from 'hyperview/src/services'; @@ -93,18 +89,23 @@ export default class HvSelectSingle extends PureComponent { const props = createProps(this.props.element, this.props.stylesheets, { ...this.props.options, }); - return React.createElement( - View, - props, - ...Render.renderChildren( - this.props.element, - this.props.stylesheets, - this.props.onUpdate as HvComponentOnUpdate, - { - ...this.props.options, - onSelect: this.onSelect, - }, - ), + const { key, ...otherProps } = props; + return ( + + + ); } } diff --git a/src/components/hv-text/index.ts b/src/components/hv-text/index.tsx similarity index 52% rename from src/components/hv-text/index.ts rename to src/components/hv-text/index.tsx index 3a10deaed..cfe1da3b1 100644 --- a/src/components/hv-text/index.ts +++ b/src/components/hv-text/index.tsx @@ -1,10 +1,10 @@ import * as Namespaces from 'hyperview/src/services/namespaces'; -import * as Render from 'hyperview/src/services/render'; import type { HvComponentOnUpdate, HvComponentProps, } from 'hyperview/src/types'; import React, { PureComponent } from 'react'; +import HvChildren from 'hyperview/src/core/components/hv-children'; import { LOCAL_NAME } from 'hyperview/src/types'; import { Text } from 'react-native'; import { addHref } from 'hyperview/src/core/hyper-ref'; @@ -15,36 +15,47 @@ export default class HvText extends PureComponent { static localName = LOCAL_NAME.TEXT; - render() { - const { skipHref } = this.props.options || {}; + Component = () => { const props = createProps( this.props.element, this.props.stylesheets, this.props.options, ); - const component = React.createElement( - Text, - props, - ...Render.renderChildren( + const { key, ...otherProps } = props; + return ( + + + + ); + }; + + render() { + const { Component } = this; + const { skipHref } = this.props.options || {}; + + return skipHref ? ( + + ) : ( + addHref( + , this.props.element, this.props.stylesheets, this.props.onUpdate as HvComponentOnUpdate, - { - ...this.props.options, - preformatted: - this.props.element.getAttribute('preformatted') === 'true', - }, - ), + this.props.options, + ) ); - - return skipHref - ? component - : addHref( - component, - this.props.element, - this.props.stylesheets, - this.props.onUpdate as HvComponentOnUpdate, - this.props.options, - ); } } diff --git a/src/components/hv-view/index.tsx b/src/components/hv-view/index.tsx index 142c5e070..10586c444 100644 --- a/src/components/hv-view/index.tsx +++ b/src/components/hv-view/index.tsx @@ -112,7 +112,7 @@ export default class HvView extends PureComponent { (acc, element, index) => { if ( typeof element !== 'string' && - element?.props.element?.getAttribute('sticky') === 'true' + element?.props.element?.getAttribute?.('sticky') === 'true' ) { return [...acc, index]; } @@ -173,10 +173,9 @@ export default class HvView extends PureComponent { } } - const children = Render.renderChildren( + const children = Render.buildChildArray( this.props.element, - this.props.stylesheets, - this.props.onUpdate as HvComponentOnUpdate, + this.props.onUpdate, { ...this.props.options, ...(scrollable && hasInputFields @@ -189,47 +188,51 @@ export default class HvView extends PureComponent { } : {}), }, + this.props.stylesheets, ); /* eslint-disable react/jsx-props-no-spreading */ if (scrollable) { if (hasInputFields) { - return React.createElement( - KeyboardAwareScrollView, - { - element: this.props.element, - ...this.getCommonProps(), - ...this.getScrollViewProps(children), - ...this.getKeyboardAwareScrollViewProps(inputFieldRefs), - }, - ...children, + return ( + + {children} + ); } - return React.createElement( - ScrollView, - { - element: this.props.element, - ...this.getCommonProps(), - ...this.getScrollViewProps(children), - }, - ...children, + return ( + + {children} + ); } if (!keyboardAvoiding && safeArea) { - return React.createElement( - SafeAreaView, - this.getCommonProps(), - ...children, - ); + return {children}; } if (keyboardAvoiding) { - return React.createElement( - KeyboardAvoidingView, - { ...this.getCommonProps(), behavior: 'position' }, - ...children, + return ( + + {children} + ); } - return React.createElement(View, this.getCommonProps(), ...children); + return ( + + {children} + + ); /* eslint-enable react/jsx-props-no-spreading */ }; diff --git a/src/core/components/hv-children/index.tsx b/src/core/components/hv-children/index.tsx new file mode 100644 index 000000000..b05b7e23f --- /dev/null +++ b/src/core/components/hv-children/index.tsx @@ -0,0 +1,18 @@ +import * as Render from 'hyperview/src/services/render'; +import type { HvComponentProps } from 'hyperview/src/types'; + +/** + * Returns the children of an element as an array of HvElement components + * @param {HvComponentProps} props - The props of the component + * @returns {Array} - The array of children + */ +export default ( + props: HvComponentProps, +): Array => { + return Render.buildChildArray( + props.element, + props.onUpdate, + props.options, + props.stylesheets, + ); +}; diff --git a/src/core/components/keyboard-aware-scroll-view/types.ts b/src/core/components/keyboard-aware-scroll-view/types.ts index bf95b5e11..b21c98ffb 100644 --- a/src/core/components/keyboard-aware-scroll-view/types.ts +++ b/src/core/components/keyboard-aware-scroll-view/types.ts @@ -1,6 +1,7 @@ import { ScrollView, TextInput } from 'react-native'; export type Props = { + children?: React.ReactNode; getTextInputRefs?: () => Array | null | undefined; onScroll?: ScrollView['props']['onScroll']; scrollToBottomOnKBShow?: boolean; diff --git a/src/core/hyper-ref/index.tsx b/src/core/hyper-ref/index.tsx index c45a80855..3f2a345f6 100644 --- a/src/core/hyper-ref/index.tsx +++ b/src/core/hyper-ref/index.tsx @@ -3,7 +3,6 @@ import * as Dom from 'hyperview/src/services/dom'; import * as Events from 'hyperview/src/services/events'; import * as Logging from 'hyperview/src/services/logging'; import * as Namespaces from 'hyperview/src/services/namespaces'; -import * as Render from 'hyperview/src/services/render'; import { BEHAVIOR_ATTRIBUTES, LOCAL_NAME, @@ -466,9 +465,12 @@ export const addHref = ( return component; } - return React.createElement( - HyperRef, - { element, onUpdate, options, stylesheets }, - ...Render.renderChildren(element, stylesheets, onUpdate, options), + return ( + ); }; diff --git a/src/services/render/index.tsx b/src/services/render/index.tsx index 4dc5d51a6..c8cfb12db 100644 --- a/src/services/render/index.tsx +++ b/src/services/render/index.tsx @@ -82,3 +82,26 @@ export const renderChildNodes = ( } return children; }; + +/** + * Converts an element's childNodes into an array of HvElement components. + * @returns An array of HvElement components. + */ +export const buildChildArray = ( + element: Element, + onUpdate: HvComponentOnUpdate, + options: HvComponentOptions, + stylesheets: StyleSheets, +): Array | null | string> => { + if (!element || !element.childNodes) { + return []; + } + return Array.from(element.childNodes).map(node => ( + + )); +}; From 4e83b63c21b93653f27f41ce7ac7b589af30deac Mon Sep 17 00:00:00 2001 From: Hardin Gray Date: Wed, 28 May 2025 19:07:03 -0400 Subject: [PATCH 4/7] chore(render): remove demo changes (#1174) Removing changes made to demo until the next HV release. --- demo/src/Components/NavBack/NavBack.tsx | 17 +++++++---------- src/core/components/hv-root/index.tsx | 3 --- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/demo/src/Components/NavBack/NavBack.tsx b/demo/src/Components/NavBack/NavBack.tsx index 915063c40..1b251e797 100644 --- a/demo/src/Components/NavBack/NavBack.tsx +++ b/demo/src/Components/NavBack/NavBack.tsx @@ -1,8 +1,8 @@ -import React, { useContext } from 'react'; import type { HvComponentProps } 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'; @@ -25,15 +25,12 @@ const NavBack = (props: HvComponentProps) => { if (!element) { return null; } - - return ( - - ); + return (Hyperview.renderElement( + element, + props.stylesheets, + props.onUpdate, + props.options, + ) as unknown) as JSX.Element; }; NavBack.namespaceURI = namespaceURI; diff --git a/src/core/components/hv-root/index.tsx b/src/core/components/hv-root/index.tsx index c5c43eec3..bef922895 100644 --- a/src/core/components/hv-root/index.tsx +++ b/src/core/components/hv-root/index.tsx @@ -25,7 +25,6 @@ import { UpdateAction, } from 'hyperview/src/types'; import React, { PureComponent } from 'react'; -import HvElement from 'hyperview/src/core/components/hv-element'; import HvRoute from 'hyperview/src/core/components/hv-route'; import { Linking } from 'react-native'; import { XNetworkRetryAction } from 'hyperview/src/services/dom/types'; @@ -44,8 +43,6 @@ export default class Hyperview extends PureComponent { static renderElement = Render.renderElement; - static HvElement = HvElement; - behaviorRegistry: BehaviorRegistry; componentRegistry: Components.Registry; From b190235edcb9487297d420637fe750b71204f860 Mon Sep 17 00:00:00 2001 From: Hardin Gray Date: Thu, 29 May 2025 10:14:44 -0400 Subject: [PATCH 5/7] chore: export components --- src/core/components/hv-root/index.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/core/components/hv-root/index.tsx b/src/core/components/hv-root/index.tsx index bef922895..b0b03f812 100644 --- a/src/core/components/hv-root/index.tsx +++ b/src/core/components/hv-root/index.tsx @@ -25,6 +25,8 @@ import { UpdateAction, } from 'hyperview/src/types'; import React, { PureComponent } from 'react'; +import HvChildren from 'hyperview/src/core/components/hv-children'; +import HvElement from 'hyperview/src/core/components/hv-element'; import HvRoute from 'hyperview/src/core/components/hv-route'; import { Linking } from 'react-native'; import { XNetworkRetryAction } from 'hyperview/src/services/dom/types'; @@ -43,6 +45,10 @@ export default class Hyperview extends PureComponent { static renderElement = Render.renderElement; + static HvElement = HvElement; + + static HvChildren = HvChildren; + behaviorRegistry: BehaviorRegistry; componentRegistry: Components.Registry; From 6860f949ff60a3598ac9f938165f44b8fab5b823 Mon Sep 17 00:00:00 2001 From: Hardin Gray Date: Thu, 29 May 2025 10:15:04 -0400 Subject: [PATCH 6/7] chore: replace renderElement() --- demo/src/Components/NavBack/NavBack.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/demo/src/Components/NavBack/NavBack.tsx b/demo/src/Components/NavBack/NavBack.tsx index 1b251e797..7c53816d1 100644 --- a/demo/src/Components/NavBack/NavBack.tsx +++ b/demo/src/Components/NavBack/NavBack.tsx @@ -1,8 +1,8 @@ +import React, { useContext } from 'react'; import type { HvComponentProps } 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'; @@ -25,12 +25,14 @@ const NavBack = (props: HvComponentProps) => { if (!element) { return null; } - return (Hyperview.renderElement( - element, - props.stylesheets, - props.onUpdate, - props.options, - ) as unknown) as JSX.Element; + return ( + + ); }; NavBack.namespaceURI = namespaceURI; From 9a9d06539b2351ccade43c6521549340cccd9470 Mon Sep 17 00:00:00 2001 From: Hardin Gray Date: Thu, 29 May 2025 10:31:19 -0400 Subject: [PATCH 7/7] chore: replace renderChildren() --- .../Components/BottomSheet/ContentSection.tsx | 15 ++++---- demo/src/Components/BottomSheet/StopPoint.tsx | 15 ++++---- .../BottomTabBar/BottomTabBarItem.tsx | 33 +++++++++++------- demo/src/Components/Filter/Filter.tsx | 16 +++++---- demo/src/Components/Map/Map.tsx | 14 ++++---- demo/src/Components/Map/MapMarker.tsx | 13 ++++--- .../Components/SafeAreaView/SafeAreaView.tsx | 13 ++++--- .../ScrollOpacity/ScrollOpacity.tsx | 17 ++++++---- demo/src/Core/BottomTabBar/index.tsx | 34 ++++++++++--------- 9 files changed, 95 insertions(+), 75 deletions(-) diff --git a/demo/src/Components/BottomSheet/ContentSection.tsx b/demo/src/Components/BottomSheet/ContentSection.tsx index 75e45c99e..d63be2dff 100644 --- a/demo/src/Components/BottomSheet/ContentSection.tsx +++ b/demo/src/Components/BottomSheet/ContentSection.tsx @@ -17,13 +17,16 @@ const BottomSheetContentSection = (props: HvComponentProps) => { [key, setContentSectionHeight], ); - const children = Hyperview.renderChildren( - props.element, - props.stylesheets, - props.onUpdate, - props.options, + return ( + + + ); - return {children}; }; BottomSheetContentSection.namespaceURI = namespace; diff --git a/demo/src/Components/BottomSheet/StopPoint.tsx b/demo/src/Components/BottomSheet/StopPoint.tsx index 3304aeb32..4dbd5852a 100644 --- a/demo/src/Components/BottomSheet/StopPoint.tsx +++ b/demo/src/Components/BottomSheet/StopPoint.tsx @@ -5,13 +5,16 @@ import { View } from 'react-native'; import { namespace } from './types'; const BottomSheetStopPoint = (props: HvComponentProps) => { - const children = Hyperview.renderChildren( - props.element, - props.stylesheets, - props.onUpdate, - props.options, + return ( + + + ); - return {children}; }; BottomSheetStopPoint.namespaceURI = namespace; diff --git a/demo/src/Components/BottomTabBar/BottomTabBarItem.tsx b/demo/src/Components/BottomTabBar/BottomTabBarItem.tsx index 8c9d71a38..065f72b48 100644 --- a/demo/src/Components/BottomTabBar/BottomTabBarItem.tsx +++ b/demo/src/Components/BottomTabBar/BottomTabBarItem.tsx @@ -1,7 +1,7 @@ -import * as Render from 'hyperview/src/services/render'; -import type { HvComponentOnUpdate, HvComponentProps } from 'hyperview'; +import React, { useState } from 'react'; import { TouchableWithoutFeedback, View } from 'react-native'; -import { createElement, useState } from 'react'; +import type { HvComponentProps } from 'hyperview'; +import Hyperview from 'hyperview'; import { createEventHandler } from 'hyperview/src/core/hyper-ref'; import { createProps } from 'hyperview/src/services'; import { namespaceURI } from './constants'; @@ -36,17 +36,24 @@ const BottomTabBarItem = (props: HvComponentProps) => { // component to ensure proper layout. outerProps.style = { flex: p.style.flex }; } - const component = createElement( - View, - p, - ...Render.renderChildren( - props.element, - props.stylesheets, - props.onUpdate as HvComponentOnUpdate, - newOptions, - ), + return ( + + + + + ); - return createElement(TouchableWithoutFeedback, outerProps, component); }; BottomTabBarItem.namespaceURI = namespaceURI; diff --git a/demo/src/Components/Filter/Filter.tsx b/demo/src/Components/Filter/Filter.tsx index 88f0063d0..6abf360f1 100644 --- a/demo/src/Components/Filter/Filter.tsx +++ b/demo/src/Components/Filter/Filter.tsx @@ -1,8 +1,8 @@ import * as Logging from 'hyperview/src/services/logging'; import Hyperview, { Events, LOCAL_NAME, Namespaces } from 'hyperview'; +import React, { useEffect } from 'react'; import type { HvComponentProps } from 'hyperview'; import { findElements } from '../../Helpers'; -import { useEffect } from 'react'; type FormDataPart = { fieldName: string; @@ -101,12 +101,14 @@ const Filter = (props: HvComponentProps) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.element]); - return (Hyperview.renderChildren( - props.element, - props.stylesheets, - props.onUpdate, - props.options, - ) as unknown) as JSX.Element; + return ( + + ); }; Filter.namespaceURI = FILTER_NS; diff --git a/demo/src/Components/Map/Map.tsx b/demo/src/Components/Map/Map.tsx index 158334415..00f676b33 100644 --- a/demo/src/Components/Map/Map.tsx +++ b/demo/src/Components/Map/Map.tsx @@ -81,13 +81,6 @@ const Map = (props: HvComponentProps) => { }); }; - const children = Hyperview.renderChildren( - props.element, - props.stylesheets, - props.onUpdate, - props.options, - ); - return ( { toolbarEnabled={false} zoomEnabled={false} > - {children} + ); }; diff --git a/demo/src/Components/Map/MapMarker.tsx b/demo/src/Components/Map/MapMarker.tsx index e524225b4..cc249f262 100644 --- a/demo/src/Components/Map/MapMarker.tsx +++ b/demo/src/Components/Map/MapMarker.tsx @@ -16,11 +16,14 @@ const MapMarker = (props: HvComponentProps) => { latitude: parseFloat(getAttribute('latitude') || '0'), longitude: parseFloat(getAttribute('longitude') || '0'), }; - const children = Hyperview.renderChildren( - props.element, - props.stylesheets, - props.onUpdate, - props.options, + + const children = ( + ); if (Platform.OS === 'web') { // eslint-disable-next-line @typescript-eslint/ban-ts-comment diff --git a/demo/src/Components/SafeAreaView/SafeAreaView.tsx b/demo/src/Components/SafeAreaView/SafeAreaView.tsx index ff810216d..42d8420e2 100644 --- a/demo/src/Components/SafeAreaView/SafeAreaView.tsx +++ b/demo/src/Components/SafeAreaView/SafeAreaView.tsx @@ -34,12 +34,6 @@ const SafeAreaView = (props: HvComponentProps) => { props.element.getAttributeNS(namespaceURI, 'mode') || defaultMode; const insets = props.element.getAttributeNS(namespaceURI, 'insets') || defaultInsets; - const children = Hyperview.renderChildren( - props.element, - props.stylesheets, - props.onUpdate, - props.options, - ); const extraStyle = Hyperview.createStyleProp( props.element, props.stylesheets, @@ -91,7 +85,12 @@ const SafeAreaView = (props: HvComponentProps) => { return ( // eslint-disable-next-line react/jsx-props-no-spreading - {children} + ); }; diff --git a/demo/src/Components/ScrollOpacity/ScrollOpacity.tsx b/demo/src/Components/ScrollOpacity/ScrollOpacity.tsx index 1e09414f4..3e557e877 100644 --- a/demo/src/Components/ScrollOpacity/ScrollOpacity.tsx +++ b/demo/src/Components/ScrollOpacity/ScrollOpacity.tsx @@ -54,13 +54,16 @@ const ScrollOpacity = (props: HvComponentProps) => { }).start(); }, [duration, opacity, opacityRange, position, scrollRange]); - const children = (Hyperview.renderChildren( - props.element, - props.stylesheets, - props.onUpdate, - props.options, - ) as unknown) as JSX.Element; - return {children}; + return ( + + + + ); }; ScrollOpacity.namespaceURI = namespaceURI; diff --git a/demo/src/Core/BottomTabBar/index.tsx b/demo/src/Core/BottomTabBar/index.tsx index 41816c4b8..c03e5fc0c 100644 --- a/demo/src/Core/BottomTabBar/index.tsx +++ b/demo/src/Core/BottomTabBar/index.tsx @@ -1,8 +1,8 @@ -import * as Render from 'hyperview/src/services/render'; +import React, { useCallback } from 'react'; import type { HvComponentOnUpdate } from 'hyperview'; +import Hyperview from 'hyperview'; import type { Props } from './types'; import { useBottomTabBarContext } from '../../Contexts'; -import { useCallback } from 'react'; /** * Component used by Hyperview to render a custom bottom tab bar. @@ -40,18 +40,20 @@ export const BottomTabBar = (navProps: Props): JSX.Element | null => { return null; } - return (Render.renderChildren( - props.element, - props.stylesheets, - onUpdateCustom, - { - ...props.options, - onSelect: (route: string | null | undefined) => { - if (route) { - navigation.navigate(route); - } - }, - targetId: state.routes[state.index].name, - }, - ) as unknown) as JSX.Element; + return ( + { + if (route) { + navigation.navigate(route); + } + }, + targetId: state.routes[state.index].name, + }} + stylesheets={props.stylesheets} + /> + ); };