From a6e2245160850fff15da8e1e0f863b5f5f42c2c4 Mon Sep 17 00:00:00 2001 From: Fatih Kalifa Date: Fri, 6 Aug 2021 12:03:17 +0700 Subject: [PATCH] use createElement --- src/index.browser.ts | 39 ++++++++++++++++++++++++++++----------- src/index.ts | 40 +++++++++++++++++++++++++++++++++++++--- src/mapAttribute.ts | 44 ++++++++------------------------------------ 3 files changed, 73 insertions(+), 50 deletions(-) diff --git a/src/index.browser.ts b/src/index.browser.ts index 8035ab8..e2c03a7 100644 --- a/src/index.browser.ts +++ b/src/index.browser.ts @@ -72,7 +72,7 @@ function toReactNode( attrs.key = key.toString(); const tag = node.tagName.toLowerCase() as HTMLTags; - const props = mapAttribute(tag, attrs, preserveAttributes, getPropName); + const props = mapAttribute(tag, attrs, preserveAttributes, getPropInfo); const children = Array.from(node.childNodes) .map((childNode, i) => { @@ -108,12 +108,15 @@ function toReactNode( return reactCreateElement(tag, props, transform, reactChildren); } -const specialPropsMap: Record = { +// map HTML attribute to react props, and optionally DOM prop by using array +// if DOM prop is same as attribute name, use single item array +const attributePropMap: Record = { for: 'htmlFor', class: 'className', - allowfullscreen: 'allowFullScreen', + // react prop and DOM prop have different casing + allowfullscreen: ['allowFullScreen', 'allowFullscreen'], autocomplete: 'autoComplete', - autofocus: 'autoFocus', + autofocus: ['autoFocus'], contenteditable: 'contentEditable', spellcheck: 'spellCheck', srcdoc: 'srcDoc', @@ -127,21 +130,35 @@ const specialPropsMap: Record = { * * For other edge cases we can use specialPropsMaps */ -function getPropName(originalTag: HTMLTags, attributeName: string): string { +function getPropInfo(tagName: HTMLTags, attributeName: string) { + const propName = attributePropMap[attributeName]; + const el = document.createElement(tagName); + // handle edge cases first - if (specialPropsMap[attributeName]) { - return specialPropsMap[attributeName]; + if (propName) { + const reactProp = Array.isArray(propName) ? propName[0] : propName; + const domProp = Array.isArray(propName) + ? propName[1] || attributeName + : propName; + return { name: reactProp, isBoolean: checkBooleanAttribute(el, domProp) }; } - const el = document.createElement(originalTag); - for (let propName in el) { if (propName.toLowerCase() === attributeName.toLowerCase()) { - return propName; + return { name: propName, isBoolean: checkBooleanAttribute(el, propName) }; } } - return attributeName; + return { + name: attributeName, + isBoolean: checkBooleanAttribute(el, attributeName), + }; +} + +function checkBooleanAttribute(el: HTMLElement, prop: any) { + el.setAttribute(prop, ''); + // @ts-ignore + return el[prop] === true; } function reactCreateElement( diff --git a/src/index.ts b/src/index.ts index c0928e7..2718e04 100644 --- a/src/index.ts +++ b/src/index.ts @@ -60,7 +60,7 @@ function toReactNode( const props = Object.assign( {}, - mapAttribute(name, attribs, preserveAttributes, getPropName), + mapAttribute(name, attribs, preserveAttributes, getPropInfo), { key } ); @@ -125,6 +125,40 @@ import attributes from './attribute.json'; type AttributeMap = Record; const attrs = attributes; -function getPropName(_originalTag: HTMLTags, attributeName: string) { - return attrs[attributeName] || attributeName; +function getPropInfo(_originalTag: HTMLTags, attributeName: string) { + const propName = attrs[attributeName] || attributeName; + return { + name: propName, + isBoolean: BOOLEAN_ATTRIBUTES.includes(propName), + }; } + +const BOOLEAN_ATTRIBUTES = [ + // https://github.com/facebook/react/blob/cae635054e17a6f107a39d328649137b83f25972/packages/react-dom/src/shared/DOMProperty.js#L319 + 'allowFullScreen', + 'async', + // Note: there is a special case that prevents it from being written to the DOM + // on the client side because the browsers are inconsistent. Instead we call focus(). + 'autoFocus', + 'autoPlay', + 'controls', + 'default', + 'defer', + 'disabled', + 'disablePictureInPicture', + 'disableRemotePlayback', + 'formNoValidate', + 'hidden', + 'loop', + 'noModule', + 'noValidate', + 'open', + 'playsInline', + 'readOnly', + 'required', + 'reversed', + 'scoped', + 'seamless', + // Microdata + 'itemScope', +]; diff --git a/src/mapAttribute.ts b/src/mapAttribute.ts index e242dbb..0dc10f8 100644 --- a/src/mapAttribute.ts +++ b/src/mapAttribute.ts @@ -13,7 +13,10 @@ export default function mapAttribute( originalTag: HTMLTags, attrs = {} as RawAttributes, preserveAttributes: Array, - getPropName: (originalTag: HTMLTags, attributeName: string) => string + getPropInfo: ( + originalTag: HTMLTags, + attributeName: string + ) => { name: string; isBoolean: boolean } ): Attributes { return Object.keys(attrs).reduce((result, attr) => { // ignore inline event attribute @@ -39,12 +42,11 @@ export default function mapAttribute( } } - const name = getPropName(originalTag, attributeName); - const isBooleanAttribute = BOOLEAN_ATTRIBUTES.includes(name); - if (name === 'style') { + const prop = getPropInfo(originalTag, attributeName); + if (prop.name === 'style') { // if there's an attribute called style, this means that the value must be exists // even if it's an empty string - result[name] = convertStyle(attrs.style!); + result[prop.name] = convertStyle(attrs.style!); } else { const value = attrs[attr]; // Convert attribute value to boolean attribute if needed @@ -52,7 +54,7 @@ export default function mapAttribute( const booleanAttrributeValue = value === '' || String(value).toLowerCase() === attributeName.toLowerCase(); - result[name] = isBooleanAttribute ? booleanAttrributeValue : value; + result[prop.name] = prop.isBoolean ? booleanAttrributeValue : value; } return result; @@ -112,33 +114,3 @@ function hypenColonToCamelCase(str: string): string { return char.toUpperCase(); }); } - -const BOOLEAN_ATTRIBUTES = [ - // https://github.com/facebook/react/blob/cae635054e17a6f107a39d328649137b83f25972/packages/react-dom/src/shared/DOMProperty.js#L319 - 'allowFullScreen', - 'async', - // Note: there is a special case that prevents it from being written to the DOM - // on the client side because the browsers are inconsistent. Instead we call focus(). - 'autoFocus', - 'autoPlay', - 'controls', - 'default', - 'defer', - 'disabled', - 'disablePictureInPicture', - 'disableRemotePlayback', - 'formNoValidate', - 'hidden', - 'loop', - 'noModule', - 'noValidate', - 'open', - 'playsInline', - 'readOnly', - 'required', - 'reversed', - 'scoped', - 'seamless', - // Microdata - 'itemScope', -];