@keenmate/svelte-spa-router 1.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/LICENSE.md +21 -0
- package/README.md +507 -0
- package/Router.d.ts +221 -0
- package/Router.svelte +644 -0
- package/active.d.ts +23 -0
- package/active.js +119 -0
- package/constants.js +1 -0
- package/helpers/url-helpers.js +29 -0
- package/nightwatch.conf.cjs +52 -0
- package/package.json +70 -0
- package/wrap.d.ts +41 -0
- package/wrap.js +93 -0
package/active.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {parse} from "regexparam"
|
|
2
|
+
import {BasePath, HashRoutingEnabled, loc} from "./Router.svelte"
|
|
3
|
+
import {get} from "svelte/store"
|
|
4
|
+
|
|
5
|
+
// List of nodes to update
|
|
6
|
+
const nodes = []
|
|
7
|
+
|
|
8
|
+
// Current location
|
|
9
|
+
let location
|
|
10
|
+
|
|
11
|
+
// Function that updates all nodes marking the active ones
|
|
12
|
+
function checkActive(el) {
|
|
13
|
+
const matchesLocation = el.pattern.test(location)
|
|
14
|
+
toggleClasses(el, el.className, matchesLocation)
|
|
15
|
+
toggleClasses(el, el.inactiveClassName, !matchesLocation)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function toggleClasses(el, className, shouldAdd) {
|
|
19
|
+
(className || "").split(" ").forEach((cls) => {
|
|
20
|
+
if (!cls) {
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
// Remove the class firsts
|
|
24
|
+
el.node.classList.remove(cls)
|
|
25
|
+
|
|
26
|
+
// If the pattern doesn't match, then set the class
|
|
27
|
+
if (shouldAdd) {
|
|
28
|
+
el.node.classList.add(cls)
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Listen to changes in the location
|
|
34
|
+
loc.subscribe((value) => {
|
|
35
|
+
// Update the location
|
|
36
|
+
location = value.location + (value.querystring ? "?" + value.querystring : "")
|
|
37
|
+
|
|
38
|
+
// Update all nodes
|
|
39
|
+
nodes.map(checkActive)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @typedef {Object} ActiveOptions
|
|
44
|
+
* @property {string|RegExp} [path] - Path expression that makes the link active when matched (must start with '/' or '*'); default is the link's href
|
|
45
|
+
* @property {string} [className] - CSS class to apply to the element when active; default value is "active"
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Svelte Action for automatically adding the "active" class to elements (links, or any other DOM element) when the current location matches a certain path.
|
|
50
|
+
*
|
|
51
|
+
* @param {HTMLElement} node - The target node (automatically set by Svelte)
|
|
52
|
+
* @param {ActiveOptions|string|RegExp} [opts] - Can be an object of type ActiveOptions, or a string (or regular expressions) representing ActiveOptions.path.
|
|
53
|
+
* @returns {{destroy: function(): void}} Destroy function
|
|
54
|
+
*/
|
|
55
|
+
export default function active(node, opts) {
|
|
56
|
+
const basePath = get(BasePath)
|
|
57
|
+
|
|
58
|
+
// Check options
|
|
59
|
+
if (opts && (typeof opts == "string" || (typeof opts == "object" && opts instanceof RegExp))) {
|
|
60
|
+
// Interpret strings and regular expressions as opts.path
|
|
61
|
+
opts = {
|
|
62
|
+
path: opts
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
// Ensure opts is a dictionary
|
|
66
|
+
opts = opts || {}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Path defaults to link target
|
|
70
|
+
if (!opts.path && node.hasAttribute("href")) {
|
|
71
|
+
opts.path = node.getAttribute("href")
|
|
72
|
+
|
|
73
|
+
if (get(HashRoutingEnabled)) {
|
|
74
|
+
if (opts.path && opts.path.length > 1 && opts.path.charAt(0) == "#") {
|
|
75
|
+
opts.path = opts.path.substring(1)
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
if (opts.path.startsWith(basePath)) {
|
|
79
|
+
opts.path = opts.path.substring(basePath.length)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Default class name
|
|
85
|
+
if (!opts.className) {
|
|
86
|
+
opts.className = "active"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// If path is a string, it must start with '/' or '*'
|
|
90
|
+
if (!opts.path ||
|
|
91
|
+
typeof opts.path == "string" && (opts.path.length < 1 || (opts.path.charAt(0) != "/" && opts.path.charAt(0) != "*"))
|
|
92
|
+
) {
|
|
93
|
+
throw Error("Invalid value for \"path\" argument")
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// If path is not a regular expression already, make it
|
|
97
|
+
const {pattern} = typeof opts.path == "string" ?
|
|
98
|
+
parse(opts.path) :
|
|
99
|
+
{pattern: opts.path}
|
|
100
|
+
|
|
101
|
+
// Add the node to the list
|
|
102
|
+
const el = {
|
|
103
|
+
node,
|
|
104
|
+
className: opts.className,
|
|
105
|
+
inactiveClassName: opts.inactiveClassName,
|
|
106
|
+
pattern
|
|
107
|
+
}
|
|
108
|
+
nodes.push(el)
|
|
109
|
+
|
|
110
|
+
// Trigger the action right away
|
|
111
|
+
checkActive(el)
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
// When the element is destroyed, remove it from the list
|
|
115
|
+
destroy() {
|
|
116
|
+
nodes.splice(nodes.indexOf(el), 1)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
package/constants.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const SvelteSPARouterNavigationEvent = "popstate"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {BasePath} from "../Router.svelte"
|
|
2
|
+
import {get} from "svelte/store"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {Object} Location
|
|
6
|
+
* @property {string} location - Location (page/view), for example `/book`
|
|
7
|
+
* @property {string} [querystring] - Querystring from the hash, as a string not parsed
|
|
8
|
+
*/
|
|
9
|
+
export function joinPaths(...paths) {
|
|
10
|
+
if (!paths || !paths.length) {
|
|
11
|
+
return get(BasePath)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return paths
|
|
15
|
+
.map(x => x.trim())
|
|
16
|
+
.filter(x => x)
|
|
17
|
+
.map((x, i, arr) => {
|
|
18
|
+
if (i === 0) {
|
|
19
|
+
return x.replace(/\/$/, "")
|
|
20
|
+
} else if (i < arr.length - 1) {
|
|
21
|
+
return x.replace(/(^\/|\/$)/, "")
|
|
22
|
+
} else {
|
|
23
|
+
return x.replace(/^\//, "")
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
.map((x) => x.trim())
|
|
27
|
+
.filter(x => x)
|
|
28
|
+
.join("/")
|
|
29
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Selenium configuration
|
|
2
|
+
const seleniumHost = process.env.SELENIUM_HOST || "127.0.0.1"
|
|
3
|
+
const seleniumPort = parseInt(process.env.SELENIUM_PORT || "4444", 10)
|
|
4
|
+
|
|
5
|
+
// Launch URL - where the server is
|
|
6
|
+
const launchUrl = process.env.LAUNCH_URL || "http://localhost:5050"
|
|
7
|
+
|
|
8
|
+
// Increase max listeners to avoid a warning
|
|
9
|
+
require("events").EventEmitter.defaultMaxListeners = 100
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
src_folders: [
|
|
13
|
+
"test/cases/"
|
|
14
|
+
],
|
|
15
|
+
|
|
16
|
+
output_folder: "result",
|
|
17
|
+
|
|
18
|
+
test_runner: {
|
|
19
|
+
type: "mocha",
|
|
20
|
+
options: {
|
|
21
|
+
ui: "bdd",
|
|
22
|
+
reporter: "list"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
test_settings: {
|
|
27
|
+
default: {
|
|
28
|
+
launch_url: launchUrl
|
|
29
|
+
},
|
|
30
|
+
"selenium.chrome": {
|
|
31
|
+
selenium: {
|
|
32
|
+
start_process: false,
|
|
33
|
+
host: seleniumHost,
|
|
34
|
+
port: seleniumPort
|
|
35
|
+
},
|
|
36
|
+
webdriver: {
|
|
37
|
+
start_process: false
|
|
38
|
+
},
|
|
39
|
+
desiredCapabilities: {
|
|
40
|
+
browserName: "chrome",
|
|
41
|
+
chromeOptions: {
|
|
42
|
+
args: [
|
|
43
|
+
"--headless",
|
|
44
|
+
"--no-sandbox",
|
|
45
|
+
"--disable-gpu"
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
acceptSslCerts: true
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@keenmate/svelte-spa-router",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "1.0.1",
|
|
5
|
+
"description": "Router for SPAs using Svelte 4",
|
|
6
|
+
"main": "Router.svelte",
|
|
7
|
+
"svelte": "Router.svelte",
|
|
8
|
+
"types": "Router.d.ts",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"exports": {
|
|
11
|
+
"./active": {
|
|
12
|
+
"types": "./active.d.ts",
|
|
13
|
+
"import": "./active.js"
|
|
14
|
+
},
|
|
15
|
+
"./wrap": {
|
|
16
|
+
"types": "./wrap.d.ts",
|
|
17
|
+
"import": "./wrap.js"
|
|
18
|
+
},
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./Router.d.ts",
|
|
21
|
+
"svelte": "./Router.svelte"
|
|
22
|
+
},
|
|
23
|
+
"./*": {
|
|
24
|
+
"import": "./*"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build-test-app": "(cd test/app && npx rollup -c)",
|
|
29
|
+
"start-test-app": "npx serve -n -l 5050 test/app/dist",
|
|
30
|
+
"eslint": "npx eslint -c .eslintrc.cjs --ext .js,.svelte,.html .",
|
|
31
|
+
"lint": "npm run eslint",
|
|
32
|
+
"nightwatch": "npx nightwatch -e selenium.chrome -c nightwatch.conf.cjs",
|
|
33
|
+
"test": "npm run nightwatch"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/keenmate/svelte-spa-router.git"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"router",
|
|
41
|
+
"svelte",
|
|
42
|
+
"svelte3",
|
|
43
|
+
"svelte4",
|
|
44
|
+
"spa"
|
|
45
|
+
],
|
|
46
|
+
"author": "Alessandro Segala (@ItalyPaleAle)",
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"funding": "https://github.com/sponsors/ItalyPaleAle",
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/ItalyPaleAle/svelte-spa-router/issues"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://github.com/ItalyPaleAle/svelte-spa-router#readme",
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"regexparam": "2.0.2"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@rollup/plugin-commonjs": "^25.0.7",
|
|
58
|
+
"@rollup/plugin-node-resolve": "^15.2.3",
|
|
59
|
+
"chromedriver": "^119.0.1",
|
|
60
|
+
"eslint": "^8.55.0",
|
|
61
|
+
"eslint-plugin-html": "^7.1.0",
|
|
62
|
+
"eslint-plugin-svelte": "^2.35.1",
|
|
63
|
+
"nightwatch": "^2.6.23",
|
|
64
|
+
"rollup": "^4.7.0",
|
|
65
|
+
"rollup-plugin-css-only": "^4.5.2",
|
|
66
|
+
"rollup-plugin-svelte": "^7.1.6",
|
|
67
|
+
"serve": "^14.2.1",
|
|
68
|
+
"svelte": "^4.2.8"
|
|
69
|
+
}
|
|
70
|
+
}
|
package/wrap.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {ComponentType} from "svelte"
|
|
2
|
+
import {AsyncSvelteComponent, RoutePrecondition, WrappedComponent} from "./Router"
|
|
3
|
+
|
|
4
|
+
/** Options object for the call to `wrap` */
|
|
5
|
+
export interface WrapOptions {
|
|
6
|
+
/** Svelte component to load (this is incompatible with `asyncComponent`) */
|
|
7
|
+
component?: ComponentType
|
|
8
|
+
|
|
9
|
+
/** Function that returns a Promise that fulfills with a Svelte component (e.g. `{asyncComponent: () => import('Foo.svelte')}`) */
|
|
10
|
+
asyncComponent?: AsyncSvelteComponent
|
|
11
|
+
|
|
12
|
+
/** Svelte component to be displayed while the async route is loading (as a placeholder); when unset or false-y, no component is shown while component */
|
|
13
|
+
loadingComponent?: ComponentType
|
|
14
|
+
|
|
15
|
+
/** Optional dictionary passed to the `loadingComponent` component as params (for an exported prop called `params`) */
|
|
16
|
+
loadingParams?: object
|
|
17
|
+
|
|
18
|
+
/** Optional object that will be passed to events such as `routeLoading`, `routeLoaded`, `conditionsFailed` */
|
|
19
|
+
userData?: object
|
|
20
|
+
|
|
21
|
+
/** Optional key-value dictionary of static props that will be passed to the component. The props are expanded with {...props}, so the key in the dictionary becomes the name of the prop. */
|
|
22
|
+
props?: object
|
|
23
|
+
|
|
24
|
+
/** Route pre-conditions to add, which will be executed in order */
|
|
25
|
+
conditions?: RoutePrecondition[] | RoutePrecondition
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Wraps a component to enable multiple capabilities:
|
|
30
|
+
*
|
|
31
|
+
* 1. Using dynamically-imported component, with (e.g. `{asyncComponent: () => import('Foo.svelte')}`), which also allows bundlers to do code-splitting.
|
|
32
|
+
* 2. Adding route pre-conditions (e.g. `{conditions: [...]}`)
|
|
33
|
+
* 3. Adding static props that are passed to the component
|
|
34
|
+
* 4. Adding custom userData, which is passed to route events (e.g. route loaded events) or to route pre-conditions (e.g. `{userData: {foo: 'bar}}`)
|
|
35
|
+
*
|
|
36
|
+
* @param args Arguments object
|
|
37
|
+
* @returns Wrapped component
|
|
38
|
+
*/
|
|
39
|
+
export function wrap(args: WrapOptions): WrappedComponent
|
|
40
|
+
|
|
41
|
+
export default wrap
|
package/wrap.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} WrappedComponent Object returned by the `wrap` method
|
|
3
|
+
* @property {SvelteComponent} component - Component to load (this is always asynchronous)
|
|
4
|
+
* @property {RoutePrecondition[]} [conditions] - Route pre-conditions to validate
|
|
5
|
+
* @property {Object} [props] - Optional dictionary of static props
|
|
6
|
+
* @property {Object} [userData] - Optional user data dictionary
|
|
7
|
+
* @property {bool} _sveltesparouter - Internal flag; always set to true
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @callback AsyncSvelteComponent
|
|
12
|
+
* @returns {Promise<SvelteComponent>} Returns a Promise that resolves with a Svelte component
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @callback RoutePrecondition
|
|
17
|
+
* @param {RouteDetail} detail - Route detail object
|
|
18
|
+
* @returns {boolean|Promise<boolean>} If the callback returns a false-y value, it's interpreted as the precondition failed, so it aborts loading the component (and won't process other pre-condition callbacks)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} WrapOptions Options object for the call to `wrap`
|
|
23
|
+
* @property {SvelteComponent} [component] - Svelte component to load (this is incompatible with `asyncComponent`)
|
|
24
|
+
* @property {AsyncSvelteComponent} [asyncComponent] - Function that returns a Promise that fulfills with a Svelte component (e.g. `{asyncComponent: () => import('Foo.svelte')}`)
|
|
25
|
+
* @property {SvelteComponent} [loadingComponent] - Svelte component to be displayed while the async route is loading (as a placeholder); when unset or false-y, no component is shown while component
|
|
26
|
+
* @property {object} [loadingParams] - Optional dictionary passed to the `loadingComponent` component as params (for an exported prop called `params`)
|
|
27
|
+
* @property {object} [userData] - Optional object that will be passed to events such as `routeLoading`, `routeLoaded`, `conditionsFailed`
|
|
28
|
+
* @property {object} [props] - Optional key-value dictionary of static props that will be passed to the component. The props are expanded with {...props}, so the key in the dictionary becomes the name of the prop.
|
|
29
|
+
* @property {RoutePrecondition[]|RoutePrecondition} [conditions] - Route pre-conditions to add, which will be executed in order
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Wraps a component to enable multiple capabilities:
|
|
34
|
+
* 1. Using dynamically-imported component, with (e.g. `{asyncComponent: () => import('Foo.svelte')}`), which also allows bundlers to do code-splitting.
|
|
35
|
+
* 2. Adding route pre-conditions (e.g. `{conditions: [...]}`)
|
|
36
|
+
* 3. Adding static props that are passed to the component
|
|
37
|
+
* 4. Adding custom userData, which is passed to route events (e.g. route loaded events) or to route pre-conditions (e.g. `{userData: {foo: 'bar}}`)
|
|
38
|
+
*
|
|
39
|
+
* @param {WrapOptions} args - Arguments object
|
|
40
|
+
* @returns {WrappedComponent} Wrapped component
|
|
41
|
+
*/
|
|
42
|
+
export function wrap(args) {
|
|
43
|
+
if (!args) {
|
|
44
|
+
throw Error("Parameter args is required")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// We need to have one and only one of component and asyncComponent
|
|
48
|
+
// This does a "XNOR"
|
|
49
|
+
if (!args.component == !args.asyncComponent) {
|
|
50
|
+
throw Error("One and only one of component and asyncComponent is required")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// If the component is not async, wrap it into a function returning a Promise
|
|
54
|
+
if (args.component) {
|
|
55
|
+
args.asyncComponent = () => Promise.resolve(args.component)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Parameter asyncComponent and each item of conditions must be functions
|
|
59
|
+
if (typeof args.asyncComponent != "function") {
|
|
60
|
+
throw Error("Parameter asyncComponent must be a function")
|
|
61
|
+
}
|
|
62
|
+
if (args.conditions) {
|
|
63
|
+
// Ensure it's an array
|
|
64
|
+
if (!Array.isArray(args.conditions)) {
|
|
65
|
+
args.conditions = [args.conditions]
|
|
66
|
+
}
|
|
67
|
+
for (let i = 0; i < args.conditions.length; i++) {
|
|
68
|
+
if (!args.conditions[i] || typeof args.conditions[i] != "function") {
|
|
69
|
+
throw Error("Invalid parameter conditions[" + i + "]")
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check if we have a placeholder component
|
|
75
|
+
if (args.loadingComponent) {
|
|
76
|
+
args.asyncComponent.loading = args.loadingComponent
|
|
77
|
+
args.asyncComponent.loadingParams = args.loadingParams || undefined
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Returns an object that contains all the functions to execute too
|
|
81
|
+
// The _sveltesparouter flag is to confirm the object was created by this router
|
|
82
|
+
const obj = {
|
|
83
|
+
component: args.asyncComponent,
|
|
84
|
+
userData: args.userData,
|
|
85
|
+
conditions: (args.conditions && args.conditions.length) ? args.conditions : undefined,
|
|
86
|
+
props: (args.props && Object.keys(args.props).length) ? args.props : {},
|
|
87
|
+
_sveltesparouter: true
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return obj
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default wrap
|