-
Notifications
You must be signed in to change notification settings - Fork 49.8k
Add custom element property support behind a flag #22184
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 12 commits
a2f57e0
92f1e1c
52166d9
a5bb048
660e770
a84a2e6
db7e13d
74a7d9d
7bb6fa4
55a1e3c
23d406b
8a2651b
ae33345
9bec8b1
af292bc
1a093e5
9d6d1dd
dc1e6c2
6fa57fb
333d3d7
632c96c
b26e31f
ed4f899
4da5c57
91acb79
3cf8e44
1fe88e2
7e6dc19
7f67c45
97ea2b4
5d641c2
fead37f
77afc53
c198d82
3b0d45b
7509c6d
a59042e
39b142e
1c86699
b043bfb
37ccabe
8fcf649
4bd3b44
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ | |
|
|
||
| import { | ||
| getPropertyInfo, | ||
| getCustomElementPropertyInfo, | ||
| shouldIgnoreAttribute, | ||
| shouldRemoveAttribute, | ||
| isAttributeNameSafe, | ||
|
|
@@ -19,6 +20,7 @@ import sanitizeURL from '../shared/sanitizeURL'; | |
| import { | ||
| disableJavaScriptURLs, | ||
| enableTrustedTypesIntegration, | ||
| enableCustomElementPropertySupport, | ||
| } from 'shared/ReactFeatureFlags'; | ||
| import {isOpaqueHydratingObject} from './ReactDOMHostConfig'; | ||
|
|
||
|
|
@@ -139,15 +141,49 @@ export function setValueForProperty( | |
| value: mixed, | ||
| isCustomComponentTag: boolean, | ||
| ) { | ||
| const propertyInfo = getPropertyInfo(name); | ||
| let propertyInfo = getPropertyInfo(name); | ||
| if (shouldIgnoreAttribute(name, propertyInfo, isCustomComponentTag)) { | ||
| return; | ||
| } | ||
|
|
||
| if (enableCustomElementPropertySupport && isCustomComponentTag && name[0] === 'o' && name[1] === 'n') { | ||
| let eventName = name.replace(/Capture$/, ''); | ||
| const useCapture = name !== eventName; | ||
josepharhar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const nameLower = eventName.toLowerCase(); | ||
| if (nameLower in node) { | ||
|
||
| eventName = nameLower; | ||
| } | ||
| eventName = eventName.slice(2); | ||
|
|
||
| const listenersObjName = eventName + (useCapture ? 'true' : 'false'); | ||
| const alreadyHadListener = (node: any)._listeners && (node: any)._listeners[listenersObjName]; | ||
|
|
||
| if (typeof value === 'function' || alreadyHadListener) { | ||
|
||
| if (!(node: any)._listeners) { | ||
gaearon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| (node: any)._listeners = {}; | ||
josepharhar marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| (node: any)._listeners[listenersObjName] = value; | ||
| const proxy = useCapture ? eventProxyCapture : eventProxy; | ||
| if (value) { | ||
| if (!alreadyHadListener) { | ||
| node.addEventListener(eventName, proxy, useCapture); | ||
| } | ||
| } else { | ||
| node.removeEventListener(eventName, proxy, useCapture); | ||
| } | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| if (shouldRemoveAttribute(name, value, propertyInfo, isCustomComponentTag)) { | ||
| value = null; | ||
| } | ||
| if (enableCustomElementPropertySupport && isCustomComponentTag) { | ||
| propertyInfo = getCustomElementPropertyInfo(name, node); | ||
| } | ||
| // If the prop isn't in the special list, treat it as a simple attribute. | ||
| if (isCustomComponentTag || propertyInfo === null) { | ||
| if (propertyInfo === null || (isCustomComponentTag && !enableCustomElementPropertySupport)) { | ||
| if (isAttributeNameSafe(name)) { | ||
| const attributeName = name; | ||
| if (value === null) { | ||
|
|
@@ -204,3 +240,11 @@ export function setValueForProperty( | |
| } | ||
| } | ||
| } | ||
|
|
||
| function eventProxy(e: Event) { | ||
josepharhar marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| this._listeners[e.type + 'false'](e); | ||
| } | ||
|
|
||
| function eventProxyCapture(e: Event) { | ||
| this._listeners[e.type + 'true'](e); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,7 +11,7 @@ import type {ReactNodeList} from 'shared/ReactTypes'; | |
|
|
||
| import {Children} from 'react'; | ||
|
|
||
| import {enableFilterEmptyStringAttributesDOM} from 'shared/ReactFeatureFlags'; | ||
| import {enableFilterEmptyStringAttributesDOM, enableCustomElementPropertySupport} from 'shared/ReactFeatureFlags'; | ||
|
|
||
| import type { | ||
| Destination, | ||
|
|
@@ -1128,12 +1128,17 @@ function pushStartCustomElement( | |
|
|
||
| let children = null; | ||
| let innerHTML = null; | ||
| for (const propKey in props) { | ||
| for (let propKey in props) { | ||
| if (hasOwnProperty.call(props, propKey)) { | ||
| const propValue = props[propKey]; | ||
| if (propValue == null) { | ||
| continue; | ||
| } | ||
| if (enableCustomElementPropertySupport && propKey === 'className') { | ||
| // className gets rendered as class on the client, so it should be | ||
| // rendered as class on the server. | ||
| propKey = 'class'; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about htmlFor? Is that needed here?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. className is special because all elements have a builtin
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea this make sense because htmlFor doesn't have any semantics on custom element which are just Generic HTML elements. There are a number of other ones that are case sensitive for similar reasons. contentEditable, tabIndex, etc. They probably work if used correctly, but it would be nice to add a warning if they use the wrong case. There are some that just won't work like Similarly there are some we have to be careful with like
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm surprised innerText and textContent aren't reserved properties in React. Doesn't seem right.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
contentEditable is interesting, it looks like the attribute can be either
I mean, you can assign to offsetHeight from script and the browser doesn't complain about it... but yeah I guess it would be weird to have a state where you can assign to an attribute called Should I consider every global attribute and every built in property?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For the read only properties like offsetHeight, I just pushed a commit which emits the warning: 1a093e5 However, the test fails because it throws an exception when assigning to the read only properties - but this exception doesn't occur in the browser or in JSDOM... what kind of browser/DOM setup is being used in these tests?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. “use strict” throws, right?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh wow, today I learned...
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe if you render it on the server and hydrate it wouldn’t error. The reason for the warning would be so that if you render it on the server, during development you should know not to. So you can test hydrating it.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, that makes sense! |
||
| } | ||
| switch (propKey) { | ||
| case 'children': | ||
| children = propValue; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -206,6 +206,25 @@ export function getPropertyInfo(name: string): PropertyInfo | null { | |
| return properties.hasOwnProperty(name) ? properties[name] : null; | ||
| } | ||
|
|
||
| export function getCustomElementPropertyInfo( | ||
| name: string, | ||
| node: Element) { | ||
| if (name in (node: any)) { | ||
| const acceptsBooleans = (typeof (node: any)[name]) === 'boolean'; | ||
josepharhar marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return { | ||
|
||
| acceptsBooleans, | ||
| type: acceptsBooleans ? BOOLEAN : STRING, | ||
| mustUseProperty: true, | ||
| propertyName: name, | ||
| attributeName: name, | ||
| attributeNamespace: null, | ||
| sanitizeURL: false, | ||
josepharhar marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| removeEmptyString: false, | ||
| }; | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| function PropertyInfoRecord( | ||
| name: string, | ||
| type: PropertyType, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.