Skip to content

Instantly share code, notes, and snippets.

@greglittlefield-wf
Created June 1, 2015 05:45
Show Gist options
  • Save greglittlefield-wf/549ff3021624d7599492 to your computer and use it in GitHub Desktop.
Save greglittlefield-wf/549ff3021624d7599492 to your computer and use it in GitHub Desktop.
react-dart cloneElement wrapper
library react_wrappers;
import 'dart:js';
import 'package:react/react_client.dart';
JsObject _React = context['React'];
Map _getInternal(JsObject jsThis) => jsThis[PROPS][INTERNAL];
/// Helper with logic borrowed from react-dart that returns a JsObject version of props,
/// preprocessed for consumption by React JS and prepared for consumption by the react-dart wrapper internals/
JsObject _convertDartProps(Map extendedProps) {
var convertedProps = newJsObjectEmpty();
// Transfer over key and ref if they're specified so React JS knows about them.
if (extendedProps.containsKey('key')) {
convertedProps['key'] = extendedProps['key'];
}
if (extendedProps.containsKey('ref')) {
convertedProps['ref'] = extendedProps['ref'];
}
// Put Dart props inside the internal object, which will be accessed and manipulated by the react-dart wrapper.
convertedProps[INTERNAL] = {PROPS: extendedProps};
return convertedProps;
}
/// Dart wrapper for React.cloneElement.
///
/// _From the JS docs:_
/// > Clone and return a new ReactElement using element as the starting point.
/// > The resulting element will have the original element's props with the new props merged in shallowly.
/// > New children will replace existing children.
/// > Unlike React.addons.cloneWithProps, key and ref from the original element will be preserved.
/// > There is no special behavior for merging any props (unlike cloneWithProps).
/// > See the [v0.13 RC2 blog post](https://facebook.github.io/react/blog/2015/03/03/react-v0.13-rc2.html) for additional details.
JsObject cloneElement(JsObject element, [Map props, List children]) {
JsObject convertedProps;
Map internal = _getInternal(element);
if (internal == null) {
// Plain JS component
convertedProps = props != null ? newJsMap(props) : null;
} else {
// react-dart component
Map oldConfig = internal[PROPS];
Map extendedProps = new Map.from(oldConfig);
if (props != null) {
extendedProps.addAll(props);
}
if (children != null) {
extendedProps['children'] = children;
}
convertedProps = _convertDartProps(extendedProps);
}
List jsMethodArgs = [element, convertedProps];
if (children != null) {
jsMethodArgs.add(new JsArray.from(children));
}
return _React.callMethod('cloneElement', jsMethodArgs);;
}
library react_wrappers_test;
import 'dart:js';
import 'package:react/react_client.dart';
import 'package:react/react.dart' as react;
import 'package:react/react_test_utils.dart' as react_test_utils;
import 'package:unittest/unittest.dart';
import 'react_wrappers.dart';
/// Returns the props for a React JS component instance, shallow-converted to a Dart Map for convenience.
Map getJsProps(JsObject instance) {
JsObject props = instance[PROPS];
Map convertedProps = {};
JsArray keys = (context['Object'] as JsObject).callMethod('keys', [props]);
keys.forEach((key) {
convertedProps[key] = props[key];
});
return convertedProps;
}
/// Returns the native Dart component associated with a React JS component instance, or null if the component is not Dart-based.
react.Component getDartComponent(JsObject instance) {
var internal = _getInternal(instance);
if (internal != null) {
return internal[COMPONENT];
}
return null;
}
/// Returns the internal Map used by react-dart to maintain the native Dart component.
Map _getInternal(JsObject instance) => instance[PROPS][INTERNAL];
/// Convenience method/shorthand for react_test_utils.renderIntoDocument.
JsObject render(JsObject component) => react_test_utils.renderIntoDocument(component);
/// Main entry point for button group testing
main() {
group('Dart wrappers for React:', () {
group('cloneElement()', () {
const List testChildren = const ['child1', 'child2'];
const Map testProps = const {
'originalProp': 'original',
'originalPropToOverride': 'original'
};
group('returns a clone', () {
test('for a plain React JS component', () {
var original = react.div(testProps, testChildren);
var clone = cloneElement(original);
// If these JsObject are equal, then they proxy the same JS props object.
expect(clone[PROPS], isNot(equals(original[PROPS])));
Map originalProps = getJsProps(clone);
Map cloneProps = getJsProps(original);
// Verify all props (children included) are equal.
expect(cloneProps, equals(originalProps));
});
test('for a Dart component', () {
var original = TestComponentFactory(testProps, testChildren);
var clone = cloneElement(original);
// If these JsObject are equal, then they proxy the same JS props object.
expect(clone[PROPS], isNot(equals(original[PROPS])));
Map originalProps = getJsProps(clone);
Map cloneProps = getJsProps(original);
// Verify all props (children and react-dart internals included) are equal.
expect(cloneProps, equals(originalProps));
var dartRendered = getDartComponent(render(original));
var dartRenderedClone = getDartComponent(render(clone));
expect(dartRenderedClone, isNot(same(dartRendered)));
expect(dartRenderedClone.props, equals(dartRendered.props));
});
});
group('returns a clone with added/overridden props', () {
const Map testPropsToAdd = const {
'originalPropToOverride': 'clone',
'propToAdd': 'clone'
};
const Map expectedPropsMerge = const {
'originalProp': 'original',
'originalPropToOverride': 'clone',
'propToAdd': 'clone',
'children': testChildren
};
test('for a plain React JS component', () {
var original = react.div(testProps, testChildren);
var clone = cloneElement(original, testPropsToAdd);
Map cloneProps = getJsProps(clone);
// Verify all props (children included) are equal.
expect(cloneProps, equals(expectedPropsMerge));
});
test('for a Dart component', () {
var original = TestComponentFactory(testProps, testChildren);
var clone = cloneElement(original, testPropsToAdd);
var renderedClone = render(clone);
// Verify all props are equal.
Map cloneDartProps = getDartComponent(renderedClone).props;
expect(cloneDartProps, equals(expectedPropsMerge));
});
});
group('updates the "key" and "ref" props properly', () {
const Map originalKeyRefProps = const {
'key': 'original',
'ref': 'original'
};
const Map overrideKeyRefProps = const {
'key': 'clone',
'ref': 'clone'
};
test('for a plain React JS component', () {
var original = react.div(originalKeyRefProps, testChildren);
var clone = cloneElement(original, overrideKeyRefProps);
// Verify that "key" and "ref" are overridden according to React
expect(clone['key'], equals(overrideKeyRefProps['key']));
expect(clone['ref'], equals(overrideKeyRefProps['ref']));
});
test('for a Dart component', () {
var original, clone;
// The 'ref' property can only be used from within a render() method, so use RenderingContainerComponent
// to clone and render the test component.
var holder = RenderingContainerComponentFactory({
'renderer': () {
original = TestComponentFactory(originalKeyRefProps, testChildren);
clone = cloneElement(original, overrideKeyRefProps);
return clone;
}
});
var renderedHolder = render(holder);
// Verify that "key" and "ref" are overridden according to React
expect(clone['key'], equals(overrideKeyRefProps['key']));
expect(clone['ref'], equals(overrideKeyRefProps['ref']));
var renderedClone = react_test_utils.findRenderedComponentWithType(renderedHolder, TestComponentFactory);
// Verify that the "key" and "ref" props are overridden according the Dart component.
Map cloneDartProps = getDartComponent(renderedClone).props;
expect(cloneDartProps['key'], equals(overrideKeyRefProps['key']));
expect(cloneDartProps['ref'], equals(overrideKeyRefProps['ref']));
});
});
group('returns a clone with replaced children', () {
const List testOverrideChildren = const ['child3', 'child4'];
test('for a plain React JS component', () {
var original = react.div(testProps, testChildren);
var clone = cloneElement(original, null, testOverrideChildren);
Map cloneProps = getJsProps(clone);
expect(cloneProps['children'], equals(testOverrideChildren));
});
test('for a Dart component', () {
var original = TestComponentFactory(testProps, testChildren);
var clone = cloneElement(original, null, testOverrideChildren);
var renderedClone = render(clone);
// Verify that children are overridden according to React
Map cloneProps = getJsProps(renderedClone);
expect(cloneProps['children'], equals(testOverrideChildren));
// Verify that children are overridden according the Dart component.
Map cloneDartProps = getDartComponent(renderedClone).props;
expect(cloneDartProps['children'], equals(testOverrideChildren));
});
});
});
});
}
/// Helper component for testing a Dart (react-dart) React component with cloneElement.
ReactComponentFactory TestComponentFactory = react.registerComponent(() => new TestComponent());
class TestComponent extends react.Component {
@override
render() => react.div({});
}
/// Helper component that renders whatever you tell it to. Necessary for rendering components with the 'ref' prop.
ReactComponentFactory RenderingContainerComponentFactory = react.registerComponent(() => new RenderingContainerComponent());
class RenderingContainerComponent extends react.Component {
@override
render() => props['renderer']();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment