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'