Skip to content

Commit

Permalink
Merge pull request Workiva#10 from aaronlademann-wf/add-flux-components
Browse files Browse the repository at this point in the history
Add FluxUiComponent / FluxUiStatefulComponent
  • Loading branch information
jacehensley-wf authored Oct 26, 2016
2 parents 80b68b7 + 59abfa2 commit 3908a9d
Show file tree
Hide file tree
Showing 12 changed files with 540 additions and 22 deletions.
78 changes: 78 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,8 @@ that you get for free from OverReact, you're ready to start building your own cu
1. Start with one of the [component boilerplate templates](#component-boilerplate-templates) below.
* [Component](#component-boilerplate) _(props only)_
* [Stateful Component](#stateful-component-boilerplate) _(props + state)_
* [Flux Component](#flux-component-boilerplate) _(props + store + actions)_
* [Stateful Flux Component](#stateful-flux-component-boilerplate) _(props + state + store + actions)_
2. Fill in your props and rendering/lifecycle logic.
3. Consume your component with the fluent interface.
4. Run [the app you’ve set up to consume `over_react`](#using-it-in-your-project)
Expand Down Expand Up @@ -586,6 +588,82 @@ that you get for free from OverReact, you're ready to start building your own cu
}
}
```

* #### Flux Component Boilerplate

```dart
import 'dart:html';
import 'package:react/react.dart' as react;
import 'package:react/react_dom.dart' as react_dom;
import 'package:react/react_client.dart';
import 'package:over_react/over_react.dart';
@Factory()
UiFactory<BazProps> Baz;
@Props()
class BazProps extends FluxUiProps<BazActions, BazStore> {
// Props go here, declared as fields.
// `actions` and `store` are already defined for you!
}

@Component()
class BazComponent extends FluxUiComponent<BazProps> {
getDefaultProps() => (newProps()
// Cascade default props here
);

@override
render() {
// Return the rendered component contents here.
// The `props` variables is typed; no need for string keys!
// E.g., `props.actions`, `props.store`.
}
}
```

* #### Stateful Flux Component Boilerplate

```dart
import 'dart:html';
import 'package:react/react.dart' as react;
import 'package:react/react_dom.dart' as react_dom;
import 'package:react/react_client.dart';
import 'package:over_react/over_react.dart';
@Factory()
UiFactory<BazProps> Baz;
@Props()
class BazProps extends FluxUiProps<BazActions, BazStore> {
// Props go here, declared as fields.
// `actions` and `store` are already defined for you!
}

@State()
class BazState extends UiState {
// State goes here, declared as fields.
}

@Component()
class BazComponent extends FluxUiStatefulComponent<BazProps, BazState> {
getDefaultProps() => (newProps()
// Cascade default props here
);

@override
Map getInitialState() => (newState()
// Cascade initial state here
);

@override
render() {
// Return the rendered component contents here.
// The `props` variables is typed; no need for string keys!
// E.g., `props.actions`, `props.store`.
}
}
```


### Common Pitfalls
Expand Down
1 change: 1 addition & 0 deletions lib/over_react.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export 'src/component/dummy_component.dart';
export 'src/component/prop_mixins.dart';
export 'src/component/prop_typedefs.dart';
export 'src/component/resize_sensor.dart';
export 'src/component_declaration/flux_component.dart';
export 'src/component_declaration/transformer_helpers.dart';
export 'src/util/character_constants.dart';
export 'src/util/class_names.dart';
Expand Down
12 changes: 10 additions & 2 deletions lib/src/component_declaration/component_base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,13 @@ typedef TProps BuilderOnlyUiFactory<TProps extends UiProps>();

typedef dynamic _RefTypedef(String ref);

/// The basis for a over_react component, extending [react.Component]. (Successor to [BaseComponent]).
/// The basis for a over_react component.
///
/// Includes support for strongly-typed props and utilities for prop and CSS classname forwarding.
///
/// Extends [react.Component].
///
/// Related: [UiStatefulComponent]
abstract class UiComponent<TProps extends UiProps> extends react.Component {
/// Returns the component of the specified [ref].
/// > `react.Component` if it is a Dart component
Expand Down Expand Up @@ -200,9 +204,13 @@ abstract class UiComponent<TProps extends UiProps> extends react.Component {
// ----------------------------------------------------------------------
}

/// The basis for a stateful over_react component, extending [react.Component]. (Successor to [BaseComponentWithState]).
/// The basis for a stateful over_react component.
///
/// Includes support for strongly-typed props and state and utilities for prop and CSS classname forwarding.
///
/// Extends [react.Component].
///
/// Related: [UiComponent]
abstract class UiStatefulComponent<TProps extends UiProps, TState extends UiState> extends UiComponent<TProps> {
// ----------------------------------------------------------------------
// ----------------------------------------------------------------------
Expand Down
150 changes: 150 additions & 0 deletions lib/src/component_declaration/flux_component.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
library over_react.component_declaration.flux_component;

import 'dart:async';
import 'package:w_flux/w_flux.dart';
import './annotations.dart' as annotations;
import './transformer_helpers.dart';

/// Builds on top of [UiProps], adding typed props for [Action]s and [Store]s in order to integrate with w_flux.
///
/// Use with the over_react transformer via the `@Props()` ([annotations.Props]) annotation.
abstract class FluxUiProps<ActionsT, StoresT> extends UiProps {
String get _actionsPropKey => '${propKeyNamespace}actions';
String get _storePropKey => '${propKeyNamespace}store';

/// The prop defined by [ActionsT] that holds all [Action]s that
/// this component needs access to.
///
/// There is no strict rule on the [ActionsT] type. Depending on application
/// structure, there may be [Action]s available directly on this object, or
/// this object may represent a hierarchy of actions.
ActionsT get actions => props[_actionsPropKey];
set actions(ActionsT value) => props[_actionsPropKey] = value;

/// The prop defined by [StoresT].
///
/// This object should either be an instance of [Store] or should provide access to one or more [Store]s.
///
/// __Instead of storing state within this component via `setState`, it is recommended that data be
/// pulled directly from these stores.__ This ensures that the data being used is always up to date
/// and leaves the state management logic to the stores.
///
/// If this component only needs data from a single [Store], then [StoresT]
/// should be an instance of [Store]. This allows the default implementation
/// of [redrawOn] to automatically subscribe to the store.
///
/// If this component needs data from multiple [Store] instances, then
/// [StoresT] should be a class that provides access to these multiple stores.
/// Then, you can explicitly select the [Store] instances that should be
/// listened to by overriding [_FluxComponentMixin.redrawOn].
StoresT get store => props[_storePropKey];
set store(StoresT value) => props[_storePropKey] = value;
}

/// Builds on top of [UiComponent], adding w_flux integration, much like the [FluxComponent] in w_flux.
///
/// * Flux components are responsible for rendering application views and turning
/// user interactions and events into [Action]s.
/// * Flux components can use data from one or many [Store] instances to define
/// the resulting component.
///
/// Use with the over_react transformer via the `@Component()` ([annotations.Component]) annotation.
abstract class FluxUiComponent<TProps extends FluxUiProps> extends UiComponent<TProps>
with _FluxComponentMixin<TProps>, BatchedRedraws {}

/// Builds on top of [UiStatefulComponent], adding `w_flux` integration, much like the [FluxComponent] in w_flux.
///
/// * Flux components are responsible for rendering application views and turning
/// user interactions and events into [Action]s.
/// * Flux components can use data from one or many [Store] instances to define
/// the resulting component.
///
/// Use with the over_react transformer via the `@Component()` ([annotations.Component]) annotation.
abstract class FluxUiStatefulComponent<TProps extends FluxUiProps, TState extends UiState>
extends UiStatefulComponent<TProps, TState>
with _FluxComponentMixin<TProps>, BatchedRedraws {}

/// Helper mixin to keep [FluxUiComponent] and [FluxUiStatefulComponent] clean/DRY.
///
/// Private so it will only get used in this file, since having lifecycle methods in a mixin is risky.
abstract class _FluxComponentMixin<TProps extends FluxUiProps> {
TProps get props;
bool shouldBatchRedraw;
redraw([callback()]);

/// List of store subscriptions created when the component mounts.
///
/// These subscriptions are canceled when the component is unmounted.
List<StreamSubscription> _subscriptions = [];

void componentWillMount() {
/// Subscribe to all applicable stores.
///
/// [Store]s returned by [redrawOn] will have their triggers mapped directly to this components
/// redraw function.
///
/// [Store]s included in the [getStoreHandlers] result will be listened to and wired up to their
/// respective handlers.
Map<Store, Function> handlers = new Map.fromIterable(redrawOn(),
value: (_) => (_) => redraw())..addAll(getStoreHandlers());
handlers.forEach((store, handler) {
StreamSubscription subscription = store.listen(handler);
_subscriptions.add(subscription);
});
}

void componentWillUnmount() {
// Ensure that unmounted components don't batch render
shouldBatchRedraw = false;

// Cancel all store subscriptions.
_subscriptions.forEach((StreamSubscription subscription) {
if (subscription != null) {
subscription.cancel();
}
});
}

/// Define the list of [Store] instances that this component should listen to.
///
/// When any of the returned [Store]s update their state, this component will
/// redraw.
///
/// If [store] is of type [Store] (in other words, if this component has a
/// single Store passed in), this will return a list with said store as the
/// only element by default. Otherwise, an empty list is returned.
///
/// If [store] is actually a composite object with multiple stores, this
/// method should be overridden to return a list with the stores that should
/// be listened to.
///
/// @override
/// redrawOn() => [store.tasks, store.users];
List<Store> redrawOn() {
if (props.store is Store) {
return [props.store];
} else {
return [];
}
}

/// If you need more fine-grained control over store trigger handling,
/// override this method to return a Map of stores to handlers.
///
/// Whenever a store in the returned map triggers, the respective handler will be called.
///
/// Handlers defined here take precedence over the [redrawOn] handling.
/// If possible, however, [redrawOn] should be used instead of this in order
/// to avoid keeping additional state within this component and manually
/// managing redraws.
Map<Store, Function> getStoreHandlers() {
return {};
}

/// Register a [subscription] that should be canceled when the component unmounts.
///
/// Cancellation will be handled automatically by [componentWillUnmount].
void addSubscription(StreamSubscription subscription) {
_subscriptions.add(subscription);
}
}
Loading

0 comments on commit 3908a9d

Please sign in to comment.