brew-js-react 0.6.3 → 0.6.5

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/view.js CHANGED
@@ -1,9 +1,10 @@
1
- import { Component, Fragment, createContext, createElement, useContext, useEffect, useState } from "react";
2
- import { useAsync } from "zeta-dom-react";
1
+ import { Component, createContext, createElement, useContext, useLayoutEffect } from "react";
2
+ import { createAsyncScope, useAsync } from "zeta-dom-react";
3
3
  import dom, { reportError } from "zeta-dom/dom";
4
4
  import { ZetaEventContainer } from "zeta-dom/events";
5
- import { any, arrRemove, catchAsync, createPrivateStore, defineObservableProperty, defineOwnProperty, definePrototype, each, exclude, executeOnce, extend, freeze, grep, isArray, isFunction, isThenable, isUndefinedOrNull, keys, makeArray, map, noop, pick, randomId, setImmediate, single, throwNotFunction, watch } from "zeta-dom/util";
5
+ import { always, any, arrRemove, catchAsync, createPrivateStore, defineObservableProperty, defineOwnProperty, definePrototype, delay, each, exclude, executeOnce, extend, fill, freeze, grep, isArray, isFunction, isPlainObject, isThenable, isUndefinedOrNull, keys, makeArray, map, noop, pick, randomId, setImmediate, single, throwNotFunction, watch } from "zeta-dom/util";
6
6
  import { animateIn, animateOut } from "brew-js/anim";
7
+ import { toQueryString } from "brew-js/util/common";
7
8
  import { removeQueryAndHash } from "brew-js/util/path";
8
9
  import { app, onAppInit } from "./app.js";
9
10
  import { ViewStateContainer } from "./hooks.js";
@@ -20,7 +21,7 @@ const StateContext = createContext(rootContext);
20
21
 
21
22
  var errorView;
22
23
  /** @type {Partial<Zeta.ZetaEventType<"beforepageload", Brew.RouterEventMap, Element>>} */
23
- var event = {};
24
+ var event = { waitFor: noop };
24
25
 
25
26
  onAppInit(function () {
26
27
  app.on('beforepageload', function (e) {
@@ -41,7 +42,7 @@ onAppInit(function () {
41
42
  rootState.setPage(app.page);
42
43
  });
43
44
 
44
- function ViewContext(view, page, parent) {
45
+ export function ViewContext(view, page, parent) {
45
46
  var self = this;
46
47
  defineOwnProperty(self, 'view', view || null, true);
47
48
  defineOwnProperty(self, 'parent', parent || null, true);
@@ -59,6 +60,7 @@ function ViewContext(view, page, parent) {
59
60
  });
60
61
  });
61
62
  }
63
+ defineOwnProperty(ViewContext, 'root', rootContext, true);
62
64
 
63
65
  definePrototype(ViewContext, {
64
66
  getChildren: function () {
@@ -66,72 +68,93 @@ definePrototype(ViewContext, {
66
68
  return v.currentContext;
67
69
  });
68
70
  },
71
+ setErrorView: function (errorView, error) {
72
+ var wrapper = _(this).wrapper;
73
+ return wrapper && errorView && !wrapper.setState({ error, errorView });
74
+ },
69
75
  on: function (event, handler) {
70
76
  return emitter.add(this, event, handler);
71
77
  }
72
78
  });
73
79
 
74
- function ErrorBoundary() {
75
- Component.apply(this, arguments);
80
+ function ErrorBoundary(props) {
81
+ Component.call(this, props);
76
82
  this.state = {};
83
+ _(props.context).wrapper = this;
77
84
  }
78
- ErrorBoundary.contextType = StateContext;
79
85
 
80
86
  definePrototype(ErrorBoundary, Component, {
87
+ componentDidMount: function () {
88
+ var self = this;
89
+ self.componentWillUnmount = watch(self.props.context, 'page', function () {
90
+ self.state.errorView = null;
91
+ self.forceUpdate();
92
+ });
93
+ },
81
94
  componentDidCatch: function (error) {
82
95
  var self = this;
83
- if (errorView && !self.state.error) {
84
- self.setState({ error });
96
+ if (errorView && self.state.errorView !== errorView) {
97
+ self.setState({ error, errorView });
85
98
  } else {
86
- // ensure promise sent to beforepageload event is resolved
87
- self.props.onComponentLoaded();
88
- reportError(error, self.context.container);
99
+ self.props.onLoad();
100
+ reportError(error, self.props.context.container);
89
101
  }
90
102
  },
91
103
  render: function () {
92
104
  var self = this;
93
- if (!self.context.container) {
105
+ var context = self.props.context;
106
+ if (!context.container) {
94
107
  setImmediate(function () {
108
+ extend(self, createAsyncScope(context.container));
109
+ dom.on(context.container, 'error', function (e) {
110
+ return emitter.emit(e, context, { error: e.error });
111
+ });
95
112
  self.forceUpdate();
96
113
  });
97
114
  return null;
98
115
  }
99
- var props = {
100
- view: self.context.view,
101
- error: self.state.error,
102
- reset: self.reset.bind(self)
103
- };
104
- var onComponentLoaded = self.props.onComponentLoaded;
105
- var onError = self.componentDidCatch.bind(self);
106
- if (props.error) {
107
- return createElement(errorView, { onComponentLoaded, onError, viewProps: props });
116
+ var errorView = self.state.errorView;
117
+ if (errorView) {
118
+ self.props.onLoad();
119
+ return createElement(self.Provider, null, createElement(self.state.errorView, {
120
+ view: context.view,
121
+ error: self.state.error,
122
+ reset: self.reset.bind(self)
123
+ }));
108
124
  }
109
- return createElement(props.view, { onComponentLoaded, onError, viewProps: self.props.viewProps });
125
+ var onError = self.componentDidCatch.bind(self);
126
+ var viewProps = {
127
+ errorHandler: self.errorHandler,
128
+ navigationType: event.navigationType,
129
+ viewContext: context,
130
+ viewData: context.page.data || {}
131
+ };
132
+ return createElement(self.Provider, null, createElement(context.view, extend({ viewProps, onError }, self.props)));
110
133
  },
111
134
  reset: function () {
112
- this.setState({ error: null });
135
+ this.setState({ errorView: null });
113
136
  }
114
137
  });
115
138
 
116
139
  function ViewContainer() {
117
140
  Component.apply(this, arguments);
141
+ this.views = [];
118
142
  }
119
143
  ViewContainer.contextType = StateContext;
120
144
 
121
145
  definePrototype(ViewContainer, Component, {
122
- setActive: noop,
123
146
  componentDidMount: function () {
124
147
  var self = this;
125
148
  var parent = _(self.context).children;
126
149
  var unwatch = watch(app.route, function () {
127
- self.setActive(self.getViewComponent() === self.currentViewComponent);
150
+ self.setActive(self.getViewComponent() === (self.currentContext || '').view);
128
151
  });
129
152
  self.componentWillUnmount = function () {
130
153
  self.setActive(false);
131
154
  arrRemove(parent, self);
132
155
  unwatch();
133
156
  setImmediate(function () {
134
- if (self.unmountView && !self.currentContext.active) {
157
+ if (self.currentContext && !self.currentContext.active) {
135
158
  self.unmountView();
136
159
  }
137
160
  });
@@ -145,13 +168,13 @@ definePrototype(ViewContainer, Component, {
145
168
  if (self.context.active) {
146
169
  self.updateView();
147
170
  }
148
- (self.onRender || noop)();
149
- return createElement(Fragment, null, self.prevView, self.currentView);
171
+ self.onRender();
172
+ return self.views;
150
173
  },
151
174
  updateView: function () {
152
175
  var self = this;
153
176
  var V = self.getViewComponent();
154
- var viewChanged = V !== self.currentViewComponent;
177
+ var viewChanged = V !== (self.currentContext || '').view;
155
178
  if (V && (viewChanged || !(self.children || '')[0])) {
156
179
  // ensure the current path actually corresponds to the matched view
157
180
  // when some views are not included in the list of allowed views
@@ -161,64 +184,68 @@ definePrototype(ViewContainer, Component, {
161
184
  return;
162
185
  }
163
186
  }
164
- if (V && viewChanged) {
165
- (self.unmountView || noop)(true);
166
-
187
+ var state = routeMap.get(V) || {};
188
+ if ((self.views[2] || '').key === state.id) {
189
+ return;
190
+ }
191
+ self.views[2] = null;
192
+ self.abort();
193
+ if (!V || !viewChanged) {
194
+ self.setActive(true);
195
+ self.setPage(app.page);
196
+ return;
197
+ }
198
+ event.waitFor(new Promise(function (resolve) {
167
199
  var context = new ViewContext(V, app.page, self.context);
168
- var state = routeMap.get(V);
169
- var onComponentLoaded;
170
- var promise = new Promise(function (resolve) {
171
- onComponentLoaded = resolve;
172
- });
173
- var unmountView = onComponentLoaded;
200
+ var rootProps = self.props.rootProps;
174
201
  var initElement = executeOnce(function (element) {
175
- context.container = element;
176
- promise.then(function () {
177
- if (self.currentContext === context) {
178
- unmountView = function () {
179
- self.prevView = self.currentView;
180
- app.emit('pageleave', element, { pathname: context.page.path, view: V }, true);
181
- animateOut(element, 'show').then(function () {
182
- self.prevView = undefined;
183
- self.forceUpdate();
184
- });
185
- };
186
- animateIn(element, 'show', '[brew-view]', true);
187
- app.emit('pageenter', element, { pathname: context.page.path, view: V }, true);
188
- }
189
- });
202
+ defineOwnProperty(context, 'container', element, true);
203
+ self.currentContext = self.currentContext || context;
190
204
  });
191
- var viewProps = function () {
192
- return freeze({
193
- navigationType: event.navigationType,
194
- viewContext: context,
195
- viewData: context.page.data || {}
196
- });
197
- };
198
- var view = createElement(StateContext.Provider, { key: state.id, value: context },
199
- createElement(ViewStateContainer, null,
200
- createElement('div', extend({}, self.props.rootProps, { ref: initElement, 'brew-view': '' }),
201
- createElement(ErrorBoundary, { onComponentLoaded, viewProps }))));
202
- extend(self, _(context), {
203
- currentContext: context,
204
- currentView: view,
205
- currentViewComponent: V,
206
- unmountView: executeOnce(function () {
207
- self.setActive(false);
205
+ var onLoad = executeOnce(function () {
206
+ var element = context.container;
207
+ var promise = self.unmountView();
208
+ self.unmountView = executeOnce(function () {
208
209
  state.rendered--;
209
- unmountView();
210
- })
210
+ self.setActive(false);
211
+ app.emit('pageleave', element, { pathname: context.page.path, view: V }, true);
212
+ return animateOut(element, 'show').then(function () {
213
+ self.views[0] = null;
214
+ self.forceUpdate();
215
+ });
216
+ });
217
+ always(promise, delay).then(function () {
218
+ app.emit('pageenter', element, { pathname: context.page.path, view: V }, true);
219
+ });
220
+ self.views.shift();
221
+ self.currentContext = context;
222
+ extend(self, _(context));
223
+ state.rendered++;
224
+ animateIn(element, 'show', '[brew-view]', true);
225
+ resolve();
211
226
  });
212
- state.rendered++;
213
- (event.waitFor || noop)(promise);
214
- }
215
- (self.setPage || noop)(app.page);
227
+ context.on('error', function () {
228
+ return (rootProps.onError || noop).apply(this, arguments);
229
+ });
230
+ self.abort = resolve;
231
+ self.views[2] = createElement(StateContext.Provider, { key: state.id, value: context },
232
+ createElement(ViewStateContainer, null,
233
+ createElement('div', extend(exclude(rootProps, ['loader', 'onError']), { ref: initElement, 'brew-view': '' }),
234
+ createElement(ErrorBoundary, { onLoad, context, self, loader: rootProps.loader }))));
235
+ }));
216
236
  },
217
237
  getViewComponent: function () {
218
238
  var props = this.props;
219
- return any(props.views, isViewMatched) || props.defaultView;
239
+ return any(props.views, function (v) {
240
+ return matchViewParams(v, app.route);
241
+ }) || props.defaultView;
220
242
  }
221
243
  });
244
+ fill(ViewContainer.prototype, 'abort onRender setActive setPage unmountView', noop);
245
+
246
+ function normalizePart(value, part) {
247
+ return isUndefinedOrNull(value) || value === '' || value === part ? '' : value[0] === part ? value : part + value;
248
+ }
222
249
 
223
250
  function getCurrentParams(view, params) {
224
251
  var state = routeMap.get(view);
@@ -271,31 +298,24 @@ function createViewComponent(factory) {
271
298
  factory = createElement.bind(null, factory);
272
299
  }
273
300
  return function fn(props) {
274
- var viewContext = useContext(StateContext);
275
- var viewProps = useState(props.viewProps);
276
- var children = !promise && factory(viewProps[0]);
301
+ var children = promise || factory(props.viewProps);
277
302
  if (isThenable(children)) {
278
303
  promise = children;
279
- children = null;
280
304
  catchAsync(promise);
305
+ } else {
306
+ useLayoutEffect(props.onLoad, []);
307
+ return children;
281
308
  }
282
- var state = useAsync(function () {
283
- return promise.then(null, props.onError);
284
- }, !!promise)[1];
285
- var loaded = !promise || !state.loading;
286
- useEffect(function () {
287
- state.elementRef(viewContext.container);
288
- // listen to property directly so that it is invoked after pagechange event handlers in actual component
289
- return watch(viewContext, 'page', function () {
290
- viewProps[1](props.viewProps);
309
+ var component = useAsync(function () {
310
+ return promise.then(null, function (error) {
311
+ promise = null;
312
+ props.onError(error);
291
313
  });
292
- }, []);
293
- useEffect(function () {
294
- if (loaded) {
295
- setImmediate(props.onComponentLoaded);
296
- }
297
- }, [loaded]);
298
- return children || (state.value ? createElement(state.value.default, viewProps[0]) : null);
314
+ })[0];
315
+ if (component) {
316
+ props.onLoad();
317
+ }
318
+ return component ? createElement(component.default, props.viewProps) : (props.self.currentContext === props.context && props.loader) || null;
299
319
  };
300
320
  }
301
321
 
@@ -304,7 +324,8 @@ export function useViewContext() {
304
324
  }
305
325
 
306
326
  export function isViewMatched(view) {
307
- return matchViewParams(view, app.route);
327
+ var route = app.route;
328
+ return matchViewParams(view, route) && resolvePath(view, route) === route.toString();
308
329
  }
309
330
 
310
331
  export function isViewRendered(view) {
@@ -347,7 +368,7 @@ export function registerView(factory, routeParams) {
347
368
  }
348
369
 
349
370
  export function registerErrorView(factory) {
350
- errorView = createViewComponent(factory);
371
+ errorView = throwNotFunction(factory);
351
372
  }
352
373
 
353
374
  export function renderView() {
@@ -364,10 +385,12 @@ export function renderView() {
364
385
  }
365
386
 
366
387
  export function resolvePath(view, params) {
367
- if (!routeMap.has(view)) {
368
- return '/';
388
+ var suffix = '';
389
+ if (isArray(params)) {
390
+ suffix = normalizePart(isPlainObject(params[1]) ? toQueryString(params[1]) : params[1], '?') + normalizePart(params[2], '#');
391
+ params = params[0];
369
392
  }
370
- return app.route.getPath(getCurrentParams(view, params));
393
+ return (routeMap.has(view) ? app.route.getPath(getCurrentParams(view, params)) : '/') + suffix;
371
394
  }
372
395
 
373
396
  export function linkTo(view, params) {