Skip to content

Commit

Permalink
Merge pull request #743 from Workiva/FED-7-utils-tests
Browse files Browse the repository at this point in the history
FED-7 Move over uiJsComponent + tests
  • Loading branch information
rmconsole5-wk authored May 12, 2022
2 parents f896c59 + d326cec commit c969e6b
Show file tree
Hide file tree
Showing 12 changed files with 2,877 additions and 5 deletions.
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].
///
/// 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>(
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 @@ -37,6 +37,7 @@ dev_dependencies:
glob: '>=1.2.0<3.0.0'
io: '>=0.3.2+1 <2.0.0'
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
Loading

0 comments on commit c969e6b

Please sign in to comment.