Skip to content
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

Injectable DOM properties #141

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/core/React.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ var ReactDOM = require('ReactDOM');
var ReactMount = require('ReactMount');
var ReactProps = require('ReactProps');
var ReactServerRendering = require('ReactServerRendering');
var DOMProperty = require('DOMProperty');

var ReactDefaultInjection = require('ReactDefaultInjection');

ReactDefaultInjection.inject();

var React = {
DOM: ReactDOM,
DOMProperty: DOMProperty,
Props: ReactProps,
initializeTouchEvents: function(shouldUseTouch) {
ReactMount.useTouchEvents = shouldUseTouch;
Expand Down
11 changes: 5 additions & 6 deletions src/core/ReactDefaultInjection.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ var ReactDOM = require('ReactDOM');
var ReactDOMForm = require('ReactDOMForm');
var ReactDOMTextarea = require('ReactDOMTextarea');

var DefaultDOMPropertyConfig = require('DefaultDOMPropertyConfig');
var DOMProperty = require('DOMProperty');

var DefaultEventPluginOrder = require('DefaultEventPluginOrder');
var EnterLeaveEventPlugin = require('EnterLeaveEventPlugin');
var ChangeEventPlugin = require('ChangeEventPlugin');
Expand All @@ -46,17 +49,13 @@ function inject() {
'ChangeEventPlugin': ChangeEventPlugin
});

/**
* This is a bit of a hack. We need to override the <form> element to be a
* composite component because IE8 does not bubble or capture submit to the
* top level. In order to make this work with our dependency graph we need to
* inject it here.
*/
ReactDOM.injection.injectComponentClasses({
form: ReactDOMForm,
// TODO: Inject `ReactDOMInput`.
textarea: ReactDOMTextarea
});

DOMProperty.injection.injectDOMPropertyConfig(DefaultDOMPropertyConfig);
}

module.exports = {
Expand Down
269 changes: 105 additions & 164 deletions src/dom/DOMProperty.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,96 @@

var invariant = require('invariant');

var DOMPropertyInjection = {
/**
* Mapping from normalized, camelcased property names to a configuration that
* specifies how the associated DOM property should be accessed or rendered.
*/
MUST_USE_ATTRIBUTE: 0x1,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Octal literals are deprecated in es5 but it looks like hex still works. I guess it doesn't actually matter but I would consider using parseInt...

EDIT: realize now that this was already there, just shuffled

MUST_USE_PROPERTY: 0x2,
HAS_BOOLEAN_VALUE: 0x4,
HAS_SIDE_EFFECTS: 0x8,

/**
* Inject some specialized knowledge about the DOM. This takes a config object with the
* following properties:
*
* isCustomAttribute: function that given an attribute name will return true if it can
* be inserted into the DOM verbatim. Useful for data-* or aria-* attributes where it's
* impossible to enumerate all of the possible attribute names,
*
* Properties: object mapping DOM property name to one of the DOMPropertyInjection
* constants or null. If your attribute isn't in here, it won't get written to the DOM.
*
* DOMAttributeNames: object mapping React attribute name to the DOM attribute name.
* Attribute names not specified use the **lowercase** normalized name.
*
* DOMPropertyNames: similar to DOMAttributeNames but for DOM properties. Property
* names not specified use the normalized name.
*
* DOMMutationMethods: Properties that require special mutation methods. If `value` is
* undefined, the mutation method should unset the property.
*
* @param {object} domPropertyConfig the config as described above.
*/
injectDOMPropertyConfig: function(domPropertyConfig) {
var Properties = domPropertyConfig.Properties || {};
var DOMAttributeNames = domPropertyConfig.DOMAttributeNames || {};
var DOMPropertyNames = domPropertyConfig.DOMPropertyNames || {};
var DOMMutationMethods = domPropertyConfig.DOMMutationMethods || {};

if (domPropertyConfig.isCustomAttribute) {
DOMProperty._isCustomAttributeFunctions.push(domPropertyConfig.isCustomAttribute);
}

for (var propName in Properties) {
invariant(
!DOMProperty.isStandardName[propName],
'injectDOMPropertyConfig(...): You\'re trying to inject DOM property \'%s\' which ' +
'has already been injected. You may be accidentally injecting the same DOM ' +
'property config twice, or you may be injecting two configs that have conflicting ' +
'property names.',
propName
);

DOMProperty.isStandardName[propName] = true;

DOMProperty.getAttributeName[propName] =
DOMAttributeNames[propName] || propName.toLowerCase();

DOMProperty.getPropertyName[propName] =
DOMPropertyNames[propName] || propName;

var mutationMethod = DOMMutationMethods[propName];
if (mutationMethod) {
DOMProperty.getMutationMethod[propName] = mutationMethod;
}

var propConfig = Properties[propName];
DOMProperty.mustUseAttribute[propName] =
propConfig & DOMPropertyInjection.MUST_USE_ATTRIBUTE;
DOMProperty.mustUseProperty[propName] =
propConfig & DOMPropertyInjection.MUST_USE_PROPERTY;
DOMProperty.hasBooleanValue[propName] =
propConfig & DOMPropertyInjection.HAS_BOOLEAN_VALUE;
DOMProperty.hasSideEffects[propName] =
propConfig & DOMPropertyInjection.HAS_SIDE_EFFECTS;

invariant(
!DOMProperty.mustUseAttribute[propName] ||
!DOMProperty.mustUseProperty[propName],
'DOMProperty: Cannot use require using both attribute and property: %s',
propName
);
invariant(
DOMProperty.mustUseProperty[propName] ||
!DOMProperty.hasSideEffects[propName],
'DOMProperty: Properties that have side effects must use property: %s',
propName
);
}
}
};
var defaultValueCache = {};

/**
Expand Down Expand Up @@ -94,13 +184,23 @@ var DOMProperty = {
*/
hasSideEffects: {},

/**
* All of the isCustomAttribute() functions that have been injected.
*/
_isCustomAttributeFunctions: [],

/**
* Checks whether a property name is a custom attribute.
* @method
*/
isCustomAttribute: RegExp.prototype.test.bind(
/^(data|aria)-[a-z_][a-z\d_.\-]*$/
),
isCustomAttribute: function(attributeName) {
for (var i = 0; i < DOMProperty._isCustomAttributeFunctions.length; i++) {
if (DOMProperty._isCustomAttributeFunctions[i].call(null, attributeName)) {
return true;
}
}
return false;
},

/**
* Returns the default property value for a DOM property (i.e., not an
Expand All @@ -121,168 +221,9 @@ var DOMProperty = {
nodeDefaults[prop] = testElement[prop];
}
return nodeDefaults[prop];
}
};

/**
* Mapping from normalized, camelcased property names to a configuration that
* specifies how the associated DOM property should be accessed or rendered.
*/
var MustUseAttribute = 0x1;
var MustUseProperty = 0x2;
var HasBooleanValue = 0x4;
var HasSideEffects = 0x8;

var Properties = {
/**
* Standard Properties
*/
accept: null,
action: null,
ajaxify: MustUseAttribute,
allowFullScreen: MustUseAttribute | HasBooleanValue,
alt: null,
autoComplete: null,
autoplay: HasBooleanValue,
cellPadding: null,
cellSpacing: null,
checked: MustUseProperty | HasBooleanValue,
className: MustUseProperty,
colSpan: null,
contentEditable: null,
controls: MustUseProperty | HasBooleanValue,
data: null, // For `<object />` acts as `src`.
dir: null,
disabled: MustUseProperty | HasBooleanValue,
draggable: null,
enctype: null,
height: MustUseAttribute,
href: null,
htmlFor: null,
max: null,
method: null,
min: null,
multiple: MustUseProperty | HasBooleanValue,
name: null,
poster: null,
preload: null,
placeholder: null,
rel: null,
required: HasBooleanValue,
role: MustUseAttribute,
scrollLeft: MustUseProperty,
scrollTop: MustUseProperty,
selected: MustUseProperty | HasBooleanValue,
spellCheck: null,
src: null,
step: null,
style: null,
tabIndex: null,
target: null,
title: null,
type: null,
value: MustUseProperty | HasSideEffects,
width: MustUseAttribute,
wmode: MustUseAttribute,
/**
* SVG Properties
*/
cx: MustUseProperty,
cy: MustUseProperty,
d: MustUseProperty,
fill: MustUseProperty,
fx: MustUseProperty,
fy: MustUseProperty,
points: MustUseProperty,
r: MustUseProperty,
stroke: MustUseProperty,
strokeLinecap: MustUseProperty,
strokeWidth: MustUseProperty,
transform: MustUseProperty,
x: MustUseProperty,
x1: MustUseProperty,
x2: MustUseProperty,
version: MustUseProperty,
viewBox: MustUseProperty,
y: MustUseProperty,
y1: MustUseProperty,
y2: MustUseProperty,
spreadMethod: MustUseProperty,
offset: MustUseProperty,
stopColor: MustUseProperty,
stopOpacity: MustUseProperty,
gradientUnits: MustUseProperty,
gradientTransform: MustUseProperty
};

/**
* Attribute names not specified use the **lowercase** normalized name.
*/
var DOMAttributeNames = {
className: 'class',
htmlFor: 'for',
strokeLinecap: 'stroke-linecap',
strokeWidth: 'stroke-width',
stopColor: 'stop-color',
stopOpacity: 'stop-opacity'
};

/**
* Property names not specified use the normalized name.
*/
var DOMPropertyNames = {
autoComplete: 'autocomplete',
spellCheck: 'spellcheck'
};
},

/**
* Properties that require special mutation methods. If `value` is undefined,
* the mutation method should unset the property.
*/
var DOMMutationMethods = {
/**
* Setting `className` to null may cause it to be set to the string "null".
*
* @param {DOMElement} node
* @param {*} value
*/
className: function(node, value) {
node.className = value || '';
}
injection: DOMPropertyInjection
};

for (var propName in Properties) {
DOMProperty.isStandardName[propName] = true;

DOMProperty.getAttributeName[propName] =
DOMAttributeNames[propName] || propName.toLowerCase();

DOMProperty.getPropertyName[propName] =
DOMPropertyNames[propName] || propName;

var mutationMethod = DOMMutationMethods[propName];
if (mutationMethod) {
DOMProperty.getMutationMethod[propName] = mutationMethod;
}

var propConfig = Properties[propName];
DOMProperty.mustUseAttribute[propName] = propConfig & MustUseAttribute;
DOMProperty.mustUseProperty[propName] = propConfig & MustUseProperty;
DOMProperty.hasBooleanValue[propName] = propConfig & HasBooleanValue;
DOMProperty.hasSideEffects[propName] = propConfig & HasSideEffects;

invariant(
!DOMProperty.mustUseAttribute[propName] ||
!DOMProperty.mustUseProperty[propName],
'DOMProperty: Cannot use require using both attribute and property: %s',
propName
);
invariant(
DOMProperty.mustUseProperty[propName] ||
!DOMProperty.hasSideEffects[propName],
'DOMProperty: Properties that have side effects must use property: %s',
propName
);
}

module.exports = DOMProperty;
Loading