diff --git a/README.md b/README.md index 11f19db4..b8c86929 100644 --- a/README.md +++ b/README.md @@ -256,3 +256,17 @@ void main() { ``` To test the Dart wrapper, take a look at [test/react_test_utils_test.dart](test). + +## Contributing + +### Running Tests + +Dart VM: `pub run test -p content-shell` +dart2js: `pub run test -p chrome` + +### Build JS + +After modifying dart_helpers.js, run: +``` +$ tool/build_js.sh +``` diff --git a/example/test/context_test.dart b/example/test/context_test.dart new file mode 100644 index 00000000..fedcb020 --- /dev/null +++ b/example/test/context_test.dart @@ -0,0 +1,22 @@ +import 'dart:html'; + +import 'package:react/react_dom.dart' as react_dom; +import 'package:react/react_client.dart'; + +import 'react_test_components.dart'; + +void main() { + setClientConfiguration(); + + react_dom.render(contextComponent({}, + contextConsumerComponent({}), + ), querySelector('#content')); + + react_dom.render(contextComponent({}, + contextConsumerComponent({}), + ), querySelector('#content')); + + react_dom.render(contextComponent({}, + contextConsumerComponent({}, grandchildContextConsumerComponent({})), + ), querySelector('#content')); +} diff --git a/example/test/context_test.html b/example/test/context_test.html new file mode 100644 index 00000000..216fde50 --- /dev/null +++ b/example/test/context_test.html @@ -0,0 +1,14 @@ + + + + + context_test + + +
+ + + + + + diff --git a/example/test/get_dom_node_test.dart b/example/test/get_dom_node_test.dart index 76b55b37..da54ca77 100644 --- a/example/test/get_dom_node_test.dart +++ b/example/test/get_dom_node_test.dart @@ -20,7 +20,7 @@ class _ChildComponent extends react.Component { react.div({}, [ "Test element", counter.toString(), - react.button({"onClick": (_) { counter++;redraw();} }, "Increase counter") + react.button({'key': 'button', "onClick": (_) { counter++;redraw();} }, "Increase counter") ]); } @@ -40,12 +40,12 @@ class SimpleComponent extends react.Component { render() => react.div({}, [ - react.span({"ref": "refToSpan"}, "Test"), - react.span({}, counter), - react.button({"onClick": (_) => (react_dom.findDOMNode(this) as HtmlElement).children.first.text = (++counter).toString()},"Increase counter"), - react.br({}), - ChildComponent({"ref": "refToElement"}), - react.button({"onClick": (_) => window.alert((this.ref('refToElement') as _ChildComponent).counter.toString())}, "Show value of child element"), + react.span({'key': 'span1', "ref": "refToSpan"}, "Test"), + react.span({'key': 'span2'}, counter), + react.button({'key': 'button1', "onClick": (_) => (react_dom.findDOMNode(this) as HtmlElement).children.first.text = (++counter).toString()},"Increase counter"), + react.br({'key': 'br'}), + ChildComponent({'key': 'child', "ref": "refToElement"}), + react.button({'key': 'button2', "onClick": (_) => window.alert((this.ref('refToElement') as _ChildComponent).counter.toString())}, "Show value of child element"), ]); } diff --git a/example/test/react_test.dart b/example/test/react_test.dart index 3cf14128..2e572a9c 100644 --- a/example/test/react_test.dart +++ b/example/test/react_test.dart @@ -8,10 +8,10 @@ import "react_test_components.dart"; void main() { setClientConfiguration(); react_dom.render(mainComponent({}, [ - helloGreeter({}, []), - listComponent({}, []), + helloGreeter({'key': 'hello'}, []), + listComponent({'key': 'list'}, []), //clockComponent({"name": 'my-clock'}, []), - checkBoxComponent({}, []) + checkBoxComponent({'key': 'checkbox'}, []) ] ), querySelector('#content')); } diff --git a/example/test/react_test_components.dart b/example/test/react_test_components.dart index d1efb9ff..cf4a2c4a 100644 --- a/example/test/react_test_components.dart +++ b/example/test/react_test_components.dart @@ -30,8 +30,8 @@ class _HelloGreeter extends react.Component { render() { return react.div({}, [ - react.input({'ref': 'myInput', 'value': bind('name'), 'onChange': onInputChange}), - helloComponent({'name': state['name']}) + react.input({'key': 'input', 'ref': 'myInput', 'value': bind('name'), 'onChange': onInputChange}), + helloComponent({'key': 'hello', 'name': state['name']}) ]); } } @@ -47,8 +47,8 @@ class _CheckBoxComponent extends react.Component { render() { return react.div({}, [ - react.label({'className': this.state["checked"] ? 'striked' : 'not-striked'}, 'do the dishes'), - react.input({'type': 'checkbox', 'value': bind('checked')}, []) + react.label({'key': 'label', 'className': this.state["checked"] ? 'striked' : 'not-striked'}, 'do the dishes'), + react.input({'key': 'input', 'type': 'checkbox', 'value': bind('checked')}) ]); } } @@ -136,8 +136,8 @@ class _ListComponent extends react.Component { } return react.div({}, [ - react.button({"onClick": addItem}, "addItem"), - react.ul({}, items), + react.button({"onClick": addItem, 'key': 'button'}, "addItem"), + react.ul({'key': 'list'}, items), ]); } } @@ -152,3 +152,61 @@ class _MainComponent extends react.Component { } var mainComponent = react.registerComponent(() => new _MainComponent()); + +class _ContextComponent extends react.Component { + @override + Iterable get childContextKeys => const ['foo', 'bar', 'renderCount']; + + @override + Map getChildContext() => { + 'foo': {'object': 'with value'}, + 'bar': true, + 'renderCount': this.state['renderCount'] + }; + + render() { + return react.ul({}, + react.button({'onClick': _onButtonClick}, 'Redraw'), + react.br({}), + 'ContextComponent.getChildContext(): ', + getChildContext().toString(), + react.br({}), + react.br({}), + props['children'] + ); + } + + _onButtonClick(event) { + this.setState({'renderCount': (this.state['renderCount'] ?? 0) + 1}); + } +} +var contextComponent = react.registerComponent(() => new _ContextComponent()); + +class _ContextConsumerComponent extends react.Component { + @override + Iterable get contextKeys => const ['foo']; + + render() { + return react.ul({}, + 'ContextConsumerComponent.context: ', + context.toString(), + react.br({}), + react.br({}), + props['children'] + ); + } +} +var contextConsumerComponent = react.registerComponent(() => new _ContextConsumerComponent()); + +class _GrandchildContextConsumerComponent extends react.Component { + @override + Iterable get contextKeys => const ['renderCount']; + + render() { + return react.ul({}, + 'GrandchildContextConsumerComponent.context: ', + context.toString(), + ); + } +} +var grandchildContextConsumerComponent = react.registerComponent(() => new _GrandchildContextConsumerComponent()); diff --git a/example/test/ref_test.dart b/example/test/ref_test.dart index 770268cf..5e7303f1 100644 --- a/example/test/ref_test.dart +++ b/example/test/ref_test.dart @@ -45,23 +45,23 @@ class _ParentComponent extends react.Component { render() => react.div({},[ - react.h1({}, "String refs"), - react.h4({}, ""), - react.input({"ref": "inputRef"}), - react.button({"onClick": showInputValue}, "Print input element value"), - react.h4({}, "ChildComponent"), - ChildComponent({"ref": "childRef"}), - react.button({"onClick": showChildValue}, "Print child value"), - react.button({"onClick": incrementChildValue}, "Increment child value"), + react.h1({'key': 'string-h1'}, "String refs"), + react.h4({'key': 'string-h4'}, ""), + react.input({'key': 'string-input', "ref": "inputRef"}), + react.button({'key': 'string-show-input', "onClick": showInputValue}, "Print input element value"), + react.h4({'key': 'string-h4-child'}, "ChildComponent"), + ChildComponent({'key': 'string-child', "ref": "childRef"}), + react.button({'key': 'string-show-button', "onClick": showChildValue}, "Print child value"), + react.button({'key': 'string-increment-button', "onClick": incrementChildValue}, "Increment child value"), - react.h1({}, "Callback refs"), - react.h4({}, ""), - react.input({"ref": (instance) => _inputCallbackRef = instance}), - react.button({"onClick": showInputCallbackRefValue}, "Print input element value"), - react.h4({}, "ChildComponent"), - ChildComponent({"ref": (instance) => _childCallbackRef = instance}), - react.button({"onClick": showChildCallbackRefValue}, "Print child value"), - react.button({"onClick": incrementChildCallbackRefValue}, "Increment child value"), + react.h1({'key': 'h1-callback'}, "Callback refs"), + react.h4({'key': 'h4-callback-input'}, ""), + react.input({'key': 'callback-input', "ref": (instance) => _inputCallbackRef = instance}), + react.button({'key': 'callback-show-input', "onClick": showInputCallbackRefValue}, "Print input element value"), + react.h4({'key': 'callback-child-h4'}, "ChildComponent"), + ChildComponent({'key': 'callback-child', "ref": (instance) => _childCallbackRef = instance}), + react.button({'key': 'callback-show-button', "onClick": showChildCallbackRefValue}, "Print child value"), + react.button({'key': 'callback-increment-button', "onClick": incrementChildCallbackRefValue}, "Increment child value"), ]); } diff --git a/example/test/speed_test.dart b/example/test/speed_test.dart index af7288a2..e52310b2 100644 --- a/example/test/speed_test.dart +++ b/example/test/speed_test.dart @@ -56,9 +56,9 @@ class _Hello extends react.Component { for(var elem in data){ children.add( react.div({'key': elem[0]},[ - react.span({}, elem[0]), + react.span({'key': 'span1'}, elem[0]), " ", - react.span({}, elem[1]) + react.span({'key': 'span2'}, elem[1]) ]) ); } diff --git a/js_src/dart_helpers.js b/js_src/dart_helpers.js index 45939220..80cfc607 100644 --- a/js_src/dart_helpers.js +++ b/js_src/dart_helpers.js @@ -5,10 +5,10 @@ function _getProperty(obj, key) { return obj[key]; } function _setProperty(obj, key, value) { return obj[key] = value; } -function _createReactDartComponentClassConfig(dartInteropStatics, componentStatics) { - return { +function _createReactDartComponentClassConfig(dartInteropStatics, componentStatics, jsConfig) { + var config = { getInitialState: function() { - this.dartComponent = dartInteropStatics.initComponent(this, this.props.internal, componentStatics); + this.dartComponent = dartInteropStatics.initComponent(this, this.props.internal, this.context, componentStatics); return {}; }, componentWillMount: function() { @@ -17,14 +17,14 @@ function _createReactDartComponentClassConfig(dartInteropStatics, componentStati componentDidMount: function() { dartInteropStatics.handleComponentDidMount(this.dartComponent); }, - componentWillReceiveProps: function(nextProps) { - dartInteropStatics.handleComponentWillReceiveProps(this.dartComponent, nextProps.internal); + componentWillReceiveProps: function(nextProps, nextContext) { + dartInteropStatics.handleComponentWillReceiveProps(this.dartComponent, nextProps.internal, nextContext); }, - shouldComponentUpdate: function(nextProps, nextState) { - return dartInteropStatics.handleShouldComponentUpdate(this.dartComponent); + shouldComponentUpdate: function(nextProps, nextState, nextContext) { + return dartInteropStatics.handleShouldComponentUpdate(this.dartComponent, nextContext); }, - componentWillUpdate: function(nextProps, nextState) { - dartInteropStatics.handleComponentWillUpdate(this.dartComponent); + componentWillUpdate: function(nextProps, nextState, nextContext) { + dartInteropStatics.handleComponentWillUpdate(this.dartComponent, nextContext); }, componentDidUpdate: function(prevProps, prevState) { dartInteropStatics.handleComponentDidUpdate(this.dartComponent, prevProps.internal); @@ -36,6 +36,34 @@ function _createReactDartComponentClassConfig(dartInteropStatics, componentStati return dartInteropStatics.handleRender(this.dartComponent); } }; + + // React limits the accessible context entries + // to the keys specified in childContextTypes/contextTypes. + + var childContextKeys = jsConfig && jsConfig.childContextKeys; + var contextKeys = jsConfig && jsConfig.contextKeys; + + if (childContextKeys && childContextKeys.length !== 0) { + config.childContextTypes = {}; + for (var i = 0; i < childContextKeys.length; i++) { + config.childContextTypes[childContextKeys[i]] = React.PropTypes.object; + } + + // Only declare this when `childContextKeys` is non-empty to avoid unnecessarily + // creating interop context objects for components that won't use it. + config.getChildContext = function() { + return dartInteropStatics.handleGetChildContext(this.dartComponent); + }; + } + + if (contextKeys && contextKeys.length !== 0) { + config.contextTypes = {}; + for (var i = 0; i < contextKeys.length; i++) { + config.contextTypes[contextKeys[i]] = React.PropTypes.object; + } + } + + return config; } function _markChildValidated(child) { diff --git a/lib/react.dart b/lib/react.dart index e64b4808..eaad263a 100644 --- a/lib/react.dart +++ b/lib/react.dart @@ -10,6 +10,8 @@ import 'package:react/src/typedefs.dart'; /// Top-level ReactJS [Component class](https://facebook.github.io/react/docs/react-component.html) /// which provides the [ReactJS Component API](https://facebook.github.io/react/docs/react-component.html#reference) abstract class Component { + Map _context; + /// A private field that backs [props], which is exposed via getter/setter so /// it can be overridden in strong mode. /// @@ -37,6 +39,12 @@ abstract class Component { /// TODO: Switch back to a plain field once this issue is fixed. Ref _ref; + /// The React context map of this component, passed down from its ancestors' [getChildContext] value. + /// + /// Only keys declared in this component's [contextKeys] will be present. + Map get context => _context; + set context(Map value) => _context = value; + /// ReactJS [Component] props. /// /// Related: [state] @@ -81,13 +89,20 @@ abstract class Component { /// Bind the value of input to [state[key]]. bind(key) => [state[key], (value) => setState({key: value})]; - initComponentInternal(props, _jsRedraw, [Ref ref, _jsThis]) { + initComponentInternal(props, _jsRedraw, [Ref ref, _jsThis, context]) { this._jsRedraw = _jsRedraw; this.ref = ref; this._jsThis = _jsThis; + _initContext(context); _initProps(props); } + /// Initializes context + _initContext(context) { + this.context = new Map.from(context ?? const {}); + this.nextContext = this.context; + } + _initProps(props) { this.props = new Map.from(props); this.nextProps = this.props; @@ -95,15 +110,27 @@ abstract class Component { initStateInternal() { this.state = new Map.from(getInitialState()); + // Call `transferComponentState` to get state also to `_prevState` transferComponentState(); } + /// Private reference to the value of [context] for the upcoming render cycle. + /// + /// Useful for ReactJS lifecycle methods [shouldComponentUpdateWithContext] and [componentWillUpdateWithContext]. + Map nextContext; + /// Private reference to the value of [state] for the upcoming render cycle. /// /// Useful for ReactJS lifecycle methods [shouldComponentUpdate], [componentWillUpdate] and [componentDidUpdate]. Map _nextState = null; + /// Reference to the value of [context] from the previous render cycle, used internally for proxying + /// the ReactJS lifecycle method. + /// + /// __DO NOT set__ from anywhere outside react-dart lifecycle internals. + Map prevContext; + /// Reference to the value of [state] from the previous render cycle, used internally for proxying /// the ReactJS lifecycle method and [componentDidUpdate]. /// @@ -119,7 +146,8 @@ abstract class Component { /// Reference to the value of [props] for the upcoming render cycle. /// - /// Used internally for proxying ReactJS lifecycle methods [shouldComponentUpdate], [componentWillReceiveProps], and [componentWillUpdate]. + /// Used internally for proxying ReactJS lifecycle methods [shouldComponentUpdate], [componentWillReceiveProps], and + /// [componentWillUpdate] as well as the context-specific variants. /// /// __DO NOT set__ from anywhere outside react-dart lifecycle internals. Map nextProps; @@ -204,17 +232,50 @@ abstract class Component { /// /// Calling [setState] within this function will not trigger an additional [render]. /// + /// __Note__: Choose either this method or [componentWillReceivePropsWithContext]. They are both called at the same + /// time so using both provides no added benefit. + /// /// See: void componentWillReceiveProps(Map newProps) {} + /// ReactJS lifecycle method that is invoked when a `Component` is receiving [newProps]. + /// + /// This method is not called for the initial [render]. + /// + /// Use this as an opportunity to react to a prop or context transition before [render] is called by updating the + /// [state] using [setState]. The old props and context can be accessed via [props] and [context], respectively. + /// + /// Calling [setState] within this function will not trigger an additional [render]. + /// + /// __Note__: Choose either this method or [componentWillReceiveProps]. They are both called at the same time so using + /// both provides no added benefit. + /// + /// See: + void componentWillReceivePropsWithContext(Map newProps, nextContext) {} + /// ReactJS lifecycle method that is invoked before rendering when [nextProps] or [nextState] are being received. /// - /// Use this as an opportunity to return false when you're certain that the transition to the new props and state + /// Use this as an opportunity to return `false` when you're certain that the transition to the new props and state /// will not require a component update. /// + /// __Note__: This method is called after [shouldComponentUpdateWithContext]. When it returns `null`, the result of + /// this method is used, but this is not called if a valid `bool` is returned from [shouldComponentUpdateWithContext]. + /// /// See: bool shouldComponentUpdate(Map nextProps, Map nextState) => true; + /// ReactJS lifecycle method that is invoked before rendering when [nextProps], [nextState], or [nextContext] are + /// being received. + /// + /// Use this as an opportunity to return `false` when you're certain that the transition to the new props, state, and + /// context will not require a component update. + /// + /// __Note__: This method is called before [shouldComponentUpdate]. Returning `null` will defer the update to the + /// result of [shouldComponentUpdate], but [shouldComponentUpdate] is not called if a valid `bool` is returned. + /// + /// See: + bool shouldComponentUpdateWithContext(Map nextProps, Map nextState, Map nextContext) => null; + /// ReactJS lifecycle method that is invoked immediately before rendering when [nextProps] or [nextState] are being /// received. /// @@ -222,9 +283,25 @@ abstract class Component { /// /// Use this as an opportunity to perform preparation before an update occurs. /// + /// __Note__: Choose either this method or [componentWillUpdateWithContext]. They are both called at the same time so + /// using both provides no added benefit. + /// /// See: void componentWillUpdate(Map nextProps, Map nextState) {} + /// ReactJS lifecycle method that is invoked immediately before rendering when [nextProps], [nextState], or + /// [nextContext] are being received. + /// + /// This method is not called for the initial [render]. + /// + /// Use this as an opportunity to perform preparation before an update occurs. + /// + /// __Note__: Choose either this method or [componentWillUpdate]. They are both called at the same time so using both + /// provides no added benefit. + /// + /// See: + void componentWillUpdateWithContext(Map nextProps, Map nextState, Map nextContext) {} + /// ReactJS lifecycle method that is invoked immediately after the `Component`'s updates are flushed to the DOM. /// /// This method is not called for the initial [render]. @@ -243,6 +320,21 @@ abstract class Component { /// See: void componentWillUnmount() {} + /// Returns a Map of context to be passed to descendant components. + /// + /// Only keys present in [childContextKeys] will be used; all others will be ignored. + Map getChildContext() => const {}; + + /// The keys this component uses in its child context map (returned by [getChildContext]). + /// + /// __This method is called only once, upon component registration.__ + Iterable get childContextKeys => const []; + + /// The keys of context used by this component. + /// + /// __This method is called only once, upon component registration.__ + Iterable get contextKeys => const []; + /// Invoked once before the `Component` is mounted. The return value will be used as the initial value of [state]. /// /// See: diff --git a/lib/react.js b/lib/react.js index 855306e4..ee40e64d 100644 --- a/lib/react.js +++ b/lib/react.js @@ -4324,10 +4324,10 @@ module.exports = ReactPropTypesSecret; function _getProperty(obj, key) { return obj[key]; } function _setProperty(obj, key, value) { return obj[key] = value; } -function _createReactDartComponentClassConfig(dartInteropStatics, componentStatics) { - return { +function _createReactDartComponentClassConfig(dartInteropStatics, componentStatics, jsConfig) { + var config = { getInitialState: function() { - this.dartComponent = dartInteropStatics.initComponent(this, this.props.internal, componentStatics); + this.dartComponent = dartInteropStatics.initComponent(this, this.props.internal, this.context, componentStatics); return {}; }, componentWillMount: function() { @@ -4336,14 +4336,14 @@ function _createReactDartComponentClassConfig(dartInteropStatics, componentStati componentDidMount: function() { dartInteropStatics.handleComponentDidMount(this.dartComponent); }, - componentWillReceiveProps: function(nextProps) { - dartInteropStatics.handleComponentWillReceiveProps(this.dartComponent, nextProps.internal); + componentWillReceiveProps: function(nextProps, nextContext) { + dartInteropStatics.handleComponentWillReceiveProps(this.dartComponent, nextProps.internal, nextContext); }, - shouldComponentUpdate: function(nextProps, nextState) { - return dartInteropStatics.handleShouldComponentUpdate(this.dartComponent); + shouldComponentUpdate: function(nextProps, nextState, nextContext) { + return dartInteropStatics.handleShouldComponentUpdate(this.dartComponent, nextContext); }, - componentWillUpdate: function(nextProps, nextState) { - dartInteropStatics.handleComponentWillUpdate(this.dartComponent); + componentWillUpdate: function(nextProps, nextState, nextContext) { + dartInteropStatics.handleComponentWillUpdate(this.dartComponent, nextContext); }, componentDidUpdate: function(prevProps, prevState) { dartInteropStatics.handleComponentDidUpdate(this.dartComponent, prevProps.internal); @@ -4355,6 +4355,34 @@ function _createReactDartComponentClassConfig(dartInteropStatics, componentStati return dartInteropStatics.handleRender(this.dartComponent); } }; + + // React limits the accessible context entries + // to the keys specified in childContextTypes/contextTypes. + + var childContextKeys = jsConfig && jsConfig.childContextKeys; + var contextKeys = jsConfig && jsConfig.contextKeys; + + if (childContextKeys && childContextKeys.length !== 0) { + config.childContextTypes = {}; + for (var i = 0; i < childContextKeys.length; i++) { + config.childContextTypes[childContextKeys[i]] = React.PropTypes.object; + } + + // Only declare this when `childContextKeys` is non-empty to avoid unnecessarily + // creating interop context objects for components that won't use it. + config.getChildContext = function() { + return dartInteropStatics.handleGetChildContext(this.dartComponent); + }; + } + + if (contextKeys && contextKeys.length !== 0) { + config.contextTypes = {}; + for (var i = 0; i < contextKeys.length; i++) { + config.contextTypes[contextKeys[i]] = React.PropTypes.object; + } + } + + return config; } function _markChildValidated(child) { diff --git a/lib/react_client.dart b/lib/react_client.dart index 30c0e561..bf4814f1 100644 --- a/lib/react_client.dart +++ b/lib/react_client.dart @@ -178,12 +178,32 @@ dynamic _convertArgsToChildren(List childrenArgs) { } } +@JS('Object.keys') +external List _objectKeys(Object object); + +InteropContextValue _jsifyContext(Map context) { + var interopContext = new InteropContextValue(); + context.forEach((key, value) { + setProperty(interopContext, key, new ReactDartContextInternal(value)); + }); + + return interopContext; +} + +Map _unjsifyContext(InteropContextValue interopContext) { + // TODO consider using `contextKeys` for this if perf of objectKeys is bad. + return new Map.fromIterable(_objectKeys(interopContext), value: (key) { + ReactDartContextInternal internal = getProperty(interopContext, key); + return internal?.value; + }); +} + /// The static methods that proxy JS component lifecycle methods to Dart components. final ReactDartInteropStatics _dartInteropStatics = (() { var zone = Zone.current; /// Wrapper for [Component.getInitialState]. - Component initComponent(ReactComponent jsThis, ReactDartComponentInternal internal, ComponentStatics componentStatics) => zone.run(() { + Component initComponent(ReactComponent jsThis, ReactDartComponentInternal internal, InteropContextValue context, ComponentStatics componentStatics) => zone.run(() { void jsRedraw() { jsThis.setState(emptyJsMap); } @@ -197,7 +217,7 @@ final ReactDartInteropStatics _dartInteropStatics = (() { }; Component component = componentStatics.componentFactory() - ..initComponentInternal(internal.props, jsRedraw, getRef, jsThis) + ..initComponentInternal(internal.props, jsRedraw, getRef, jsThis, _unjsifyContext(context)) ..initStateInternal(); // Return the component so that the JS proxying component can store it, @@ -205,6 +225,10 @@ final ReactDartInteropStatics _dartInteropStatics = (() { return component; }); + InteropContextValue handleGetChildContext(Component component) => zone.run(() { + return _jsifyContext(component.getChildContext()); + }); + /// Wrapper for [Component.componentWillMount]. void handleComponentWillMount(Component component) => zone.run(() { component @@ -224,10 +248,14 @@ final ReactDartInteropStatics _dartInteropStatics = (() { /// 1. Update [Component.props] using the value stored to [Component.nextProps] /// in `componentWillReceiveProps`. - /// 2. Update [Component.state] by calling [Component.transferComponentState] - void _afterPropsChange(Component component) { - component.props = component.nextProps; // [1] - component.transferComponentState(); // [2] + /// 2. Update [Component.context] using the value stored to [Component.nextContext] + /// in `componentWillReceivePropsWithContext`. + /// 3. Update [Component.state] by calling [Component.transferComponentState] + void _afterPropsChange(Component component, InteropContextValue nextContext) { + component + ..props = component.nextProps // [1] + ..context = component.nextContext // [2] + ..transferComponentState(); // [3] } void _clearPrevState(Component component) { @@ -248,33 +276,50 @@ final ReactDartInteropStatics _dartInteropStatics = (() { } /// Wrapper for [Component.componentWillReceiveProps]. - void handleComponentWillReceiveProps(Component component, ReactDartComponentInternal nextInternal) => zone.run(() { + void handleComponentWillReceiveProps(Component component, ReactDartComponentInternal nextInternal, InteropContextValue nextContext) => zone.run(() { var nextProps = _getNextProps(component, nextInternal); + var newContext = _unjsifyContext(nextContext); + component ..nextProps = nextProps - ..componentWillReceiveProps(nextProps); + ..nextContext = newContext + ..componentWillReceiveProps(nextProps) + ..componentWillReceivePropsWithContext(nextProps, newContext); }); /// Wrapper for [Component.shouldComponentUpdate]. - bool handleShouldComponentUpdate(Component component) => zone.run(() { + bool handleShouldComponentUpdate(Component component, InteropContextValue nextContext) => zone.run(() { _callSetStateTransactionalCallbacks(component); - if (component.shouldComponentUpdate(component.nextProps, component.nextState)) { + // If shouldComponentUpdateWithContext returns a valid bool (default implementation returns null), + // then don't bother calling `shouldComponentUpdate` and have it trump. + bool shouldUpdate = + component.shouldComponentUpdateWithContext(component.nextProps, component.nextState, component.nextContext); + + if (shouldUpdate == null) { + shouldUpdate = component.shouldComponentUpdate(component.nextProps, component.nextState); + } + + if (shouldUpdate) { return true; } else { // If component should not update, update props / transfer state because componentWillUpdate will not be called. - _afterPropsChange(component); + _afterPropsChange(component, nextContext); _callSetStateCallbacks(component); // Clear out prevState after it's done being used so it's not retained _clearPrevState(component); return false; - } + } }); /// Wrapper for [Component.componentWillUpdate]. - void handleComponentWillUpdate(Component component) => zone.run(() { - component.componentWillUpdate(component.nextProps, component.nextState); - _afterPropsChange(component); + void handleComponentWillUpdate(Component component, InteropContextValue nextContext) => zone.run(() { + /// Call `componentWillUpdate` and the context variant + component + ..componentWillUpdate(component.nextProps, component.nextState) + ..componentWillUpdateWithContext(component.nextProps, component.nextState, component.nextContext); + + _afterPropsChange(component, nextContext); }); /// Wrapper for [Component.componentDidUpdate]. @@ -282,7 +327,10 @@ final ReactDartInteropStatics _dartInteropStatics = (() { /// Uses [prevState] which was transferred from [Component.nextState] in [componentWillUpdate]. void handleComponentDidUpdate(Component component, ReactDartComponentInternal prevInternal) => zone.run(() { var prevInternalProps = prevInternal.props; + + /// Call `componentDidUpdate` and the context variant component.componentDidUpdate(prevInternalProps, component.prevState); + _callSetStateCallbacks(component); // Clear out prevState after it's done being used so it's not retained _clearPrevState(component); @@ -304,6 +352,7 @@ final ReactDartInteropStatics _dartInteropStatics = (() { return new ReactDartInteropStatics( initComponent: allowInterop(initComponent), + handleGetChildContext: allowInterop(handleGetChildContext), handleComponentWillMount: allowInterop(handleComponentWillMount), handleComponentDidMount: allowInterop(handleComponentDidMount), handleComponentWillReceiveProps: allowInterop(handleComponentWillReceiveProps), @@ -318,18 +367,24 @@ final ReactDartInteropStatics _dartInteropStatics = (() { /// Returns a new [ReactComponentFactory] which produces a new JS /// [`ReactClass` component class](https://facebook.github.io/react/docs/top-level-api.html#react.createclass). ReactDartComponentFactoryProxy _registerComponent(ComponentFactory componentFactory, [Iterable skipMethods = const []]) { + var componentInstance = componentFactory(); var componentStatics = new ComponentStatics(componentFactory); + var jsConfig = new JsComponentConfig( + childContextKeys: componentInstance.childContextKeys, + contextKeys: componentInstance.contextKeys, + ); + /// Create the JS [`ReactClass` component class](https://facebook.github.io/react/docs/top-level-api.html#react.createclass) /// with custom JS lifecycle methods. var reactComponentClass = React.createClass( - createReactDartComponentClassConfig(_dartInteropStatics, componentStatics) - ..displayName = componentFactory().displayName + createReactDartComponentClassConfig(_dartInteropStatics, componentStatics, jsConfig) + ..displayName = componentInstance.displayName ); // Cache default props and store them on the ReactClass so they can be used // by ReactDartComponentFactoryProxy and externally. - final Map defaultProps = new Map.unmodifiable(componentFactory().getDefaultProps()); + final Map defaultProps = new Map.unmodifiable(componentInstance.getDefaultProps()); reactComponentClass.dartDefaultProps = defaultProps; return new ReactDartComponentFactoryProxy(reactComponentClass); diff --git a/lib/react_client/react_interop.dart b/lib/react_client/react_interop.dart index b26188ab..b9e3d170 100644 --- a/lib/react_client/react_interop.dart +++ b/lib/react_client/react_interop.dart @@ -83,6 +83,8 @@ class ReactClassConfig { Function componentWillUpdate, Function componentDidUpdate, Function componentWillUnmount, + Function getChildContext, + Map childContextTypes, Function getDefaultProps, Function getInitialState, Function render @@ -158,6 +160,18 @@ class ReactComponent { // Interop internals // ---------------------------------------------------------------------------- +/// A JavaScript interop class representing a value in a React JS `context` object. +/// +/// Used for storing/accessing Dart [ReactDartContextInternal] objects in `context` +/// in a way that's opaque to the JS, and avoids the need to use dart2js interceptors. +/// +/// __For internal/advanced use only.__ +@JS() +@anonymous +class InteropContextValue { + external factory InteropContextValue(); +} + /// A JavaScript interop class representing a React JS `props` object. /// /// Used for storing/accessing [ReactDartComponentInternal] objects in @@ -190,6 +204,16 @@ class ReactDartComponentInternal { Map props; } +/// Internal react-dart information used to proxy React JS lifecycle to Dart +/// [Component] instances. +/// +/// __For internal/advanced use only.__ +class ReactDartContextInternal { + final dynamic value; + + ReactDartContextInternal(this.value); +} + /// Marks [child] as validated, as if it were passed into [React.createElement] /// as a variadic child. /// @@ -213,14 +237,20 @@ void markChildrenValidated(List children) { /// [dartInteropStatics] and [componentStatics] internally to proxy between /// the JS and Dart component instances. @JS('_createReactDartComponentClassConfig') -external ReactClassConfig createReactDartComponentClassConfig(ReactDartInteropStatics dartInteropStatics, ComponentStatics componentStatics); - -typedef Component _InitComponent(ReactComponent jsThis, ReactDartComponentInternal internal, ComponentStatics componentStatics); +external ReactClassConfig createReactDartComponentClassConfig( + ReactDartInteropStatics dartInteropStatics, + ComponentStatics componentStatics, + [JsComponentConfig jsConfig] +); + +typedef Component _InitComponent(ReactComponent jsThis, ReactDartComponentInternal internal, InteropContextValue context, ComponentStatics componentStatics); +typedef InteropContextValue _HandleGetChildContext(Component component); typedef void _HandleComponentWillMount(Component component); typedef void _HandleComponentDidMount(Component component); -typedef void _HandleComponentWillReceiveProps(Component component, ReactDartComponentInternal nextInternal); -typedef bool _HandleShouldComponentUpdate(Component component); -typedef void _HandleComponentWillUpdate(Component component); +typedef void _HandleComponentWillReceiveProps(Component component, ReactDartComponentInternal nextInternal, InteropContextValue nextContext); +typedef bool _HandleShouldComponentUpdate(Component component, InteropContextValue nextContext); +typedef void _HandleComponentWillUpdate(Component component, InteropContextValue nextContext); +// Ignore prevContext in componentDidUpdate, since it's not supported in React 16 typedef void _HandleComponentDidUpdate(Component component, ReactDartComponentInternal prevInternal); typedef void _HandleComponentWillUnmount(Component component); typedef dynamic _HandleRender(Component component); @@ -231,6 +261,7 @@ typedef dynamic _HandleRender(Component component); class ReactDartInteropStatics { external factory ReactDartInteropStatics({ _InitComponent initComponent, + _HandleGetChildContext handleGetChildContext, _HandleComponentWillMount handleComponentWillMount, _HandleComponentDidMount handleComponentDidMount, _HandleComponentWillReceiveProps handleComponentWillReceiveProps, @@ -253,3 +284,14 @@ class ComponentStatics { ComponentStatics(this.componentFactory); } + +/// Additional configuration passed to [createReactDartComponentClassConfig] +/// that needs to be directly accessible by that JS code. +@JS() +@anonymous +class JsComponentConfig { + external factory JsComponentConfig({ + Iterable childContextKeys, + Iterable contextKeys, + }); +} diff --git a/lib/react_prod.js b/lib/react_prod.js index 2ce98061..9f1a0952 100644 --- a/lib/react_prod.js +++ b/lib/react_prod.js @@ -16,10 +16,10 @@ function _getProperty(obj, key) { return obj[key]; } function _setProperty(obj, key, value) { return obj[key] = value; } -function _createReactDartComponentClassConfig(dartInteropStatics, componentStatics) { - return { +function _createReactDartComponentClassConfig(dartInteropStatics, componentStatics, jsConfig) { + var config = { getInitialState: function() { - this.dartComponent = dartInteropStatics.initComponent(this, this.props.internal, componentStatics); + this.dartComponent = dartInteropStatics.initComponent(this, this.props.internal, this.context, componentStatics); return {}; }, componentWillMount: function() { @@ -28,14 +28,14 @@ function _createReactDartComponentClassConfig(dartInteropStatics, componentStati componentDidMount: function() { dartInteropStatics.handleComponentDidMount(this.dartComponent); }, - componentWillReceiveProps: function(nextProps) { - dartInteropStatics.handleComponentWillReceiveProps(this.dartComponent, nextProps.internal); + componentWillReceiveProps: function(nextProps, nextContext) { + dartInteropStatics.handleComponentWillReceiveProps(this.dartComponent, nextProps.internal, nextContext); }, - shouldComponentUpdate: function(nextProps, nextState) { - return dartInteropStatics.handleShouldComponentUpdate(this.dartComponent); + shouldComponentUpdate: function(nextProps, nextState, nextContext) { + return dartInteropStatics.handleShouldComponentUpdate(this.dartComponent, nextContext); }, - componentWillUpdate: function(nextProps, nextState) { - dartInteropStatics.handleComponentWillUpdate(this.dartComponent); + componentWillUpdate: function(nextProps, nextState, nextContext) { + dartInteropStatics.handleComponentWillUpdate(this.dartComponent, nextContext); }, componentDidUpdate: function(prevProps, prevState) { dartInteropStatics.handleComponentDidUpdate(this.dartComponent, prevProps.internal); @@ -47,6 +47,34 @@ function _createReactDartComponentClassConfig(dartInteropStatics, componentStati return dartInteropStatics.handleRender(this.dartComponent); } }; + + // React limits the accessible context entries + // to the keys specified in childContextTypes/contextTypes. + + var childContextKeys = jsConfig && jsConfig.childContextKeys; + var contextKeys = jsConfig && jsConfig.contextKeys; + + if (childContextKeys && childContextKeys.length !== 0) { + config.childContextTypes = {}; + for (var i = 0; i < childContextKeys.length; i++) { + config.childContextTypes[childContextKeys[i]] = React.PropTypes.object; + } + + // Only declare this when `childContextKeys` is non-empty to avoid unnecessarily + // creating interop context objects for components that won't use it. + config.getChildContext = function() { + return dartInteropStatics.handleGetChildContext(this.dartComponent); + }; + } + + if (contextKeys && contextKeys.length !== 0) { + config.contextTypes = {}; + for (var i = 0; i < contextKeys.length; i++) { + config.contextTypes[contextKeys[i]] = React.PropTypes.object; + } + } + + return config; } function _markChildValidated(child) { diff --git a/lib/react_test.dart b/lib/react_test.dart index 86b21336..a2502b38 100644 --- a/lib/react_test.dart +++ b/lib/react_test.dart @@ -34,7 +34,7 @@ initializeComponent(Component component, [Map props = const {}, List children, r if (redraw == null) redraw = () {}; var extendedProps = new Map.from(component.getDefaultProps()) ..addAll(props); - component.initComponentInternal(extendedProps, redraw, ref); + component.initComponentInternal(extendedProps, redraw, ref, null, component.context); component.initStateInternal(); component.componentWillMount(); } diff --git a/lib/react_with_addons.js b/lib/react_with_addons.js index ef122fab..01337889 100644 --- a/lib/react_with_addons.js +++ b/lib/react_with_addons.js @@ -5965,10 +5965,10 @@ module.exports = ReactPropTypesSecret; function _getProperty(obj, key) { return obj[key]; } function _setProperty(obj, key, value) { return obj[key] = value; } -function _createReactDartComponentClassConfig(dartInteropStatics, componentStatics) { - return { +function _createReactDartComponentClassConfig(dartInteropStatics, componentStatics, jsConfig) { + var config = { getInitialState: function() { - this.dartComponent = dartInteropStatics.initComponent(this, this.props.internal, componentStatics); + this.dartComponent = dartInteropStatics.initComponent(this, this.props.internal, this.context, componentStatics); return {}; }, componentWillMount: function() { @@ -5977,14 +5977,14 @@ function _createReactDartComponentClassConfig(dartInteropStatics, componentStati componentDidMount: function() { dartInteropStatics.handleComponentDidMount(this.dartComponent); }, - componentWillReceiveProps: function(nextProps) { - dartInteropStatics.handleComponentWillReceiveProps(this.dartComponent, nextProps.internal); + componentWillReceiveProps: function(nextProps, nextContext) { + dartInteropStatics.handleComponentWillReceiveProps(this.dartComponent, nextProps.internal, nextContext); }, - shouldComponentUpdate: function(nextProps, nextState) { - return dartInteropStatics.handleShouldComponentUpdate(this.dartComponent); + shouldComponentUpdate: function(nextProps, nextState, nextContext) { + return dartInteropStatics.handleShouldComponentUpdate(this.dartComponent, nextContext); }, - componentWillUpdate: function(nextProps, nextState) { - dartInteropStatics.handleComponentWillUpdate(this.dartComponent); + componentWillUpdate: function(nextProps, nextState, nextContext) { + dartInteropStatics.handleComponentWillUpdate(this.dartComponent, nextContext); }, componentDidUpdate: function(prevProps, prevState) { dartInteropStatics.handleComponentDidUpdate(this.dartComponent, prevProps.internal); @@ -5996,6 +5996,34 @@ function _createReactDartComponentClassConfig(dartInteropStatics, componentStati return dartInteropStatics.handleRender(this.dartComponent); } }; + + // React limits the accessible context entries + // to the keys specified in childContextTypes/contextTypes. + + var childContextKeys = jsConfig && jsConfig.childContextKeys; + var contextKeys = jsConfig && jsConfig.contextKeys; + + if (childContextKeys && childContextKeys.length !== 0) { + config.childContextTypes = {}; + for (var i = 0; i < childContextKeys.length; i++) { + config.childContextTypes[childContextKeys[i]] = React.PropTypes.object; + } + + // Only declare this when `childContextKeys` is non-empty to avoid unnecessarily + // creating interop context objects for components that won't use it. + config.getChildContext = function() { + return dartInteropStatics.handleGetChildContext(this.dartComponent); + }; + } + + if (contextKeys && contextKeys.length !== 0) { + config.contextTypes = {}; + for (var i = 0; i < contextKeys.length; i++) { + config.contextTypes[contextKeys[i]] = React.PropTypes.object; + } + } + + return config; } function _markChildValidated(child) { diff --git a/lib/react_with_react_dom_prod.js b/lib/react_with_react_dom_prod.js index 82e9ddd3..adae4484 100644 --- a/lib/react_with_react_dom_prod.js +++ b/lib/react_with_react_dom_prod.js @@ -16,10 +16,10 @@ function _getProperty(obj, key) { return obj[key]; } function _setProperty(obj, key, value) { return obj[key] = value; } -function _createReactDartComponentClassConfig(dartInteropStatics, componentStatics) { - return { +function _createReactDartComponentClassConfig(dartInteropStatics, componentStatics, jsConfig) { + var config = { getInitialState: function() { - this.dartComponent = dartInteropStatics.initComponent(this, this.props.internal, componentStatics); + this.dartComponent = dartInteropStatics.initComponent(this, this.props.internal, this.context, componentStatics); return {}; }, componentWillMount: function() { @@ -28,14 +28,14 @@ function _createReactDartComponentClassConfig(dartInteropStatics, componentStati componentDidMount: function() { dartInteropStatics.handleComponentDidMount(this.dartComponent); }, - componentWillReceiveProps: function(nextProps) { - dartInteropStatics.handleComponentWillReceiveProps(this.dartComponent, nextProps.internal); + componentWillReceiveProps: function(nextProps, nextContext) { + dartInteropStatics.handleComponentWillReceiveProps(this.dartComponent, nextProps.internal, nextContext); }, - shouldComponentUpdate: function(nextProps, nextState) { - return dartInteropStatics.handleShouldComponentUpdate(this.dartComponent); + shouldComponentUpdate: function(nextProps, nextState, nextContext) { + return dartInteropStatics.handleShouldComponentUpdate(this.dartComponent, nextContext); }, - componentWillUpdate: function(nextProps, nextState) { - dartInteropStatics.handleComponentWillUpdate(this.dartComponent); + componentWillUpdate: function(nextProps, nextState, nextContext) { + dartInteropStatics.handleComponentWillUpdate(this.dartComponent, nextContext); }, componentDidUpdate: function(prevProps, prevState) { dartInteropStatics.handleComponentDidUpdate(this.dartComponent, prevProps.internal); @@ -47,6 +47,34 @@ function _createReactDartComponentClassConfig(dartInteropStatics, componentStati return dartInteropStatics.handleRender(this.dartComponent); } }; + + // React limits the accessible context entries + // to the keys specified in childContextTypes/contextTypes. + + var childContextKeys = jsConfig && jsConfig.childContextKeys; + var contextKeys = jsConfig && jsConfig.contextKeys; + + if (childContextKeys && childContextKeys.length !== 0) { + config.childContextTypes = {}; + for (var i = 0; i < childContextKeys.length; i++) { + config.childContextTypes[childContextKeys[i]] = React.PropTypes.object; + } + + // Only declare this when `childContextKeys` is non-empty to avoid unnecessarily + // creating interop context objects for components that won't use it. + config.getChildContext = function() { + return dartInteropStatics.handleGetChildContext(this.dartComponent); + }; + } + + if (contextKeys && contextKeys.length !== 0) { + config.contextTypes = {}; + for (var i = 0; i < contextKeys.length; i++) { + config.contextTypes[contextKeys[i]] = React.PropTypes.object; + } + } + + return config; } function _markChildValidated(child) { diff --git a/test/lifecycle_test.dart b/test/lifecycle_test.dart index 434cab09..c10c59b4 100644 --- a/test/lifecycle_test.dart +++ b/test/lifecycle_test.dart @@ -103,16 +103,17 @@ void main() { 'children': const [] }; - Map matchCall(String memberName, {args: anything, props: anything, state: anything}) { + Map matchCall(String memberName, {args: anything, props: anything, state: anything, context: anything}) { return { 'memberName': memberName, 'arguments': args, 'props': props, 'state': state, + 'context': context, }; } - test('recieves correct lifecycle calls on component mount', () { + test('receives correct lifecycle calls on component mount', () { _LifecycleTest component = getDartComponent( render(LifecycleTest({})) ); @@ -125,7 +126,7 @@ void main() { ])); }); - test('recieves correct lifecycle calls on component unmount order', () { + test('receives correct lifecycle calls on component unmount order', () { var mountNode = new DivElement(); var instance = react_dom.render(LifecycleTest({}), mountNode); _LifecycleTest component = getDartComponent(instance); @@ -139,7 +140,96 @@ void main() { ])); }); - test('recieves updated props with correct lifecycle calls and defaults properly merged in', () { + test('does not call getChildContext when childContextKeys is empty', () { + var mountNode = new DivElement(); + var instance = react_dom.render(ContextWrapperWithoutKeys({'foo': false}, LifecycleTestWithContext({})), mountNode); + _ContextWrapperWithoutKeys component = getDartComponent(instance); + + expect(component.lifecycleCalls, isEmpty); + }); + + test('calls getChildContext when childContextKeys exist', () { + var mountNode = new DivElement(); + var instance = react_dom.render(ContextWrapper({'foo': false}, LifecycleTestWithContext({})), mountNode); + _ContextWrapper component = getDartComponent(instance); + + expect(component.lifecycleCalls, equals([ + matchCall('getChildContext'), + ])); + }); + + test('receives updated context with correct lifecycle calls', () { + _LifecycleTestWithContext component; + + Map initialProps = { + 'foo': false, + 'initialProp': 'initial', + 'children': const [] + }; + Map newProps = { + 'children': const [], + 'foo': true, + 'newProp': 'new', + }; + + final Map initialPropsWithDefaults = unmodifiableMap({} + ..addAll(defaultProps) + ..addAll(initialProps) + ); + final Map newPropsWithDefaults = unmodifiableMap({} + ..addAll(defaultProps) + ..addAll(newProps) + ); + + const Map expectedState = const {}; + + const Map initialContext = const { + 'foo': false + }; + + const Map expectedContext = const { + 'foo': true + }; + + Map refMap = { + 'ref': ((ref) => component = ref), + }; + + // Add the 'ref' prop separately so it isn't an expected prop since React removes it internally + var initialPropsWithRef = new Map.from(initialProps)..addAll(refMap); + var newPropsWithRef = new Map.from(newPropsWithDefaults)..addAll(refMap); + + // Render the initial instance + var mountNode = new DivElement(); + react_dom.render(ContextWrapper({'foo': false}, LifecycleTestWithContext(initialPropsWithRef)), mountNode); + + // Verify initial context/setup + expect(component.lifecycleCalls, equals([ + matchCall('getInitialState', props: initialPropsWithDefaults, context: initialContext), + matchCall('componentWillMount', props: initialPropsWithDefaults, context: initialContext), + matchCall('render', props: initialPropsWithDefaults, context: initialContext), + matchCall('componentDidMount', props: initialPropsWithDefaults, context: initialContext), + ])); + + // Clear the lifecycle calls for to not duplicate the initial calls below + component.lifecycleCalls.clear(); + + // Trigger a re-render with new content + react_dom.render(ContextWrapper({'foo': true}, LifecycleTestWithContext(newPropsWithRef)), mountNode); + + // Verify updated context/setup + expect(component.lifecycleCalls, equals([ + matchCall('componentWillReceiveProps', args: [newPropsWithDefaults], props: initialPropsWithDefaults, context: initialContext), + matchCall('componentWillReceivePropsWithContext', args: [newPropsWithDefaults, expectedContext], props: initialPropsWithDefaults, context: initialContext), + matchCall('shouldComponentUpdateWithContext', args: [newPropsWithDefaults, expectedState, expectedContext], props: initialPropsWithDefaults, context: initialContext), + matchCall('componentWillUpdate', args: [newPropsWithDefaults, expectedState], props: initialPropsWithDefaults, context: initialContext), + matchCall('componentWillUpdateWithContext', args: [newPropsWithDefaults, expectedState, expectedContext], props: initialPropsWithDefaults, context: initialContext), + matchCall('render', props: newPropsWithDefaults, context: expectedContext), + matchCall('componentDidUpdate', args: [initialPropsWithDefaults, expectedState], props: newPropsWithDefaults, context: expectedContext), + ])); + }); + + test('receives updated props with correct lifecycle calls and defaults properly merged in', () { const Map initialProps = const { 'initialProp': 'initial', 'children': const [] @@ -159,6 +249,7 @@ void main() { ); const Map expectedState = const {}; + const Map expectedContext = const {}; var mountNode = new DivElement(); var instance = react_dom.render(LifecycleTest(initialProps), mountNode); @@ -169,11 +260,13 @@ void main() { react_dom.render(LifecycleTest(newProps), mountNode); expect(component.lifecycleCalls, equals([ - matchCall('componentWillReceiveProps', args: [newPropsWithDefaults], props: initialPropsWithDefaults), - matchCall('shouldComponentUpdate', args: [newPropsWithDefaults, expectedState], props: initialPropsWithDefaults), - matchCall('componentWillUpdate', args: [newPropsWithDefaults, expectedState], props: initialPropsWithDefaults), - matchCall('render', props: newPropsWithDefaults), - matchCall('componentDidUpdate', args: [initialPropsWithDefaults, expectedState], props: newPropsWithDefaults), + matchCall('componentWillReceiveProps', args: [newPropsWithDefaults], props: initialPropsWithDefaults), + matchCall('componentWillReceivePropsWithContext', args: [newPropsWithDefaults, expectedContext], props: initialPropsWithDefaults), + matchCall('shouldComponentUpdateWithContext', args: [newPropsWithDefaults, expectedState, expectedContext], props: initialPropsWithDefaults), + matchCall('componentWillUpdate', args: [newPropsWithDefaults, expectedState], props: initialPropsWithDefaults), + matchCall('componentWillUpdateWithContext', args: [newPropsWithDefaults, expectedState, expectedContext], props: initialPropsWithDefaults), + matchCall('render', props: newPropsWithDefaults), + matchCall('componentDidUpdate', args: [initialPropsWithDefaults, expectedState], props: newPropsWithDefaults), ])); }); @@ -193,6 +286,8 @@ void main() { 'getInitialState': (_) => initialState }); + final Map newContext = const {}; + final Map expectedProps = unmodifiableMap( defaultProps, initialProps, @@ -206,10 +301,11 @@ void main() { component.setState(stateDelta); expect(component.lifecycleCalls, equals([ - matchCall('shouldComponentUpdate', args: [expectedProps, newState], state: initialState), - matchCall('componentWillUpdate', args: [expectedProps, newState], state: initialState), - matchCall('render', state: newState), - matchCall('componentDidUpdate', args: [expectedProps, initialState], state: newState), + matchCall('shouldComponentUpdateWithContext', args: [expectedProps, newState, newContext], state: initialState), + matchCall('componentWillUpdate', args: [expectedProps, newState], state: initialState), + matchCall('componentWillUpdateWithContext', args: [expectedProps, newState, newContext], state: initialState), + matchCall('render', state: newState), + matchCall('componentDidUpdate', args: [expectedProps, initialState], state: newState), ])); }); @@ -224,6 +320,7 @@ void main() { const Map stateDelta = const { 'newState': 'new', }; + const Map expectedContext = const {}; final Map lifecycleTestProps = unmodifiableMap({ 'getInitialState': (_) => initialState, @@ -255,17 +352,21 @@ void main() { expect(component.lifecycleCalls, equals([ matchCall('componentWillReceiveProps', args: [newPropsWithDefaults], props: initialPropsWithDefaults, state: initialState), - matchCall('shouldComponentUpdate', args: [newPropsWithDefaults, newState], props: initialPropsWithDefaults, state: initialState), + matchCall('componentWillReceivePropsWithContext', args: [newPropsWithDefaults, expectedContext], props: initialPropsWithDefaults, state: initialState), + matchCall('shouldComponentUpdateWithContext', args: [newPropsWithDefaults, newState, expectedContext], props: initialPropsWithDefaults, state: initialState), matchCall('componentWillUpdate', args: [newPropsWithDefaults, newState], props: initialPropsWithDefaults, state: initialState), + matchCall('componentWillUpdateWithContext', args: [newPropsWithDefaults, newState, expectedContext], props: initialPropsWithDefaults, state: initialState), matchCall('render', props: newPropsWithDefaults, state: newState), matchCall('componentDidUpdate', args: [initialPropsWithDefaults, initialState], props: newPropsWithDefaults, state: newState), ])); }); - group('when shouldComponentUpdate returns false:', () { - test('recieves updated props with correct lifecycle calls and does not rerender', () { + void testShouldUpdates({bool shouldComponentUpdateWithContext, bool shouldComponentUpdate}) { + test('receives updated props with correct lifecycle calls and does not rerender', () { + final Map expectedContext = const {}; final Map initialProps = unmodifiableMap({ - 'shouldComponentUpdate': (_, __, ___) => false, + 'shouldComponentUpdate': (_, __, ___) => shouldComponentUpdate, + 'shouldComponentUpdateWithContext': (_, __, ___, ____) => shouldComponentUpdateWithContext, 'initialProp': 'initial', 'children': const [] }); @@ -291,14 +392,24 @@ void main() { react_dom.render(LifecycleTest(newProps), mountNode); - expect(component.lifecycleCalls, equals([ + List calls = [ matchCall('componentWillReceiveProps', args: [newPropsWithDefaults], props: initialPropsWithDefaults), - matchCall('shouldComponentUpdate', args: [newPropsWithDefaults, expectedState], props: initialPropsWithDefaults), - ])); + matchCall('componentWillReceivePropsWithContext', args: [newPropsWithDefaults, expectedContext], props: initialPropsWithDefaults), + matchCall('shouldComponentUpdateWithContext', args: [newPropsWithDefaults, expectedState, expectedContext], props: initialPropsWithDefaults), + ]; + + if (shouldComponentUpdateWithContext == null) { + calls.add( + matchCall('shouldComponentUpdate', args: [newPropsWithDefaults, expectedState], props: initialPropsWithDefaults), + ); + } + + expect(component.lifecycleCalls, equals(calls)); expect(component.props, equals(newPropsWithDefaults)); }); test('updates state with correct lifecycle calls and does not rerender', () { + const Map expectedContext = const {}; const Map initialState = const { 'initialState': 'initial', }; @@ -312,7 +423,8 @@ void main() { final Map initialProps = unmodifiableMap({ 'getInitialState': (_) => initialState, - 'shouldComponentUpdate': (_, __, ___) => false, + 'shouldComponentUpdate': (_, __, ___) => shouldComponentUpdate, + 'shouldComponentUpdateWithContext': (_, __, ___, ____) => shouldComponentUpdateWithContext, }); final Map expectedProps = unmodifiableMap( @@ -324,9 +436,17 @@ void main() { component.setState(stateDelta); - expect(component.lifecycleCalls, equals([ - matchCall('shouldComponentUpdate', args: [expectedProps, newState], state: initialState), - ])); + List calls = [ + matchCall('shouldComponentUpdateWithContext', args: [expectedProps, newState, expectedContext], state: initialState), + ]; + + if (shouldComponentUpdateWithContext == null) { + calls.add( + matchCall('shouldComponentUpdate', args: [expectedProps, newState], state: initialState), + ); + } + + expect(component.lifecycleCalls, equals(calls)); expect(component.state, equals(newState)); }); @@ -343,7 +463,8 @@ void main() { }; final Map lifecycleTestProps = unmodifiableMap({ - 'shouldComponentUpdate': (_, __, ___) => false, + 'shouldComponentUpdate': (_, __, ___) => shouldComponentUpdate, + 'shouldComponentUpdateWithContext': (_, __, ___, ____) => shouldComponentUpdateWithContext, 'getInitialState': (_) => initialState, 'componentWillReceiveProps': (_LifecycleTest component, Map props) { component.setState(stateDelta); @@ -363,6 +484,8 @@ void main() { defaultProps, newProps, emptyChildrenProps ); + final Map expectedContext = const {}; + var mountNode = new DivElement(); var instance = react_dom.render(LifecycleTest(initialProps), mountNode); _LifecycleTest component = getDartComponent(instance); @@ -371,11 +494,28 @@ void main() { react_dom.render(LifecycleTest(newProps), mountNode); - expect(component.lifecycleCalls, equals([ - matchCall('componentWillReceiveProps', args: [newPropsWithDefaults], props: initialPropsWithDefaults, state: initialState), - matchCall('shouldComponentUpdate', args: [newPropsWithDefaults, newState], props: initialPropsWithDefaults, state: initialState), - ])); + List calls = [ + matchCall('componentWillReceiveProps', args: [newPropsWithDefaults], props: initialPropsWithDefaults, state: initialState), + matchCall('componentWillReceivePropsWithContext', args: [newPropsWithDefaults, expectedContext], props: initialPropsWithDefaults, state: initialState), + matchCall('shouldComponentUpdateWithContext', args: [newPropsWithDefaults, newState, expectedContext], props: initialPropsWithDefaults, state: initialState), + ]; + + if (shouldComponentUpdateWithContext == null) { + calls.add( + matchCall('shouldComponentUpdate', args: [newPropsWithDefaults, newState], props: initialPropsWithDefaults, state: initialState), + ); + } + + expect(component.lifecycleCalls, equals(calls)); }); + } + + group('when shouldComponentUpdate returns false:', () { + testShouldUpdates(shouldComponentUpdateWithContext: null, shouldComponentUpdate: false); + }); + + group('when shouldComponentUpdateWithContext returns false:', () { + testShouldUpdates(shouldComponentUpdateWithContext: false, shouldComponentUpdate: false); }); group('calls the setState callback, and transactional setState callback in the correct order', () { @@ -427,6 +567,38 @@ external List getUpdatingSetStateLifeCycleCalls(); @JS() external List getNonUpdatingSetStateLifeCycleCalls(); +/// A test helper to record lifecycle calls +abstract class LifecycleTestHelper { + Map context; + Map props; + Map state; + + List lifecycleCalls = []; + + dynamic lifecycleCall(String memberName, {List arguments: const [], defaultReturnValue()}) { + lifecycleCalls.add({ + 'memberName': memberName, + 'arguments': arguments, + 'props': props == null ? null : new Map.from(props), + 'state': state == null ? null : new Map.from(state), + 'context': new Map.from(context ?? const {}), + }); + + var lifecycleCallback = props == null ? null : props[memberName]; + if (lifecycleCallback != null) { + return Function.apply(lifecycleCallback, [] + ..add(this) + ..addAll(arguments)); + } + + if (defaultReturnValue != null) { + return defaultReturnValue(); + } + + return null; + } +} + ReactDartComponentFactoryProxy SetStateTest = react.registerComponent(() => new _SetStateTest()); class _SetStateTest extends react.Component { @override @@ -440,6 +612,11 @@ class _SetStateTest extends react.Component { recordLifecyleCall('componentWillReceiveProps'); } + @override + componentWillReceivePropsWithContext(_, __) { + recordLifecyleCall('componentWillReceivePropsWithContext'); + } + @override componentWillUpdate(_, __) { recordLifecyleCall('componentWillUpdate'); @@ -513,32 +690,48 @@ class _DefaultPropsTest extends react.Component { render() => false; } -ReactDartComponentFactoryProxy LifecycleTest = react.registerComponent(() => new _LifecycleTest()); -class _LifecycleTest extends react.Component { - List lifecycleCalls = []; +ReactDartComponentFactoryProxy ContextWrapperWithoutKeys = react.registerComponent(() => new _ContextWrapperWithoutKeys()); +class _ContextWrapperWithoutKeys extends react.Component with LifecycleTestHelper { + @override + Iterable get childContextKeys => const []; - dynamic lifecycleCall(String memberName, {List arguments: const [], defaultReturnValue()}) { - lifecycleCalls.add({ - 'memberName': memberName, - 'arguments': arguments, - 'props': props == null ? null : new Map.from(props), - 'state': state == null ? null : new Map.from(state), - }); + @override + Map getChildContext() { + lifecycleCall('getChildContext'); + return { + 'foo': props['foo'], + 'extraContext': props['extraContext'], + }; + } - var lifecycleCallback = props == null ? null : props[memberName]; - if (lifecycleCallback != null) { - return Function.apply(lifecycleCallback, [] - ..add(this) - ..addAll(arguments)); - } + dynamic render() => react.div({}, props['children']); +} - if (defaultReturnValue != null) { - return defaultReturnValue(); - } +ReactDartComponentFactoryProxy ContextWrapper = react.registerComponent(() => new _ContextWrapper()); +class _ContextWrapper extends react.Component with LifecycleTestHelper { + @override + Iterable get childContextKeys => const ['foo', 'extraContext']; - return null; + @override + Map getChildContext() { + lifecycleCall('getChildContext'); + return { + 'foo': props['foo'], + 'extraContext': props['extraContext'], + }; } + dynamic render() => react.div({}, props['children']); +} + +ReactDartComponentFactoryProxy LifecycleTestWithContext = react.registerComponent(() => new _LifecycleTestWithContext()); +class _LifecycleTestWithContext extends _LifecycleTest { + @override + Iterable get contextKeys => const ['foo']; // only listening to one context key +} + +ReactDartComponentFactoryProxy LifecycleTest = react.registerComponent(() => new _LifecycleTest()); +class _LifecycleTest extends react.Component with LifecycleTestHelper { void componentWillMount() => lifecycleCall('componentWillMount'); void componentDidMount() => lifecycleCall('componentDidMount'); void componentWillUnmount() => lifecycleCall('componentWillUnmount'); @@ -546,9 +739,15 @@ class _LifecycleTest extends react.Component { void componentWillReceiveProps(newProps) => lifecycleCall('componentWillReceiveProps', arguments: [new Map.from(newProps)]); + void componentWillReceivePropsWithContext(newProps, newContext) => + lifecycleCall('componentWillReceivePropsWithContext', arguments: [new Map.from(newProps), new Map.from(newContext)]); + void componentWillUpdate(nextProps, nextState) => lifecycleCall('componentWillUpdate', arguments: [new Map.from(nextProps), new Map.from(nextState)]); + void componentWillUpdateWithContext(nextProps, nextState, nextContext) => + lifecycleCall('componentWillUpdateWithContext', arguments: [new Map.from(nextProps), new Map.from(nextState), new Map.from(nextContext)]); + void componentDidUpdate(prevProps, prevState) => lifecycleCall('componentDidUpdate', arguments: [new Map.from(prevProps), new Map.from(prevState)]); @@ -556,6 +755,10 @@ class _LifecycleTest extends react.Component { lifecycleCall('shouldComponentUpdate', arguments: [new Map.from(nextProps), new Map.from(nextState)], defaultReturnValue: () => true); + bool shouldComponentUpdateWithContext(nextProps, nextState, nextContext) => + lifecycleCall('shouldComponentUpdateWithContext', arguments: [new Map.from(nextProps), new Map.from(nextState), new Map.from(nextContext)], + defaultReturnValue: () => true); + dynamic render() => lifecycleCall('render', defaultReturnValue: () => react.div({})); diff --git a/tool/build_js.sh b/tool/build_js.sh index b40eb643..0395a0fe 100755 --- a/tool/build_js.sh +++ b/tool/build_js.sh @@ -12,12 +12,25 @@ DART_HELPERS_JS="js_src/dart_helpers.js" rm lib/*.js cat js_src/react.js $DART_HELPERS_JS > lib/react.js +echo 'Created lib/react.js' + cat js_src/react.min.js $DART_HELPERS_JS > lib/react_prod.js +echo 'Created lib/react_prod.js' + cat js_src/react-with-addons.js $DART_HELPERS_JS > lib/react_with_addons.js +echo 'Created lib/react_with_addons.js' cp js_src/react-dom.js lib/react_dom.js +echo 'Created lib/react_dom.js' + cp js_src/react-dom.min.js lib/react_dom_prod.js +echo 'Created lib/react_dom_prod.js' + cp js_src/react-dom-server.js lib/react_dom_server.js +echo 'Created lib/react_dom_server.js' + cp js_src/react-dom-server.min.js lib/react_dom_server_prod.js +echo 'Created lib/react_dom_server_prod.js' cat lib/react_prod.js lib/react_dom_prod.js > lib/react_with_react_dom_prod.js +echo 'Created lib/react_with_react_dom_prod.js'