@brandup/ui-app 1.0.43 → 2.0.1

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/dist/mjs/index.js CHANGED
@@ -1,23 +1,44 @@
1
1
  import { UIElement } from '@brandup/ui';
2
2
  import { Guid } from '@brandup/ui-helpers';
3
3
 
4
+ /** Runs a chain of middlewares, invoking the named method on each in registration order. */
4
5
  class MiddlewareInvoker {
6
+ /** Middleware handled by this invoker node. */
5
7
  middleware;
6
8
  __next;
9
+ __tail = this;
10
+ /**
11
+ * @param middleware Middleware handled by this invoker node.
12
+ */
7
13
  constructor(middleware) {
8
14
  this.middleware = middleware;
9
15
  }
16
+ /**
17
+ * Append a middleware to the end of the chain.
18
+ * @param middleware Middleware to add.
19
+ */
10
20
  next(middleware) {
11
- if (this.__next)
12
- this.__next.next(middleware);
13
- else
14
- this.__next = new MiddlewareInvoker(middleware);
21
+ const invoker = new MiddlewareInvoker(middleware);
22
+ this.__tail.__next = invoker;
23
+ this.__tail = invoker;
15
24
  }
25
+ /**
26
+ * Invoke the given method across the whole middleware chain.
27
+ * @param method Name of the middleware method to call (e.g. "start", "navigate").
28
+ * @param context Invocation context.
29
+ * @returns Promise resolved when the chain has completed.
30
+ */
16
31
  invoke(method, context) {
17
32
  return this.__exec(method, context);
18
33
  }
19
34
  async __exec(method, context) {
20
- const nextFunc = () => this.__next ? this.__next.__exec(method, context) : Promise.resolve();
35
+ let nextCalled = false;
36
+ const nextFunc = () => {
37
+ if (nextCalled)
38
+ throw new Error(`Middleware "${this.middleware.name}" called next() more than once for method "${method}".`);
39
+ nextCalled = true;
40
+ return this.__next ? this.__next.__exec(method, context) : Promise.resolve();
41
+ };
21
42
  context.abort.throwIfAborted();
22
43
  const methodFunc = this.middleware[method];
23
44
  if (typeof methodFunc === "function") {
@@ -51,14 +72,18 @@ var constants = /*#__PURE__*/Object.freeze({
51
72
  default: result
52
73
  });
53
74
 
75
+ /** Unique name of the built-in state middleware. */
54
76
  const STATE_MIDDLEWARE_NAME = "app-state";
77
+ /**
78
+ * Create the built-in state middleware that toggles loading/loaded/ready CSS state
79
+ * classes on the application element during start, load, navigation and submit.
80
+ * @returns The state middleware instance.
81
+ */
55
82
  const StateMiddlewareFactory = () => {
56
83
  let counter = 0;
57
- //let beginTime: number;
58
84
  const begin = (context) => {
59
85
  const prev = counter++;
60
86
  if (prev === 0) {
61
- //beginTime = Date.now();
62
87
  context.app.element?.classList.remove(result.STATE_CLASS.LOADED);
63
88
  context.app.element?.classList.add(result.STATE_CLASS.LOADING);
64
89
  }
@@ -77,6 +102,9 @@ const StateMiddlewareFactory = () => {
77
102
  begin(context);
78
103
  try {
79
104
  await next();
105
+ // on success the begin() is left open on purpose: the loading state
106
+ // must persist through "loaded" and the first navigation, which closes
107
+ // it (see the "first" branch in navigate). On error we close it here.
80
108
  }
81
109
  catch (reason) {
82
110
  end(context);
@@ -99,12 +127,15 @@ const StateMiddlewareFactory = () => {
99
127
  await next();
100
128
  }
101
129
  finally {
130
+ end(context); // close the begin() of this navigation
131
+ // the first navigation also closes the loading opened by start(),
132
+ // which intentionally leaves its begin() open until the page is rendered
102
133
  if (context.source == "first")
103
134
  end(context);
104
- end(context);
105
135
  }
106
136
  },
107
137
  submit: async (context, next) => {
138
+ begin(context);
108
139
  try {
109
140
  await next();
110
141
  }
@@ -118,10 +149,15 @@ const StateMiddlewareFactory = () => {
118
149
  };
119
150
  };
120
151
 
152
+ /** Unique name of the built-in hyperlink middleware. */
121
153
  const HYPERLINK_MIDDLEWARE_NAME = "app-hyperlink";
154
+ /**
155
+ * Create the built-in hyperlink middleware that intercepts clicks on application links
156
+ * (anchors with the `applink` class or elements with `data-nav-url`) and routes them through
157
+ * application navigation. Honors meta/ctrl-click and `target="_blank"` to open in a new tab.
158
+ * @returns The hyperlink middleware instance.
159
+ */
122
160
  const HyperLinkMiddlewareFactory = () => {
123
- let __ctrlPressed = false;
124
- const onKeyDownUp = (e) => __ctrlPressed = e.ctrlKey;
125
161
  let onClick;
126
162
  return {
127
163
  name: HYPERLINK_MIDDLEWARE_NAME,
@@ -139,11 +175,9 @@ const HyperLinkMiddlewareFactory = () => {
139
175
  if (elem.classList.contains(result.NavUrlClassName) || elem.hasAttribute(result.NavUrlAttributeName))
140
176
  break;
141
177
  }
142
- if (elem === e.currentTarget)
143
- return;
144
178
  elem = elem.parentElement;
145
179
  }
146
- if (!elem || __ctrlPressed || elem.getAttribute("target") === "_blank")
180
+ if (!elem || e.ctrlKey || e.metaKey || elem.getAttribute("target") === "_blank")
147
181
  return;
148
182
  e.preventDefault();
149
183
  e.stopPropagation();
@@ -154,8 +188,12 @@ const HyperLinkMiddlewareFactory = () => {
154
188
  url = elem.getAttribute("href");
155
189
  else if (elem.hasAttribute(result.NavUrlAttributeName))
156
190
  url = elem.getAttribute(result.NavUrlAttributeName);
157
- else
158
- throw "Not found url for navigation.";
191
+ else {
192
+ // matched an applink element with no resolvable url (malformed markup);
193
+ // swallow the click rather than throwing inside a global event listener
194
+ console.warn("Application hyperlink: clicked element has no navigation url.");
195
+ return;
196
+ }
159
197
  if (elem.classList.contains(result.LoadingElementClass))
160
198
  return;
161
199
  elem.classList.add(result.LoadingElementClass);
@@ -169,18 +207,20 @@ const HyperLinkMiddlewareFactory = () => {
169
207
  .catch(() => { })
170
208
  .finally(() => elem.classList.remove(result.LoadingElementClass));
171
209
  }, false);
172
- window.addEventListener("keydown", onKeyDownUp, false);
173
- window.addEventListener("keyup", onKeyDownUp, false);
174
210
  },
175
211
  stop: (_context, next) => {
176
212
  window.removeEventListener("click", onClick, false);
177
- window.removeEventListener("keydown", onKeyDownUp, false);
178
- window.removeEventListener("keyup", onKeyDownUp, false);
179
213
  return next();
180
214
  }
181
215
  };
182
216
  };
183
217
 
218
+ /**
219
+ * Parse a url into its components relative to the application base path.
220
+ * @param basePath Application base path.
221
+ * @param url Url to parse. If null, the current `window.location` is used.
222
+ * @returns Parsed url parts.
223
+ */
184
224
  const parseUrl = (basePath, url) => {
185
225
  const loc = window.location;
186
226
  let origin = loc.origin;
@@ -212,7 +252,7 @@ const parseUrl = (basePath, url) => {
212
252
  path = loc.pathname;
213
253
  query = new URLSearchParams(url);
214
254
  }
215
- else if (url.startsWith("http")) {
255
+ else if (/^https?:\/\//i.test(url)) {
216
256
  const u = new URL(url);
217
257
  if (u.origin != origin) {
218
258
  origin = u.origin;
@@ -248,7 +288,7 @@ const parseUrl = (basePath, url) => {
248
288
  path = path.substring(0, path.length - 1);
249
289
  path = path.toLowerCase();
250
290
  if (basePath) {
251
- if (path.toLowerCase().startsWith(basePath.toLowerCase())) {
291
+ if (path.startsWith(basePath.toLowerCase())) {
252
292
  path = path.substring(basePath.length);
253
293
  if (!path)
254
294
  path = '/';
@@ -291,8 +331,10 @@ const extendQuery = (url, query) => {
291
331
  query.forEach((v, k) => url.query.append(k, v.toString()));
292
332
  }
293
333
  else {
294
- for (let key in query) {
334
+ for (const key in query) {
295
335
  const value = query[key];
336
+ if (value === null || typeof value === "undefined")
337
+ continue;
296
338
  if (!Array.isArray(value)) {
297
339
  url.query.set(key, value);
298
340
  }
@@ -314,6 +356,14 @@ const rebuildUrl = (parsedUrl) => {
314
356
  parsedUrl.relative = relativeUrl;
315
357
  parsedUrl.full = parsedUrl.hash ? `${parsedUrl.url}#${parsedUrl.hash}` : parsedUrl.url;
316
358
  };
359
+ /**
360
+ * Build a relative url from a base path with optional path, query and hash.
361
+ * @param basePath Application base path.
362
+ * @param path Optional path appended to the base path.
363
+ * @param query Optional query parameters.
364
+ * @param hash Optional hash.
365
+ * @returns Relative url with base path.
366
+ */
317
367
  const buildUrl = (basePath, path, query, hash) => {
318
368
  let url = basePath;
319
369
  if (url == '/')
@@ -358,6 +408,7 @@ const buildUrl = (basePath, path, query, hash) => {
358
408
  }
359
409
  return url;
360
410
  };
411
+ /** Url helper functions. */
361
412
  var urlHelper = {
362
413
  parseUrl,
363
414
  extendQuery,
@@ -369,7 +420,9 @@ var url = /*#__PURE__*/Object.freeze({
369
420
  default: urlHelper
370
421
  });
371
422
 
423
+ /** Type name of the base {@link Application} class. */
372
424
  const APP_TYPENAME = "brandup-ui-app";
425
+ /** Reason value thrown when a navigation is overridden (e.g. by a redirect). */
373
426
  const NAV_OVERIDE_ERROR = "NavigationOveride";
374
427
  /**
375
428
  * Base application class.
@@ -386,8 +439,13 @@ class Application extends UIElement {
386
439
  __isRuned;
387
440
  __middlewares = {};
388
441
  __globalSubmit;
442
+ __onPopStateHandler;
389
443
  __execNav; // current navigation invoking
390
444
  __lastNav; // last success navigation
445
+ /**
446
+ * @param env Application environment.
447
+ * @param model Application model.
448
+ */
391
449
  constructor(env, model, ..._args) {
392
450
  super();
393
451
  this.env = env;
@@ -396,6 +454,7 @@ class Application extends UIElement {
396
454
  this.invoker = new MiddlewareInvoker(core);
397
455
  this.__abort = new AbortController();
398
456
  }
457
+ /** Type name of the application. */
399
458
  get typeName() { return APP_TYPENAME; }
400
459
  /** Current navigation context. */
401
460
  get current() { return this.__lastNav?.context; }
@@ -408,25 +467,36 @@ class Application extends UIElement {
408
467
  this.__isInited = true;
409
468
  this.onInitialize();
410
469
  middlewares.forEach(middleware => {
411
- var name = middleware.name;
470
+ const name = middleware.name;
412
471
  if (this.__middlewares.hasOwnProperty(name))
413
472
  throw new Error(`Middleware "${name}" already registered.`);
414
473
  this.__middlewares[name] = middleware;
415
474
  this.invoker.next(middleware);
416
475
  });
417
476
  }
418
- /** Initialize application instance. */
477
+ /**
478
+ * Initialize application instance. Override to register additional middlewares.
479
+ * Registers the built-in state and hyperlink middlewares by default.
480
+ */
419
481
  onInitialize() {
420
482
  this.invoker.next(StateMiddlewareFactory());
421
483
  this.invoker.next(HyperLinkMiddlewareFactory());
422
484
  }
423
- /** Begin run application. */
485
+ /**
486
+ * Called at the beginning of application run, before middlewares start.
487
+ * Override to perform async setup work.
488
+ * @returns Promise resolved when starting work is complete.
489
+ */
424
490
  onStarting() { return Promise.resolve(); }
425
- /** Complate run application. */
491
+ /**
492
+ * Called after the application has fully started.
493
+ * Override to perform async work after start.
494
+ * @returns Promise resolved when post-start work is complete.
495
+ */
426
496
  onStared() { return Promise.resolve(); }
427
497
  /**
428
- * Get middleware by type.
429
- * @param type Type of middleware.
498
+ * Get registered middleware by its unique name.
499
+ * @param name Unique name of middleware.
430
500
  * @returns Middleware instance.
431
501
  */
432
502
  middleware(name) {
@@ -465,7 +535,7 @@ class Application extends UIElement {
465
535
  await this.invoker.invoke("loaded", context);
466
536
  console.info("app load success");
467
537
  this.__abort.signal.throwIfAborted();
468
- window.addEventListener("popstate", (e) => this.__onPopState(context, e));
538
+ window.addEventListener("popstate", this.__onPopStateHandler = (e) => this.__onPopState(context, e));
469
539
  element.addEventListener("submit", this.__globalSubmit = (e) => {
470
540
  const form = e.target;
471
541
  if (!form.classList.contains(result.FormClassName))
@@ -474,23 +544,24 @@ class Application extends UIElement {
474
544
  this.__onSubmit({ form, button: e.submitter instanceof HTMLButtonElement ? e.submitter : null })
475
545
  .catch(() => { });
476
546
  }, false);
477
- await this.onStared();
478
- console.info("app runned");
479
547
  }
480
548
  catch (reason) {
481
549
  console.error(`app run error: ${reason}`);
482
550
  throw reason;
483
551
  }
552
+ // Run the first navigation before onStared(): its navigate middleware always
553
+ // closes the startup loading state (in a finally, even on error), so the app
554
+ // can never get stuck "loading" if onStared throws.
484
555
  try {
485
556
  await this.nav({ data: context.data, abort: this.__abort.signal });
486
557
  }
487
558
  catch (reason) {
488
- if (reason === NAV_OVERIDE_ERROR) {
489
- console.info(`app run nav overided`);
490
- return context;
491
- }
492
- throw reason;
559
+ if (reason !== NAV_OVERIDE_ERROR)
560
+ throw reason;
561
+ console.info(`app run nav overided`);
493
562
  }
563
+ await this.onStared();
564
+ console.info("app runned");
494
565
  return context;
495
566
  }
496
567
  /**
@@ -510,54 +581,19 @@ class Application extends UIElement {
510
581
  action = "first";
511
582
  else {
512
583
  const isChangedUrl = this.__lastNav?.context.url.toLowerCase() !== navUrl.url.toLowerCase();
513
- const hasHash = !!this.__lastNav?.context.hash || !!navUrl.hash;
584
+ const hasHash = !!navUrl.hash;
514
585
  if (isChangedUrl)
515
- action = "url-change"; // если изменился url
586
+ action = "url-change"; // url changed
516
587
  else
517
588
  action = hasHash ? "hash" : "url-no-change";
518
589
  }
519
- let parentNav;
520
- if (this.__execNav && this.__execNav.status === "work") {
521
- parentNav = this.__execNav;
522
- parentNav.abort.abort(NAV_OVERIDE_ERROR);
523
- parentNav.context.overided = true;
524
- }
525
- const navIndex = parentNav ? parentNav.context.index + 1 : 1;
526
- const navAbort = new AbortController();
527
- const aborts = [this.__abort.signal, navAbort.signal];
528
- if (abort)
529
- aborts.push(abort);
530
- const complextAbort = AbortSignal.any(aborts);
590
+ const base = this.__beginNav(abort);
531
591
  const context = {
532
- index: navIndex,
533
- id: Guid.createGuid(),
534
- source: isFirst ? "first" : "nav",
535
- app: this,
536
- abort: complextAbort,
537
- current: this.__lastNav?.context,
538
- parent: parentNav?.context,
539
- overided: false,
540
- action: action,
541
- data,
542
- url: navUrl.url,
543
- origin: navUrl.origin,
544
- pathAndQuery: navUrl.relative,
545
- basePath: navUrl.basePath,
546
- path: navUrl.path,
547
- query: navUrl.query,
548
- hash: navUrl.hash,
549
- external: navUrl.external,
550
- replace,
551
- scope,
552
- redirect: async (options) => {
553
- complextAbort.throwIfAborted();
554
- const result = await this.nav(options);
555
- complextAbort.throwIfAborted();
556
- return result;
557
- }
592
+ ...this.__createContext(base, navUrl, isFirst ? "first" : "nav", action, data, replace),
593
+ scope
558
594
  };
559
- const currentNav = { method: "navigate", context, abort: navAbort, status: "work" };
560
- return await this.__execNavigate(currentNav, parentNav);
595
+ const currentNav = { method: "navigate", context, abort: base.navAbort, status: "work" };
596
+ return await this.__execNavigate(currentNav, base.parentNav);
561
597
  }
562
598
  /**
563
599
  * Reload page with nav.
@@ -573,9 +609,14 @@ class Application extends UIElement {
573
609
  this.__abort.signal.throwIfAborted();
574
610
  window.location.reload();
575
611
  }
612
+ /**
613
+ * Destroy application: abort pending navigations, run the middleware "stop" chain and release listeners.
614
+ * @param contextData Stop context data.
615
+ * @returns Promise of the stop context.
616
+ */
576
617
  async destroy(contextData) {
577
618
  if (this.__abort.signal.aborted)
578
- return Promise.reject('Application already destroyed.');
619
+ return Promise.reject(new Error('Application already destroyed.'));
579
620
  this.__abort.abort();
580
621
  console.info("app destroy begin");
581
622
  if (this.__execNav)
@@ -584,6 +625,8 @@ class Application extends UIElement {
584
625
  this.__lastNav.abort.abort();
585
626
  if (this.__globalSubmit)
586
627
  this.element?.removeEventListener("submit", this.__globalSubmit);
628
+ if (this.__onPopStateHandler)
629
+ window.removeEventListener("popstate", this.__onPopStateHandler);
587
630
  const destroyAbort = new AbortController();
588
631
  const context = {
589
632
  abort: destroyAbort.signal,
@@ -619,6 +662,10 @@ class Application extends UIElement {
619
662
  const { form, button = null, query, data = {} } = opt;
620
663
  if ((!button || !button.formNoValidate) && !form.checkValidity())
621
664
  throw new Error('Form is invalid.');
665
+ // guard before applying any loading class, otherwise a rejected re-entrant
666
+ // submit would leave the button stuck in the loading state
667
+ if (form.classList.contains(result.LoadingElementClass))
668
+ throw new Error('Form already submitting.');
622
669
  let replace = form.hasAttribute(result.NavUrlReplaceAttributeName);
623
670
  let method = form.method;
624
671
  let enctype = form.enctype;
@@ -634,62 +681,30 @@ class Application extends UIElement {
634
681
  if (button.hasAttribute(result.NavUrlReplaceAttributeName))
635
682
  replace = true;
636
683
  }
637
- if (form.classList.contains(result.LoadingElementClass))
638
- throw new Error('Form already submitting.');
639
684
  form.classList.add(result.LoadingElementClass);
640
685
  method = method.toUpperCase();
641
686
  try {
642
- if (method === "GET")
643
- await this.nav({ url, query: new FormData(form), data: data, replace, abort: opt.abort });
687
+ if (method === "GET") {
688
+ const getUrl = urlHelper.parseUrl(this.env.basePath, url);
689
+ urlHelper.extendQuery(getUrl, new FormData(form));
690
+ if (query)
691
+ urlHelper.extendQuery(getUrl, query);
692
+ await this.nav({ url: getUrl.url, data: data, replace, abort: opt.abort });
693
+ }
644
694
  else {
645
695
  const navUrl = urlHelper.parseUrl(this.env.basePath, url);
646
696
  if (query)
647
697
  urlHelper.extendQuery(navUrl, query);
648
- let parentNav;
649
- if (this.__execNav && this.__execNav.status === "work") {
650
- parentNav = this.__execNav;
651
- parentNav.abort.abort(NAV_OVERIDE_ERROR);
652
- parentNav.context.overided = true;
653
- }
654
- const navIndex = parentNav ? parentNav.context.index + 1 : 1;
655
- const submitAbort = new AbortController();
656
- const aborts = [this.__abort.signal, submitAbort.signal];
657
- if (opt.abort)
658
- aborts.push(opt.abort);
659
- const complextAbort = AbortSignal.any(aborts);
660
- let context = {
661
- index: navIndex,
662
- id: Guid.createGuid(),
663
- source: "submit",
664
- app: this,
665
- abort: complextAbort,
666
- current: this.__lastNav?.context,
667
- parent: parentNav?.context,
668
- overided: false,
669
- action: "submit",
670
- data,
698
+ const base = this.__beginNav(opt.abort);
699
+ const context = {
700
+ ...this.__createContext(base, navUrl, "submit", "submit", data, replace),
671
701
  form,
672
702
  button,
673
703
  method,
674
- enctype,
675
- url: navUrl.url,
676
- origin: navUrl.origin,
677
- pathAndQuery: navUrl.relative,
678
- basePath: navUrl.basePath,
679
- path: navUrl.path,
680
- query: navUrl.query,
681
- hash: navUrl.hash,
682
- external: navUrl.external,
683
- replace,
684
- redirect: async (options) => {
685
- complextAbort.throwIfAborted();
686
- const result = await this.nav(options);
687
- complextAbort.throwIfAborted();
688
- return result;
689
- }
704
+ enctype
690
705
  };
691
- const currentNav = { method: "submit", context, abort: submitAbort, status: "work" };
692
- await this.__execNavigate(currentNav);
706
+ const currentNav = { method: "submit", context, abort: base.navAbort, status: "work" };
707
+ await this.__execNavigate(currentNav, base.parentNav);
693
708
  }
694
709
  }
695
710
  finally {
@@ -701,7 +716,53 @@ class Application extends UIElement {
701
716
  __onPopState(_context, event) {
702
717
  const popUrl = location.href;
703
718
  console.log(`popstate: ${popUrl}`, event.state);
704
- this.nav({ url: popUrl, data: { popstate: event.state } });
719
+ this.nav({ url: popUrl, data: { popstate: event.state } })
720
+ .catch(() => { });
721
+ }
722
+ /** Detect parent (overriding) navigation and compose the abort signal shared by a new navigation. */
723
+ __beginNav(abort) {
724
+ let parentNav;
725
+ if (this.__execNav && this.__execNav.status === "work") {
726
+ parentNav = this.__execNav;
727
+ parentNav.abort.abort(NAV_OVERIDE_ERROR);
728
+ parentNav.context.overided = true;
729
+ }
730
+ const navAbort = new AbortController();
731
+ const aborts = [this.__abort.signal, navAbort.signal];
732
+ if (abort)
733
+ aborts.push(abort);
734
+ return { parentNav, navAbort, abort: AbortSignal.any(aborts) };
735
+ }
736
+ /** Build the navigation context fields shared by nav and submit. */
737
+ __createContext(base, navUrl, source, action, data, replace) {
738
+ const { parentNav, abort } = base;
739
+ return {
740
+ index: parentNav ? parentNav.context.index + 1 : 1,
741
+ id: Guid.createGuid(),
742
+ source,
743
+ app: this,
744
+ abort,
745
+ current: this.__lastNav?.context,
746
+ parent: parentNav?.context,
747
+ overided: false,
748
+ action,
749
+ data,
750
+ url: navUrl.url,
751
+ origin: navUrl.origin,
752
+ pathAndQuery: navUrl.relative,
753
+ basePath: navUrl.basePath,
754
+ path: navUrl.path,
755
+ query: navUrl.query,
756
+ hash: navUrl.hash,
757
+ external: navUrl.external,
758
+ replace,
759
+ redirect: async (options) => {
760
+ abort.throwIfAborted();
761
+ const result = await this.nav(options);
762
+ abort.throwIfAborted();
763
+ return result;
764
+ }
765
+ };
705
766
  }
706
767
  async __execNavigate(nav, parent) {
707
768
  if (parent)
@@ -737,51 +798,76 @@ class Application extends UIElement {
737
798
  }
738
799
  }
739
800
 
801
+ /** Fluent builder that configures the application type, middlewares and model, then constructs an {@link Application}. */
740
802
  class ApplicationBuilder {
741
803
  __model;
742
804
  __appType = (Application);
743
805
  __middlewares = [];
806
+ /**
807
+ * @param model Application model used when building the application.
808
+ */
744
809
  constructor(model) {
745
810
  this.__model = model;
746
811
  }
812
+ /**
813
+ * Set a custom application type to instantiate instead of the base {@link Application}.
814
+ * @param appType Application class constructor.
815
+ * @returns The builder, for chaining.
816
+ */
747
817
  useApp(appType) {
748
818
  this.__appType = appType;
749
819
  return this;
750
820
  }
821
+ /**
822
+ * Register a middleware via its factory function. Middlewares run in registration order.
823
+ * @param createFunc Factory that creates the middleware instance.
824
+ * @param params Optional arguments passed to the factory.
825
+ * @returns The builder, for chaining.
826
+ */
751
827
  useMiddleware(createFunc, ...params) {
752
828
  let midl = createFunc(...params);
753
829
  this.__middlewares.push(midl);
754
830
  return this;
755
831
  }
832
+ /**
833
+ * Build and initialize the application instance.
834
+ * @param env Application environment (a base path of `/` is normalized to empty).
835
+ * @param args Extra arguments forwarded to the application constructor.
836
+ * @returns The initialized application instance.
837
+ */
756
838
  build(env, ...args) {
757
- if (!env.basePath || env.basePath == '/')
758
- env.basePath = '';
759
- const app = new this.__appType(env, this.__model, ...args);
839
+ const appEnv = { ...env };
840
+ if (!appEnv.basePath || appEnv.basePath == '/')
841
+ appEnv.basePath = '';
842
+ const app = new this.__appType(appEnv, this.__model, ...args);
760
843
  app.initialize(this.__middlewares);
761
844
  return app;
762
845
  }
763
846
  }
764
847
 
765
- HTMLElement.prototype.navUrl = function (url) {
766
- if (this instanceof HTMLAnchorElement)
767
- this.href = url;
768
- else
769
- this.dataset.navUrl = url;
770
- this.classList.add(result.NavUrlClassName);
771
- return this;
772
- };
773
- HTMLElement.prototype.nav = function (app, path, query, hash) {
774
- const url = app.buildUrl(path, query, hash);
775
- return this.navUrl(url);
776
- };
777
- HTMLElement.prototype.navReplace = function () {
778
- this.setAttribute(result.NavUrlReplaceAttributeName, "");
779
- return this;
780
- };
781
- HTMLElement.prototype.navScope = function (scope) {
782
- this.setAttribute(result.NavUrlScopeAttributeName, scope);
783
- return this;
784
- };
848
+ // Guarded so importing the module does not throw in a non-DOM environment (SSR/Node).
849
+ if (typeof HTMLElement !== "undefined") {
850
+ HTMLElement.prototype.navUrl = function (url) {
851
+ if (this instanceof HTMLAnchorElement)
852
+ this.href = url;
853
+ else
854
+ this.dataset.navUrl = url;
855
+ this.classList.add(result.NavUrlClassName);
856
+ return this;
857
+ };
858
+ HTMLElement.prototype.nav = function (app, path, query, hash) {
859
+ const url = app.buildUrl(path, query, hash);
860
+ return this.navUrl(url);
861
+ };
862
+ HTMLElement.prototype.navReplace = function () {
863
+ this.setAttribute(result.NavUrlReplaceAttributeName, "");
864
+ return this;
865
+ };
866
+ HTMLElement.prototype.navScope = function (scope) {
867
+ this.setAttribute(result.NavUrlScopeAttributeName, scope);
868
+ return this;
869
+ };
870
+ }
785
871
 
786
872
  export { APP_TYPENAME, Application, ApplicationBuilder, constants as CONSTANTS, NAV_OVERIDE_ERROR, url as UrlHelper };
787
873
  //# sourceMappingURL=index.js.map