Skip to content

Commit ca65403

Browse files
Merge pull request #317 from Workiva/CPLAT-4887-PropTypes
CPLAT-4887: propTypes in Component2
2 parents aae7660 + 81b005f commit ca65403

17 files changed

Lines changed: 1179 additions & 137 deletions

lib/src/component_declaration/component_base.dart

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ library over_react.component_declaration.component_base;
1616

1717
import 'dart:async';
1818
import 'dart:collection';
19+
import 'dart:js';
1920

2021
import 'package:meta/meta.dart';
2122
import 'package:over_react/over_react.dart';
@@ -87,7 +88,85 @@ class UiComponent2BridgeImpl extends Component2BridgeImpl {
8788

8889
@override
8990
JsMap jsifyPropTypes(covariant UiComponent2 component, Map propTypes) {
90-
// todo implement jsifyPropTypes
91+
Error _getErrorFromConsumerValidator(
92+
dynamic _validator,
93+
JsBackedMap _props,
94+
String _propName,
95+
String _componentName,
96+
String _location,
97+
String _propFullName
98+
) {
99+
var convertedProps = component.typedPropsFactoryJs(_props);
100+
Error error = _validator(convertedProps, _propName, _componentName, _location, _propFullName);
101+
return error;
102+
}
103+
104+
// Add [PropValidator]s for props annotated as required.
105+
var newPropTypes = Map.from(propTypes);
106+
component.consumedProps?.forEach((ConsumedProps consumedProps) {
107+
consumedProps.props.forEach((PropDescriptor prop) {
108+
if (!prop.isRequired) return;
109+
110+
Error requiredPropValidator(
111+
Map _props,
112+
String _propName,
113+
String _componentName,
114+
String _location,
115+
String _propFullName,
116+
) {
117+
Error consumerError;
118+
// Check if the consumer has specified a propType for this key.
119+
if(propTypes[prop.key] != null) {
120+
consumerError = _getErrorFromConsumerValidator(
121+
propTypes[prop.key],
122+
JsBackedMap.from(_props),
123+
_propName,
124+
_componentName,
125+
_location,
126+
_propFullName
127+
);
128+
}
129+
130+
if (consumerError != null) return consumerError;
131+
132+
if (prop.isNullable && _props.containsKey(prop.key)) return null;
133+
if (!prop.isNullable && _props[prop.key] != null) return null;
134+
135+
if (_props[_propName] == null) {
136+
return new PropError.required(_propName, prop.errorMessage);
137+
}
138+
139+
return null;
140+
}
141+
142+
newPropTypes[prop.key] = requiredPropValidator;
143+
});
144+
});
145+
146+
// Wrap consumer-provided and required validators with ones that convert plain props maps into typed ones.
147+
return JsBackedMap.from(newPropTypes.map((_propKey, _validator) {
148+
JsPropValidator handlePropValidator = (
149+
JsMap _props,
150+
String _propName,
151+
String _componentName,
152+
String _location,
153+
String _propFullName,
154+
// This is a required argument of PropTypes validators but is hidden from the JS consumer.
155+
String secret,
156+
) {
157+
Error error = _getErrorFromConsumerValidator(
158+
_validator,
159+
JsBackedMap.fromJs(_props),
160+
_propName,
161+
_componentName,
162+
_location,
163+
_propFullName
164+
);
165+
return error == null ? null : JsError(error.toString());
166+
};
167+
168+
return MapEntry(_propKey, allowInterop(handlePropValidator));
169+
})).jsObject;
91170
}
92171

93172
/// A version of [setStateWithTypedUpdater] whose updater is passed typed views

lib/src/component_declaration/component_base/component_base_2.dart

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ abstract class UiComponent2<TProps extends UiProps> extends react.Component2
8888
@override
8989
@Deprecated('4.0.0')
9090
Map copyUnconsumedProps() {
91-
var consumedPropKeys = consumedProps?.map((ConsumedProps consumedProps) => consumedProps.keys) ?? const [];
91+
var consumedPropKeys = consumedProps
92+
?.map((ConsumedProps consumedProps) => consumedProps.keys) ??
93+
const [];
9294

9395
return copyProps(keySetsToOmit: consumedPropKeys);
9496
}
@@ -127,7 +129,9 @@ abstract class UiComponent2<TProps extends UiProps> extends react.Component2
127129
@override
128130
@Deprecated('4.0.0')
129131
Map copyUnconsumedDomProps() {
130-
var consumedPropKeys = consumedProps?.map((ConsumedProps consumedProps) => consumedProps.keys) ?? const [];
132+
var consumedPropKeys = consumedProps
133+
?.map((ConsumedProps consumedProps) => consumedProps.keys) ??
134+
const [];
131135

132136
return copyProps(onlyCopyDomProps: true, keySetsToOmit: consumedPropKeys);
133137
}
@@ -152,17 +156,20 @@ abstract class UiComponent2<TProps extends UiProps> extends react.Component2
152156
forwardUnconsumedProps(this.props, propsToUpdate: props, keySetsToOmit:
153157
consumedPropKeys, onlyCopyDomProps: true);
154158
}
155-
159+
156160
/// Returns a copy of this component's props with React props optionally omitted, and
157161
/// with the specified [keysToOmit] and [keySetsToOmit] omitted.
158162
@override
159-
Map copyProps({bool omitReservedReactProps: true, bool onlyCopyDomProps: false, Iterable keysToOmit, Iterable<Iterable> keySetsToOmit}) {
163+
Map copyProps(
164+
{bool omitReservedReactProps: true,
165+
bool onlyCopyDomProps: false,
166+
Iterable keysToOmit,
167+
Iterable<Iterable> keySetsToOmit}) {
160168
return getPropsToForward(this.props,
161169
omitReactProps: omitReservedReactProps,
162170
onlyCopyDomProps: onlyCopyDomProps,
163171
keysToOmit: keysToOmit,
164-
keySetsToOmit: keySetsToOmit
165-
);
172+
keySetsToOmit: keySetsToOmit);
166173
}
167174

168175
/// Throws a [PropError] if [appliedProps] are invalid.
@@ -181,24 +188,21 @@ abstract class UiComponent2<TProps extends UiProps> extends react.Component2
181188
/// throw new PropError.value(tProps.items, 'items', 'must have an even number of items, because reasons');
182189
/// }
183190
/// }
191+
/// __Deprecated.__ Use [propTypes] instead. Will be removed in the `4.0.0` release.
192+
@Deprecated('4.0.0')
184193
@mustCallSuper
185194
@override
186195
void validateProps(Map appliedProps) {
187-
validateRequiredProps(appliedProps);
196+
throw UnsupportedError('[validateProps] is not supported in Component2, use [propTypes] instead.');
188197
}
189198

190199
/// Validates that props with the `@requiredProp` annotation are present.
200+
/// __Deprecated.__ Use [propTypes] instead. Will be removed in the `4.0.0` release.
201+
@Deprecated('4.0.0')
202+
@mustCallSuper
191203
@override
192204
void validateRequiredProps(Map appliedProps) {
193-
consumedProps?.forEach((ConsumedProps consumedProps) {
194-
consumedProps.props.forEach((PropDescriptor prop) {
195-
if (!prop.isRequired) return;
196-
if (prop.isNullable && appliedProps.containsKey(prop.key)) return;
197-
if (!prop.isNullable && appliedProps[prop.key] != null) return;
198-
199-
throw new PropError.required(prop.key, prop.errorMessage);
200-
});
201-
});
205+
throw UnsupportedError('[validateRequiredProps] is not supported in Component2, use [propTypes] instead.');
202206
}
203207

204208
/// Returns a new ClassNameBuilder with className and blacklist values added from [CssClassPropsMixin.className] and
@@ -270,6 +274,32 @@ abstract class UiComponent2<TProps extends UiProps> extends react.Component2
270274
// END Typed props helpers
271275
// ----------------------------------------------------------------------
272276
// ----------------------------------------------------------------------
277+
278+
/// Allows usage of PropValidator functions to check the validity of a prop passed to it.
279+
/// When an invalid value is provided for a prop, a warning will be shown in the JavaScript console.
280+
/// For performance reasons, propTypes is only checked in development mode.
281+
///
282+
/// Override with a custom implementation to easily add validation.
283+
///
284+
/// get propTypes => {
285+
/// getPropKey((props) => props.twoObjects, typedPropsFactory):
286+
/// (props, propName, componentName, location, propFullName) {
287+
/// final length = props.twoObjects?.length;
288+
/// if (length != 2) {
289+
/// return new PropError.value(length, propName, 'must have a length of 2');
290+
/// }
291+
/// return null;
292+
/// },
293+
/// };
294+
///
295+
/// `getPropKey` is a staticlly typed helper to get the string key for a prop.
296+
///
297+
/// __Note:__ An improved version of `getPropKey` will be offered once
298+
/// https://jira.atl.workiva.net/browse/CPLAT-6655 is completed.
299+
///
300+
/// For more info see: https://www.npmjs.com/package/prop-types
301+
@override
302+
Map<String, react.PropValidator<TProps>> get propTypes => {};
273303
}
274304

275305
/// The basis for a _stateful_ over_react component that is compatible with ReactJS 16 ([react.Component2]).
@@ -300,8 +330,8 @@ abstract class UiComponent2<TProps extends UiProps> extends react.Component2
300330
/// )(props.children);
301331
/// }
302332
/// }
303-
abstract class UiStatefulComponent2<TProps extends UiProps, TState extends UiState>
304-
extends UiComponent2<TProps>
333+
abstract class UiStatefulComponent2<TProps extends UiProps,
334+
TState extends UiState> extends UiComponent2<TProps>
305335
implements UiStatefulComponent<TProps, TState> {
306336
// ----------------------------------------------------------------------
307337
// ----------------------------------------------------------------------

lib/src/util/prop_key_util.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import 'dart:collection';
2121
/// __Example:__
2222
///
2323
/// var valuePropKey = getPropKey((props) => props.value, TextInput);
24-
String getPropKey(void accessProp(Map keySpy), Map factory(Map props)) {
24+
String getPropKey<T extends Map>(void accessProp(T keySpy), T factory(Map props)) {
2525
return _getKey((Map keySpy) {
2626
return accessProp(factory(keySpy));
2727
});

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ authors:
66
- Workiva UI Platform Chapter <uip@workiva.com>
77
- Workiva App Frameworks Team <appframeworks@workiva.com>
88
environment:
9-
sdk: '>=2.1.0 <3.0.0'
9+
sdk: '>=2.2.2 <3.0.0'
1010

1111
dependencies:
1212
analyzer: '>=0.35.0 <0.37.0'

0 commit comments

Comments
 (0)