Skip to content

CPLAT-3968 CPLAT-3969 Add unconvertJsProps() util to react_client.dart #153

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Feb 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jobs:
dart: stable
script:
- dartanalyzer .
- dartfmt -n --set-exit-if-changed .
- pub run dependency_validator -i build_runner,build_test,build_web_compilers
- pub run test -p chrome
- pub run build_runner test -- -p chrome
Expand Down
103 changes: 35 additions & 68 deletions lib/react_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import "package:react/react_client/js_interop_helpers.dart";
import 'package:react/react_client/react_interop.dart';
import "package:react/react_dom.dart";
import "package:react/react_dom_server.dart";
import "package:react/src/react_client/event_prop_key_to_event_factory.dart";
import "package:react/src/react_client/synthetic_event_wrappers.dart" as events;
import 'package:react/src/typedefs.dart';
import 'package:react/src/ddc_emulated_function_name_bug.dart'
Expand Down Expand Up @@ -510,6 +511,32 @@ _convertBoundValues(Map args) {
/// to the original Dart functions (the input of [_convertEventHandlers]).
final Expando<Function> _originalEventHandlers = new Expando();

/// Returns the props for a [ReactElement] or composite [ReactComponent] [instance],
/// shallow-converted to a Dart Map for convenience.
///
/// If `style` is specified in props, then it too is shallow-converted and included
/// in the returned Map.
///
/// Any JS event handlers included in the props for the given [instance] will be
/// unconverted such that the original JS handlers are returned instead of their
/// Dart synthetic counterparts.
Map unconvertJsProps(/* ReactElement|ReactComponent */ instance) {
var props = _dartifyJsMap(instance.props);
eventPropKeyToEventFactory.keys.forEach((key) {
if (props.containsKey(key)) {
props[key] = unconvertJsEventHandler(props[key]) ?? props[key];
}
});

// Convert the nested style map so it can be read by Dart code.
var style = props['style'];
if (style != null) {
props['style'] = _dartifyJsMap(style);
}

return props;
}

/// Returns the original Dart handler function that, within [_convertEventHandlers],
/// was converted/wrapped into the function [jsConvertedEventHandler] to be passed to the JS.
///
Expand All @@ -529,7 +556,7 @@ Function unconvertJsEventHandler(Function jsConvertedEventHandler) {
_convertEventHandlers(Map args) {
var zone = Zone.current;
args.forEach((propKey, value) {
var eventFactory = _eventPropKeyToEventFactory[propKey];
var eventFactory = eventPropKeyToEventFactory[propKey];
if (eventFactory != null && value != null) {
// Apply allowInterop here so that the function we store in [_originalEventHandlers]
// is the same one we'll retrieve from the JS props.
Expand All @@ -544,72 +571,11 @@ _convertEventHandlers(Map args) {
});
}

/// A mapping from event prop keys to their respective event factories.
///
/// Used in [_convertEventHandlers] for efficient event handler conversion.
final Map<String, Function> _eventPropKeyToEventFactory = (() {
var eventPropKeyToEventFactory = <String, Function>{
// SyntheticClipboardEvent
'onCopy': syntheticClipboardEventFactory,
'onCut': syntheticClipboardEventFactory,
'onPaste': syntheticClipboardEventFactory,

// SyntheticKeyboardEvent
'onKeyDown': syntheticKeyboardEventFactory,
'onKeyPress': syntheticKeyboardEventFactory,
'onKeyUp': syntheticKeyboardEventFactory,

// SyntheticFocusEvent
'onFocus': syntheticFocusEventFactory,
'onBlur': syntheticFocusEventFactory,

// SyntheticFormEvent
'onChange': syntheticFormEventFactory,
'onInput': syntheticFormEventFactory,
'onSubmit': syntheticFormEventFactory,
'onReset': syntheticFormEventFactory,

// SyntheticMouseEvent
'onClick': syntheticMouseEventFactory,
'onContextMenu': syntheticMouseEventFactory,
'onDoubleClick': syntheticMouseEventFactory,
'onDrag': syntheticMouseEventFactory,
'onDragEnd': syntheticMouseEventFactory,
'onDragEnter': syntheticMouseEventFactory,
'onDragExit': syntheticMouseEventFactory,
'onDragLeave': syntheticMouseEventFactory,
'onDragOver': syntheticMouseEventFactory,
'onDragStart': syntheticMouseEventFactory,
'onDrop': syntheticMouseEventFactory,
'onMouseDown': syntheticMouseEventFactory,
'onMouseEnter': syntheticMouseEventFactory,
'onMouseLeave': syntheticMouseEventFactory,
'onMouseMove': syntheticMouseEventFactory,
'onMouseOut': syntheticMouseEventFactory,
'onMouseOver': syntheticMouseEventFactory,
'onMouseUp': syntheticMouseEventFactory,

// SyntheticTouchEvent
'onTouchCancel': syntheticTouchEventFactory,
'onTouchEnd': syntheticTouchEventFactory,
'onTouchMove': syntheticTouchEventFactory,
'onTouchStart': syntheticTouchEventFactory,

// SyntheticUIEvent
'onScroll': syntheticUIEventFactory,

// SyntheticWheelEvent
'onWheel': syntheticWheelEventFactory,
};

// Add support for capturing variants; e.g., onClick/onClickCapture
for (var key in eventPropKeyToEventFactory.keys.toList()) {
eventPropKeyToEventFactory[key + 'Capture'] =
eventPropKeyToEventFactory[key];
}

return eventPropKeyToEventFactory;
})();
/// Returns a Dart Map copy of the JS property key-value pairs in [jsMap].
Map _dartifyJsMap(jsMap) {
return new Map.fromIterable(_objectKeys(jsMap),
value: (key) => getProperty(jsMap, key));
}

/// Wrapper for [SyntheticEvent].
SyntheticEvent syntheticEventFactory(events.SyntheticEvent e) {
Expand Down Expand Up @@ -712,7 +678,8 @@ SyntheticFormEvent syntheticFormEventFactory(events.SyntheticFormEvent e) {
}

/// Wrapper for [SyntheticDataTransfer].
SyntheticDataTransfer syntheticDataTransferFactory(events.SyntheticDataTransfer dt) {
SyntheticDataTransfer syntheticDataTransferFactory(
events.SyntheticDataTransfer dt) {
if (dt == null) return null;
List<File> files = [];
if (dt.files != null) {
Expand Down
68 changes: 68 additions & 0 deletions lib/src/react_client/event_prop_key_to_event_factory.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import 'package:react/react_client.dart';

/// A mapping from event prop keys to their respective event factories.
///
/// Used in [_convertEventHandlers] for efficient event handler conversion.
final Map<String, Function> eventPropKeyToEventFactory = (() {
var _eventPropKeyToEventFactory = <String, Function>{
// SyntheticClipboardEvent
'onCopy': syntheticClipboardEventFactory,
'onCut': syntheticClipboardEventFactory,
'onPaste': syntheticClipboardEventFactory,

// SyntheticKeyboardEvent
'onKeyDown': syntheticKeyboardEventFactory,
'onKeyPress': syntheticKeyboardEventFactory,
'onKeyUp': syntheticKeyboardEventFactory,

// SyntheticFocusEvent
'onFocus': syntheticFocusEventFactory,
'onBlur': syntheticFocusEventFactory,

// SyntheticFormEvent
'onChange': syntheticFormEventFactory,
'onInput': syntheticFormEventFactory,
'onSubmit': syntheticFormEventFactory,
'onReset': syntheticFormEventFactory,

// SyntheticMouseEvent
'onClick': syntheticMouseEventFactory,
'onContextMenu': syntheticMouseEventFactory,
'onDoubleClick': syntheticMouseEventFactory,
'onDrag': syntheticMouseEventFactory,
'onDragEnd': syntheticMouseEventFactory,
'onDragEnter': syntheticMouseEventFactory,
'onDragExit': syntheticMouseEventFactory,
'onDragLeave': syntheticMouseEventFactory,
'onDragOver': syntheticMouseEventFactory,
'onDragStart': syntheticMouseEventFactory,
'onDrop': syntheticMouseEventFactory,
'onMouseDown': syntheticMouseEventFactory,
'onMouseEnter': syntheticMouseEventFactory,
'onMouseLeave': syntheticMouseEventFactory,
'onMouseMove': syntheticMouseEventFactory,
'onMouseOut': syntheticMouseEventFactory,
'onMouseOver': syntheticMouseEventFactory,
'onMouseUp': syntheticMouseEventFactory,

// SyntheticTouchEvent
'onTouchCancel': syntheticTouchEventFactory,
'onTouchEnd': syntheticTouchEventFactory,
'onTouchMove': syntheticTouchEventFactory,
'onTouchStart': syntheticTouchEventFactory,

// SyntheticUIEvent
'onScroll': syntheticUIEventFactory,

// SyntheticWheelEvent
'onWheel': syntheticWheelEventFactory,
};

// Add support for capturing variants; e.g., onClick/onClickCapture
for (var key in _eventPropKeyToEventFactory.keys.toList()) {
_eventPropKeyToEventFactory[key + 'Capture'] =
_eventPropKeyToEventFactory[key];
}

return _eventPropKeyToEventFactory;
})();
156 changes: 156 additions & 0 deletions test/react_client_test.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,154 @@
@TestOn('browser')
library react_test_utils_test;

import 'dart:html' show DivElement;

import 'package:js/js.dart';
import 'package:test/test.dart';

import 'package:react/react.dart' as react show div;
import 'package:react/react_client.dart';
import 'package:react/react_client/react_interop.dart'
show React, ReactClassConfig, ReactComponent;
import 'package:react/react_client/js_interop_helpers.dart';
import 'package:react/react_dom.dart' as react_dom;
import 'package:react/src/react_client/event_prop_key_to_event_factory.dart';

main() {
setClientConfiguration();

group('unconvertJsProps', () {
const List testChildren = const ['child1', 'child2'];
const Map<String, dynamic> testStyle = const {'background': 'white'};

test('returns props for a composite JS component ReactElement', () {
ReactElement instance = testJsComponentFactory({
'jsProp': 'js',
'style': testStyle,
}, testChildren);

expect(
unconvertJsProps(instance),
equals({
'jsProp': 'js',
'style': testStyle,
'children': testChildren,
}),
);
});

test('returns props for a composite JS ReactComponent', () {
var mountNode = new DivElement();
ReactComponent renderedInstance = react_dom.render(
testJsComponentFactory({
'jsProp': 'js',
'style': testStyle,
}, testChildren),
mountNode);

expect(
unconvertJsProps(renderedInstance),
equals({
'jsProp': 'js',
'style': testStyle,
'children': testChildren,
}),
);
});

test(
'returns props for a composite JS ReactComponent, even when the props change',
() {
var mountNode = new DivElement();
ReactComponent renderedInstance = react_dom.render(
testJsComponentFactory({
'jsProp': 'js',
'style': testStyle,
}, testChildren),
mountNode);

expect(
unconvertJsProps(renderedInstance),
equals({
'jsProp': 'js',
'style': testStyle,
'children': testChildren,
}),
);

renderedInstance = react_dom.render(
testJsComponentFactory({
'jsProp': 'other js',
'style': testStyle,
}, testChildren),
mountNode);

expect(
unconvertJsProps(renderedInstance),
equals({
'jsProp': 'other js',
'style': testStyle,
'children': testChildren,
}));
});

test('returns props for a DOM component ReactElement', () {
ReactElement instance = react.div({
'domProp': 'dom',
'style': testStyle,
}, testChildren);

expect(
unconvertJsProps(instance),
equals({
'domProp': 'dom',
'style': testStyle,
'children': testChildren,
}));
});

group('should unconvert JS event handlers', () {
Function createHandler(eventKey) => (_) {
print(eventKey);
};
Map<String, Function> originalHandlers;
Map props;

setUp(() {
originalHandlers = {};
props = {};

for (final key in eventPropKeyToEventFactory.keys) {
props[key] = originalHandlers[key] = createHandler(key);
}
});

test('for a DOM element', () {
var component = react.div(props);
var jsProps = unconvertJsProps(component);
for (final key in eventPropKeyToEventFactory.keys) {
expect(jsProps[key], isNotNull,
reason: 'JS event handler prop should not be null');
expect(jsProps[key], same(originalHandlers[key]),
reason: 'JS event handler prop was not unconverted');
}
});

test(
', except for a JS composite component (handlers should already be unconverted)',
() {
var component = testJsComponentFactory(props);
var jsProps = unconvertJsProps(component);
for (final key in eventPropKeyToEventFactory.keys) {
expect(jsProps[key], isNotNull,
reason: 'JS event handler prop should not be null');
expect(jsProps[key], same(allowInterop(originalHandlers[key])),
reason: 'JS event handler prop was unexpectedly modified');
}
});
});
});

group('unconvertJsEventHandler', () {
test('returns null when the input is null', () {
var result;
Expand All @@ -17,3 +160,16 @@ main() {
});
});
}

/// A factory for a JS composite component, for use in testing.
final Function testJsComponentFactory = (() {
var componentClass = React.createClass(new ReactClassConfig(
displayName: 'testJsComponent',
render: allowInterop(() => react.div({}, 'test js component'))));

var reactFactory = React.createFactory(componentClass);

return ([props = const {}, children]) {
return reactFactory(jsify(props), listifyChildren(children));
};
})();
Loading