brew-js-react 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mixin.d.ts CHANGED
@@ -25,7 +25,7 @@ export * from "./mixins/ScrollableMixin";
25
25
  export {
26
26
  Mixin,
27
27
  AnimateMixin,
28
- AnimateSequenceItemMixin,
28
+ AnimateSequenceMixin,
29
29
  AnimateSequenceItemMixin,
30
30
  ClassNameMixin,
31
31
  FlyoutMixin,
package/package.json CHANGED
@@ -1,24 +1,19 @@
1
1
  {
2
2
  "name": "brew-js-react",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "unpkg": "dist/brew-js-react.min.js",
9
- "scripts": {
10
- "build": "webpack",
11
- "test": "cross-env NODE_OPTIONS=--experimental-vm-modules npx jest",
12
- "snapshot": "npm run test -- -u",
13
- "version": "npm run build && git add -A dist",
14
- "release": "node ./npm-publish.cjs"
15
- },
9
+ "scripts": {},
16
10
  "author": "misonou",
17
11
  "license": "ISC",
18
12
  "homepage": "https://hackmd.io/@misonou/brew-js-react",
19
13
  "repository": "github:misonou/brew-js-react",
20
14
  "dependencies": {
21
- "brew-js": ">=0.4.5",
15
+ "@misonou/react-dom-client": "^1.0.3",
16
+ "brew-js": ">=0.4.7",
22
17
  "waterpipe": "^2.5.0",
23
18
  "zeta-dom": ">=0.3.1",
24
19
  "zeta-dom-react": ">=0.2.1"
@@ -32,6 +27,7 @@
32
27
  "@babel/preset-env": "^7.16.11",
33
28
  "@babel/preset-react": "^7.16.7",
34
29
  "@jest/globals": "^26.6.2",
30
+ "@misonou/test-utils": "^1.0.2",
35
31
  "@testing-library/dom": "^8.11.3",
36
32
  "@testing-library/react": "^12.1.2",
37
33
  "@testing-library/react-hooks": "^7.0.2",
package/view.d.ts CHANGED
@@ -2,10 +2,26 @@ export type ViewComponentRootProps = React.DetailedHTMLProps<React.HTMLAttribute
2
2
  export type ViewComponent<P> = React.FC<P>;
3
3
 
4
4
  export interface ViewContainerState {
5
+ readonly container: HTMLElement;
5
6
  readonly view: ViewComponent<any>;
6
7
  readonly active: boolean;
7
8
  }
8
9
 
10
+ export interface ErrorViewProps {
11
+ /**
12
+ * Gets the original view component in which error has thrown.
13
+ */
14
+ view: ViewComponent<any>;
15
+ /**
16
+ * Gets the error thrown from the original view component.
17
+ */
18
+ error: any;
19
+ /**
20
+ * Re-renders the original view component with initial state.
21
+ */
22
+ reset(): void;
23
+ }
24
+
9
25
  export function useViewContainerState(): ViewContainerState;
10
26
 
11
27
  /**
@@ -16,6 +32,21 @@ export function useViewContainerState(): ViewContainerState;
16
32
  */
17
33
  export function registerView<P>(factory: () => Promise<{ default: React.ComponentType<P> }>, params: Zeta.Dictionary<null | string | RegExp | ((value: string) => boolean)>): ViewComponent<P>;
18
34
 
35
+ /**
36
+ * Registers view component with specific route paramters.
37
+ * Route parameters will be matched against current route state by {@link renderView}.
38
+ * @param component A React component.
39
+ * @param params A dictionary containing route parameters.
40
+ */
41
+ export function registerView<P>(component: React.ComponentType<P>, params: Zeta.Dictionary<null | string | RegExp | ((value: string) => boolean)>): ViewComponent<P>;
42
+
43
+ /**
44
+ * Registers a default error view to be displayed when view component failed to render.
45
+ * The previous registered error view, if any, will be overriden when called multiple times.
46
+ * @param component A react component.
47
+ */
48
+ export function registerErrorView(component: React.ComponentType<ErrorViewProps>): void;
49
+
19
50
  /**
20
51
  * Determines whether a registered view component matches current route state.
21
52
  * However it does not imply if the view is in fact being rendered.
package/view.js CHANGED
@@ -1,225 +1,277 @@
1
- import React, { useRef } from "react";
2
- import { combineRef, useAsync } from "zeta-dom-react";
3
- import dom from "./include/zeta-dom/dom.js";
4
- import { notifyAsync } from "./include/zeta-dom/domLock.js";
5
- import { any, combineFn, defineObservableProperty, definePrototype, each, exclude, executeOnce, extend, grep, isFunction, keys, makeArray, map, noop, pick, randomId, setImmediate, single, watch } from "./include/zeta-dom/util.js";
6
- import { animateIn, animateOut } from "./include/brew-js/anim.js";
7
- import { removeQueryAndHash } from "./include/brew-js/util/path.js";
8
- import { app } from "./app.js";
9
- import { ViewStateContainer } from "./hooks.js";
10
-
11
- const root = dom.root;
12
- const routeMap = new Map();
13
- const usedParams = {};
14
- const sortedViews = [];
15
- const StateContext = React.createContext(Object.freeze({ active: true }));
16
-
17
- function ViewContainer() {
18
- React.Component.apply(this, arguments);
19
- this.stateId = history.state;
20
- }
21
-
22
- definePrototype(ViewContainer, React.Component, {
23
- componentDidMount: function () {
24
- /** @type {any} */
25
- var self = this;
26
- self.componentWillUnmount = combineFn(
27
- watch(app.route, function () {
28
- self.setActive(self.getViewComponent() === self.currentViewComponent);
29
- }),
30
- app.on('beforepageload', function (e) {
31
- self.waitFor = e.waitFor;
32
- self.stateId = history.state;
33
- self.forceUpdate();
34
- })
35
- );
36
- },
37
- componentDidCatch: function (error) {
38
- dom.emit('error', this.parentElement || root, { error }, true);
39
- },
40
- render: function () {
41
- /** @type {any} */
42
- var self = this;
43
- if (history.state !== self.stateId) {
44
- return self.lastChild || null;
45
- }
46
- var V = self.getViewComponent();
47
- if (V) {
48
- // ensure the current path actually corresponds to the matched view
49
- // when some views are not included in the list of allowed views
50
- var targetPath = linkTo(V, getCurrentParams(V, true));
51
- if (targetPath !== removeQueryAndHash(app.path)) {
52
- app.navigate(targetPath, true);
53
- }
54
- }
55
- if (V && V !== self.currentViewComponent) {
56
- var prevElement = self.currentElement;
57
- if (prevElement) {
58
- self.setActive(false);
59
- self.prevView = self.currentView;
60
- self.currentElement = undefined;
61
- app.emit('pageleave', prevElement, { pathname: self.currentPath }, true);
62
- animateOut(prevElement, 'show').then(function () {
63
- self.prevView = undefined;
64
- self.forceUpdate();
65
- });
66
- }
67
- var resolve;
68
- var promise = new Promise(function (resolve_) {
69
- resolve = resolve_;
70
- });
71
- var state = { view: V };
72
- var view = React.createElement(StateContext.Provider, { key: routeMap.get(V).id, value: state },
73
- React.createElement(ViewStateContainer, null,
74
- React.createElement(V, {
75
- rootProps: self.props.rootProps,
76
- onComponentLoaded: executeOnce(function (element) {
77
- self.currentElement = element;
78
- self.parentElement = element.parentElement;
79
- setImmediate(function () {
80
- resolve();
81
- animateIn(element, 'show');
82
- app.emit('pageenter', element, { pathname: app.path }, true);
83
- });
84
- })
85
- })));
86
- extend(self, {
87
- currentPath: app.path,
88
- currentView: view,
89
- currentViewComponent: V,
90
- setActive: defineObservableProperty(state, 'active', true, true)
91
- });
92
- (self.waitFor || noop)(promise);
93
- notifyAsync(self.parentElement || root, promise);
94
- }
95
- var child = React.createElement(React.Fragment, null, self.prevView, self.currentView);
96
- self.lastChild = child;
97
- return child;
98
- },
99
- getViewComponent: function () {
100
- var props = this.props;
101
- return any(props.views, isViewMatched) || props.defaultView;
102
- }
103
- });
104
-
105
- function getCurrentParams(view, includeAll, params) {
106
- var state = routeMap.get(view);
107
- if (!state.maxParams) {
108
- var matchers = exclude(state.matchers, ['remainingSegments']);
109
- var matched = map(app.routes, function (v) {
110
- var route = app.parseRoute(v);
111
- var matched = route.length && !any(matchers, function (v, i) {
112
- var pos = route.params[i];
113
- return (v ? !(pos >= 0) : pos < route.minLength) || (!isFunction(v) && !route.match(i, v));
114
- });
115
- return matched ? route : null;
116
- });
117
- if (matched[1]) {
118
- matched = grep(matched, function (v) {
119
- return !single(v.params, function (v, i) {
120
- return usedParams[i] && !matchers[i];
121
- });
122
- });
123
- }
124
- if (matched[0]) {
125
- var last = matched.slice(-1)[0];
126
- state.maxParams = keys(extend.apply(0, [{}].concat(matched.map(function (v) {
127
- return v.params;
128
- }))));
129
- state.minParams = map(last.params, function (v, i) {
130
- return state.params[i] || v >= last.minLength ? null : i;
131
- });
132
- }
133
- }
134
- return pick(params || app.route, includeAll ? state.maxParams : state.minParams);
135
- }
136
-
137
- function sortViews(a, b) {
138
- return (routeMap.get(b) || {}).matchCount - (routeMap.get(a) || {}).matchCount;
139
- }
140
-
141
- function matchViewParams(view, route) {
142
- var params = routeMap.get(view);
143
- return !!params && !single(params.matchers, function (v, i) {
144
- var value = route[i] || '';
145
- return isFunction(v) ? !v(value) : (v || '') !== value;
146
- });
147
- }
148
-
149
- export function useViewContainerState() {
150
- return React.useContext(StateContext);
151
- }
152
-
153
- export function isViewMatched(view) {
154
- return matchViewParams(view, app.route);
155
- }
156
-
157
- export function matchView(path, views) {
158
- var route = app.route;
159
- if (typeof path === 'string') {
160
- route = route.parse(path);
161
- } else {
162
- views = path;
163
- }
164
- views = views ? makeArray(views).sort(sortViews) : sortedViews;
165
- return any(views, function (v) {
166
- return matchViewParams(v, route);
167
- }) || undefined;
168
- }
169
-
170
- export function registerView(factory, routeParams) {
171
- var Component = function (props) {
172
- var state = useAsync(factory);
173
- var ref = useRef();
174
- if (state[0] || state[1].error) {
175
- (props.onComponentLoaded || noop)(ref.current);
176
- }
177
- return React.createElement('div', extend({}, props.rootProps, {
178
- ref: combineRef(ref, state[1].elementRef),
179
- children: state[0] && React.createElement(state[0].default)
180
- }));
181
- };
182
- routeParams = extend({}, routeParams);
183
- each(routeParams, function (i, v) {
184
- usedParams[i] = true;
185
- if (v instanceof RegExp) {
186
- routeParams[i] = v.test.bind(v);
187
- }
188
- });
189
- routeMap.set(Component, {
190
- id: randomId(),
191
- matchCount: keys(routeParams).length,
192
- matchers: routeParams,
193
- params: pick(routeParams, function (v) {
194
- return typeof v === 'string';
195
- })
196
- });
197
- sortedViews.push(Component);
198
- sortedViews.sort(sortViews);
199
- return Component;
200
- }
201
-
202
- export function renderView() {
203
- var views = makeArray(arguments);
204
- var rootProps = isFunction(views[0]) ? {} : views.shift();
205
- var defaultView = views[0];
206
- views.sort(sortViews);
207
- return React.createElement(ViewContainer, { rootProps, views, defaultView });
208
- }
209
-
210
- export function linkTo(view, params) {
211
- var state = routeMap.get(view);
212
- if (!state) {
213
- return '/';
214
- }
215
- var newParams = extend(getCurrentParams(view), getCurrentParams(view, true, params), state.params);
216
- return app.route.getPath(newParams);
217
- }
218
-
219
- export function navigateTo(view, params) {
220
- return app.navigate(linkTo(view, params));
221
- }
222
-
223
- export function redirectTo(view, params) {
224
- return app.navigate(linkTo(view, params), true);
225
- }
1
+ import React from "react";
2
+ import { useAsync } from "zeta-dom-react";
3
+ import dom from "./include/zeta-dom/dom.js";
4
+ import { notifyAsync } from "./include/zeta-dom/domLock.js";
5
+ import { any, combineFn, defineObservableProperty, definePrototype, each, exclude, executeOnce, extend, grep, isFunction, isThenable, isUndefinedOrNull, keys, makeArray, map, noop, pick, randomId, single, throwNotFunction, watch } from "./include/zeta-dom/util.js";
6
+ import { animateIn, animateOut } from "./include/brew-js/anim.js";
7
+ import { removeQueryAndHash } from "./include/brew-js/util/path.js";
8
+ import { app } from "./app.js";
9
+ import { ViewStateContainer } from "./hooks.js";
10
+
11
+ const root = dom.root;
12
+ const routeMap = new Map();
13
+ const usedParams = {};
14
+ const sortedViews = [];
15
+ const StateContext = React.createContext(Object.freeze({ container: root, active: true }));
16
+
17
+ var errorView;
18
+
19
+ function ErrorBoundary() {
20
+ React.Component.apply(this, arguments);
21
+ this.state = {};
22
+ }
23
+ ErrorBoundary.contextType = StateContext;
24
+
25
+ definePrototype(ErrorBoundary, React.Component, {
26
+ componentDidCatch: function (error) {
27
+ var self = this;
28
+ if (errorView && !self.state.error) {
29
+ self.setState({ error });
30
+ } else {
31
+ dom.emit('error', self.context.container, { error }, true);
32
+ }
33
+ },
34
+ render: function () {
35
+ var self = this;
36
+ var props = {
37
+ view: self.context.view,
38
+ error: self.state.error,
39
+ reset: self.reset.bind(self)
40
+ };
41
+ var onComponentLoaded = self.props.onComponentLoaded;
42
+ if (props.error) {
43
+ return React.createElement(errorView, { onComponentLoaded, viewProps: props });
44
+ }
45
+ return React.createElement(props.view, { onComponentLoaded });
46
+ },
47
+ reset: function () {
48
+ this.setState({ error: null });
49
+ }
50
+ });
51
+
52
+ function ViewContainer() {
53
+ React.Component.apply(this, arguments);
54
+ this.stateId = history.state;
55
+ }
56
+
57
+ definePrototype(ViewContainer, React.Component, {
58
+ componentDidMount: function () {
59
+ /** @type {any} */
60
+ var self = this;
61
+ self.componentWillUnmount = combineFn(
62
+ watch(app.route, function () {
63
+ self.setActive(self.getViewComponent() === self.currentViewComponent);
64
+ }),
65
+ app.on('beforepageload', function (e) {
66
+ self.waitFor = e.waitFor;
67
+ self.stateId = history.state;
68
+ self.forceUpdate();
69
+ })
70
+ );
71
+ },
72
+ render: function () {
73
+ /** @type {any} */
74
+ var self = this;
75
+ if (history.state !== self.stateId) {
76
+ return self.lastChild || null;
77
+ }
78
+ var V = self.getViewComponent();
79
+ if (V) {
80
+ // ensure the current path actually corresponds to the matched view
81
+ // when some views are not included in the list of allowed views
82
+ var targetPath = linkTo(V, getCurrentParams(V, true));
83
+ if (targetPath !== removeQueryAndHash(app.path)) {
84
+ app.navigate(targetPath, true);
85
+ }
86
+ }
87
+ if (V && V !== self.currentViewComponent) {
88
+ var prevElement = self.currentElement;
89
+ if (prevElement) {
90
+ self.setActive(false);
91
+ self.prevView = self.currentView;
92
+ self.currentElement = undefined;
93
+ app.emit('pageleave', prevElement, { pathname: self.currentPath }, true);
94
+ animateOut(prevElement, 'show').then(function () {
95
+ self.prevView = undefined;
96
+ self.forceUpdate();
97
+ });
98
+ }
99
+ var onComponentLoaded;
100
+ var promise = new Promise(function (resolve) {
101
+ onComponentLoaded = resolve;
102
+ });
103
+ var initElement = executeOnce(function (element) {
104
+ self.currentElement = element;
105
+ state.container = element;
106
+ promise.then(function () {
107
+ animateIn(element, 'show');
108
+ app.emit('pageenter', element, { pathname: app.path }, true);
109
+ });
110
+ notifyAsync(element, promise);
111
+ });
112
+ var state = { view: V };
113
+ var view = React.createElement(StateContext.Provider, { key: routeMap.get(V).id, value: state },
114
+ React.createElement(ViewStateContainer, null,
115
+ React.createElement('div', extend({}, self.props.rootProps, { ref: initElement }),
116
+ React.createElement(ErrorBoundary, { onComponentLoaded }))));
117
+ extend(self, {
118
+ currentPath: app.path,
119
+ currentView: view,
120
+ currentViewComponent: V,
121
+ setActive: defineObservableProperty(state, 'active', true, true)
122
+ });
123
+ (self.waitFor || noop)(promise);
124
+ }
125
+ var child = React.createElement(React.Fragment, null, self.prevView, self.currentView);
126
+ self.lastChild = child;
127
+ return child;
128
+ },
129
+ getViewComponent: function () {
130
+ var props = this.props;
131
+ return any(props.views, isViewMatched) || props.defaultView;
132
+ }
133
+ });
134
+
135
+ function getCurrentParams(view, includeAll, params) {
136
+ var state = routeMap.get(view);
137
+ if (!state.maxParams) {
138
+ var matchers = exclude(state.matchers, ['remainingSegments']);
139
+ var matched = map(app.routes, function (v) {
140
+ var route = app.parseRoute(v);
141
+ var matched = route.length && !any(matchers, function (v, i) {
142
+ var pos = route.params[i];
143
+ return (v ? !(pos >= 0) : pos < route.minLength) || (!isFunction(v) && !route.match(i, v));
144
+ });
145
+ return matched ? route : null;
146
+ });
147
+ if (matched[1]) {
148
+ matched = grep(matched, function (v) {
149
+ return !single(v.params, function (v, i) {
150
+ return usedParams[i] && !matchers[i];
151
+ });
152
+ });
153
+ }
154
+ if (matched[0]) {
155
+ var last = matched.slice(-1)[0];
156
+ state.maxParams = keys(extend.apply(0, [{ remainingSegments: true }].concat(matched.map(function (v) {
157
+ return v.params;
158
+ }))));
159
+ state.minParams = map(last.params, function (v, i) {
160
+ return state.params[i] || v >= last.minLength ? null : i;
161
+ });
162
+ }
163
+ }
164
+ return pick(params || app.route, includeAll ? state.maxParams : state.minParams);
165
+ }
166
+
167
+ function sortViews(a, b) {
168
+ return (routeMap.get(b) || {}).matchCount - (routeMap.get(a) || {}).matchCount;
169
+ }
170
+
171
+ function matchViewParams(view, route) {
172
+ var params = routeMap.get(view);
173
+ return !!params && !single(params.matchers, function (v, i) {
174
+ var value = route[i] || '';
175
+ return isFunction(v) ? !v(value) : (v || '') !== value;
176
+ });
177
+ }
178
+
179
+ function createViewComponent(factory) {
180
+ var promise;
181
+ throwNotFunction(factory);
182
+ if (factory.prototype instanceof React.Component) {
183
+ factory = React.createElement.bind(null, factory);
184
+ }
185
+ return function (props) {
186
+ var viewProps = Object.freeze(props.viewProps || {});
187
+ var children = !promise && factory(viewProps);
188
+ if (isThenable(children)) {
189
+ promise = children;
190
+ children = null;
191
+ }
192
+ var state = useAsync(function () {
193
+ return promise.then(function (s) {
194
+ return React.createElement(s.default, viewProps);
195
+ });
196
+ }, !!promise)[1];
197
+ if (!promise || !state.loading) {
198
+ props.onComponentLoaded();
199
+ if (state.error) {
200
+ throw state.error;
201
+ }
202
+ }
203
+ return children || state.value || React.createElement(React.Fragment);
204
+ };
205
+ }
206
+
207
+ export function useViewContainerState() {
208
+ return React.useContext(StateContext);
209
+ }
210
+
211
+ export function isViewMatched(view) {
212
+ return matchViewParams(view, app.route);
213
+ }
214
+
215
+ export function matchView(path, views) {
216
+ var route = app.route;
217
+ if (typeof path === 'string') {
218
+ route = route.parse(path);
219
+ } else {
220
+ views = path;
221
+ }
222
+ views = views ? makeArray(views).sort(sortViews) : sortedViews;
223
+ return any(views, function (v) {
224
+ return matchViewParams(v, route);
225
+ }) || undefined;
226
+ }
227
+
228
+ export function registerView(factory, routeParams) {
229
+ var Component = createViewComponent(factory);
230
+ routeParams = extend({}, routeParams);
231
+ each(routeParams, function (i, v) {
232
+ usedParams[i] = true;
233
+ if (v instanceof RegExp) {
234
+ routeParams[i] = v.test.bind(v);
235
+ }
236
+ });
237
+ routeMap.set(Component, {
238
+ id: randomId(),
239
+ matchCount: keys(routeParams).length,
240
+ matchers: routeParams,
241
+ params: pick(routeParams, function (v) {
242
+ return isUndefinedOrNull(v) || typeof v === 'string';
243
+ })
244
+ });
245
+ sortedViews.push(Component);
246
+ sortedViews.sort(sortViews);
247
+ return Component;
248
+ }
249
+
250
+ export function registerErrorView(factory) {
251
+ errorView = createViewComponent(factory);
252
+ }
253
+
254
+ export function renderView() {
255
+ var views = makeArray(arguments);
256
+ var rootProps = isFunction(views[0]) ? {} : views.shift();
257
+ var defaultView = views[0];
258
+ views.sort(sortViews);
259
+ return React.createElement(ViewContainer, { rootProps, views, defaultView });
260
+ }
261
+
262
+ export function linkTo(view, params) {
263
+ var state = routeMap.get(view);
264
+ if (!state) {
265
+ return '/';
266
+ }
267
+ var newParams = extend(getCurrentParams(view), getCurrentParams(view, true, params || {}), state.params);
268
+ return app.route.getPath(newParams);
269
+ }
270
+
271
+ export function navigateTo(view, params) {
272
+ return app.navigate(linkTo(view, params));
273
+ }
274
+
275
+ export function redirectTo(view, params) {
276
+ return app.navigate(linkTo(view, params), true);
277
+ }
@@ -1,3 +0,0 @@
1
- {
2
- "type": "commonjs"
3
- }