@aref-shojaei/router 1.0.1 → 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/README.md +332 -205
- package/build/router.min.js +1 -1
- package/example/assets/css/styles.css +45 -45
- package/example/assets/js/app.js +7 -7
- package/example/assets/js/router.min.js +1 -1
- package/example/index.html +26 -26
- package/example/server.js +17 -17
- package/index.js +6 -4
- package/package.json +10 -4
- package/src/dto/route.js +13 -17
- package/src/exception.js +5 -5
- package/src/page.js +91 -91
- package/src/route.js +103 -103
- package/src/router.js +207 -207
- package/src/utils/element.js +23 -22
- package/src/utils/selector.js +59 -57
- package/src/view.js +24 -24
- package/tests/unit/element.test.js +32 -32
- package/tests/unit/page.test.js +48 -48
- package/tests/unit/route.test.js +82 -82
- package/tests/unit/router.test.js +55 -55
- package/tests/unit/selector.test.js +42 -42
- package/tests/unit/view.test.js +46 -46
- package/webpack.config.js +23 -0
- package/.babelrc +0 -3
package/README.md
CHANGED
|
@@ -1,205 +1,332 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
>
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
Route.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
```
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# 🚀 Router - Lightweight JavaScript SPA Router
|
|
4
|
+
|
|
5
|
+
A lightweight and powerful client-side routing library for building Single Page Applications (SPA) with pure JavaScript.
|
|
6
|
+
|
|
7
|
+
Define routes, manage navigation, handle dynamic parameters, apply middleware, and create seamless page transitions without refreshing the browser.
|
|
8
|
+
|
|
9
|
+
<img src="https://github.com/user-attachments/assets/f1831dc5-f72e-4d7a-806d-b9c01b4be29a" alt="Router SPA Banner" />
|
|
10
|
+
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## ✨ Features
|
|
16
|
+
|
|
17
|
+
* 🚀 Client-side SPA routing
|
|
18
|
+
* 🔗 Browser History API support
|
|
19
|
+
* 📄 Single and grouped routes
|
|
20
|
+
* 🔥 Dynamic route parameters
|
|
21
|
+
* 🛡 Route middleware system
|
|
22
|
+
* 🔀 Route redirection
|
|
23
|
+
* 🏷 Dynamic page titles
|
|
24
|
+
* ⚡ No page refresh navigation
|
|
25
|
+
* 🪶 Lightweight and dependency-free
|
|
26
|
+
* 🧩 Simple and expressive API
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
# 📦 Installation
|
|
31
|
+
|
|
32
|
+
## Using NPM
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install @aref-shojaei/router
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Using Yarn
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
yarn add @aref-shojaei/router
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
# 🚀 Quick Start
|
|
47
|
+
|
|
48
|
+
Create your `app.js` file:
|
|
49
|
+
|
|
50
|
+
```javascript
|
|
51
|
+
import { Router, Route } from "@aref-shojaei/router";
|
|
52
|
+
|
|
53
|
+
// Configure browser environment
|
|
54
|
+
Router.configure({
|
|
55
|
+
window,
|
|
56
|
+
document
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Create routes
|
|
60
|
+
Route.add("/", () => "Home Page");
|
|
61
|
+
|
|
62
|
+
Route.group("/auth", () => {
|
|
63
|
+
Route.add("/login", () => "Login Page");
|
|
64
|
+
Route.add("/register", () => "Register Page");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Dynamic routes
|
|
68
|
+
Route.add("/users/{id}", ({ params: { id } }) => {
|
|
69
|
+
return `User #${id}`;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Start the router
|
|
73
|
+
Router.run();
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
# 🌐 HTML Integration
|
|
79
|
+
|
|
80
|
+
The router automatically intercepts internal links and updates the page without refreshing the browser.
|
|
81
|
+
|
|
82
|
+
```html
|
|
83
|
+
<body>
|
|
84
|
+
<nav>
|
|
85
|
+
<a href="/">Home</a>
|
|
86
|
+
<a href="/auth/login">Login</a>
|
|
87
|
+
<a href="/auth/register">Register</a>
|
|
88
|
+
<a href="/users/10">User Profile</a>
|
|
89
|
+
</nav>
|
|
90
|
+
|
|
91
|
+
<div id="root"></div>
|
|
92
|
+
|
|
93
|
+
<script src="app.js"></script>
|
|
94
|
+
</body>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
# 🏗 Router Workflow
|
|
100
|
+
|
|
101
|
+
```txt
|
|
102
|
+
User Click
|
|
103
|
+
|
|
|
104
|
+
|
|
|
105
|
+
Browser Event
|
|
106
|
+
|
|
|
107
|
+
|
|
|
108
|
+
Router Engine
|
|
109
|
+
|
|
|
110
|
+
|
|
|
111
|
+
Route Matching
|
|
112
|
+
|
|
|
113
|
+
|
|
|
114
|
+
Middleware
|
|
115
|
+
|
|
|
116
|
+
|
|
|
117
|
+
Component Rendering
|
|
118
|
+
|
|
|
119
|
+
|
|
|
120
|
+
DOM Update
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
# 📚 Routing Guide
|
|
126
|
+
|
|
127
|
+
## Basic Routes
|
|
128
|
+
|
|
129
|
+
Create simple routes:
|
|
130
|
+
|
|
131
|
+
```javascript
|
|
132
|
+
Route.add("/", () => "Welcome Page");
|
|
133
|
+
|
|
134
|
+
Route.add("/about", () => "About Page");
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Route Groups
|
|
140
|
+
|
|
141
|
+
Organize related routes together.
|
|
142
|
+
|
|
143
|
+
```javascript
|
|
144
|
+
Route.group("/auth", () => {
|
|
145
|
+
Route.add("/login", () => "Login Page");
|
|
146
|
+
Route.add("/register", () => "Register Page");
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Generated routes:
|
|
151
|
+
|
|
152
|
+
```txt
|
|
153
|
+
/auth/login
|
|
154
|
+
/auth/register
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Dynamic Routes
|
|
160
|
+
|
|
161
|
+
Capture values directly from the URL.
|
|
162
|
+
|
|
163
|
+
```javascript
|
|
164
|
+
Route.add("/users/{id}", ({ params: { id } }) => {
|
|
165
|
+
return `User ID: ${id}`;
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Multiple parameters:
|
|
170
|
+
|
|
171
|
+
```javascript
|
|
172
|
+
Route.add(
|
|
173
|
+
"/courses/{category}/{name}",
|
|
174
|
+
({ params: { category, name } }) => {
|
|
175
|
+
return `${category} / ${name}`;
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
# 🛡 Middleware
|
|
183
|
+
|
|
184
|
+
Middleware allows you to execute custom logic before rendering a route.
|
|
185
|
+
|
|
186
|
+
Common use cases:
|
|
187
|
+
|
|
188
|
+
* Authentication checks
|
|
189
|
+
* Logging
|
|
190
|
+
* Analytics
|
|
191
|
+
* Permissions
|
|
192
|
+
* Data preparation
|
|
193
|
+
|
|
194
|
+
Example:
|
|
195
|
+
|
|
196
|
+
```javascript
|
|
197
|
+
const logger = () => {
|
|
198
|
+
console.log("[LOG] Route visited");
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
Route.add(
|
|
202
|
+
"/dashboard",
|
|
203
|
+
() => "Dashboard"
|
|
204
|
+
).middleware([logger]);
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Group middleware:
|
|
208
|
+
|
|
209
|
+
```javascript
|
|
210
|
+
Route.group("/admin", () => {
|
|
211
|
+
Route.add("/users", () => "Users");
|
|
212
|
+
Route.add("/settings", () => "Settings");
|
|
213
|
+
}).middleware([logger]);
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
# 🔀 Route Redirection
|
|
219
|
+
|
|
220
|
+
Redirect users to another route.
|
|
221
|
+
|
|
222
|
+
```javascript
|
|
223
|
+
Route.add("/login", () => "Login Page");
|
|
224
|
+
|
|
225
|
+
Route.add("/dashboard", () => {
|
|
226
|
+
return Route.redirect("/login");
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
# 🏷 Page Titles
|
|
233
|
+
|
|
234
|
+
Change the browser tab title dynamically.
|
|
235
|
+
|
|
236
|
+
```javascript
|
|
237
|
+
Route.add("/", () => "Home")
|
|
238
|
+
.title("My Awesome SPA");
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
If no title is specified, the router uses the default `document.title`.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
# 📂 Example Project Structure
|
|
246
|
+
|
|
247
|
+
```txt
|
|
248
|
+
my-spa/
|
|
249
|
+
|
|
|
250
|
+
├── index.html
|
|
251
|
+
|
|
|
252
|
+
├── app.js
|
|
253
|
+
|
|
|
254
|
+
└── node_modules/
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
# 💡 Use Cases
|
|
260
|
+
|
|
261
|
+
Router can be used for:
|
|
262
|
+
|
|
263
|
+
* Single Page Applications (SPA)
|
|
264
|
+
* Dashboard interfaces
|
|
265
|
+
* Admin panels
|
|
266
|
+
* Personal websites
|
|
267
|
+
* Progressive Web Applications
|
|
268
|
+
* Educational projects to learn routing concepts
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
# 🔥 Why Router?
|
|
273
|
+
|
|
274
|
+
Traditional websites require a full page reload for every navigation.
|
|
275
|
+
|
|
276
|
+
With Router:
|
|
277
|
+
|
|
278
|
+
```txt
|
|
279
|
+
Traditional Website
|
|
280
|
+
|
|
281
|
+
Click Link
|
|
282
|
+
|
|
|
283
|
+
HTTP Request
|
|
284
|
+
|
|
|
285
|
+
Server Response
|
|
286
|
+
|
|
|
287
|
+
Reload Entire Page
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
SPA Router
|
|
291
|
+
|
|
292
|
+
Click Link
|
|
293
|
+
|
|
|
294
|
+
JavaScript Router
|
|
295
|
+
|
|
|
296
|
+
Change Route
|
|
297
|
+
|
|
|
298
|
+
Update DOM
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
This results in a faster and smoother user experience.
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
# 🧠 Concepts You Will Learn
|
|
306
|
+
|
|
307
|
+
By studying this project, you can understand:
|
|
308
|
+
|
|
309
|
+
* Browser History API
|
|
310
|
+
* URL parsing
|
|
311
|
+
* Route matching algorithms
|
|
312
|
+
* Dynamic parameters
|
|
313
|
+
* Middleware patterns
|
|
314
|
+
* Event handling
|
|
315
|
+
* Client-side rendering
|
|
316
|
+
* SPA architecture
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
# 👨💻 Author
|
|
321
|
+
|
|
322
|
+
**Aref Shojaei**
|
|
323
|
+
- 📧 Email: [arefshojaei82@gmail.com](mailto:arefshojaei82@gmail.com)
|
|
324
|
+
- 🐙 GitHub: [@ArefShojaei](https://github.com/ArefShojaei)
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
# ⭐ Show Your Support
|
|
329
|
+
|
|
330
|
+
If this project helps you understand SPA architecture and client-side routing, consider giving it a **Star ⭐ on GitHub**.
|
|
331
|
+
|
|
332
|
+
Your support helps the project grow.
|
package/build/router.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
(()=>{"use strict";class t extends Error{constructor(t){super(t),this.name="Invalid argument error"}}class e{static#t="";static#e="";static#s;constructor(){throw new Error(`${new.target.name} class must not be called with "new" keyword!`)}static setDocument(e){if("object"!=typeof e)throw new t("'document' must be a Document object!");this.#s=e}static _getDocument(){return this.#s}static setTitle(e){if("string"!=typeof e)throw new t("'title' must be a string!");this.#e=e,this.#i()}static getTitle(){return this.#e||this.#t}static setRootTitle(e){if("string"!=typeof e)throw new t("'value' must be a string!");this.#t=e,this.#i()}static getRootTitle(){return this.#t}static#i(){const t=this._getDocument();this.getTitle()?t.title=this.getTitle():t.title=this.getRootTitle()}}class s{constructor(){throw new Error(`${new.target.name} class must not be called with "new" keyword!`)}static render(e,s={}){if("function"!=typeof e)throw new t("'template' must be a function!");try{return e(s)}catch(t){console.error("Error during template rendering: ",t)}}}class i{static#o=[];constructor(){throw new Error(`${new.target.name} class must not be called with "new" keyword!`)}static findAll(e,s){if("string"!=typeof e)throw new t("'element' must be an HTMLElement object!");if("object"!=typeof s)throw new t("'document' must be a Document object!");const i=s.querySelectorAll(e);return this._setElements(i),this}static each(e){if("function"!=typeof e)throw new t("'callback' must be a function!");const s=this._getElements();this.#o.length&&s.forEach(e)}static _setElements(t){this.#o.push(...t)}static _getElements(){return this.#o}}class o{constructor(){throw new Error(`${new.target.name} class must not be called with "new" keyword!`)}static onClick(e,s){if("object"!=typeof e)throw new t("'element' must be an HTMLElement object!");if("function"!=typeof s)throw new t("'callback' must be a function!");e.addEventListener("click",s)}}class r{title="";template;middlewares=[];meta={params:{},query:{}};constructor({title:t,template:e}){this.title=t??this.title,this.template=e}}class n{static _window;static _document;static _rootElement="#root";static _routes={};static _currentRoute="";static _routePrefix="";static _defaultRoute=new r({title:"404",template:()=>"404 | Page not found!"});constructor(){throw new Error(`${new.target.name} class must not be called with "new" keyword!`)}static configure({window:e,document:s,selector:i}){if("object"!=typeof e)throw new t("'window' must be a Window object!");if("object"!=typeof s)throw new t("'document' must be a Document object!");if(i&&"string"!=typeof i)throw new t("'selector' must be a string!");i&&(this._rootElement=i),this._window=e,this._document=s}static#r(t){for(const e in this._routes){const s=new RegExp(`^${e.replace(/\{(\w+)\}/g,"(?<$1>[^/{}]+)")}$`);if(!s.test(t))continue;const{groups:i}=s.exec(t);return this.#n(e),this.#a(e,i),this._routes[e]}}static#n(t){const e={},{search:s}=location;if(!s.length)return!1;s.slice(1).split("&").forEach((t=>{const[s,i]=t.split("=");e[s]=i})),this._routes[t].meta.query={...e}}static#a(t,e){this._routes[t].meta.params={...e}}static _setRouteToURL(e){if("string"!=typeof e||!e.startsWith("/"))throw new t("'route' must be a string starting with \"/\"!");this._window.history.pushState({},"",e)}static#c(t){try{const{title:i,template:o,middlewares:r,meta:n}=this.#r(t)??this._defaultRoute;e.setTitle(i),this.#u(r),this._document.querySelector(this._rootElement).innerHTML=s.render(o,n)}catch(e){console.error("Error to inject route template:",t,e)}}static#l(){this._window.addEventListener("popstate",(t=>{const e=t.target.location.pathname;this.#c(e)}))}static#h(){const{pathname:t}=this._window.location;this.#c(t)}static#w(){i.findAll("a",this._document).each((t=>{o.onClick(t,(t=>{if(t.target.hasAttribute("data-link"))return!1;t.preventDefault();const e=t.target.getAttribute("href");this._setRouteToURL(e),this.#c(e)}))}))}static#u(t){try{t.length&&t.forEach((t=>t()))}catch(t){console.error("Error executing middleware:",t)}}static run(s=(()=>{})){if("function"!=typeof s)throw new t("'callback' must be a function!");e.setDocument(this._document),e.setRootTitle(this._document.title),this.#l(),this.#h(),this.#w(),s()}}window.Router=n,window.Route=class extends n{constructor(){throw new Error(`${new.target.name} class must not be called with "new" keyword!`)}static
|
|
1
|
+
(()=>{"use strict";class t extends Error{constructor(t){super(t),this.name="Invalid argument error"}}class e{static#t="";static#e="";static#s;constructor(){throw new Error(`${new.target.name} class must not be called with "new" keyword!`)}static setDocument(e){if("object"!=typeof e)throw new t("'document' must be a Document object!");this.#s=e}static _getDocument(){return this.#s}static setTitle(e){if("string"!=typeof e)throw new t("'title' must be a string!");this.#e=e,this.#i()}static getTitle(){return this.#e||this.#t}static setRootTitle(e){if("string"!=typeof e)throw new t("'value' must be a string!");this.#t=e,this.#i()}static getRootTitle(){return this.#t}static#i(){const t=this._getDocument();this.getTitle()?t.title=this.getTitle():t.title=this.getRootTitle()}}class s{constructor(){throw new Error(`${new.target.name} class must not be called with "new" keyword!`)}static render(e,s={}){if("function"!=typeof e)throw new t("'template' must be a function!");try{return e(s)}catch(t){console.error("Error during template rendering: ",t)}}}class i{static#o=[];constructor(){throw new Error(`${new.target.name} class must not be called with "new" keyword!`)}static findAll(e,s){if("string"!=typeof e)throw new t("'element' must be an HTMLElement object!");if("object"!=typeof s)throw new t("'document' must be a Document object!");const i=s.querySelectorAll(e);return this._setElements(i),this}static each(e){if("function"!=typeof e)throw new t("'callback' must be a function!");const s=this._getElements();this.#o.length&&s.forEach(e)}static _setElements(t){this.#o.push(...t)}static _getElements(){return this.#o}}class o{constructor(){throw new Error(`${new.target.name} class must not be called with "new" keyword!`)}static onClick(e,s){if("object"!=typeof e)throw new t("'element' must be an HTMLElement object!");if("function"!=typeof s)throw new t("'callback' must be a function!");e.addEventListener("click",s)}}class r{title="";template;middlewares=[];meta={params:{},query:{}};constructor({title:t,template:e}){this.title=t??this.title,this.template=e}}class n{static _window;static _document;static _rootElement="#root";static _routes={};static _currentRoute="";static _routePrefix="";static _defaultRoute=new r({title:"404",template:()=>"404 | Page not found!"});constructor(){throw new Error(`${new.target.name} class must not be called with "new" keyword!`)}static configure({window:e,document:s,selector:i}){if("object"!=typeof e)throw new t("'window' must be a Window object!");if("object"!=typeof s)throw new t("'document' must be a Document object!");if(i&&"string"!=typeof i)throw new t("'selector' must be a string!");i&&(this._rootElement=i),this._window=e,this._document=s}static#r(t){for(const e in this._routes){const s=new RegExp(`^${e.replace(/\{(\w+)\}/g,"(?<$1>[^/{}]+)")}$`);if(!s.test(t))continue;const{groups:i}=s.exec(t);return this.#n(e),this.#a(e,i),this._routes[e]}}static#n(t){const e={},{search:s}=location;if(!s.length)return!1;s.slice(1).split("&").forEach((t=>{const[s,i]=t.split("=");e[s]=i})),this._routes[t].meta.query={...e}}static#a(t,e){this._routes[t].meta.params={...e}}static _setRouteToURL(e){if("string"!=typeof e||!e.startsWith("/"))throw new t("'route' must be a string starting with \"/\"!");this._window.history.pushState({},"",e)}static#c(t){try{const{title:i,template:o,middlewares:r,meta:n}=this.#r(t)??this._defaultRoute;e.setTitle(i),this.#u(r),this._document.querySelector(this._rootElement).innerHTML=s.render(o,n)}catch(e){console.error("Error to inject route template:",t,e)}}static#l(){this._window.addEventListener("popstate",(t=>{const e=t.target.location.pathname;this.#c(e)}))}static#h(){const{pathname:t}=this._window.location;this.#c(t)}static#w(){i.findAll("a",this._document).each((t=>{o.onClick(t,(t=>{if(t.target.hasAttribute("data-link"))return!1;t.preventDefault();const e=t.target.getAttribute("href");this._setRouteToURL(e),this.#c(e)}))}))}static#u(t){try{t.length&&t.forEach((t=>t()))}catch(t){console.error("Error executing middleware:",t)}}static run(s=(()=>{})){if("function"!=typeof s)throw new t("'callback' must be a function!");e.setDocument(this._document),e.setRootTitle(this._document.title),this.#l(),this.#h(),this.#w(),s()}}window.Router=n,window.Route=class extends n{constructor(){throw new Error(`${new.target.name} class must not be called with "new" keyword!`)}static add(e,s){if("string"!=typeof e)throw new t("'route' must be a string!");if("function"!=typeof s)throw new t("'callback' must be a function!");return this._routes[this._routePrefix+e]=new r({template:s}),this._currentRoute=e,this}static group(e,s){if("string"!=typeof e||!e.startsWith("/"))throw new t("'prefix' must be a string!");if("function"!=typeof s)throw new t("'callback' must be a function!");const i=this._routePrefix;return this._routePrefix=e,s(),this._routePrefix=i,this}static middleware(e){if(!Array.isArray(e))throw new t("'middlewares' must be an array!");if(this._routePrefix)for(const t in this._routes)t.startsWith(this._routePrefix)&&this._routes[t].middlewares.push(...e);else this._routes[this._routePrefix+this._currentRoute].middlewares.push(...e)}static title(e){if("string"!=typeof e)throw new t("'value' must be a string!");this._routes[this._routePrefix+this._currentRoute].title=e}static redirect(e){if("string"!=typeof e||!e.startsWith("/"))throw new t("'to' must be a string starting route with \"/\"!");this._setRouteToURL(e);const{template:i,meta:o}=this._routes[e]??this._defaultRoute;return s.render(i,o)}}})();
|
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
* {
|
|
2
|
-
box-sizing: border-box;
|
|
3
|
-
padding: 0;
|
|
4
|
-
margin: 0;
|
|
5
|
-
font-family: sans-serif;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
body {
|
|
9
|
-
display: flex;
|
|
10
|
-
flex-direction: column;
|
|
11
|
-
align-items: center;
|
|
12
|
-
padding: 24px;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
ul {
|
|
16
|
-
display: flex;
|
|
17
|
-
align-items: center;
|
|
18
|
-
margin-top: 16px;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
ul li {
|
|
22
|
-
margin: 0 24px;
|
|
23
|
-
list-style: none;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
ul li a {
|
|
27
|
-
padding: 4 8px;
|
|
28
|
-
color: #81a6f4;
|
|
29
|
-
text-decoration: none;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
ul li a:hover {
|
|
33
|
-
color: #2b2b2a;
|
|
34
|
-
transition: all .5s ease;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
#root {
|
|
39
|
-
padding: 16px;
|
|
40
|
-
text-align: center;
|
|
41
|
-
margin-top: 40px;
|
|
42
|
-
background: #ded7d7;
|
|
43
|
-
width: 600px;
|
|
44
|
-
height: 250px;
|
|
45
|
-
border-radius: 16px;
|
|
1
|
+
* {
|
|
2
|
+
box-sizing: border-box;
|
|
3
|
+
padding: 0;
|
|
4
|
+
margin: 0;
|
|
5
|
+
font-family: sans-serif;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
body {
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
align-items: center;
|
|
12
|
+
padding: 24px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
ul {
|
|
16
|
+
display: flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
margin-top: 16px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
ul li {
|
|
22
|
+
margin: 0 24px;
|
|
23
|
+
list-style: none;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
ul li a {
|
|
27
|
+
padding: 4 8px;
|
|
28
|
+
color: #81a6f4;
|
|
29
|
+
text-decoration: none;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
ul li a:hover {
|
|
33
|
+
color: #2b2b2a;
|
|
34
|
+
transition: all .5s ease;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
#root {
|
|
39
|
+
padding: 16px;
|
|
40
|
+
text-align: center;
|
|
41
|
+
margin-top: 40px;
|
|
42
|
+
background: #ded7d7;
|
|
43
|
+
width: 600px;
|
|
44
|
+
height: 250px;
|
|
45
|
+
border-radius: 16px;
|
|
46
46
|
}
|
package/example/assets/js/app.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
Router.configure({ window, document })
|
|
2
|
-
|
|
3
|
-
Route.
|
|
4
|
-
Route.
|
|
5
|
-
Route.
|
|
6
|
-
Route.
|
|
7
|
-
|
|
1
|
+
Router.configure({ window, document })
|
|
2
|
+
|
|
3
|
+
Route.add("/", () => "Welcome Page")
|
|
4
|
+
Route.add("/users", () => "Users Page")
|
|
5
|
+
Route.add("/users/{name}", ({ params : { name } }) => `User #${name} Page`)
|
|
6
|
+
Route.add("/redirection", () => Route.redirect("/"))
|
|
7
|
+
|
|
8
8
|
Router.run()
|