Skip to content

Commit adc16aa

Browse files
Merge version 1.33.0 into master_dart1
2 parents 4456be0 + 0beb273 commit adc16aa

File tree

5 files changed

+232
-0
lines changed

5 files changed

+232
-0
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# OverReact Changelog
22

3+
## 1.33.0
4+
5+
> [Complete `1.33.0` Changeset](https://github.com/Workiva/over_react/compare/1.32.0...1.33.0)
6+
7+
* [#266] Add `ErrorBoundary` Component
8+
> This component does not actually hook into any ReactJS 16 lifecycle yet. It won't until support for ReactJS 16 is added to react-dart in version 5.0.0, and to over_react in version 3.0.0.
9+
310
## 1.32.0
411

512
> [Complete `1.32.0` Changeset](https://github.com/Workiva/over_react/compare/1.31.0...1.32.0)

lib/over_react.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export 'src/component/abstract_transition.dart';
3333
export 'src/component/abstract_transition_props.dart';
3434
export 'src/component/aria_mixin.dart';
3535
export 'src/component/callback_typedefs.dart';
36+
export 'src/component/error_boundary.dart';
3637
export 'src/component/dom_components.dart';
3738
export 'src/component/dummy_component.dart';
3839
export 'src/component/prop_mixins.dart';
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import 'package:meta/meta.dart';
2+
import 'package:over_react/over_react.dart';
3+
4+
// ignore: uri_has_not_been_generated
5+
part 'error_boundary.over_react.g.dart';
6+
7+
// TODO: Need to type the second argument once react-dart implements bindings for the ReactJS "componentStack".
8+
typedef _ComponentDidCatchCallback(Error error, /*ComponentStack*/dynamic componentStack);
9+
10+
// TODO: Need to type the second argument once react-dart implements bindings for the ReactJS "componentStack".
11+
typedef ReactElement _FallbackUiRenderer(Error error, /*ComponentStack*/dynamic componentStack);
12+
13+
/// A higher-order component that will catch ReactJS errors anywhere within the child component tree and
14+
/// display a fallback UI instead of the component tree that crashed.
15+
///
16+
/// Optionally, use the [ErrorBoundaryProps.onComponentDidCatch]
17+
/// to send error / stack trace information to a logging endpoint for your application.
18+
///
19+
/// > __NOTE: This component does not yet do any of this__.
20+
/// >
21+
/// > It will begin providing the boundary / fallback UI behavior once support
22+
/// for ReactJS 16 is released in over_react version 3.0.0
23+
@Factory()
24+
// ignore: undefined_identifier
25+
UiFactory<ErrorBoundaryProps> ErrorBoundary = $ErrorBoundary;
26+
27+
@Props()
28+
class _$ErrorBoundaryProps extends UiProps {
29+
/// An optional callback that will be called with an [Error] and a `ComponentStack`
30+
/// containing information about which component in the tree threw the error when
31+
/// the `componentDidCatch` lifecycle method is called.
32+
///
33+
/// This callback can be used to log component errors like so:
34+
///
35+
/// (ErrorBoundary()
36+
/// ..onComponentDidCatch = (error, componentStack) {
37+
/// // It is up to you to implement the service / thing that calls the service.
38+
/// logComponentStackToAService(error, componentStack);
39+
/// }
40+
/// )(
41+
/// // The rest of your component tree
42+
/// )
43+
///
44+
/// > See: <https://reactjs.org/docs/react-component.html#componentdidcatch>
45+
_ComponentDidCatchCallback onComponentDidCatch;
46+
47+
/// A renderer that will be used to render "fallback" UI instead of the child
48+
/// component tree that crashed.
49+
///
50+
/// > Default: [ErrorBoundaryComponent._renderDefaultFallbackUI]
51+
_FallbackUiRenderer fallbackUIRenderer;
52+
}
53+
54+
@State()
55+
class _$ErrorBoundaryState extends UiState {
56+
/// Whether the tree that the [ErrorBoundary] is wrapping around threw an error.
57+
///
58+
/// When `true`, fallback UI will be rendered using [ErrorBoundaryProps.fallbackUIRenderer].
59+
bool hasError;
60+
}
61+
62+
@Component(isWrapper: true)
63+
class ErrorBoundaryComponent<T extends ErrorBoundaryProps, S extends ErrorBoundaryState>
64+
extends UiStatefulComponent<T, S> {
65+
Error _error;
66+
/*ComponentStack*/dynamic _componentStack;
67+
68+
@override
69+
Map getDefaultProps() => (newProps()
70+
..fallbackUIRenderer = _renderDefaultFallbackUI
71+
);
72+
73+
@override
74+
Map getInitialState() => (newState()
75+
..hasError = false
76+
);
77+
78+
@mustCallSuper
79+
/*@override*/
80+
S getDerivedStateFromError(_) {
81+
return newState()..hasError = true;
82+
}
83+
84+
@mustCallSuper
85+
/*@override*/
86+
void componentDidCatch(Error error, /*ComponentStack*/dynamic componentStack) {
87+
_error = error;
88+
_componentStack = componentStack;
89+
90+
if (props.onComponentDidCatch != null) {
91+
props.onComponentDidCatch(error, componentStack);
92+
}
93+
}
94+
95+
@override
96+
render() => state.hasError
97+
? props.fallbackUIRenderer(_error, _componentStack)
98+
: props.children.single;
99+
100+
ReactElement _renderDefaultFallbackUI(_, __) =>
101+
throw new UnimplementedError('Fallback UI will not be supported until support for ReactJS 16 is released in version 3.0.0');
102+
103+
@mustCallSuper
104+
@override
105+
void validateProps([Map appliedProps]) {
106+
super.validateProps(appliedProps);
107+
final children = domProps(appliedProps).children;
108+
109+
if (children.length != 1) {
110+
throw new PropError.value(children, 'children', 'ErrorBoundary accepts only a single child.');
111+
} else if (!isValidElement(children.single)) {
112+
throw new PropError.value(children, 'children', 'ErrorBoundary accepts only a single ReactComponent child.');
113+
}
114+
}
115+
}
116+
117+
// ignore: mixin_of_non_class, undefined_class
118+
class ErrorBoundaryProps extends _$ErrorBoundaryProps with _$ErrorBoundaryPropsAccessorsMixin {
119+
// ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value
120+
static const PropsMeta meta = $metaForErrorBoundaryProps;
121+
}
122+
123+
// ignore: mixin_of_non_class, undefined_class
124+
class ErrorBoundaryState extends _$ErrorBoundaryState with _$ErrorBoundaryStateAccessorsMixin {
125+
// ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value
126+
static const StateMeta meta = $metaForErrorBoundaryState;
127+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright 2019 Workiva Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
@Timeout(const Duration(seconds: 2))
16+
library error_boundary_test;
17+
18+
import 'package:over_react/over_react.dart';
19+
import 'package:over_react_test/over_react_test.dart';
20+
import 'package:test/test.dart';
21+
22+
void main() {
23+
group('ErrorBoundary', () {
24+
TestJacket<ErrorBoundaryComponent> jacket;
25+
ReactElement dummyChild;
26+
27+
setUp(() {
28+
dummyChild = Dom.div()('hi there');
29+
});
30+
31+
tearDown(() {
32+
jacket = null;
33+
dummyChild = null;
34+
});
35+
36+
// TODO: Add tests that exercise the actual ReactJS 16 error lifecycle methods once implemented.
37+
38+
test('initializes with the expected default prop values', () {
39+
jacket = mount(ErrorBoundary()(dummyChild));
40+
41+
expect(() => ErrorBoundary(jacket.getProps()).fallbackUIRenderer(null, null), throwsUnimplementedError);
42+
});
43+
44+
test('initializes with the expected initial state values', () {
45+
jacket = mount(ErrorBoundary()(dummyChild));
46+
47+
expect(jacket.getDartInstance().state.hasError, isFalse);
48+
});
49+
50+
group('renders', () {
51+
test('its child when `state.error` is false', () {
52+
jacket = mount(ErrorBoundary()(dummyChild));
53+
expect(jacket.getDartInstance().state.hasError, isFalse, reason: 'test setup sanity check');
54+
55+
expect(jacket.getNode().text, 'hi there');
56+
});
57+
58+
group('fallback UI when `state.error` is true', () {
59+
test('', () {
60+
jacket = mount(ErrorBoundary()(dummyChild));
61+
final component = jacket.getDartInstance();
62+
63+
// Using throws for now since this is temporary, and the throwsUnimplementedError doesn't work here for some reason
64+
expect(() => component.setState(component.newState()..hasError = true), throws);
65+
});
66+
67+
// TODO: Update this test to assert the error / component stack values passed to the callback once the actual ReactJS 16 error lifecycle methods are implemented.
68+
test('and props.fallbackUIRenderer is set', () {
69+
ReactElement _fallbackUIRenderer(_, __) {
70+
return Dom.h4()('Something super not awesome just happened.');
71+
}
72+
73+
jacket = mount((ErrorBoundary()..fallbackUIRenderer = _fallbackUIRenderer)(dummyChild));
74+
final component = jacket.getDartInstance();
75+
component.setState(component.newState()..hasError = true);
76+
77+
expect(jacket.getNode(), hasNodeName('H4'));
78+
expect(jacket.getNode().text, 'Something super not awesome just happened.');
79+
});
80+
});
81+
});
82+
83+
group('throws a PropError when', () {
84+
test('more than one child is provided', () {
85+
expect(() => mount(ErrorBoundary()(dummyChild, dummyChild)),
86+
throwsPropError_Value([dummyChild, dummyChild], 'children'));
87+
});
88+
89+
test('an invalid child is provided', () {
90+
expect(() => mount(ErrorBoundary()('oh hai')),
91+
throwsPropError_Value(['oh hai'], 'children'));
92+
});
93+
});
94+
});
95+
}

test/over_react_component_test.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import 'package:test/test.dart';
2525

2626
import 'over_react/component/abstract_transition_test.dart' as abstract_transition_test;
2727
import 'over_react/component/dom_components_test.dart' as dom_components_test;
28+
import 'over_react/component/error_boundary_test.dart' as error_boundary_test;
2829
import 'over_react/component/prop_mixins_test.dart' as prop_mixins_test;
2930
import 'over_react/component/resize_sensor_test.dart' as resize_sensor_test;
3031

@@ -35,6 +36,7 @@ void main() {
3536
enableTestMode();
3637

3738
abstract_transition_test.main();
39+
error_boundary_test.main();
3840
dom_components_test.main();
3941
prop_mixins_test.main();
4042
resize_sensor_test.main();

0 commit comments

Comments
 (0)