Skip to content
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

FED-7 Move over uiJsComponent + tests #743

Merged
merged 31 commits into from
May 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e3eae44
Create files for prop_conversion
sydneesampson-wk May 7, 2022
100b701
Create tests for prop conversion
sydneesampson-wk May 7, 2022
c1d2790
Create js_component
sydneesampson-wk May 7, 2022
40abc63
Create tests for js_component
sydneesampson-wk May 7, 2022
386c85f
Create weak_map (dependency for prop_conversion)
sydneesampson-wk May 7, 2022
d48eb84
Update react testing library
sydneesampson-wk May 9, 2022
ea7bd03
Import weak map
sydneesampson-wk May 9, 2022
9e1e66d
Update imports for js component tests
sydneesampson-wk May 9, 2022
0d110d8
Update imports for prop conv tests, update button
sydneesampson-wk May 9, 2022
7adabe0
Update imports
sydneesampson-wk May 9, 2022
82b9ba0
Add ref test cases
sydneesampson-wk May 9, 2022
1ba0cb3
Update import
sydneesampson-wk May 9, 2022
25089ee
Merge branch 'master' into FED-7-utils-tests
sydneesampson-wk May 9, 2022
e49d4e5
Add button dart to test folder
sydneesampson-wk May 9, 2022
679f489
Update prop conv tests
sydneesampson-wk May 9, 2022
87ccf42
Import button
sydneesampson-wk May 9, 2022
2c94f9b
Update buttons with domProps
sydneesampson-wk May 10, 2022
22cad0b
Export js component, prop conversion
sydneesampson-wk May 10, 2022
f51c233
Update exports
sydneesampson-wk May 10, 2022
e5c5314
Remove button.dart
sydneesampson-wk May 10, 2022
615c47a
Build
sydneesampson-wk May 10, 2022
b984fc2
Add new tests to over_react_util_test
sydneesampson-wk May 10, 2022
1ac4050
Add TestJsComponent to util_test html
sydneesampson-wk May 10, 2022
2b4c0ac
Update tests with _TestJsComponent
sydneesampson-wk May 10, 2022
3071efe
Build
sydneesampson-wk May 10, 2022
c853985
Add missing script tag to test
sydneesampson-wk May 10, 2022
55a1873
Format
brianphillips-wk May 10, 2022
4329621
Fix WeakMap dart2js errors
greglittlefield-wf May 11, 2022
72fa33b
Update test/over_react/util/prop_conversion_test.dart
sydneesampson-wk May 11, 2022
be92ba8
Update comment
brianphillips-wk May 11, 2022
d326cec
Fix typed props map tests
brianphillips-wk May 11, 2022
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
16 changes: 16 additions & 0 deletions lib/js_component.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright 2022 Workiva Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

export 'src/util/js_component.dart';
export 'src/util/prop_conversion.dart';
48 changes: 48 additions & 0 deletions lib/src/util/js_component.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import 'package:over_react/over_react.dart';
import 'package:react/react_client/component_factory.dart';

/// Creates a Dart component factory that wraps a ReactJS [factoryProxy].
brianphillips-wk marked this conversation as resolved.
Show resolved Hide resolved
///
/// More in-depth documentation for wrapping JS components is coming soon.
///
/// Example:
/// ```dart
/// UiFactory<ButtonProps> Button = uiJsComponent(
/// ReactJsComponentFactoryProxy(MaterialUI.Button),
/// _$ButtonConfig, // ignore: undefined_identifier
/// );
///
/// @Props(keyNamespace: '')
/// mixin ButtonProps on UiProps {}
/// ```
UiFactory<TProps> uiJsComponent<TProps extends UiProps>(
sydneesampson-wk marked this conversation as resolved.
Show resolved Hide resolved
ReactJsComponentFactoryProxy factoryProxy,
dynamic _config,
) {
ArgumentError.checkNotNull(_config, '_config');

if (_config is! UiFactoryConfig<TProps>) {
throw ArgumentError(
'_config should be a UiFactory<TProps>. Make sure you are '
r'using either the generated factory config (i.e. _$FooConfig) or manually '
'declaring your config correctly.');
}

// ignore: invalid_use_of_protected_member
final propsFactory = (_config as UiFactoryConfig<TProps>).propsFactory;
ArgumentError.checkNotNull(_config, '_config');

TProps _uiFactory([Map backingMap]) {
TProps builder;
if (backingMap == null) {
builder = propsFactory.jsMap(JsBackedMap());
} else if (backingMap is JsBackedMap) {
builder = propsFactory.jsMap(backingMap);
} else {
builder = propsFactory.map(backingMap);
}
return builder..componentFactory = factoryProxy;
}

return _uiFactory;
}
127 changes: 127 additions & 0 deletions lib/src/util/prop_conversion.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import 'package:js/js.dart';
import 'package:js/js_util.dart';
import 'package:meta/meta.dart';
import 'package:over_react/over_react.dart' show Ref;
import 'package:over_react/src/util/weak_map.dart';
import 'package:react/react_client/js_backed_map.dart';
import 'package:react/react_client/component_factory.dart';
import 'package:react/react_client/react_interop.dart' show JsRef;

// Export JsMap since props that utilize jsifyMapProp/unjsifyMapProp
// via custom getters/setters will need JsMap to avoid implicit cast errors.
export 'package:react/react_client/js_backed_map.dart' show JsMap;

/// Returns a JS-deep-converted version of [value] for storage in the props map, or null if [value] is null.
///
/// For use in JS component prop setters where the component expects a JS object, but typing the getter/setter
/// as a Dart Map is more convenient to the consumer reading/writing the props.
///
/// Should be used alongside [unjsifyMapProp].
///
/// For more information on why this conversion is done in the getter/setter,
/// see documentation around Dart wrapper component prop conversion issues.
/// FIXME CPLAT-15060 add reference/link to documentation
JsMap jsifyMapProp(Map value) {
if (value == null) return null;
// Use generateJsProps so that, in addition to deeply converting props, we also properly convert the `ref` prop.
// Dart props get deep converted (just like they would when invoking the ReactJsComponentFactoryProxy anyways),
// but that's a tradeoff we're willing to make, given that there's no perfect solution,
// and shouldn't happen often since we shouldn't be passing Dart values to JS components.
// As a workaround, consumers can wrap any Dart values in an opaque Dart object (similar to DartValueWrapper).
return generateJsProps(value);
}

/// Returns [value] converted to a Dart map and [Map.cast]ed to the provided generics, or null if [value] is null.
///
/// For use in JS component prop getters where the component expects a JS object, but typing the getter/setter
/// as a Dart Map is more convenient to the consumer reading/writing the props.
///
/// Should be used alongside [jsifyMapProp].
///
/// For more information on why this conversion is done in the getter/setter,
/// see documentation around Dart wrapper component prop conversion issues.
/// FIXME CPLAT-15060 add reference/link to documentation
Map<K, V> unjsifyMapProp<K, V>(JsMap value) {
if (value == null) return null;
// Don't deep unconvert so that JS values don't get converted incorrectly to Maps. See jsifyMapProp for more information.
return JsBackedMap.backedBy(value).cast();
}

/// Returns [value] converted to its JS ref representation for storage in a props map, or null of the [value] is null.
///
/// For use in JS component prop getters where the component expects a JS ref, but accepting Dart refs
/// is more convenient to the consumer reading/writing the props.
///
/// Should be used alongside [unjsifyRefProp].
///
/// Normally ref props get converted in [ReactJsComponentFactoryProxy.build] and [jsifyMapProp],
/// but that same conversion for props not under the default `'ref'` key doesn't occur automatically,
/// which is where this function comes in.
dynamic jsifyRefProp(dynamic value) {
// Case 1: null
if (value == null) return null;

// Case 2a: Dart callback refs
// Case 2b: JS callback refs
// allowInterop is technically redundant, since that will get done in ReactJsComponentFactoryProxy.build
// (or jsifyMapProp for props that accept props maps, like ButtonProps.TouchRippleProps),
// but we'll do it here anyways since we know it'll be needed.
if (value is Function) return allowInterop(value);

// Case 2: Dart ref objects
if (value is Ref) {
// Store the original Dart ref so we can retrieve it later in unjsifyRefProp.
// See _dartRefForJsRef comment for more info.
_dartRefForJsRef.set(value.jsRef, value);
return value.jsRef;
}

// Case 3: JS ref objects
return value;
}

/// Returns a [JsRef] object converted back into its Dart [Ref] object (if it was converted via [jsifyRefProp],
/// or if [value] is not a [JsRef], passes through [value] (including null).
///
/// For use in JS component prop getters where the component expects a JS ref, but accepting Dart refs
/// is more convenient to the consumer reading/writing the props.
///
/// Should be used alongside [unjsifyRefProp].
///
/// Note that Dart refs currently lose their reified types when jsified/unjsified.
dynamic unjsifyRefProp(dynamic value,
{@visibleForTesting bool throwOnUnhandled = false}) {
// Case 1: null
if (value == null) return null;

// Case 2: JS callback refs
if (value is Function) return value;

// Case 2: JS ref objects
if (value is! Ref && value is JsRef && hasProperty(value, 'current')) {
// Return the original Dart ref is there is one, otherwise return the JsRef itself.
// See _dartRefForJsRef comment for more info.
return _dartRefForJsRef.get(value) ?? value;
}

// Case 3: unreachable?
// We shouldn't ever get here, but just pass through the value in case there's
// a case we didn't handle properly above (or throw for testing purposes).
if (throwOnUnhandled) {
throw ArgumentError.value(value, 'value', 'Unhandled case');
}
return value;
}

/// A weak mapping from JsRef objects to the original Dart Refs they back.
///
/// Useful for
/// 1. Preserving the reified type of the Dart ref when it gets jsified/unjsified
/// 2. Telling whether the ref was originally a Dart or JS ref
///
/// We're using WeakMap here so that we don't have a global object strongly referencing and thus retaining refs,
/// and not because strong references from the JS ref to the Dart ref would be problematic.
///
/// We also have to use a WeakMap instead of a JS property (or an Expando, whose DDC implementation uses JS properties),
/// since those can't be used with sealed JS objects (like React.createRef() objects in development builds, and potentially other cases).
final _dartRefForJsRef = WeakMap();
31 changes: 31 additions & 0 deletions lib/src/util/weak_map.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
@JS()
library react_material_ui.weak_map;

import 'package:js/js.dart';

/// Bindings for the JS WeakMap class, a collection of key/value pairs in which the keys are weakly referenced.
///
/// For more info, see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap
///
/// This API is supported in IE11 and all modern browsers.
///
/// This class has no type parameters since they cause issues in dart2js: https://github.com/dart-lang/sdk/issues/43373
@JS()
class WeakMap {
external WeakMap();

/// Removes any value associated to the [key]. `has(key)` will return false afterwards.
///
/// Returns whether a value was associated with the [key].
external bool delete(Object key);

/// Returns the value associated to the [key], or null if there is none.
external Object get(Object key);

/// Returns whether a value has been associated to the key.
external bool has(Object key);

/// Sets the value for the [key].
// void since IE11 returns undefined, and you can cascade on the object instead of returning it in Dart.
external void set(Object key, Object value);
}
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ dev_dependencies:
glob: ^1.2.0
io: ^0.3.2+1
mockito: ^4.1.1
react_testing_library: ^2.1.0
over_react_test: ^2.10.2
pedantic: ^1.8.0
test: ^1.15.7
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading