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-279: Add improved prop forwarding method #829

Merged
merged 34 commits into from
Aug 14, 2023
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
07c20b0
spike for unused props concept
kealjones-wk Aug 1, 2022
e7c1eba
Merge branch 'master' into unused_props
kealjones-wk Jul 6, 2023
69f836f
I heard you liked forwarding props...
kealjones-wk Jul 19, 2023
09cc932
remove `include`
kealjones-wk Jul 20, 2023
638aa33
add some dom tests
kealjones-wk Jul 21, 2023
a1f18b9
Merge branch 'master' into unused_props
kealjones-wk Jul 26, 2023
37aae20
uncomment old tests and fix generic test
kealjones-wk Jul 26, 2023
a6007d7
fix warnings
kealjones-wk Jul 26, 2023
4c7a63f
fix PropsToForward doc comment
kealjones-wk Jul 26, 2023
98fe1d3
formatting
kealjones-wk Jul 26, 2023
4b76cc2
simplify consumedProps assignment in _propsToForward
kealjones-wk Jul 26, 2023
2e30341
remove commented out code
kealjones-wk Jul 26, 2023
7b2799b
format
kealjones-wk Jul 26, 2023
8a5f7d8
fix doc comments
kealjones-wk Jul 26, 2023
91a8572
add prop forwarding demo to web
kealjones-wk Jul 27, 2023
3c39d20
add test for error case
kealjones-wk Aug 9, 2023
fb07ac0
address feedback
kealjones-wk Aug 9, 2023
31a9bad
fix error test
kealjones-wk Aug 9, 2023
f8d89de
add rethrow to propsToForward try catch
kealjones-wk Aug 9, 2023
80b2bab
only run in ddc
kealjones-wk Aug 9, 2023
b20ec36
maybe fix test?
kealjones-wk Aug 9, 2023
a068cbb
fix assertion logic
kealjones-wk Aug 9, 2023
bc1a78c
uncomment out tests
kealjones-wk Aug 9, 2023
1e383db
lets not use such a complex demo
kealjones-wk Aug 10, 2023
e43030d
update docs?
kealjones-wk Aug 10, 2023
5b1de45
remove the line termination change
kealjones-wk Aug 10, 2023
0ad2e6d
Revert "remove the line termination change"
kealjones-wk Aug 10, 2023
d0b6d7d
Revert "update docs?"
kealjones-wk Aug 10, 2023
23c11e9
add other doc updates
kealjones-wk Aug 10, 2023
eef837c
update migration guide?
kealjones-wk Aug 10, 2023
81010e5
another doc update
kealjones-wk Aug 10, 2023
6bfad61
address feedback
kealjones-wk Aug 11, 2023
cfd533e
Update test/over_react/component_declaration/builder_integration_test…
kealjones-wk Aug 14, 2023
3a82784
Fix improper `having` use from suggestion
kealjones-wk Aug 14, 2023
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
123 changes: 99 additions & 24 deletions doc/new_boilerplate_migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -781,42 +781,117 @@ UiFactory<FooProps> Foo = uiFunction(
);
```

#### With Consumed Props
#### With Prop Forwarding (fka Consumed Props)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should go somewhere else? ugh i hate deciding where docs should go! @joebingham-wk i need your help lol

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think here and in the doc/props_mixin_component_composition.md section is sufficient for this PR 👍


Because functional components have no instance that track consumed props, the syntax for passing unconsumed
props changes within functional components.
Because functional components have no instance that track consumed props, the syntax for forwarding props
changes within functional components.

`UiProps` exposes a field `staticMeta` that can be used to generate an iterable containing props meta for specific mixins.
This is similar to accessing `propsMeta` within a class based component. Using the iterable returned from `staticMeta`'s
APIs (such as `forMixins`), we can generate unconsumed props and pass them to a child component.
`UiProps` exposes 2 APIs `getPropsToForward` & `addPropsToForward` that can be used to forward props
that have not been used to a child component.

This is done like so:
##### getPropsToForward
`getPropsToForward` will return a `Map` of props removing the props found in the `exclude` argument.
`exclude` is optional and will default to a `Set` with the type that `props` is statically typed as,
this only works with `mixin .. on UiProps` types. If your function component uses a Props `class` then
you must include an `exclude` argument.

Component with a single props mixin:
```dart
mixin FooPropsMixin on UiProps {
String passedProp;
String foo;
}

mixin BarPropsMixin on UiProps {
String nonPassedProp;
UiFactory<FooPropsMixin> Foo = uiFunction((props) {
return (Bar()
// Filter out props declared in FooPropsMixin
// (used as the default for `exclude` since that's what `props` is statically typed as)
// when forwarding to Bar.
..addAll(props.getPropsToForward())
)();
});
```

Component with a more than one props mixin:
```dart
mixin FooPropsMixin on UiProps {
String foo;
}

class FooBarProps = UiProps with BarPropsMixin, FooPropsMixin;
class FooProps = UiProps with BarProps, FooPropsMixin;

UiFactory<FooBarProps> FooBar = uiFunction(
(props) {
final consumedProps = props.staticMeta.forMixins({BarPropsMixin});
UiFactory<FooProps> Foo = uiFunction((props) {
return (Bar()
// Filter out props declared in FooPropsMixin when forwarding to Bar.
..addAll(props.getPropsToForward(exclude: {FooPropsMixin}))
)();
});
```

return (Foo()..addUnconsumedProps(props, consumedProps))();
},
_$FooBarConfig, // ignore: undefined_identifier
);
`domOnly` - to forward DOM props only:
```dart
mixin FooPropsMixin on UiProps {
String foo;
}

UiFactory<FooPropsMixin> Foo = uiFunction(
(props) {
return 'foo: ${props.passedProp}';
},
_$FooConfig, // ignore: undefined_identifier
);
UiFactory<FooPropsMixin> Foo = uiFunction((props) {
return (Dom.div()
// Forward only DOM based props & Filter out props declared in FooPropsMixin
// (used as the default for `exclude` since that's what `props` is statically typed as)
// when forwarding to Bar.
..addAll(props.getPropsToForward(domOnly: true))
)();
});
```

##### addPropsToForward
`addPropsToForward` has the same function signature as `getPropsToForward` but is meant to be used with the `UiProps` method `modifyProps`.

Component with a single props mixin:
```dart
mixin FooPropsMixin on UiProps {
String foo;
}

UiFactory<FooPropsMixin> Foo = uiFunction((props) {
return (Bar()
// Filter out props declared in FooPropsMixin
// (used as the default for `exclude` since that's what `props` is statically typed as)
// when forwarding to Bar.
..modifyProps(props.addPropsToForward())
)();
});
```

Component with a more than one props mixin:
```dart
mixin FooPropsMixin on UiProps {
String foo;
}

class FooProps = UiProps with BarProps, FooPropsMixin;

UiFactory<FooProps> Foo = uiFunction((props) {
return (Bar()
// Filter out props declared in FooPropsMixin when forwarding to Bar.
..modifyProps(props.addPropsToForward(exclude: {FooPropsMixin}))
)();
});
```

`domOnly` - to forward DOM props only:
```dart
mixin FooPropsMixin on UiProps {
String foo;
}

UiFactory<FooPropsMixin> Foo = uiFunction((props) {
return (Dom.div()
// Forward only DOM based props & Filter out props declared in FooPropsMixin
// (used as the default for `exclude` since that's what `props` is statically typed as)
// when forwarding to Bar.
..modifyProps(props.addPropsToForward(domOnly: true))
)();
});
```

#### With UiProps
Expand Down
25 changes: 11 additions & 14 deletions doc/props_mixin_component_composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

In our core `UiProps` documentation, the pattern of [composing multiple props mixins into a single component props API](../README.md#with-other-mixins) is introduced.

This example builds on that, showing a lightweight example a common use-case for such composition.
This example builds on that, showing a lightweight example a common use-case for such composition.

We'll show three components
We'll show three components

1. A `Foo` component that has its own props API - and default rendering behavior when rendered standalone.
1. A `FooBar` component that has its own props API, in addition to the `Foo` props API. This allows consumers to set props declared in `FooPropsMixin`, which will be forwarded to the `Foo` component it renders.
Expand All @@ -16,7 +16,7 @@ import 'package:over_react/over_react.dart';

part 'foo.over_react.g.dart';

UiFactory<FooPropsMixin> Foo =
UiFactory<FooPropsMixin> Foo =
castUiFactory(_$Foo); // ignore: undefined_identifier

mixin FooPropsMixin on UiProps {
Expand All @@ -35,7 +35,7 @@ class FooComponent extends UiComponent2<FooPropsMixin> {
..modifyProps(addUnconsumedDomProps)
..className = (forwardingClassNameBuilder()..add('foo'))
)(
'Qux: ',
'Qux: ',
props.qux.map((n) => n),
props.children,
);
Expand All @@ -58,7 +58,7 @@ import 'foo.dart';

part 'foo_bar.over_react.g.dart';

UiFactory<FooBarProps> FooBar =
UiFactory<FooBarProps> FooBar =
castUiFactory(_$FooBar); // ignore: undefined_identifier

mixin BarPropsMixin on UiProps {
Expand All @@ -69,7 +69,7 @@ mixin BarPropsMixin on UiProps {
class FooBarProps = UiProps with BarPropsMixin, FooPropsMixin;

class FooBarComponent extends UiComponent2<FooBarProps> {
// Only consume the props found within BarPropsMixin, so that any prop values
// Only consume the props found within BarPropsMixin, so that any prop values
// found in FooPropsMixin get forwarded to the child Foo component via `addUnconsumedProps`.
@override
get consumedProps => propsMeta.forMixins({BarPropsMixin});
Expand Down Expand Up @@ -122,7 +122,7 @@ Produces the following HTML:
<div class="foo foo__bar foo__bar--abc">
Qux: 234
<div class="foo__bar__bizzles">
Bizzles:
Bizzles:
<ol>
<li>a</li>
<li>b</li>
Expand Down Expand Up @@ -150,12 +150,9 @@ class FooBazProps = UiProps with BarPropsMixin, FooPropsMixin;

UiFactory<FooBazProps> FooBaz = uiFunction(
(props) {
// Only consume the props found within BarPropsMixin, so that any prop values
// found in FooPropsMixin get forwarded to the child Foo component via `addUnconsumedProps`.
final consumedProps = props.staticMeta.forMixins({BarPropsMixin});

return (Foo()
..addUnconsumedProps(props, consumedProps)
// Only forward the props not belonging to BarPropsMixin to the child Foo component.
..addAll(props.getPropsToForward(exclude: {BarPropsMixin}))
..className = (forwardingClassNameBuilder()..add('foo__baz'))
)(
(Dom.div()..className = 'foo__baz__bizzles')(
Expand All @@ -165,7 +162,7 @@ UiFactory<FooBazProps> FooBaz = uiFunction(
),
),
);

ReactElement _renderBizzleItem(String bizzle) {
return (Dom.li()..key = bizzle)(bizzle);
}
Expand Down Expand Up @@ -201,7 +198,7 @@ Produces the following HTML:
<div class="foo foo__baz foo__baz--abc">
Qux: 234
<div class="foo__baz__bizzles">
Bizzles:
Bizzles:
<ol>
<li>a</li>
<li>b</li>
Expand Down
4 changes: 1 addition & 3 deletions example/builder/src/functional_consumed_props.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,12 @@ mixin SharedPropsMixin on UiProps {
class SomeParentProps = UiProps with ParentOnlyPropsMixin, SharedPropsMixin;

UiFactory<SomeParentProps> SomeParent = uiFunction((props) {
final consumedProps = props.staticMeta.forMixins({ParentOnlyPropsMixin});

return (
Dom.div()(
Dom.div()(
'The parent prop is: ${props.aParentProp}',
),
(SomeChild()..addUnconsumedProps(props, consumedProps))(),
(SomeChild()..addAll(props.getPropsToForward(exclude: {ParentOnlyPropsMixin})))(),
)
);
},
Expand Down
6 changes: 2 additions & 4 deletions example/builder/src/new_class_consumed_props.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,12 @@ class SomeParentProps = UiProps with ParentOnlyPropsMixin, SharedPropsMixin;
class SomeClassParentComponent extends UiComponent2<SomeParentProps> {
@override
render() {
final meta = props.staticMeta.forMixins({ParentOnlyPropsMixin});

return (
Dom.div()(
Dom.div()(
'The parent prop is: ${props.aParentProp}',
),
(SomeClassChild()..addUnconsumedProps(props, meta))(),
(SomeClassChild()..modifyProps(props.addPropsToForward(exclude: {ParentOnlyPropsMixin})))(),
)
);
}
Expand All @@ -58,4 +56,4 @@ class SomeClassChildComponent extends UiComponent2<SomeChildProps> {
)
);
}
}
}
117 changes: 117 additions & 0 deletions lib/src/component_declaration/builder_helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,123 @@ abstract class UiProps extends component_base.UiProps with GeneratedClass {
@toBeGenerated PropsMetaCollection get staticMeta => throw UngeneratedError(member: #meta);
}

/// Helper static extension methods to make forwarding props easier.
extension PropsToForward<T extends UiProps> on T {

/// Returns a copy of this instance's props excluding the keys found in [exclude].
///
/// [exclude] should be a `Set` of PropsMixin `Type`s.
/// If [exclude] is not set, it defaults to using the current instance's Type.
///
/// __Example:__
///
/// Component with a single props mixin:
/// ```dart
/// mixin FooPropsMixin on UiProps {
/// String foo;
/// }
///
/// UiFactory<FooPropsMixin> Foo = uiFunction((props) {
/// return (Bar()
/// // Filter out props declared in FooPropsMixin
/// // (used as the default for `exclude` since that's what `props` is statically typed as)
/// // when forwarding to Bar.
/// ..addAll(props.getPropsToForward())
/// )();
/// });
/// ```
///
/// Component with a more than one props mixin:
/// ```dart
/// mixin FooPropsMixin on UiProps {
/// String foo;
/// }
/// class FooProps = UiProps with BarProps, FooPropsMixin;
///
/// UiFactory<FooProps> Foo = uiFunction((props) {
/// return (Bar()
/// // Filter out props declared in FooPropsMixin when forwarding to Bar.
/// ..addAll(props.getPropsToForward(exclude: {FooPropsMixin}))
/// )();
/// });
/// ```
///
/// To only add DOM props, use the [domOnly] named argument.
///
/// Related: `UiComponent2`'s `addUnconsumedProps`
Map getPropsToForward({Set<Type> exclude, bool domOnly = false}) {
return _propsToForward(exclude: exclude, domOnly: domOnly, propsToUpdate: {});
}

/// A utility function to be used with `modifyProps` to add props excluding the keys found in [exclude].
///
/// [exclude] should be a `Set` of PropsMixin `Type`s.
/// If [exclude] is not set, it defaults to using the current instance's Type.
///
/// __Example:__
///
/// Component with a single props mixin:
/// ```dart
/// mixin FooPropsMixin on UiProps {
/// String foo;
/// }
///
/// UiFactory<FooPropsMixin> Foo = uiFunction((props) {
/// return (Bar()
/// // Filter out props declared in FooPropsMixin
/// // (used as the default for `exclude` since that's what `props` is statically typed as)
/// // when forwarding to Bar.
/// ..modifyProps(props.addPropsToForward())
/// )();
/// });
/// ```
///
/// Component with a more than one props mixin:
/// ```dart
/// mixin FooPropsMixin on UiProps {
/// String foo;
/// }
/// class FooProps = UiProps with BarProps, FooPropsMixin;
///
/// UiFactory<FooProp> Foo = uiFunction((props) {
/// return (Bar()
/// // Filter out props declared in FooPropsMixin when forwarding to Bar.
/// ..modifyProps(props.addPropsToForward(exclude: {FooPropsMixin}))
/// )();
/// });
/// ```
///
/// To only add DOM props, use the [domOnly] named argument.
///
/// Related: `UiComponent2`'s `addUnconsumedProps`
PropsModifier addPropsToForward({Set<Type> exclude, bool domOnly = false}) {
return (Map<dynamic, dynamic> props) {
_propsToForward(exclude: exclude, domOnly: domOnly, propsToUpdate: props);
};
}

Map _propsToForward({Set<Type> exclude, bool domOnly = false, Map propsToUpdate}) {
Iterable<PropsMeta> consumedProps = [];
try {
consumedProps = staticMeta.forMixins(exclude ?? {T}).toList();
} catch(_) {
// If [domOnly] is `true`, it is alright for the meta lookup to fail, otherwise throw the error.
greglittlefield-wf marked this conversation as resolved.
Show resolved Hide resolved
assert(exclude != null, ArgumentError('Could not find props meta for type $T.'
' If this is not a props mixin, you need to specify its mixins as the second argument. For example:'
'\n ..addAll(props.getPropsToForward(exclude: {${T}Mixin})').message);
kealjones-wk marked this conversation as resolved.
Show resolved Hide resolved
rethrow;
}
kealjones-wk marked this conversation as resolved.
Show resolved Hide resolved
final consumedPropKeys = consumedProps?.map((consumedProps) => consumedProps.keys);
forwardUnconsumedPropsV2(
props,
propsToUpdate: propsToUpdate,
keySetsToOmit: consumedPropKeys,
onlyCopyDomProps: domOnly,
);
return propsToUpdate;
}
}

/// A [dart.collection.MapView]-like class with strongly-typed getters/setters for React state.
///
/// To be used with the over_react builder to generate concrete state implementations
Expand Down
Loading