laghu 1.0.0 → 1.1.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.
package/src/router.js ADDED
@@ -0,0 +1,123 @@
1
+ // @ts-check
2
+ /**
3
+ * laghu router plugin (opt-in, separate file, own 2.5kB gzip budget).
4
+ *
5
+ * Tiny hash router for pages with a handful of views. Explicitly NOT a
6
+ * full SPA router: no nested outlets, no code splitting, no async
7
+ * resolvers. Past about five guarded routes, use a real router.
8
+ *
9
+ * Routes match hash paths: "#/user/42" hits "/user/:id" with
10
+ * params { id: "42" }. Matched renders swap HTML into the target and
11
+ * hydrate against the given scope. beforeEach guards cancel by
12
+ * returning false. Links are plain anchors (href="#/user/42");
13
+ * no interception needed because hash changes fire hashchange.
14
+ *
15
+ * @example
16
+ * LaghuRouter.route({
17
+ * target: '#view',
18
+ * scope: state,
19
+ * routes: [
20
+ * { path: '/', render: function () { return '<h1>Home</h1>'; } },
21
+ * { path: '/user/:id', render: function (p) { return '<h1>User ' + p.id + '</h1>'; } }
22
+ * ],
23
+ * notfound: function () { return '<h1>Missing</h1>'; },
24
+ * beforeEach: function (to) { return true; }
25
+ * });
26
+ */
27
+ (function () {
28
+ 'use strict';
29
+ /** @type {any} */
30
+ var current = { path: '', params: {} };
31
+ /** @type {Array<Function>} */
32
+ var afterEachFns = [];
33
+
34
+ /**
35
+ * Compile "/user/:id" into a matcher. Static segments match
36
+ * literally, :name captures one segment, * captures the rest.
37
+ * @param {string} pattern
38
+ * @returns {(path: string) => Record<string, string>|null}
39
+ */
40
+ function compile(pattern) {
41
+ /** @type {Array<string>} */
42
+ var names = [];
43
+ var src = String(pattern).split('/').map(function (seg) {
44
+ if (seg.charAt(0) === ':') { names.push(seg.slice(1)); return '([^/]+)'; }
45
+ if (seg === '*') { names.push('wild'); return '(.*)'; }
46
+ return seg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
47
+ }).join('/');
48
+ var re = new RegExp('^' + src + '$');
49
+ return function (path) {
50
+ var m = path.match(re);
51
+ if (!m) return null;
52
+ /** @type {Record<string, string>} */
53
+ var out = {};
54
+ for (var i = 0; i < names.length; i++) out[names[i]] = decodeURIComponent(m[i + 1] || '');
55
+ return out;
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Start routing. Returns a stop function that detaches the listener.
61
+ * @param {{target?: string, scope?: any, actions?: Record<string, Function>, routes?: Array<{path: string, render: Function}>, notfound?: Function, beforeEach?: Function, afterEach?: Function}} opts
62
+ * @returns {Function} Call to stop listening.
63
+ */
64
+ function route(opts) {
65
+ opts = opts || {};
66
+ var targetSel = opts.target || '#view';
67
+ var scope = opts.scope || {};
68
+ var actions = opts.actions || {};
69
+ var compiled = (opts.routes || []).map(function (r) {
70
+ return { match: compile(r.path), render: r.render };
71
+ });
72
+ function render() {
73
+ var hash = (typeof location !== 'undefined' && location.hash) || '#/';
74
+ var pathOnly = hash.charAt(0) === '#' ? hash.slice(1) : hash;
75
+ if (!pathOnly) pathOnly = '/';
76
+ var from = current;
77
+ var i, params = null, hit = null;
78
+ for (i = 0; i < compiled.length; i++) {
79
+ params = compiled[i].match(pathOnly);
80
+ if (params) { hit = compiled[i]; break; }
81
+ }
82
+ var to = { path: pathOnly, params: params || {} };
83
+ if (typeof opts.beforeEach === 'function' && opts.beforeEach(to, from) === false) return;
84
+ current = to;
85
+ var html = '';
86
+ try {
87
+ if (hit) html = hit.render(params, to) || '';
88
+ else if (typeof opts.notfound === 'function') html = opts.notfound(to) || '';
89
+ } catch (e) {
90
+ if (typeof console !== 'undefined') console.error('[laghu-router]', e);
91
+ return;
92
+ }
93
+ var B = (typeof window !== 'undefined' && /** @type {any} */ (window).Laghu) || null;
94
+ var target = typeof document !== 'undefined' ? document.querySelector(targetSel) : null;
95
+ if (B && target) B.swap(target, String(html), 'inner', scope, actions);
96
+ if (typeof opts.afterEach === 'function') {
97
+ try { opts.afterEach(to, from); } catch (e) {}
98
+ }
99
+ for (var k = 0; k < afterEachFns.length; k++) {
100
+ try { afterEachFns[k](to, from); } catch (e) {}
101
+ }
102
+ }
103
+ function onHash() { render(); }
104
+ if (typeof window !== 'undefined') window.addEventListener('hashchange', onHash);
105
+ render();
106
+ return function () {
107
+ if (typeof window !== 'undefined') window.removeEventListener('hashchange', onHash);
108
+ };
109
+ }
110
+
111
+ var api = {
112
+ version: '1.1.0',
113
+ route: route,
114
+ compile: compile,
115
+ current: function () { return current; },
116
+ navigate: function (/** @type {string} */ path) {
117
+ if (typeof location !== 'undefined') location.hash = '#' + path;
118
+ },
119
+ afterEach: function (/** @type {Function} */ fn) { afterEachFns.push(fn); }
120
+ };
121
+ // @ts-ignore
122
+ window.LaghuRouter = api;
123
+ })();