react/packages/react-dom/src/shared/ReactDOMUnknownPropertyHook.js

303 lines
8.9 KiB
JavaScript

/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
registrationNameModules,
possibleRegistrationNames,
} from 'legacy-events/EventPluginRegistry';
import warning from 'shared/warning';
import {
ATTRIBUTE_NAME_CHAR,
BOOLEAN,
RESERVED,
shouldRemoveAttributeWithWarning,
getPropertyInfo,
} from './DOMProperty';
import isCustomComponent from './isCustomComponent';
import possibleStandardNames from './possibleStandardNames';
let validateProperty = () => {};
if (__DEV__) {
const warnedProperties = {};
const hasOwnProperty = Object.prototype.hasOwnProperty;
const EVENT_NAME_REGEX = /^on./;
const INVALID_EVENT_NAME_REGEX = /^on[^A-Z]/;
const rARIA = new RegExp('^(aria)-[' + ATTRIBUTE_NAME_CHAR + ']*$');
const rARIACamel = new RegExp('^(aria)[A-Z][' + ATTRIBUTE_NAME_CHAR + ']*$');
validateProperty = function(tagName, name, value, canUseEventSystem) {
if (hasOwnProperty.call(warnedProperties, name) && warnedProperties[name]) {
return true;
}
const lowerCasedName = name.toLowerCase();
if (lowerCasedName === 'onfocusin' || lowerCasedName === 'onfocusout') {
warning(
false,
'React uses onFocus and onBlur instead of onFocusIn and onFocusOut. ' +
'All React events are normalized to bubble, so onFocusIn and onFocusOut ' +
'are not needed/supported by React.',
);
warnedProperties[name] = true;
return true;
}
// We can't rely on the event system being injected on the server.
if (canUseEventSystem) {
if (registrationNameModules.hasOwnProperty(name)) {
return true;
}
const registrationName = possibleRegistrationNames.hasOwnProperty(
lowerCasedName,
)
? possibleRegistrationNames[lowerCasedName]
: null;
if (registrationName != null) {
warning(
false,
'Invalid event handler property `%s`. Did you mean `%s`?',
name,
registrationName,
);
warnedProperties[name] = true;
return true;
}
if (EVENT_NAME_REGEX.test(name)) {
warning(
false,
'Unknown event handler property `%s`. It will be ignored.',
name,
);
warnedProperties[name] = true;
return true;
}
} else if (EVENT_NAME_REGEX.test(name)) {
// If no event plugins have been injected, we are in a server environment.
// So we can't tell if the event name is correct for sure, but we can filter
// out known bad ones like `onclick`. We can't suggest a specific replacement though.
if (INVALID_EVENT_NAME_REGEX.test(name)) {
warning(
false,
'Invalid event handler property `%s`. ' +
'React events use the camelCase naming convention, for example `onClick`.',
name,
);
}
warnedProperties[name] = true;
return true;
}
// Let the ARIA attribute hook validate ARIA attributes
if (rARIA.test(name) || rARIACamel.test(name)) {
return true;
}
if (lowerCasedName === 'innerhtml') {
warning(
false,
'Directly setting property `innerHTML` is not permitted. ' +
'For more information, lookup documentation on `dangerouslySetInnerHTML`.',
);
warnedProperties[name] = true;
return true;
}
if (lowerCasedName === 'aria') {
warning(
false,
'The `aria` attribute is reserved for future use in React. ' +
'Pass individual `aria-` attributes instead.',
);
warnedProperties[name] = true;
return true;
}
if (
lowerCasedName === 'is' &&
value !== null &&
value !== undefined &&
typeof value !== 'string'
) {
warning(
false,
'Received a `%s` for a string attribute `is`. If this is expected, cast ' +
'the value to a string.',
typeof value,
);
warnedProperties[name] = true;
return true;
}
if (typeof value === 'number' && isNaN(value)) {
warning(
false,
'Received NaN for the `%s` attribute. If this is expected, cast ' +
'the value to a string.',
name,
);
warnedProperties[name] = true;
return true;
}
const propertyInfo = getPropertyInfo(name);
const isReserved = propertyInfo !== null && propertyInfo.type === RESERVED;
// Known attributes should match the casing specified in the property config.
if (possibleStandardNames.hasOwnProperty(lowerCasedName)) {
const standardName = possibleStandardNames[lowerCasedName];
if (standardName !== name) {
warning(
false,
'Invalid DOM property `%s`. Did you mean `%s`?',
name,
standardName,
);
warnedProperties[name] = true;
return true;
}
} else if (!isReserved && name !== lowerCasedName) {
// Unknown attributes should have lowercase casing since that's how they
// will be cased anyway with server rendering.
warning(
false,
'React does not recognize the `%s` prop on a DOM element. If you ' +
'intentionally want it to appear in the DOM as a custom ' +
'attribute, spell it as lowercase `%s` instead. ' +
'If you accidentally passed it from a parent component, remove ' +
'it from the DOM element.',
name,
lowerCasedName,
);
warnedProperties[name] = true;
return true;
}
if (
typeof value === 'boolean' &&
shouldRemoveAttributeWithWarning(name, value, propertyInfo, false)
) {
if (value) {
warning(
false,
'Received `%s` for a non-boolean attribute `%s`.\n\n' +
'If you want to write it to the DOM, pass a string instead: ' +
'%s="%s" or %s={value.toString()}.',
value,
name,
name,
value,
name,
);
} else {
warning(
false,
'Received `%s` for a non-boolean attribute `%s`.\n\n' +
'If you want to write it to the DOM, pass a string instead: ' +
'%s="%s" or %s={value.toString()}.\n\n' +
'If you used to conditionally omit it with %s={condition && value}, ' +
'pass %s={condition ? value : undefined} instead.',
value,
name,
name,
value,
name,
name,
name,
);
}
warnedProperties[name] = true;
return true;
}
// Now that we've validated casing, do not validate
// data types for reserved props
if (isReserved) {
return true;
}
// Warn when a known attribute is a bad type
if (shouldRemoveAttributeWithWarning(name, value, propertyInfo, false)) {
warnedProperties[name] = true;
return false;
}
// Warn when passing the strings 'false' or 'true' into a boolean prop
if (
(value === 'false' || value === 'true') &&
propertyInfo !== null &&
propertyInfo.type === BOOLEAN
) {
warning(
false,
'Received the string `%s` for the boolean attribute `%s`. ' +
'%s ' +
'Did you mean %s={%s}?',
value,
name,
value === 'false'
? 'The browser will interpret it as a truthy value.'
: 'Although this works, it will not work as expected if you pass the string "false".',
name,
value,
);
warnedProperties[name] = true;
return true;
}
return true;
};
}
const warnUnknownProperties = function(type, props, canUseEventSystem) {
if (__DEV__) {
const unknownProps = [];
for (const key in props) {
const isValid = validateProperty(
type,
key,
props[key],
canUseEventSystem,
);
if (!isValid) {
unknownProps.push(key);
}
}
const unknownPropString = unknownProps
.map(prop => '`' + prop + '`')
.join(', ');
if (unknownProps.length === 1) {
warning(
false,
'Invalid value for prop %s on <%s> tag. Either remove it from the element, ' +
'or pass a string or number value to keep it in the DOM. ' +
'For details, see https://fb.me/react-attribute-behavior',
unknownPropString,
type,
);
} else if (unknownProps.length > 1) {
warning(
false,
'Invalid values for props %s on <%s> tag. Either remove them from the element, ' +
'or pass a string or number value to keep them in the DOM. ' +
'For details, see https://fb.me/react-attribute-behavior',
unknownPropString,
type,
);
}
}
};
export function validateProperties(type, props, canUseEventSystem) {
if (isCustomComponent(type, props)) {
return;
}
warnUnknownProperties(type, props, canUseEventSystem);
}