Skip to content

Commit

Permalink
[react jsx transform] Spread attribute -> Object.assign
Browse files Browse the repository at this point in the history
Add support for spread attributes. Transforms into an Object.assign just
like jstransform does for spread properties in object literals.

Depends on facebookarchive/esprima#22
  • Loading branch information
sebmarkbage authored and zpao committed Jun 26, 2014
1 parent 0cf686f commit e6134c3
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 3 deletions.
70 changes: 70 additions & 0 deletions vendor/fbtransform/transforms/__tests__/react-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,26 @@ describe('react jsx', function() {
);
};

// These are placeholder variables in scope that we can use to assert that a
// specific variable reference was passed, rather than an object clone of it.
var x = 123456;
var y = 789012;
var z = 345678;

var HEADER =
'/**\n' +
' * @jsx React.DOM\n' +
' */\n';

var expectObjectAssign = function(code) {
var Component = jest.genMockFunction();
var Child = jest.genMockFunction();
var objectAssignMock = jest.genMockFunction();
Object.assign = objectAssignMock;
eval(transform(HEADER + code).code);
return expect(objectAssignMock);
}

it('should convert simple tags', function() {
var code = [
'/**@jsx React.DOM*/',
Expand Down Expand Up @@ -357,4 +377,54 @@ describe('react jsx', function() {
expect(() => transform(code)).toThrow();
});

it('wraps props in Object.assign for spread attributes', function() {
var code = HEADER +
'<Component { ... x } y\n={2 } z />';
var result = HEADER +
'Component(Object.assign({}, x , {y: \n2, z: true}))';
expect(transform(code).code).toBe(result);
});

it('does not call Object.assign when there are no spreads', function() {
expectObjectAssign(
'<Component x={y} />'
).not.toBeCalled();
});

it('calls assign with a new target object for spreads', function() {
expectObjectAssign(
'<Component {...x} />'
).toBeCalledWith({}, x);
});

it('calls assign with an empty object when the spread is first', function() {
expectObjectAssign(
'<Component { ...x } y={2} />'
).toBeCalledWith({}, x, { y: 2 });
});

it('coalesces consecutive properties into a single object', function() {
expectObjectAssign(
'<Component { ... x } y={2} z />'
).toBeCalledWith({}, x, { y: 2, z: true });
});

it('avoids an unnecessary empty object when spread is not first', function() {
expectObjectAssign(
'<Component x={1} {...y} />'
).toBeCalledWith({x: 1}, y);
});

it('passes the same value multiple times to Object.assign', function() {
expectObjectAssign(
'<Component x={1} y="2" {...z} {...z}><Child /></Component>'
).toBeCalledWith({x: 1, y: "2"}, z, z);
});

it('evaluates sequences before passing them to Object.assign', function() {
expectObjectAssign(
'<Component x="1" {...(z = { y: 2 }, z)} z={3}>Text</Component>'
).toBeCalledWith({x: "1"}, { y: 2 }, {z: 3});
});

});
73 changes: 70 additions & 3 deletions vendor/fbtransform/transforms/react.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ var JSX_ATTRIBUTE_TRANSFORMS = {
}
};

/**
* Removes all non-whitespace/parenthesis characters
*/
var reNonWhiteParen = /([^\s\(\)])/g;
function stripNonWhiteParen(value) {
return value.replace(reNonWhiteParen, '');
}

function visitReactTag(traverse, object, path, state) {
var jsxObjIdent = utils.getDocblock(state).jsx;
var openingElement = object.openingElement;
Expand All @@ -57,6 +65,7 @@ function visitReactTag(traverse, object, path, state) {

utils.catchup(openingElement.range[0], state, trimLeft);


if (nameObject.type === Syntax.XJSNamespacedName && nameObject.namespace) {
throw new Error('Namespace tags are not supported. ReactJSX is not XML.');
}
Expand All @@ -75,23 +84,74 @@ function visitReactTag(traverse, object, path, state) {

var hasAttributes = attributesObject.length;

var hasAtLeastOneSpreadProperty = attributesObject.some(function(attr) {
return attr.type === Syntax.XJSSpreadAttribute;
});

// if we don't have any attributes, pass in null
if (hasAttributes) {
if (hasAtLeastOneSpreadProperty) {
utils.append('Object.assign({', state);
} else if (hasAttributes) {
utils.append('{', state);
} else {
utils.append('null', state);
}

// keep track of if the previous attribute was a spread attribute
var previousWasSpread = false;

// write attributes
attributesObject.forEach(function(attr, index) {
var isLast = index === attributesObject.length - 1;

if (attr.type === Syntax.XJSSpreadAttribute) {
// Plus 1 to skip `{`.
utils.move(attr.range[0] + 1, state);

// Close the previous object or initial object
if (!previousWasSpread) {
utils.append('}, ', state);
}

// Move to the expression start, ignoring everything except parenthesis
// and whitespace.
utils.catchup(attr.argument.range[0], state, stripNonWhiteParen);

traverse(attr.argument, path, state);

utils.catchup(attr.argument.range[1], state);

// Move to the end, ignoring parenthesis and the closing `}`
utils.catchup(attr.range[1] - 1, state, stripNonWhiteParen);

if (!isLast) {
utils.append(', ', state);
}

utils.move(attr.range[1], state);

previousWasSpread = true;

return;
}

// If the next attribute is a spread, we're effective last in this object
if (!isLast) {
isLast = attributesObject[index + 1].type === Syntax.XJSSpreadAttribute;
}

if (attr.name.namespace) {
throw new Error(
'Namespace attributes are not supported. ReactJSX is not XML.');
}
var name = attr.name.name;
var isLast = index === attributesObject.length - 1;

utils.catchup(attr.range[0], state, trimLeft);

if (previousWasSpread) {
utils.append('{', state);
}

utils.append(quoteAttrName(name), state);
utils.append(': ', state);

Expand Down Expand Up @@ -119,17 +179,24 @@ function visitReactTag(traverse, object, path, state) {
}

utils.catchup(attr.range[1], state, trimLeft);

previousWasSpread = false;

});

if (!openingElement.selfClosing) {
utils.catchup(openingElement.range[1] - 1, state, trimLeft);
utils.move(openingElement.range[1], state);
}

if (hasAttributes) {
if (hasAttributes && !previousWasSpread) {
utils.append('}', state);
}

if (hasAtLeastOneSpreadProperty) {
utils.append(')', state);
}

// filter out whitespace
var childrenToRender = object.children.filter(function(child) {
return !(child.type === Syntax.Literal
Expand Down

0 comments on commit e6134c3

Please sign in to comment.