-
Notifications
You must be signed in to change notification settings - Fork 58
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #743 from Workiva/FED-7-utils-tests
FED-7 Move over uiJsComponent + tests
- Loading branch information
Showing
12 changed files
with
2,877 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.