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/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;
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}
+ />
+ );
};
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-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-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/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/hv-element/index.tsx b/src/core/components/hv-element/index.tsx
new file mode 100644
index 000000000..55d5497de
--- /dev/null
+++ b/src/core/components/hv-element/index.tsx
@@ -0,0 +1,102 @@
+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';
+import { isRenderableElement } from 'hyperview/src/core/utils';
+
+export default (props: HvComponentProps): JSX.Element | null | string => {
+ // 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(() => {
+ if (
+ !preformatted &&
+ !inlineFormattingContext &&
+ nodeType === NODE_TYPE.ELEMENT_NODE &&
+ localName === LOCAL_NAME.TEXT
+ ) {
+ return InlineContext.formatter(element);
+ }
+ return inlineFormattingContext;
+ }, [element, inlineFormattingContext, localName, nodeType, preformatted]);
+
+ const componentProps = useMemo(() => {
+ return {
+ element,
+ onUpdate,
+ options: {
+ ...options,
+ inlineFormattingContext: formattingContext,
+ },
+ stylesheets,
+ };
+ }, [element, formattingContext, onUpdate, options, stylesheets]);
+
+ const Component = useMemo(() => {
+ if (nodeType === NODE_TYPE.ELEMENT_NODE && namespaceURI && localName) {
+ return componentRegistry?.getComponent(namespaceURI, localName);
+ }
+ return undefined;
+ }, [localName, namespaceURI, nodeType, componentRegistry]);
+
+ // 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 = element.getAttribute('key');
+
+ if (key) {
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ return ;
+ }
+ return ; // eslint-disable-line react/jsx-props-no-spreading
+ }
+ }
+
+ if (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 (preformatted) {
+ return element.nodeValue;
+ }
+ // When inline formatting context exists, lookup formatted value using node's index.
+ if (formattingContext) {
+ 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 = 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 element.nodeValue;
+ }
+ return null;
+};
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;
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/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 a272b1996..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,
@@ -22,6 +21,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 +422,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 (
@@ -460,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/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..c8cfb12db 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
- }
+ ? InlineContext.formatter(element)
+ : options.inlineFormattingContext;
- // 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];
- }
-
- // 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 = (
@@ -180,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 => (
+
+ ));
+};