@minute-spa/vanilla-ui-router 0.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.
Files changed (3) hide show
  1. package/README.md +645 -0
  2. package/index.js +113 -0
  3. package/package.json +19 -0
package/README.md ADDED
@@ -0,0 +1,645 @@
1
+ # Vanilla UI Router
2
+
3
+ A lightweight, client-side router for single-page applications (SPAs) that handles navigation, route registration, authentication, and lifecycle management. This is a standalone component of the minute-spa browser SPA application framework (work in progress).
4
+
5
+ Zero dependencies - use in your project via NPM, or simply clone the code right into your project for a truly vanilla experience that requires no packagers or bundlers!
6
+
7
+ ## Table of Contents
8
+
9
+ - [Overview](#overview)
10
+ - [Installation and Usage](#installation-and-usage)
11
+ - [Constructor](#constructor)
12
+ - [Public Methods](#public-methods)
13
+ - [Route Configuration](#route-configuration)
14
+ - [Authentication & Authorization](#authentication--authorization)
15
+ - [Navigation Listeners](#navigation-listeners)
16
+ - [Lifecycle Hooks](#lifecycle-hooks)
17
+ - [Complete Examples](#complete-examples)
18
+
19
+ ## Overview
20
+
21
+ The `VanillaUiRouter` class provides a simple yet powerful routing solution for vanilla JavaScript applications. It manages:
22
+
23
+ - **Route Registration**: Define paths and associated HTML content
24
+ - **Navigation**: Handle programmatic and browser-based navigation
25
+ - **Authentication**: Optionally implements custom authorization logic
26
+ - **Lifecycle Hooks**: Execute callbacks when pages are rendered or unmounted
27
+ - **History Management**: Integrates with the browser's History API
28
+ - **Navigation Listeners**: Subscribe to route changes
29
+
30
+ ## Installation and Usage
31
+
32
+ ### For NodeJS projects
33
+
34
+ #### Installation
35
+
36
+ In your project directory, install the dependency on the command line:
37
+ ```bash
38
+ npm install --save @minute-spa/vanilla-ui-router
39
+ ```
40
+
41
+ #### Usage
42
+
43
+ Import the package in your code:
44
+ ```javascript
45
+ import { VanillaUiRouter } from '@minute-spa/vanilla-ui-router'
46
+ ```
47
+
48
+ ### For Vanila projects
49
+
50
+ #### Installation
51
+
52
+ Clone packages/VanillaUiRouter/index.js from https://github.com/devosm1030/minute-spa into your project and rename as appropriate.
53
+
54
+ #### Usage
55
+
56
+ From an ES module, import the package in your code:
57
+ ```javascript
58
+ import { VanillaUiRouter } from '<path/to/your/file>'
59
+ ```
60
+
61
+ ## Constructor
62
+
63
+ ### `new VanillaUiRouter(rootElem, pages = [])`
64
+
65
+ Creates a new VanillaUiRouter instance.
66
+
67
+ #### Parameters
68
+
69
+ | Parameter | Type | Required | Description |
70
+ |-----------|------|----------|-------------|
71
+ | `rootElem` | `HTMLElement` or `string` | Yes | The DOM element or selector where pages will be mounted |
72
+ | `pages` | `Array<{path: string, page: Object}>` | No | Optional array of page configurations to register during initialization |
73
+
74
+ #### Returns
75
+
76
+ A new `VanillaUiRouter` instance.
77
+
78
+ #### Throws
79
+
80
+ - `Error` - If `rootElem` is not provided
81
+
82
+ #### Example
83
+
84
+ ```javascript
85
+ const rootElem = document.getElementById('app')
86
+ const homeElem = document.createElement('div')
87
+ homeElem.innerHTML = '<h1>Home</h1>'
88
+ const aboutElem = document.createElement('div')
89
+ aboutElem.innerHTML = '<h1>About</h1>'
90
+
91
+ const pages = [
92
+ { path: '/home', page: { domElem: homeElem } },
93
+ { path: '/about', page: { domElem: aboutElem } }
94
+ ]
95
+ const router = new VanillaUiRouter(rootElem, pages)
96
+ ```
97
+
98
+ ## Public Methods
99
+
100
+ ### `registerRoute(path, pageConfig)`
101
+
102
+ Registers a new route with the router.
103
+
104
+ #### Parameters
105
+
106
+ | Parameter | Type | Required | Description |
107
+ |-----------|------|----------|-------------|
108
+ | `path` | `string` | Yes | The URL path (e.g., '/home', '/about') |
109
+ | `pageConfig` | `Object` | Yes | Configuration object for the page |
110
+ | `pageConfig.domElem` | `HTMLElement` or `function` | Yes | DOM element to render, or a function that returns a DOM element |
111
+ | `pageConfig.onRendered` | `function(element)` | No | Callback executed after page is rendered |
112
+ | `pageConfig.onUnmount` | `function(element)` | No | Callback executed before page is removed |
113
+
114
+ #### Returns
115
+
116
+ The `VanillaUiRouter` instance (for method chaining).
117
+
118
+ #### Throws
119
+
120
+ - `Error` - If `path` or `domElem` is missing
121
+
122
+ #### Example
123
+
124
+ ```javascript
125
+ const profileElem = document.createElement('div')
126
+ profileElem.id = 'profile'
127
+ profileElem.innerHTML = '<h1>User Profile</h1>'
128
+
129
+ router.registerRoute('/profile', {
130
+ domElem: profileElem,
131
+ onRendered: (element) => {
132
+ console.log('Profile page rendered:', element)
133
+ // Initialize event listeners, load data, etc.
134
+ },
135
+ onUnmount: (element) => {
136
+ console.log('Profile page unmounting:', element)
137
+ // Cleanup event listeners, cancel requests, etc.
138
+ }
139
+ })
140
+
141
+ // Or use a function that returns an element if you want to
142
+ // be able to change pages dynamically between navigations.
143
+ router.registerRoute('/dashboard', {
144
+ domElem: () => {
145
+ const elem = document.createElement('div')
146
+ elem.innerHTML = '<h1>Dashboard</h1>'
147
+ return elem
148
+ }
149
+ })
150
+ ```
151
+
152
+ ### `initialNav()`
153
+
154
+ Performs the initial navigation based on the current browser URL. Should be called after all routes are registered to render the appropriate page.
155
+
156
+ #### Returns
157
+
158
+ The `VanillaUiRouter` instance (for method chaining).
159
+
160
+ #### Example
161
+
162
+ ```javascript
163
+ const homeElem = document.createElement('h1')
164
+ homeElem.textContent = 'Home'
165
+ const aboutElem = document.createElement('h1')
166
+ aboutElem.textContent = 'About'
167
+
168
+ const router = new VanillaUiRouter(document.getElementById('app'))
169
+ .registerRoute('/home', { domElem: homeElem })
170
+ .registerRoute('/about', { domElem: aboutElem })
171
+ .initialNav() // Renders the page matching current URL
172
+ ```
173
+
174
+ ### `navigateTo(path)`
175
+
176
+ Programmatically navigates to a specified path. Updates the browser URL and renders the corresponding page.
177
+
178
+ #### Parameters
179
+
180
+ | Parameter | Type | Required | Description |
181
+ |-----------|------|----------|-------------|
182
+ | `path` | `string` | Yes | The path to navigate to |
183
+
184
+ #### Returns
185
+
186
+ `undefined`
187
+
188
+ #### Behavior
189
+
190
+ - If the path is invalid and no auth callback is registered, navigation is ignored
191
+ - Updates `window.history` using `pushState`
192
+ - Triggers authentication check if configured
193
+ - Renders the new page
194
+ - Executes all registered navigation listeners
195
+
196
+ #### Example
197
+
198
+ ```javascript
199
+ // Navigate to a different page
200
+ router.navigateTo('/about')
201
+
202
+ // Navigate from within an event handler
203
+ document.getElementById('homeBtn').addEventListener('click', () => {
204
+ router.navigateTo('/home')
205
+ })
206
+ ```
207
+
208
+ ### `onPathAuth(callback)`
209
+
210
+ Optionally registers an authentication/authorization callback that is invoked before rendering any page. If your app does not require authentication/authorization, skip this registration, and all registered pages will be allowed to be navigated to/rendered.
211
+
212
+ #### Parameters
213
+
214
+ | Parameter | Type | Required | Description |
215
+ |-----------|------|----------|-------------|
216
+ | `callback` | `function(path, helpers)` | Yes | Authentication callback function |
217
+
218
+ The callback receives:
219
+ - `path` (string): The path being navigated to
220
+ - `helpers` (object): Helper functions for handling auth results
221
+ - `helpers.redirectTo(path)`: Redirect to a different path
222
+ - `helpers.renderDomElem(element)`: Render a custom DOM element instead
223
+
224
+ #### Callback Return Values
225
+
226
+ - `true` - Allow navigation to the requested path
227
+ - `false` - Block navigation (should call `redirectTo` or `renderDomElem`)
228
+
229
+ #### Returns
230
+
231
+ The `VanillaUiRouter` instance (for method chaining).
232
+
233
+ #### Throws
234
+
235
+ - `Error` - If `redirectTo` is called with an invalid path
236
+
237
+ #### Example
238
+
239
+ ```javascript
240
+ router.onPathAuth((path, { redirectTo, renderDomElem }) => {
241
+ // Check if user is authenticated
242
+ if (!isUserLoggedIn() && path !== '/login') {
243
+ // Redirect to login page
244
+ return redirectTo('/login')
245
+ }
246
+
247
+ // Check if user has permission for this path
248
+ if (path === '/admin' && !isUserAdmin()) {
249
+ // Render unauthorized message in /admin path without changing path in browser
250
+ const unauthorizedElem = document.createElement('h1')
251
+ unauthorizedElem.textContent = 'Unauthorized'
252
+ return renderDomElem(unauthorizedElem)
253
+ }
254
+
255
+ // Allow navigation
256
+ return true
257
+ })
258
+ ```
259
+
260
+ ### `registerNavListener(callback)`
261
+
262
+ Registers a callback to be executed whenever navigation occurs.
263
+
264
+ #### Parameters
265
+
266
+ | Parameter | Type | Required | Description |
267
+ |-----------|------|----------|-------------|
268
+ | `callback` | `function(path)` | Yes | Callback invoked on navigation |
269
+
270
+ The callback receives:
271
+ - `path` (string): The path that was navigated to
272
+
273
+ #### Returns
274
+
275
+ The `VanillaUiRouter` instance (for method chaining).
276
+
277
+ #### Behavior
278
+
279
+ - If a current path exists when registering, the callback is immediately invoked with that path
280
+ - The callback is invoked after every successful navigation
281
+
282
+ #### Example
283
+
284
+ ```javascript
285
+ // Update active nav item
286
+ router.registerNavListener((path) => {
287
+ navbar.activelink(path)
288
+ })
289
+ ```
290
+
291
+ ### `reauthenticate()`
292
+
293
+ Re-runs the authentication check for the current path. Useful when authentication state changes (e.g., user logs in or out). N/A if you choose not to register an auth callback for your app.
294
+
295
+ #### Returns
296
+
297
+ `undefined`
298
+
299
+ #### Example
300
+
301
+ ```javascript
302
+ // After user logs in
303
+ async function login(username, password) {
304
+ const success = await authService.login(username, password)
305
+ if (success) {
306
+ // Re-check authentication for current page
307
+ router.reauthenticate()
308
+ }
309
+ }
310
+
311
+ // After user logs out
312
+ function logout() {
313
+ authService.logout()
314
+ router.reauthenticate() // Will redirect to login if needed
315
+ }
316
+ ```
317
+
318
+ ### `isValidPath(path)`
319
+
320
+ Checks if a given path has been registered with the router.
321
+
322
+ #### Parameters
323
+
324
+ | Parameter | Type | Required | Description |
325
+ |-----------|------|----------|-------------|
326
+ | `path` | `string` | Yes | The path to check |
327
+
328
+ #### Returns
329
+
330
+ `boolean` - `true` if the path is registered, `false` otherwise
331
+
332
+ #### Example
333
+
334
+ ```javascript
335
+ if (router.isValidPath('/profile')) {
336
+ router.navigateTo('/profile')
337
+ } else {
338
+ console.log('Profile page not found')
339
+ }
340
+ ```
341
+
342
+ ## Route Configuration
343
+
344
+ ### Page Configuration Object
345
+
346
+ When registering routes, you provide a page configuration object:
347
+
348
+ ```javascript
349
+ {
350
+ domElem: HTMLElement | function, // Required: DOM element or function returning DOM element
351
+ onRendered: function(element), // Optional: Called after render
352
+ onUnmount: function(element) // Optional: Called before unmount
353
+ }
354
+ ```
355
+
356
+ ### DOM Element
357
+
358
+ The `domElem` property can be:
359
+
360
+ 1. **HTML Element**:
361
+ ```javascript
362
+ const pageElement = document.createElement('div')
363
+ pageElement.innerHTML = '<h1>Welcome</h1>'
364
+ { domElem: pageElement }
365
+ ```
366
+
367
+ 2. **Function returning HTML Element**:
368
+ ```javascript
369
+ {
370
+ domElem: () => {
371
+ const elem = document.createElement('div')
372
+ elem.innerHTML = '<h1>Welcome</h1>'
373
+ return elem
374
+ }
375
+ }
376
+ ```
377
+
378
+ Be sure to sanitize any untrusted content that could be used to build your DOM elements! For a NodeJS project, DomPurify (https://www.npmjs.com/package/dompurify) is a convenient tool for doing this.
379
+
380
+ ## Authentication & Authorization
381
+
382
+ Authentication and Authorization only occurs if you have registered an auth callback via **onPathAuth**.
383
+ ### Authentication Flow
384
+
385
+ 1. User attempts to navigate to a path
386
+ 2. Router calls the `onPathAuth` callback (if registered)
387
+ 3. Callback decides to:
388
+ - Allow navigation (return `true`)
389
+ - Redirect to another path (call `redirectTo()` and return `false`)
390
+ - Render custom DOM element (call `renderDomElem()` and return `false`)
391
+
392
+ ### Example: Protected Routes
393
+
394
+ ```javascript
395
+ const publicRoutes = ['/home', '/about', '/login']
396
+
397
+ router.onPathAuth((path, { redirectTo }) => {
398
+ // Allow public routes
399
+ if (publicRoutes.includes(path)) {
400
+ return true
401
+ }
402
+
403
+ // Check authentication for protected routes
404
+ const token = localStorage.getItem('authToken')
405
+ if (!token) {
406
+ // Store the intended destination
407
+ sessionStorage.setItem('redirectAfterLogin', path)
408
+ return redirectTo('/login')
409
+ }
410
+
411
+ return true
412
+ })
413
+ ```
414
+
415
+ ### Example: Role-Based Access
416
+
417
+ ```javascript
418
+ const adminRoutes = ['/admin', '/users', '/settings']
419
+
420
+ router.onPathAuth((path, { redirectTo, renderDomElem }) => {
421
+ const user = getCurrentUser()
422
+
423
+ if (!user && path !== '/login') {
424
+ return redirectTo('/login')
425
+ }
426
+
427
+ if (adminRoutes.includes(path) && !user.isAdmin) {
428
+ const deniedElem = document.createElement('div')
429
+ deniedElem.innerHTML = '<h1>Access Denied</h1><p>Admin access required.</p>'
430
+ return renderDomElem(deniedElem)
431
+ }
432
+
433
+ return true
434
+ })
435
+ ```
436
+
437
+ ## Navigation Listeners
438
+
439
+ Navigation listeners are invoked after every successful navigation, making them perfect for:
440
+
441
+ - Updating UI elements (navigation bars, breadcrumbs)
442
+ - Analytics tracking
443
+ - Logging
444
+ - Side effects based on current route
445
+
446
+ ### Multiple Listeners
447
+
448
+ You can register multiple navigation listeners:
449
+
450
+ ```javascript
451
+ // Update page title
452
+ router.registerNavListener((path) => {
453
+ const titles = {
454
+ '/home': 'Home',
455
+ '/about': 'About Us',
456
+ '/contact': 'Contact'
457
+ }
458
+ document.title = titles[path] || 'My App'
459
+ })
460
+
461
+ // Update navigation state
462
+ router.registerNavListener((path) => {
463
+ updateActiveNavItem(path)
464
+ })
465
+
466
+ // Track page views
467
+ router.registerNavListener((path) => {
468
+ gtag('config', 'GA_MEASUREMENT_ID', {
469
+ page_path: path
470
+ })
471
+ })
472
+ ```
473
+
474
+ ## Lifecycle Hooks
475
+
476
+ ### `onRendered` Hook
477
+
478
+ Called immediately after a page is rendered and added to the DOM.
479
+
480
+ **Use cases:**
481
+ - Initialize event listeners
482
+ - Load data
483
+ - Start animations
484
+ - Focus elements
485
+ - Initialize third-party libraries
486
+
487
+ ```javascript
488
+ const dashboardElem = document.createElement('div')
489
+ dashboardElem.id = 'dashboard'
490
+ dashboardElem.innerHTML = '...' // dashboard HTML
491
+
492
+ router.registerRoute('/dashboard', {
493
+ domElem: dashboardElem,
494
+ onRendered: (element) => {
495
+ // Initialize chart library
496
+ const chartCanvas = element.querySelector('#chart')
497
+ initializeChart(chartCanvas)
498
+
499
+ // Load data
500
+ fetchDashboardData().then(data => {
501
+ updateDashboard(element, data)
502
+ })
503
+
504
+ // Add event listeners
505
+ element.querySelector('#refresh-btn').addEventListener('click', refreshData)
506
+ }
507
+ })
508
+ ```
509
+
510
+ ### `onUnmount` Hook
511
+
512
+ Called just before a page is removed from the DOM.
513
+
514
+ **Use cases:**
515
+ - Remove event listeners
516
+ - Cancel pending requests
517
+ - Clear timers/intervals
518
+ - Cleanup third-party libraries
519
+ - Save state
520
+
521
+ ```javascript
522
+ let refreshInterval
523
+
524
+ const feedElem = document.createElement('div')
525
+ feedElem.id = 'feed'
526
+ feedElem.innerHTML = '...' // feed HTML
527
+
528
+ router.registerRoute('/live-feed', {
529
+ domElem: feedElem,
530
+ onRendered: (element) => {
531
+ // Start auto-refresh
532
+ refreshInterval = setInterval(() => {
533
+ updateFeed(element)
534
+ }, 5000)
535
+ },
536
+ onUnmount: (element) => {
537
+ // Stop auto-refresh
538
+ clearInterval(refreshInterval)
539
+
540
+ // Cancel any pending requests
541
+ abortController.abort()
542
+
543
+ console.log('Feed page cleaned up')
544
+ }
545
+ })
546
+ ```
547
+
548
+ ## Complete Examples
549
+
550
+ ### Basic Setup
551
+
552
+ ```javascript
553
+ import { VanillaUiRouter } from '@minute-spa/vanilla-ui-router'
554
+
555
+ // Create router
556
+ const router = new VanillaUiRouter(document.getElementById('app'))
557
+
558
+ // Create page elements
559
+ const homeElem = document.createElement('div')
560
+ homeElem.innerHTML = '<h1>Home Page</h1><p>Welcome!</p>'
561
+
562
+ const aboutElem = document.createElement('div')
563
+ aboutElem.innerHTML = '<h1>About Us</h1><p>Learn more about our company.</p>'
564
+
565
+ const contactElem = document.createElement('div')
566
+ contactElem.innerHTML = '<h1>Contact</h1><form>...</form>'
567
+
568
+ // Register routes
569
+ router
570
+ .registerRoute('/home', { domElem: homeElem })
571
+ .registerRoute('/about', { domElem: aboutElem })
572
+ .registerRoute('/contact', { domElem: contactElem })
573
+
574
+ // Start router
575
+ router.initialNav()
576
+ ```
577
+
578
+ ### Full-Featured Application
579
+
580
+ ```javascript
581
+ import { VanillaUiRouter } from '@minute-spa/vanilla-ui-router'
582
+ import { homePage } from './pages/home.js'
583
+ import { profilePage } from './pages/profile.js'
584
+ import { loginPage } from './pages/login.js'
585
+
586
+ // Initialize router with pages
587
+ const pages = [
588
+ { path: '/home', page: homePage },
589
+ { path: '/profile', page: profilePage },
590
+ { path: '/login', page: loginPage }
591
+ ]
592
+
593
+ const router = new VanillaUiRouter(document.getElementById('app'), pages)
594
+
595
+ // Add authentication
596
+ router.onPathAuth((path, { redirectTo }) => {
597
+ const isAuthenticated = checkAuthStatus()
598
+
599
+ if (!isAuthenticated && path !== '/login') {
600
+ sessionStorage.setItem('returnTo', path)
601
+ return redirectTo('/login')
602
+ }
603
+
604
+ if (isAuthenticated && path === '/login') {
605
+ const returnTo = sessionStorage.getItem('returnTo') || '/home'
606
+ sessionStorage.removeItem('returnTo')
607
+ return redirectTo(returnTo)
608
+ }
609
+
610
+ return true
611
+ })
612
+
613
+ // Track navigation
614
+ router.registerNavListener((path) => {
615
+ console.log('Navigated to:', path)
616
+ updateBreadcrumbs(path)
617
+ })
618
+
619
+ // Initialize
620
+ router.initialNav()
621
+ ```
622
+
623
+ ## License
624
+
625
+ MIT License
626
+
627
+ Copyright (c) 2025 Mike DeVos
628
+
629
+ Permission is hereby granted, free of charge, to any person obtaining a copy
630
+ of this software and associated documentation files (the "Software"), to deal
631
+ in the Software without restriction, including without limitation the rights
632
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
633
+ copies of the Software, and to permit persons to whom the Software is
634
+ furnished to do so, subject to the following conditions:
635
+
636
+ The above copyright notice and this permission notice shall be included in all
637
+ copies or substantial portions of the Software.
638
+
639
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
640
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
641
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
642
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
643
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
644
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
645
+ SOFTWARE.
package/index.js ADDED
@@ -0,0 +1,113 @@
1
+ /*
2
+ MIT License
3
+
4
+ Copyright (c) 2025 Mike DeVos
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
23
+ */
24
+
25
+ /*
26
+ VanillaUiRouter is a simple client-side router for vanilla JavaScript SPA applications.
27
+ */
28
+
29
+ class VanillaUiRouter {
30
+ constructor (rootElem, pages = []) {
31
+ if (!rootElem) throw new Error('Router requires a root element to mount to.')
32
+ Object.assign(this, { rootElem, routes: {}, pageElem: null, pathAuthCb: null, currPath: null, onUnmount: null, navListeners: [] })
33
+ window.onpopstate = () => this.handleNav()
34
+ pages.forEach(({ path, page }) => { this.registerRoute(path, page) })
35
+ }
36
+
37
+ registerRoute (path, { domElem = null, onRendered = null, onUnmount = null } = {}) {
38
+ if (!path || !domElem) throw new Error('Router.registerRoute requires path and an object that includes an html dom element.')
39
+ this.routes[path] = { domElem, onRendered, onUnmount }
40
+ return this
41
+ }
42
+
43
+ registerNavListener (cb) {
44
+ // these callbacks will get called whenever we nav to a new path
45
+ this.navListeners.push(cb)
46
+ if (this.currPath) cb(this.currPath)
47
+ return this
48
+ }
49
+
50
+ initialNav () {
51
+ this.handleNav()
52
+ return this
53
+ }
54
+
55
+ navigateTo (path) {
56
+ if (!this.isValidPath(path) && !this.pathAuthCb) return
57
+ window.history.pushState({}, '', path)
58
+ this.handleNav()
59
+ }
60
+
61
+ onPathAuth (callback) {
62
+ // registration of callback called on navigation to determine if path is authorized
63
+ this.pathAuthCb = callback
64
+ return this
65
+ }
66
+
67
+ isValidPath (path) {
68
+ return !!this.routes[path]
69
+ }
70
+
71
+ pathAuthorized (path) {
72
+ if (!this.pathAuthCb) return true
73
+ return this.pathAuthCb(path, {
74
+ redirectTo: path => {
75
+ if (!this.isValidPath(path)) throw new Error(`Router.pathAuthorized redirectTo path must be valid: ${path}`)
76
+ this.navigateTo(path)
77
+ return false
78
+ },
79
+ renderDomElem: elem => { this.renderDomElem(elem); return false },
80
+ })
81
+ }
82
+
83
+ reauthenticate () {
84
+ this.handleNav()
85
+ }
86
+
87
+ renderDomElem (domElem) {
88
+ if (this.pageElem) {
89
+ if (this.onUnmount) {
90
+ this.onUnmount(this.pageElem)
91
+ this.onUnmount = null
92
+ }
93
+ this.pageElem.remove()
94
+ }
95
+ this.pageElem = (typeof domElem === 'function') ? domElem() : domElem
96
+ this.rootElem.appendChild(this.pageElem)
97
+ }
98
+
99
+ handleNav () {
100
+ if (Object.keys(this.routes).length === 0) throw new Error('Router requires at least one registered path')
101
+ const { pathname: path } = new URL(window.location.href)
102
+ const pathIsValid = this.isValidPath(path)
103
+ if (!pathIsValid && !this.pathAuthCb) return this.navigateTo(Object.keys(this.routes)[0])
104
+ if (!this.pathAuthorized(path)) return // false means pathAuthCb must have redirected or rendered html, so don't proceed
105
+ this.renderDomElem(this.routes[path].domElem)
106
+ if (this.routes[path].onRendered) this.routes[path].onRendered(this.pageElem)
107
+ this.onUnmount = this.routes[path].onUnmount || null
108
+ this.currPath = path
109
+ this.navListeners.forEach(cb => cb(path))
110
+ }
111
+ }
112
+
113
+ export { VanillaUiRouter }
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@minute-spa/vanilla-ui-router",
3
+ "version": "0.0.1",
4
+ "description": "A simple vanilla client side SPA router.",
5
+ "main": "index.js",
6
+ "author": "Mike DeVos",
7
+ "license": "MIT",
8
+ "scripts": {
9
+ "publish": "npm publish --access public"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/devosm1030/minute-spa.git"
14
+ },
15
+ "bugs": {
16
+ "url": "https://github.com/devosm1030/minute-spa/issues"
17
+ },
18
+ "homepage": "https://github.com/devosm1030/minute-spa#readme"
19
+ }