Skip to content

Commit 5e561e8

Browse files
Merge pull request #10 from aaronlademann-wf/add-flux-components
Add FluxUiComponent / FluxUiStatefulComponent
2 parents 69a5b6a + bf6f27c commit 5e561e8

File tree

12 files changed

+540
-22
lines changed

12 files changed

+540
-22
lines changed

README.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,8 @@ that you get for free from OverReact, you're ready to start building your own cu
487487
1. Start with one of the [component boilerplate templates](#component-boilerplate-templates) below.
488488
* [Component](#component-boilerplate) _(props only)_
489489
* [Stateful Component](#stateful-component-boilerplate) _(props + state)_
490+
* [Flux Component](#flux-component-boilerplate) _(props + store + actions)_
491+
* [Stateful Flux Component](#stateful-flux-component-boilerplate) _(props + state + store + actions)_
490492
2. Fill in your props and rendering/lifecycle logic.
491493
3. Consume your component with the fluent interface.
492494
4. Run [the app you’ve set up to consume `over_react`](#using-it-in-your-project)
@@ -586,6 +588,82 @@ that you get for free from OverReact, you're ready to start building your own cu
586588
}
587589
}
588590
```
591+
592+
* #### Flux Component Boilerplate
593+
594+
```dart
595+
import 'dart:html';
596+
import 'package:react/react.dart' as react;
597+
import 'package:react/react_dom.dart' as react_dom;
598+
import 'package:react/react_client.dart';
599+
import 'package:over_react/over_react.dart';
600+
601+
@Factory()
602+
UiFactory<BazProps> Baz;
603+
604+
@Props()
605+
class BazProps extends FluxUiProps<BazActions, BazStore> {
606+
// Props go here, declared as fields.
607+
// `actions` and `store` are already defined for you!
608+
}
609+
610+
@Component()
611+
class BazComponent extends FluxUiComponent<BazProps> {
612+
getDefaultProps() => (newProps()
613+
// Cascade default props here
614+
);
615+
616+
@override
617+
render() {
618+
// Return the rendered component contents here.
619+
// The `props` variables is typed; no need for string keys!
620+
// E.g., `props.actions`, `props.store`.
621+
}
622+
}
623+
```
624+
625+
* #### Stateful Flux Component Boilerplate
626+
627+
```dart
628+
import 'dart:html';
629+
import 'package:react/react.dart' as react;
630+
import 'package:react/react_dom.dart' as react_dom;
631+
import 'package:react/react_client.dart';
632+
import 'package:over_react/over_react.dart';
633+
634+
@Factory()
635+
UiFactory<BazProps> Baz;
636+
637+
@Props()
638+
class BazProps extends FluxUiProps<BazActions, BazStore> {
639+
// Props go here, declared as fields.
640+
// `actions` and `store` are already defined for you!
641+
}
642+
643+
@State()
644+
class BazState extends UiState {
645+
// State goes here, declared as fields.
646+
}
647+
648+
@Component()
649+
class BazComponent extends FluxUiStatefulComponent<BazProps, BazState> {
650+
getDefaultProps() => (newProps()
651+
// Cascade default props here
652+
);
653+
654+
@override
655+
Map getInitialState() => (newState()
656+
// Cascade initial state here
657+
);
658+
659+
@override
660+
render() {
661+
// Return the rendered component contents here.
662+
// The `props` variables is typed; no need for string keys!
663+
// E.g., `props.actions`, `props.store`.
664+
}
665+
}
666+
```
589667

590668

591669
### Common Pitfalls

lib/over_react.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export 'src/component/dummy_component.dart';
2323
export 'src/component/prop_mixins.dart';
2424
export 'src/component/prop_typedefs.dart';
2525
export 'src/component/resize_sensor.dart';
26+
export 'src/component_declaration/flux_component.dart';
2627
export 'src/component_declaration/transformer_helpers.dart';
2728
export 'src/util/character_constants.dart';
2829
export 'src/util/class_names.dart';

lib/src/component_declaration/component_base.dart

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,13 @@ typedef TProps BuilderOnlyUiFactory<TProps extends UiProps>();
8686

8787
typedef dynamic _RefTypedef(String ref);
8888

89-
/// The basis for a over_react component, extending [react.Component]. (Successor to [BaseComponent]).
89+
/// The basis for a over_react component.
9090
///
9191
/// Includes support for strongly-typed props and utilities for prop and CSS classname forwarding.
92+
///
93+
/// Extends [react.Component].
94+
///
95+
/// Related: [UiStatefulComponent]
9296
abstract class UiComponent<TProps extends UiProps> extends react.Component {
9397
/// Returns the component of the specified [ref].
9498
/// > `react.Component` if it is a Dart component
@@ -200,9 +204,13 @@ abstract class UiComponent<TProps extends UiProps> extends react.Component {
200204
// ----------------------------------------------------------------------
201205
}
202206

203-
/// The basis for a stateful over_react component, extending [react.Component]. (Successor to [BaseComponentWithState]).
207+
/// The basis for a stateful over_react component.
204208
///
205209
/// Includes support for strongly-typed props and state and utilities for prop and CSS classname forwarding.
210+
///
211+
/// Extends [react.Component].
212+
///
213+
/// Related: [UiComponent]
206214
abstract class UiStatefulComponent<TProps extends UiProps, TState extends UiState> extends UiComponent<TProps> {
207215
// ----------------------------------------------------------------------
208216
// ----------------------------------------------------------------------
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
library over_react.component_declaration.flux_component;
2+
3+
import 'dart:async';
4+
import 'package:w_flux/w_flux.dart';
5+
import './annotations.dart' as annotations;
6+
import './transformer_helpers.dart';
7+
8+
/// Builds on top of [UiProps], adding typed props for [Action]s and [Store]s in order to integrate with w_flux.
9+
///
10+
/// Use with the over_react transformer via the `@Props()` ([annotations.Props]) annotation.
11+
abstract class FluxUiProps<ActionsT, StoresT> extends UiProps {
12+
String get _actionsPropKey => '${propKeyNamespace}actions';
13+
String get _storePropKey => '${propKeyNamespace}store';
14+
15+
/// The prop defined by [ActionsT] that holds all [Action]s that
16+
/// this component needs access to.
17+
///
18+
/// There is no strict rule on the [ActionsT] type. Depending on application
19+
/// structure, there may be [Action]s available directly on this object, or
20+
/// this object may represent a hierarchy of actions.
21+
ActionsT get actions => props[_actionsPropKey];
22+
set actions(ActionsT value) => props[_actionsPropKey] = value;
23+
24+
/// The prop defined by [StoresT].
25+
///
26+
/// This object should either be an instance of [Store] or should provide access to one or more [Store]s.
27+
///
28+
/// __Instead of storing state within this component via `setState`, it is recommended that data be
29+
/// pulled directly from these stores.__ This ensures that the data being used is always up to date
30+
/// and leaves the state management logic to the stores.
31+
///
32+
/// If this component only needs data from a single [Store], then [StoresT]
33+
/// should be an instance of [Store]. This allows the default implementation
34+
/// of [redrawOn] to automatically subscribe to the store.
35+
///
36+
/// If this component needs data from multiple [Store] instances, then
37+
/// [StoresT] should be a class that provides access to these multiple stores.
38+
/// Then, you can explicitly select the [Store] instances that should be
39+
/// listened to by overriding [_FluxComponentMixin.redrawOn].
40+
StoresT get store => props[_storePropKey];
41+
set store(StoresT value) => props[_storePropKey] = value;
42+
}
43+
44+
/// Builds on top of [UiComponent], adding w_flux integration, much like the [FluxComponent] in w_flux.
45+
///
46+
/// * Flux components are responsible for rendering application views and turning
47+
/// user interactions and events into [Action]s.
48+
/// * Flux components can use data from one or many [Store] instances to define
49+
/// the resulting component.
50+
///
51+
/// Use with the over_react transformer via the `@Component()` ([annotations.Component]) annotation.
52+
abstract class FluxUiComponent<TProps extends FluxUiProps> extends UiComponent<TProps>
53+
with _FluxComponentMixin<TProps>, BatchedRedraws {}
54+
55+
/// Builds on top of [UiStatefulComponent], adding `w_flux` integration, much like the [FluxComponent] in w_flux.
56+
///
57+
/// * Flux components are responsible for rendering application views and turning
58+
/// user interactions and events into [Action]s.
59+
/// * Flux components can use data from one or many [Store] instances to define
60+
/// the resulting component.
61+
///
62+
/// Use with the over_react transformer via the `@Component()` ([annotations.Component]) annotation.
63+
abstract class FluxUiStatefulComponent<TProps extends FluxUiProps, TState extends UiState>
64+
extends UiStatefulComponent<TProps, TState>
65+
with _FluxComponentMixin<TProps>, BatchedRedraws {}
66+
67+
/// Helper mixin to keep [FluxUiComponent] and [FluxUiStatefulComponent] clean/DRY.
68+
///
69+
/// Private so it will only get used in this file, since having lifecycle methods in a mixin is risky.
70+
abstract class _FluxComponentMixin<TProps extends FluxUiProps> {
71+
TProps get props;
72+
bool shouldBatchRedraw;
73+
redraw([callback()]);
74+
75+
/// List of store subscriptions created when the component mounts.
76+
///
77+
/// These subscriptions are canceled when the component is unmounted.
78+
List<StreamSubscription> _subscriptions = [];
79+
80+
void componentWillMount() {
81+
/// Subscribe to all applicable stores.
82+
///
83+
/// [Store]s returned by [redrawOn] will have their triggers mapped directly to this components
84+
/// redraw function.
85+
///
86+
/// [Store]s included in the [getStoreHandlers] result will be listened to and wired up to their
87+
/// respective handlers.
88+
Map<Store, Function> handlers = new Map.fromIterable(redrawOn(),
89+
value: (_) => (_) => redraw())..addAll(getStoreHandlers());
90+
handlers.forEach((store, handler) {
91+
StreamSubscription subscription = store.listen(handler);
92+
_subscriptions.add(subscription);
93+
});
94+
}
95+
96+
void componentWillUnmount() {
97+
// Ensure that unmounted components don't batch render
98+
shouldBatchRedraw = false;
99+
100+
// Cancel all store subscriptions.
101+
_subscriptions.forEach((StreamSubscription subscription) {
102+
if (subscription != null) {
103+
subscription.cancel();
104+
}
105+
});
106+
}
107+
108+
/// Define the list of [Store] instances that this component should listen to.
109+
///
110+
/// When any of the returned [Store]s update their state, this component will
111+
/// redraw.
112+
///
113+
/// If [store] is of type [Store] (in other words, if this component has a
114+
/// single Store passed in), this will return a list with said store as the
115+
/// only element by default. Otherwise, an empty list is returned.
116+
///
117+
/// If [store] is actually a composite object with multiple stores, this
118+
/// method should be overridden to return a list with the stores that should
119+
/// be listened to.
120+
///
121+
/// @override
122+
/// redrawOn() => [store.tasks, store.users];
123+
List<Store> redrawOn() {
124+
if (props.store is Store) {
125+
return [props.store];
126+
} else {
127+
return [];
128+
}
129+
}
130+
131+
/// If you need more fine-grained control over store trigger handling,
132+
/// override this method to return a Map of stores to handlers.
133+
///
134+
/// Whenever a store in the returned map triggers, the respective handler will be called.
135+
///
136+
/// Handlers defined here take precedence over the [redrawOn] handling.
137+
/// If possible, however, [redrawOn] should be used instead of this in order
138+
/// to avoid keeping additional state within this component and manually
139+
/// managing redraws.
140+
Map<Store, Function> getStoreHandlers() {
141+
return {};
142+
}
143+
144+
/// Register a [subscription] that should be canceled when the component unmounts.
145+
///
146+
/// Cancellation will be handled automatically by [componentWillUnmount].
147+
void addSubscription(StreamSubscription subscription) {
148+
_subscriptions.add(subscription);
149+
}
150+
}

0 commit comments

Comments
 (0)