Skip to content

Commit ae5a851

Browse files
Merge pull request #262 from cleandart/chainRefs
Add chainRef/chainRefList utilities
2 parents 8302980 + 899fa4d commit ae5a851

10 files changed

+334
-15
lines changed

.travis.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@ before_script:
1616
- pub run dependency_validator -i build_runner,build_test,build_web_compilers
1717

1818
script:
19-
- pub run build_runner test --release -- -p chrome
20-
- pub run build_runner test -- -p chrome
19+
- pub run build_runner test --release -- --preset dart2js
20+
- pub run build_runner test -- --preset dartdevc
2121
- dart ./tool/run_consumer_tests.dart --orgName Workiva --repoName over_react --testCmd "pub run dart_dev test -P dartdevc"

dart_test.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Specify chrome and VM as default platforms for running all tests,
2+
# then let the `@TestOn()` annotations determine which suites actually run
3+
platforms:
4+
- chrome
5+
- vm
6+
7+
presets:
8+
dart2js:
9+
exclude_tags: no-dart2js
10+
11+
dartdevc:
12+
exclude_tags: no-dartdevc

lib/react_client.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import 'package:react/src/ddc_emulated_function_name_bug.dart' as ddc_emulated_f
3030

3131
export 'package:react/react_client/react_interop.dart' show ReactElement, ReactJsComponentFactory, inReactDevMode, Ref;
3232
export 'package:react/react.dart' show ReactComponentFactoryProxy, ComponentFactory;
33+
export 'package:react/src/react_client/chain_refs.dart' show chainRefs, chainRefList;
3334

3435
/// The type of `Component.ref` specified as a callback.
3536
///

lib/react_client/react_interop.dart

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,15 +82,27 @@ class Ref<T> {
8282
T get current {
8383
final jsCurrent = jsRef.current;
8484

85-
if (jsCurrent is! Element) {
86-
final dartCurrent = (jsCurrent as ReactComponent)?.dartComponent;
85+
// Note: this ReactComponent check will pass for many types of JS objects,
86+
// so don't assume for sure that it's a ReactComponent
87+
if (jsCurrent is! Element && jsCurrent is ReactComponent) {
88+
final dartCurrent = jsCurrent.dartComponent;
8789

8890
if (dartCurrent != null) {
8991
return dartCurrent as T;
9092
}
9193
}
9294
return jsCurrent;
9395
}
96+
97+
/// Used internally to combine refs https://github.com/facebook/react/issues/13029
98+
@protected
99+
set current(T value) {
100+
if (value is Component) {
101+
jsRef.current = value.jsThis;
102+
} else {
103+
jsRef.current = value;
104+
}
105+
}
94106
}
95107

96108
/// A JS ref object returned by [React.createRef].
@@ -101,6 +113,10 @@ class Ref<T> {
101113
@anonymous
102114
class JsRef {
103115
external dynamic get current;
116+
117+
/// Used internally to combine refs https://github.com/facebook/react/issues/13029
118+
@protected
119+
external set current(dynamic value);
104120
}
105121

106122
/// Automatically passes a [Ref] through a component to one of its children.

lib/src/react_client/chain_refs.dart

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import 'package:js/js_util.dart';
2+
import 'package:react/react_client/react_interop.dart';
3+
4+
/// Returns a ref that updates both [ref1] and [ref2], effectively
5+
/// allowing you to set multiple refs.
6+
///
7+
/// Useful when you're using ref forwarding and want to also set your own ref.
8+
///
9+
/// For more information on this problem, see https://github.com/facebook/react/issues/13029.
10+
///
11+
/// Inputs can be callback refs, [createRef]-based refs ([Ref]),
12+
/// or raw JS `createRef` refs ([JsRef]).
13+
///
14+
/// If either [ref1] or [ref2] is null, the other ref will be passed through.
15+
///
16+
/// If both are null, null is returned.
17+
///
18+
/// ```dart
19+
/// final Example = forwardRef((props, ref) {
20+
/// final localRef = useRef<FooComponent>();
21+
/// return Foo({
22+
/// ...props,
23+
/// 'ref': chainRefs(localRef, ref),
24+
/// });
25+
/// });
26+
/// ```
27+
dynamic chainRefs(dynamic ref1, dynamic ref2) {
28+
return chainRefList([ref1, ref2]);
29+
}
30+
31+
/// Like [chainRefs], but takes in a list of [refs].
32+
dynamic chainRefList(List<dynamic> refs) {
33+
final nonNullRefs = refs.where((ref) => ref != null).toList(growable: false);
34+
35+
// Wrap in an assert so iteration doesn't take place unnecessarily
36+
assert(() {
37+
nonNullRefs.forEach(_validateChainRefsArg);
38+
return true;
39+
}());
40+
41+
// Return null if there are no refs to chain
42+
if (nonNullRefs.isEmpty) return null;
43+
44+
// Pass through the ref if there's nothing to chain it with
45+
if (nonNullRefs.length == 1) return nonNullRefs[0];
46+
47+
// Adapted from https://github.com/smooth-code/react-merge-refs/tree/v1.0.0
48+
//
49+
// Copyright 2019 Smooth Code
50+
//
51+
// Permission is hereby granted, free of charge, to any person obtaining a
52+
// copy of this software and associated documentation files (the 'Software'),
53+
// to deal in the Software without restriction, including without limitation
54+
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
55+
// and/or sell copies of the Software, and to permit persons to whom the
56+
// Software is furnished to do so, subject to the following conditions:
57+
//
58+
// The above copyright notice and this permission notice shall be included in
59+
// all copies or substantial portions of the Software.
60+
//
61+
// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
62+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
63+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
64+
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
65+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
66+
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
67+
// DEALINGS IN THE SOFTWARE.
68+
void _chainedRef(value) {
69+
for (final ref in nonNullRefs) {
70+
if (ref is Function) {
71+
ref(value);
72+
} else if (ref is Ref) {
73+
// ignore: invalid_use_of_protected_member
74+
ref.current = value;
75+
} else {
76+
// ignore: invalid_use_of_protected_member
77+
(ref as JsRef).current = value;
78+
}
79+
}
80+
}
81+
82+
return _chainedRef;
83+
}
84+
85+
void _validateChainRefsArg(dynamic ref) {
86+
if (ref is Function(Null) ||
87+
ref is Ref ||
88+
// Need to duck-type since `is JsRef` will return true for most JS objects.
89+
(ref is JsRef && hasProperty(ref, 'current'))) {
90+
return;
91+
}
92+
93+
if (ref is String) throw AssertionError('String refs cannot be chained');
94+
if (ref is Function) throw AssertionError('callback refs must take a single argument');
95+
96+
throw AssertionError('Invalid ref type: $ref');
97+
}

test/factory/common_factory_tests.dart

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'dart:js_util';
66

77
import 'package:meta/meta.dart';
88
import 'package:react/react_client/js_backed_map.dart';
9+
import 'package:react/src/react_client/chain_refs.dart';
910
import 'package:test/test.dart';
1011

1112
import 'package:react/react_client.dart';
@@ -303,15 +304,11 @@ void refTests<T>(ReactComponentFactoryProxy factory, {void verifyRefValue(dynami
303304
verifyRefValue(refObject.current);
304305
});
305306

306-
_typedCallbackRefTests<T>(factory);
307-
}
308-
309-
void _typedCallbackRefTests<T>(react.ReactComponentFactoryProxy factory) {
310-
if (T == dynamic) {
311-
throw ArgumentError('Generic parameter T must be specified');
312-
}
313-
314307
group('has functional callback refs when they are typed as', () {
308+
if (T == dynamic) {
309+
throw ArgumentError('Generic parameter T must be specified');
310+
}
311+
315312
test('`dynamic Function(dynamic)`', () {
316313
T fooRef;
317314
callbackRef(dynamic ref) {
@@ -334,6 +331,31 @@ void _typedCallbackRefTests<T>(react.ReactComponentFactoryProxy factory) {
334331
expect(fooRef, isA<T>(), reason: 'should be the correct type, not be a NativeJavaScriptObject/etc.');
335332
});
336333
});
334+
335+
group('chainRefList works', () {
336+
test('with all different types of values, ignoring null', () {
337+
final testCases = RefTestCase.allChainable<T>();
338+
339+
T refValue;
340+
rtu.renderIntoDocument(factory({
341+
'ref': chainRefList([
342+
(ref) => refValue = ref,
343+
null,
344+
null,
345+
...testCases.map((t) => t.ref),
346+
]),
347+
}));
348+
// Test setup check: verify refValue is correct,
349+
// which we'll use below to verify refs were updated.
350+
verifyRefValue(refValue);
351+
352+
for (final testCase in testCases) {
353+
testCase.verifyRefWasUpdated(refValue);
354+
}
355+
});
356+
357+
// Other cases tested in chainRefList's own tests
358+
});
337359
}
338360

339361
void _childKeyWarningTests(Function factory, {Function(ReactElement Function()) renderWithUniqueOwnerName}) {
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
@TestOn('browser')
2+
@JS()
3+
library react.chain_refs_test;
4+
5+
import 'package:js/js.dart';
6+
import 'package:react/react_client.dart';
7+
import 'package:react/src/react_client/chain_refs.dart';
8+
import 'package:test/test.dart';
9+
10+
import '../util.dart';
11+
12+
main() {
13+
group('Ref chaining utils:', () {
14+
testRef(_) {}
15+
16+
group('chainRefs', () {
17+
// Chaining and arg validation is tested via chainRefList.
18+
19+
group('skips chaining and passes through arguments when', () {
20+
test('both arguments are null', () {
21+
expect(chainRefs(null, null), isNull);
22+
});
23+
24+
test('the first argument is null', () {
25+
expect(chainRefs(null, testRef), same(testRef));
26+
});
27+
28+
test('the second argument is null', () {
29+
expect(chainRefs(testRef, null), same(testRef));
30+
});
31+
});
32+
});
33+
34+
group('chainRefList', () {
35+
// Chaining is tested functionally in refTests with each component type.
36+
37+
group('skips chaining and passes through arguments when', () {
38+
test('the list is empty', () {
39+
expect(chainRefList([]), isNull);
40+
});
41+
42+
test('there is a single element in the list', () {
43+
expect(chainRefList([testRef]), same(testRef));
44+
});
45+
46+
test('the list has only null values', () {
47+
expect(chainRefList([null, null]), isNull);
48+
});
49+
50+
test('there is a single non-null element in the list', () {
51+
expect(chainRefList([null, testRef]), same(testRef));
52+
});
53+
});
54+
55+
group('raises an assertion when inputs are invalid:', () {
56+
test('strings', () {
57+
expect(() => chainRefList([testRef, 'bad ref']), throwsA(isA<AssertionError>()));
58+
});
59+
60+
test('unsupported function types', () {
61+
expect(() => chainRefList([testRef, () {}]), throwsA(isA<AssertionError>()));
62+
});
63+
64+
test('other objects', () {
65+
expect(() => chainRefList([testRef, Object()]), throwsA(isA<AssertionError>()));
66+
});
67+
68+
// test JS interop objects since type-checking anonymous interop objects
69+
test('non-createRef anonymous JS interop objects', () {
70+
expect(() => chainRefList([testRef, JsTypeAnonymous()]), throwsA(isA<AssertionError>()));
71+
});
72+
73+
// test JS interop objects since type-checking anonymous interop objects
74+
test('non-createRef JS interop objects', () {
75+
expect(() => chainRefList([testRef, JsType()]), throwsA(isA<AssertionError>()));
76+
});
77+
}, tags: 'no-dart2js');
78+
79+
test('does not raise an assertion for valid input refs', () {
80+
expect(() => chainRefList(RefTestCase.allChainable().map((r) => r.ref).toList()), returnsNormally);
81+
});
82+
});
83+
});
84+
}
85+
86+
@JS()
87+
@anonymous
88+
class JsTypeAnonymous {
89+
external factory JsTypeAnonymous();
90+
}
91+
92+
@JS()
93+
class JsType {
94+
external JsType();
95+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head lang="en">
4+
<meta charset="UTF-8">
5+
<title></title>
6+
<script src="packages/react/react_with_addons.js"></script>
7+
<script src="packages/react/react_dom.js"></script>
8+
9+
<script>
10+
function JsType() {}
11+
</script>
12+
13+
<link rel="x-dart-test" href="chain_refs_test.dart">
14+
<script src="packages/test/dart.js"></script>
15+
</head>
16+
<body>
17+
</body>
18+
</html>

test/react_client/react_interop_test.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
@TestOn('browser')
2+
library react.react_interop_test;
3+
14
import 'dart:html';
25

36
import 'package:react/react_client.dart';

0 commit comments

Comments
 (0)