Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
67 changes: 48 additions & 19 deletions lib/src/component_declaration/flux_component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -151,33 +151,62 @@ abstract class _FluxComponentMixin<TProps extends FluxUiProps> implements Batche
/// These subscriptions are canceled when the component is unmounted.
List<StreamSubscription> _subscriptions = [];

// This is private and called by classes to work around super-calls not being supported in mixins
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Whitespace-agnostic diff in this file may help when reviewing

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, StoreHandler> handlers = new Map.fromIterable(redrawOn(),
value: (_) => (_) => redraw())..addAll(getStoreHandlers());

handlers.forEach((store, handler) {
String message = 'Cannot listen to a disposed/disposing Store.';

var isDisposedOrDisposing = store.isOrWillBeDisposed ?? false;

assert(!isDisposedOrDisposing, '$message This can be caused by BatchedRedraws '
void _validateStoreDisposalState(Store store) {
String message = 'Cannot listen to a disposed/disposing Store.';

var isDisposedOrDisposing = store.isOrWillBeDisposed ?? false;

assert(!isDisposedOrDisposing, '$message This can be caused by BatchedRedraws '
'mounting the component asynchronously after the store has been disposed. If you are '
'in a test environment, try adding an `await window.animationFrame;` before disposing your '
'store.');

if (isDisposedOrDisposing) _logger.warning(message);
if (isDisposedOrDisposing) _logger.warning(message);
}

// This is private and called by classes to work around super-calls not being supported in mixins
void _componentWillMount() {
// Subscribe to all applicable stores.
//
// Stores returned by `redrawOn()` will have their triggers mapped directly
// to `handleRedrawOn`, which invokes this component's redraw function.
//
// Stores included in the `getStoreHandlers()` result will be listened to
// and wired up to their respective handlers.
final customStoreHandlers = getStoreHandlers();
final storesWithoutCustomHandlers = redrawOn().where((store) => !customStoreHandlers.containsKey(store));

customStoreHandlers.forEach((store, handler) {
_validateStoreDisposalState(store);
StreamSubscription subscription = store.listen(handler);
_subscriptions.add(subscription);
});
storesWithoutCustomHandlers.forEach(listenToStoreForRedraw);
}

/// Used to register [handleRedrawOn] as a listener for the given [store].
///
/// Called for each of the stores returned by [redrawOn] that don't have custom
/// store handlers (defined in [getStoreHandlers]).
///
/// Override to set up custom listener behavior.
@protected
void listenToStoreForRedraw(Store store) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this have @mustCallSuper as well?

Copy link
Contributor Author

@greglittlefield-wf greglittlefield-wf Oct 24, 2018

Choose a reason for hiding this comment

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

Good question. No, since there are some cases where the listening setup might use a different mechanism.

Here's an example override

  @override
  void listenToStoreForRedraw(Store store) {
    if (store is TracedStore) {
      listenToStream(store.streamWithPayload, handleRedrawOnWithPayload);
    } else {
      super.listenToStoreForRedraw(store);
    }
  }

_validateStoreDisposalState(store);
_subscriptions.add(store.listen(handleRedrawOn));
}

/// Redraws the component for a given [store].
///
/// Called whenever an event is emitted by one of the stores returned by
/// [redrawOn] that don't have custom store handlers (defined in
/// [getStoreHandlers]).
///
/// Override and call super to add custom behavior.
@mustCallSuper
@protected
void handleRedrawOn(Store store) {
redraw();
}

// This is private and called by classes to work around super-calls not being supported in mixins
Expand Down
25 changes: 25 additions & 0 deletions test/over_react/component_declaration/flux_component_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ import 'package:over_react/over_react.dart';
import '../../test_util/test_util.dart';

part 'flux_component_test/basic.dart';
part 'flux_component_test/handler_lifecycle.dart';
part 'flux_component_test/handler_precedence.dart';
part 'flux_component_test/prop_validation.dart';
part 'flux_component_test/redraw_on.dart';
part 'flux_component_test/store_handlers.dart';
part 'flux_component_test/stateful/basic.dart';
part 'flux_component_test/stateful/handler_lifecycle.dart';
part 'flux_component_test/stateful/handler_precedence.dart';
part 'flux_component_test/stateful/prop_validation.dart';
part 'flux_component_test/stateful/redraw_on.dart';
Expand Down Expand Up @@ -138,6 +140,26 @@ void main() {
reason: 'component should no longer be listening');
});

test('should call lifecycle methods related to store handlers', () async {
final store = new TestStore();
var renderedInstance = render(testComponents.handlerLifecycle()..store = store);
dynamic component = getDartComponent(renderedInstance);

expect(component.lifecycleCalls, [
['listenToStoreForRedraw', store],
]);
component.lifecycleCalls.clear();

// Cause store to trigger, wait for it to propagate
store.trigger();
await nextTick();
await nextTick();

expect(component.lifecycleCalls, [
['handleRedrawOn', store],
]);
});

test('cancels any subscriptions added with addSubscription', () async {
// Setup a new subscription on a component
int numberOfCalls = 0;
Expand Down Expand Up @@ -225,6 +247,7 @@ abstract class BaseTestComponents {
TestPropValidationProps propValidation();
TestRedrawOnProps redrawOn();
TestStoreHandlersProps storeHandlers();
TestHandlerLifecycleProps handlerLifecycle();
}

class TestComponents extends BaseTestComponents {
Expand All @@ -233,6 +256,7 @@ class TestComponents extends BaseTestComponents {
@override TestPropValidationProps propValidation() => TestPropValidation();
@override TestRedrawOnProps redrawOn() => TestRedrawOn();
@override TestStoreHandlersProps storeHandlers() => TestStoreHandlers();
@override TestHandlerLifecycleProps handlerLifecycle() => TestHandlerLifecycle();
}

class TestStatefulComponents extends BaseTestComponents {
Expand All @@ -241,4 +265,5 @@ class TestStatefulComponents extends BaseTestComponents {
@override TestStatefulPropValidationProps propValidation() => TestStatefulPropValidation();
@override TestStatefulRedrawOnProps redrawOn() => TestStatefulRedrawOn();
@override TestStatefulStoreHandlersProps storeHandlers() => TestStatefulStoreHandlers();
@override TestStatefulHandlerLifecycleProps handlerLifecycle() => TestStatefulHandlerLifecycle();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
part of over_react.component_declaration.flux_component_test;

@Factory()
UiFactory<TestHandlerLifecycleProps> TestHandlerLifecycle;

@Props()
class TestHandlerLifecycleProps extends FluxUiProps<TestActions, TestStore> {}

@Component()
class TestHandlerLifecycleComponent extends FluxUiComponent<TestHandlerLifecycleProps> {
List<List<dynamic>> lifecycleCalls = [];

@override
void handleRedrawOn(Store store) {
lifecycleCalls.add(['handleRedrawOn', store]);
super.handleRedrawOn(store);
}

@override
void listenToStoreForRedraw(Store store) {
lifecycleCalls.add(['listenToStoreForRedraw', store]);
super.listenToStoreForRedraw(store);
}

@override
render() => Dom.div()();
}

Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ UiFactory<TestStatefulBasicProps> TestStatefulBasic;
@Props()
class TestStatefulBasicProps extends FluxUiProps implements TestBasicProps {}

@State()
class TestStatefulBasicState extends UiState {}

@Component()
class TestStatefulBasicComponent extends FluxUiComponent<TestStatefulBasicProps> {
class TestStatefulBasicComponent extends FluxUiStatefulComponent<TestStatefulBasicProps, TestStatefulBasicState> {
int numberOfRedraws = 0;

@override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
part of over_react.component_declaration.flux_component_test;

@Factory()
UiFactory<TestStatefulHandlerLifecycleProps> TestStatefulHandlerLifecycle;

@Props()
class TestStatefulHandlerLifecycleProps extends FluxUiProps<TestActions, TestStore> implements TestHandlerLifecycleProps {}

@State()
class TestStatefulHandlerLifecycleState extends UiState {}

@Component()
class TestStatefulHandlerLifecycleComponent extends FluxUiStatefulComponent<TestStatefulHandlerLifecycleProps, TestStatefulHandlerLifecycleState> {
List<List<dynamic>> lifecycleCalls = [];

@override
void handleRedrawOn(Store store) {
lifecycleCalls.add(['handleRedrawOn', store]);
super.handleRedrawOn(store);
}

@override
void listenToStoreForRedraw(Store store) {
lifecycleCalls.add(['listenToStoreForRedraw', store]);
super.listenToStoreForRedraw(store);
}

@override
render() => Dom.div()();
}

Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ UiFactory<TestStatefulHandlerPrecedenceProps> TestStatefulHandlerPrecedence;
@Props()
class TestStatefulHandlerPrecedenceProps extends FluxUiProps<TestActions, TestStores> implements TestHandlerPrecedenceProps {}

@State()
class TestStatefulHandlerPrecedenceState extends UiState {}

@Component()
class TestStatefulHandlerPrecedenceComponent extends FluxUiComponent<TestStatefulHandlerPrecedenceProps> {
class TestStatefulHandlerPrecedenceComponent extends FluxUiStatefulComponent<TestStatefulHandlerPrecedenceProps, TestStatefulHandlerPrecedenceState> {
int numberOfRedraws = 0;
int numberOfHandlerCalls = 0;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ class TestStatefulPropValidationProps extends FluxUiProps implements TestPropVal
String required;
}

@State()
class TestStatefulPropValidationState extends UiState {}

@Component()
class TestStatefulPropValidationComponent extends FluxUiComponent<TestStatefulPropValidationProps> {
class TestStatefulPropValidationComponent extends FluxUiStatefulComponent<TestStatefulPropValidationProps, TestStatefulPropValidationState> {
@override
render() => Dom.div()();
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ UiFactory<TestStatefulRedrawOnProps> TestStatefulRedrawOn;
@Props()
class TestStatefulRedrawOnProps extends FluxUiProps<TestActions, TestStores> implements TestRedrawOnProps {}

@State()
class TestStatefulRedrawOnState extends UiState {}

@Component()
class TestStatefulRedrawOnComponent extends FluxUiComponent<TestStatefulRedrawOnProps> {
class TestStatefulRedrawOnComponent extends FluxUiStatefulComponent<TestStatefulRedrawOnProps, TestStatefulRedrawOnState> {
int numberOfRedraws = 0;

@override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ UiFactory<TestStoreHandlersProps> TestStatefulStoreHandlers;
@Props()
class TestStatefulStoreHandlersProps extends FluxUiProps<TestActions, TestStore> implements TestStoreHandlersProps {}

@State()
class TestStatefulStoreHandlersState extends UiState {}

@Component()
class TestStatefulStoreHandlersComponent extends FluxUiComponent<TestStatefulStoreHandlersProps> {
class TestStatefulStoreHandlersComponent extends FluxUiStatefulComponent<TestStatefulStoreHandlersProps, TestStatefulStoreHandlersState> {
int numberOfHandlerCalls = 0;

@override
Expand Down