lahama 5.0.0 → 6.0.0

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.
Files changed (2) hide show
  1. package/dist/lahama.js +302 -38
  2. package/package.json +1 -1
package/dist/lahama.js CHANGED
@@ -384,6 +384,7 @@ function createComponentNode(vdom, parentEl, index, hostComponent) {
384
384
  const { props, events } = extractPropsAndEvents(vdom);
385
385
  const component = new Component(props, events, hostComponent);
386
386
  component.setExternalContent(children);
387
+ component.setAppContext(hostComponent?.appContext ?? {});
387
388
  component.mount(parentEl, index);
388
389
  vdom.component = component;
389
390
  vdom.el = component.firstElement;
@@ -433,10 +434,253 @@ function removeFragmentNode(vdom) {
433
434
  children.forEach(destroyDom);
434
435
  }
435
436
 
436
- function createApp(RootComponent, props = {}) {
437
+ function makeRouteMatcher(route) {
438
+ return routeHasParams(route)
439
+ ? makeMatcherWithParams(route)
440
+ : makeMatcherWithoutParams(route)
441
+ }
442
+ function routeHasParams({ path }) {
443
+ return path.includes(':')
444
+ }
445
+ const CATCH_ALL_ROUTE = '*';
446
+ function makeRouteWithoutParamsRegex( { path }) {
447
+ if (path === CATCH_ALL_ROUTE) {
448
+ return new RegExp('^.*$')
449
+ }
450
+ return new RegExp(`^${path}`)
451
+ }
452
+ function makeMatcherWithoutParams(route) {
453
+ const regex = makeRouteWithoutParamsRegex(route);
454
+ const isRedirect = typeof route.redirect === 'string';
455
+ return {
456
+ route,
457
+ isRedirect,
458
+ checkMatch(path) {
459
+ return regex.test(path)
460
+ },
461
+ extractParams() {
462
+ return {}
463
+ },
464
+ extractQuery,
465
+ }
466
+ }
467
+ function extractQuery(path) {
468
+ const queryIndex = path.indexOf('?');
469
+ if (queryIndex === -1) {
470
+ return {}
471
+ }
472
+ const search = new URLSearchParams(path.slice(queryIndex + 1));
473
+ return Object.fromEntries(search.entries())
474
+ }
475
+ function makeRouteWithParamsRegex({ path }) {
476
+ const regex = path.replace(
477
+ /:([^/]+)/g,
478
+ (_, paramName) => `(?<${paramName}>[^/]+)`
479
+ );
480
+ return new RegExp(`^${regex}$`)
481
+ }
482
+ function makeMatcherWithParams(route) {
483
+ const regex = makeRouteWithParamsRegex(route);
484
+ const isRedirect = typeof route.redirect === 'string';
485
+ return {
486
+ route,
487
+ isRedirect,
488
+ checkMatch(path) {
489
+ return regex.test(path)
490
+ },
491
+ extractParams(path) {
492
+ const { groups } = regex.exec(path);
493
+ return groups
494
+ },
495
+ extractQuery,
496
+ }
497
+ }
498
+
499
+ class Dispatcher {
500
+ #subs = new Map()
501
+ #afterHandlers = []
502
+ subscribe(commandName, handler) {
503
+ if (!this.#subs.has(commandName)) {
504
+ this.#subs.set(commandName, []);
505
+ }
506
+ const handlersArray = this.#subs.get(commandName);
507
+ if (handlersArray.includes(handler)) {
508
+ return () => {
509
+ }
510
+ }
511
+ handlersArray.push(handler);
512
+ return () => {
513
+ const idx = handlersArray.indexOf(handler);
514
+ handlersArray.splice(idx, 1);
515
+ }
516
+ }
517
+ afterEveryCommand(handler) {
518
+ this.#afterHandlers.push(handler);
519
+ return () => {
520
+ const idx = this.#afterHandlers.indexOf(handler);
521
+ this.#afterHandlers.splice(idx, 1);
522
+ }
523
+ }
524
+ dispatch(commandName, payload) {
525
+ if (this.#subs.has(commandName)) {
526
+ this.#subs.get(commandName).forEach((handler) => handler(payload));
527
+ } else {
528
+ console.warn(`No handlers for command : ${commandName}`);
529
+ }
530
+ this.#afterHandlers.forEach((handler) => handler());
531
+ }
532
+ }
533
+
534
+ const ROUTER_EVENT = 'router-event';
535
+ class HashRouter {
536
+ #matchers = []
537
+ #isInitialized = false
538
+ #onPopState = () => this.#matchCurrentRoute()
539
+ #params = {}
540
+ #query = {}
541
+ #matchedRoute = null
542
+ #dispatcher = new Dispatcher()
543
+ #subscriptions = new WeakMap()
544
+ #subscriberFns = new Set()
545
+ get params() {
546
+ return this.#params
547
+ }
548
+ get query() {
549
+ return this.#query
550
+ }
551
+ get matchedRoute() {
552
+ return this.#matchedRoute
553
+ }
554
+ constructor(routes = []) {
555
+ this.#matchers = routes.map(makeRouteMatcher);
556
+ }
557
+ get #currentRouteHash() {
558
+ const hash = document.location.hash;
559
+ if (hash === '') {
560
+ return '/'
561
+ }
562
+ return hash.slice(1)
563
+ }
564
+ async init() {
565
+ if (this.#isInitialized) {
566
+ return
567
+ }
568
+ this.#isInitialized = true;
569
+ if (document.location.hash === '') {
570
+ window.history.replaceState({}, '', '#/');
571
+ }
572
+ window.addEventListener('popstate', this.#onPopState);
573
+ }
574
+ destroy() {
575
+ if (!this.#isInitialized) {
576
+ return
577
+ }
578
+ window.removeEventListener('popstate', this.#onPopState);
579
+ Array.from(this.#subscriberFns).forEach(this.unsubscribe, this);
580
+ this.#isInitialized = false;
581
+ }
582
+ #matchCurrentRoute() {
583
+ return this.navigateTo(this.#currentRouteHash)
584
+ }
585
+ async navigateTo(path) {
586
+ const matcher = this.#matchers.find((matcher) =>
587
+ matcher.checkMatch(path)
588
+ );
589
+ if (matcher == null) {
590
+ console.warn(`[Router] No route matches path "${path}"`);
591
+ this.#matchedRoute = null;
592
+ this.#params = {};
593
+ this.#query = {};
594
+ return
595
+ }
596
+ if (matcher.isRedirect) {
597
+ return this.navigateTo(matcher.route.redirect)
598
+ }
599
+ const from = this.#matchedRoute;
600
+ const to = matcher.route;
601
+ const { shouldNavigate, shouldRedirect, redirectPath } =
602
+ await this.#canChangeRoute(from, to);
603
+ if (shouldRedirect) {
604
+ return this.navigateTo(redirectPath)
605
+ }
606
+ if (shouldNavigate) {
607
+ this.#matchedRoute = matcher.route;
608
+ this.#params = matcher.extractParams(path);
609
+ this.#query = matcher.extractQuery(path);
610
+ this.#pushState(path);
611
+ this.#dispatcher.dispatch(ROUTER_EVENT, {from, to, router: this});
612
+ }
613
+ }
614
+ #pushState(path) {
615
+ window.history.pushState({}, '', `#${path}`);
616
+ }
617
+ back() {
618
+ window.history.back();
619
+ }
620
+ forward() {
621
+ window.history.forward();
622
+ }
623
+ subscribe(handler) {
624
+ const unsubscribe = this.#dispatcher.subscribe(ROUTER_EVENT, handler);
625
+ this.#subscriptions.set(handler, unsubscribe);
626
+ this.#subscriberFns.add(handler);
627
+ }
628
+ unsubscribe(handler) {
629
+ const unsubscribe = this.#subscriptions.get(handler);
630
+ if (unsubscribe) {
631
+ unsubscribe();
632
+ this.#subscriptions.delete(handler);
633
+ this.#subscriberFns.delete(handler);
634
+ }
635
+ }
636
+ async #canChangeRoute(from, to) {
637
+ const guard = to.beforeEnter;
638
+ if (typeof guard !== 'function') {
639
+ return {
640
+ shouldRedirect : false,
641
+ shouldNavigate : true,
642
+ redirectPath : null,
643
+ }
644
+ }
645
+ const result = await guard(from?.path, to?.path);
646
+ if (result === false) {
647
+ return {
648
+ shouldRedirect : false,
649
+ shouldNavigate : false,
650
+ redirectPath: null,
651
+ }
652
+ }
653
+ if (typeof result === 'string') {
654
+ return {
655
+ shouldRedirect : true,
656
+ shouldNavigate : true,
657
+ redirectPath: result,
658
+ }
659
+ }
660
+ return {
661
+ shouldRedirect : false,
662
+ shouldNavigate : true,
663
+ redirectPath : null,
664
+ }
665
+ }
666
+ }
667
+ class NoopRouter {
668
+ init() {}
669
+ destroy() {}
670
+ navigateTo() {}
671
+ back() {}
672
+ forward() {}
673
+ subscribe() {}
674
+ unsubscribe() {}
675
+ }
676
+
677
+ function createApp(RootComponent, props = {}, options = {}) {
437
678
  let parentEl = null;
438
679
  let isMounted = false;
439
680
  let vdom = null;
681
+ const context = {
682
+ router : options.router || new NoopRouter(),
683
+ };
440
684
  function reset() {
441
685
  parentEl = null;
442
686
  isMounted = false;
@@ -449,7 +693,8 @@ function createApp(RootComponent, props = {}) {
449
693
  }
450
694
  parentEl = _parentEl;
451
695
  vdom = h(RootComponent, props);
452
- mountDom(vdom, parentEl);
696
+ mountDom(vdom, parentEl, null, { appContext : context});
697
+ context.router.init();
453
698
  isMounted = true;
454
699
  },
455
700
  unmount() {
@@ -457,6 +702,7 @@ function createApp(RootComponent, props = {}) {
457
702
  throw new Error(`The application is not mounted`)
458
703
  }
459
704
  destroyDom(vdom);
705
+ context.router.destroy();
460
706
  reset();
461
707
  }
462
708
  }
@@ -739,41 +985,6 @@ function requireFastDeepEqual () {
739
985
  var fastDeepEqualExports = requireFastDeepEqual();
740
986
  var equal = /*@__PURE__*/getDefaultExportFromCjs(fastDeepEqualExports);
741
987
 
742
- class Dispatcher {
743
- #subs = new Map()
744
- #afterHandlers = []
745
- subscribe(commandName, handler) {
746
- if (!this.#subs.has(commandName)) {
747
- this.#subs.set(commandName, []);
748
- }
749
- const handlersArray = this.#subs.get(commandName);
750
- if (handlersArray.includes(handler)) {
751
- return () => {
752
- }
753
- }
754
- handlersArray.push(handler);
755
- return () => {
756
- const idx = handlersArray.indexOf(handler);
757
- handlersArray.splice(idx, 1);
758
- }
759
- }
760
- afterEveryCommand(handler) {
761
- this.#afterHandlers.push(handler);
762
- return () => {
763
- const idx = this.#afterHandlers.indexOf(handler);
764
- this.#afterHandlers.splice(idx, 1);
765
- }
766
- }
767
- dispatch(commandName, payload) {
768
- if (this.#subs.has(commandName)) {
769
- this.#subs.get(commandName).forEach((handler) => handler(payload));
770
- } else {
771
- console.warn(`No handlers for command : ${commandName}`);
772
- }
773
- this.#afterHandlers.forEach((handler) => handler());
774
- }
775
- }
776
-
777
988
  function traverseDFS(
778
989
  vdom,
779
990
  processNode,
@@ -827,6 +1038,7 @@ function defineComponent({
827
1038
  #parentComponent = null
828
1039
  #dispatcher = new Dispatcher()
829
1040
  #subscriptions = []
1041
+ #appContext = null
830
1042
  #children = []
831
1043
  setExternalContent(children) {
832
1044
  this.#children = children;
@@ -847,6 +1059,12 @@ function defineComponent({
847
1059
  onUnMounted() {
848
1060
  return Promise.resolve(onUnmounted.call(this))
849
1061
  }
1062
+ setAppContext(appContext) {
1063
+ this.#appContext = appContext;
1064
+ }
1065
+ get appContext() {
1066
+ return this.#appContext
1067
+ }
850
1068
  get elements() {
851
1069
  if (this.#vdom == null) {
852
1070
  return []
@@ -948,4 +1166,50 @@ function defineComponent({
948
1166
  return Component;
949
1167
  }
950
1168
 
951
- export { DOM_TYPES, createApp, defineComponent, h, hFragment, hSlot, hString, nextTick };
1169
+ const RouterLink = defineComponent({
1170
+ render() {
1171
+ const { to } = this.props;
1172
+ return h(
1173
+ 'a',
1174
+ {
1175
+ href : to,
1176
+ on : {
1177
+ click : (e) => {
1178
+ e.preventDefault();
1179
+ this.appContext.router.navigateTo(to);
1180
+ },
1181
+ },
1182
+ },
1183
+ [hSlot()]
1184
+ )
1185
+ },
1186
+ });
1187
+ const RouterOutlet = defineComponent({
1188
+ state() {
1189
+ return {
1190
+ matchedRoute: null,
1191
+ subscription : null,
1192
+ }
1193
+ },
1194
+ onMounted() {
1195
+ const subscription = this.appContext.router.subscribe(({ to }) => {
1196
+ this.handleRouteChange(to);
1197
+ });
1198
+ this.updateState({ subscription });
1199
+ },
1200
+ onUnmounted() {
1201
+ const { subscription } = this.state();
1202
+ this.appContext.router.unsubscribe(subscription);
1203
+ },
1204
+ handleRouteChange(matchedRoute) {
1205
+ this.updateState({ matchedRoute});
1206
+ },
1207
+ render() {
1208
+ const { matchedRoute } = this.state;
1209
+ return h('div', {id : 'router-outlet'}, [
1210
+ matchedRoute ? h(matchedRoute.component) : null,
1211
+ ])
1212
+ }
1213
+ });
1214
+
1215
+ export { DOM_TYPES, HashRouter, RouterLink, RouterOutlet, createApp, defineComponent, h, hFragment, hSlot, hString, nextTick };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lahama",
3
- "version": "5.0.0",
3
+ "version": "6.0.0",
4
4
  "description": "",
5
5
  "main": "dist/lahama.js",
6
6
  "files": [