Skip to content

Commit 73aa624

Browse files
authored
Elroid 3 - Add Router
1 parent 39c4b56 commit 73aa624

File tree

5 files changed

+244
-27
lines changed

5 files changed

+244
-27
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const App = new ElComponent({
2+
template: `
3+
<h1 style="{{style}}">{{title}}</h1>
4+
<button el-click="Edit">Edit Tilte</button>
5+
`,
6+
el: "#app",
7+
data: {
8+
title: 'Component',
9+
style: "color: black;",
10+
methods: {
11+
Edit() {
12+
App.update({ title: "Home", style: "color: red;" });
13+
}
14+
}
15+
}
16+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
7+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
8+
<title>App</title>
9+
</head>
10+
11+
<body>
12+
<div id="app"></div>
13+
14+
<script src="Elroid.js"></script>
15+
<script type="module" src="App.js"></script>
16+
</body>
17+
18+
</html>

Elroid.js

Lines changed: 133 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,57 @@
1-
// Class Elroid
1+
// Class Elroid: A simple templating engine that compiles a template string with data and binds event listeners based on the provided options.
22
class Elroid {
33
constructor(options) {
4+
// Cache the provided element selector, data, and template.
45
this.el = options.el;
56
this.data = options.data;
67
this.template = document.querySelector(options.el).innerHTML;
78

9+
// Compile the initial template and bind events.
810
this.compile();
911
this.bindEvents();
1012
}
1113

14+
// Compile all elements matching the element selector provided in the options.
1215
compile() {
1316
const elements = document.querySelectorAll(this.el);
1417
elements.forEach((element) => {
1518
this.compileElement(element);
1619
});
1720
}
1821

22+
// Compile a single element's template string.
1923
compileElement(element) {
2024
const template = element.innerHTML;
2125
const compiled = this.compileTemplate(template);
2226
element.innerHTML = compiled;
2327
}
2428

29+
// Compile a template string with data.
2530
compileTemplate(template) {
26-
const regex = /\{\{(.*?)\}\}/g;
31+
const regex = /\{\{(.*?)\}\}/g; // Use a regex to match all instances of {{...}} in the template.
2732
const compiled = template.replace(regex, (match, p1) => {
28-
return p1.split('.').reduce((acc, key) => acc[key.trim()], this.data) || '';
33+
return p1.split('.').reduce((acc, key) => acc[key.trim()], this.data) || ''; // Replace each matched string with the corresponding data value.
2934
});
3035
return compiled;
3136
}
3237

38+
// Bind event listeners to elements with the el-click attribute.
3339
bindEvents() {
3440
const elements = document.querySelectorAll('[el-click]');
3541
elements.forEach((element) => {
3642
const methodName = element.getAttribute('el-click');
3743
const method = this.data.methods[methodName];
3844
if (method && typeof method === 'function') {
3945
element.addEventListener('click', () => {
40-
method.bind(this.data)();
41-
this.compile();
42-
this.bindEvents();
46+
method.bind(this.data)(); // Bind the method to the data object and invoke it on click.
47+
const route = this.data.route || '/';
48+
router.navigateTo(route); // Navigate to the route specified in the data object, or the root route by default.
4349
});
4450
}
4551
});
4652
}
4753

54+
// Update the data object and recompile the template.
4855
update(data) {
4956
Object.assign(this.data, data);
5057
const compiledTemplate = this.compileTemplate(this.template);
@@ -54,43 +61,53 @@ class Elroid {
5461
}
5562
}
5663

57-
// Elroid Component
64+
65+
// Class Elroid Component: A subclass of Elroid that represents a single component with its own template and data.
5866
class ElComponent {
5967
constructor(options) {
68+
// Cache the provided template, data, route, and element selector.
6069
this.template = options.template;
6170
this.data = options.data;
71+
this.route = options.route;
6272
this.el = document.querySelector(options.el);
6373

74+
// Compile the initial template and bind events.
6475
this.compile();
6576
this.bindEvents();
6677
}
6778

79+
// Compile the component's template string.
6880
compile() {
6981
const compiledTemplate = this.compileTemplate(this.template);
7082
this.el.innerHTML = compiledTemplate;
7183
}
7284

85+
// Compile a template string with data.
7386
compileTemplate(template) {
74-
const regex = /\{\{(.*?)\}\}/g;
87+
const regex = /\{\{(.*?)\}\}/g; // Use a regex to match all instances of {{...}} in the template.
7588
const compiled = template.replace(regex, (match, p1) => {
76-
return p1.split('.').reduce((acc, key) => acc[key.trim()], this.data) || '';
89+
return p1.split('.').reduce((acc, key) => acc[key.trim()], this.data) || ''; // Replace each matched string with the corresponding data value.
7790
});
7891
return compiled;
7992
}
8093

94+
// Bind event listeners to elements with the el-click attribute.
8195
bindEvents() {
8296
const elements = this.el.querySelectorAll('[el-click]');
8397
elements.forEach((element) => {
8498
const methodName = element.getAttribute('el-click');
8599
const method = this.data.methods[methodName];
86100
if (method && typeof method === 'function') {
87101
element.addEventListener('click', () => {
88-
method.bind(this.data)();
102+
method.bind(this.data)(); // Bind the method to the data object and invoke it on click.
103+
const route = this.data.route || '/';
104+
router.navigateTo(route); // Navigate to the route specified in the data object, or the root route by default.
89105
});
90106
}
91107
});
92108
}
93109

110+
// Update the data object and recompile the template.
94111
update(data) {
95112
Object.assign(this.data, data);
96113
const compiledTemplate = this.compileTemplate(this.template);
@@ -99,90 +116,179 @@ class ElComponent {
99116
}
100117
}
101118

102-
// Elroid Request
119+
120+
// ElRouter: A simple client-side router for single-page applications.
121+
class ElRouter {
122+
constructor(options) {
123+
this.routes = options.routes; // An array of route objects, where each object contains a route and a component.
124+
this.defaultRoute = options.defaultRoute; // The default route to navigate to if no matching route is found.
125+
this.errorRoute = options.errorRoute; // The error route to navigate to if a matching route is not found.
126+
this.el = options.el; // The DOM element to render components into.
127+
this.visitedRoutes = []; // An array of visited routes.
128+
129+
this.init(); // Initialize the router.
130+
}
131+
132+
// Initialize the router by setting up event listeners and handling the initial page load.
133+
init() {
134+
// Handle initial page load
135+
this.navigateTo(window.location.pathname);
136+
137+
// Handle back/forward button clicks
138+
window.addEventListener('popstate', () => {
139+
this.goToPreviousRoute();
140+
});
141+
142+
// Handle anchor tag clicks
143+
document.addEventListener('click', (event) => {
144+
const anchor = event.target.closest('a');
145+
if (anchor && anchor.getAttribute('href').startsWith('/')) {
146+
event.preventDefault();
147+
this.navigateTo(anchor.getAttribute('href'));
148+
}
149+
});
150+
}
151+
152+
// Navigate to the specified path by finding the corresponding route and rendering the component.
153+
navigateTo(path) {
154+
const route = this.findRoute(path) || this.findRoute(this.errorRoute); // Find the route object for the specified path or the error route.
155+
const { component, data } = route; // Destructure the component and data properties from the route object.
156+
157+
// Create a new component instance
158+
const elComponent = new component({ el: this.el, data });
159+
160+
// Add the current route to the visited routes array
161+
this.visitedRoutes.push(path);
162+
163+
// Update the browser history without reloading the page
164+
history.pushState({ path }, '', path);
165+
}
166+
167+
// Navigate to the previous route by retrieving the previous path from the visited routes array and rendering the corresponding component.
168+
goToPreviousRoute() {
169+
if (this.visitedRoutes.length > 1) {
170+
// Remove the current route from the visited routes array
171+
this.visitedRoutes.pop();
172+
// Retrieve the previous route from the visited routes array
173+
const previousPath = this.visitedRoutes[this.visitedRoutes.length - 1];
174+
const previousRoute = this.findRoute(previousPath) || this.findRoute(this.errorRoute);
175+
const { component: previousComponent, data: previousData } = previousRoute;
176+
177+
// Create a new component instance for the previous route
178+
const previousElComponent = new previousComponent({ el: this.el, data: previousData });
179+
180+
// Update the browser history without reloading the page
181+
history.pushState({ path: previousPath }, '', previousPath);
182+
}
183+
}
184+
185+
// Find the route object for the specified path.
186+
findRoute(path) {
187+
return this.routes.find((route) => route.route === path);
188+
}
189+
}
190+
191+
192+
// ElRequest: A simple XMLHttpRequest wrapper for making HTTP requests.
103193
class ElRequest {
104194
constructor() {
105-
this.http = new XMLHttpRequest();
106-
this.headers = {};
195+
this.http = new XMLHttpRequest(); // Create a new instance of XMLHttpRequest.
196+
this.headers = {}; // Initialize an empty headers object.
107197
}
108198

199+
// Set a header for the request.
109200
setHeader(key, value) {
110201
this.headers[key] = value;
111202
}
112203

204+
// Make a GET request.
113205
get(url = '', data = {}, callback = () => { }) {
206+
// Convert the data object to a query string.
114207
const queryString = Object.entries(data)
115208
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
116209
.join('&');
117210

118-
this.http.open('GET', `${url}?${queryString}`, true);
211+
this.http.open('GET', `${url}?${queryString}`, true); // Open a GET request to the provided URL with the query string.
119212

213+
// Set any headers provided.
120214
for (const [key, value] of Object.entries(this.headers)) {
121215
this.http.setRequestHeader(key, value);
122216
}
123217

218+
// Handle the response when it loads.
124219
this.http.onload = function () {
125220
if (this.http.status === 200) {
126-
callback(null, this.http.responseText);
221+
callback(null, this.http.responseText); // Invoke the callback with no errors and the response text.
127222
} else {
128-
callback(`Error: ${this.http.status}`);
223+
callback(`Error: ${this.http.status}`); // Invoke the callback with an error message.
129224
}
130225
}.bind(this);
131226

132-
this.http.send();
227+
this.http.send(); // Send the request.
133228
}
134229

230+
// Make a POST request.
135231
post(url = '', data = {}, callback = () => { }) {
136-
this.http.open('POST', url, true);
232+
this.http.open('POST', url, true); // Open a POST request to the provided URL.
137233

234+
// Set any headers provided.
138235
for (const [key, value] of Object.entries(this.headers)) {
139236
this.http.setRequestHeader(key, value);
140237
}
141238

239+
// Handle the response when it loads.
142240
this.http.onload = function () {
143-
callback(null, this.http.responseText);
241+
callback(null, this.http.responseText); // Invoke the callback with no errors and the response text.
144242
}.bind(this);
145243

244+
// Convert the data object to a request body string.
146245
const requestBody = Object.entries(data)
147246
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
148247
.join('&');
149248

150-
this.http.send(requestBody);
249+
this.http.send(requestBody); // Send the request with the request body.
151250
}
152251

252+
// Make a PUT request.
153253
put(url = '', data = {}, callback = () => { }) {
154-
this.http.open('PUT', url, true);
254+
this.http.open('PUT', url, true); // Open a PUT request to the provided URL.
155255

256+
// Set any headers provided.
156257
for (const [key, value] of Object.entries(this.headers)) {
157258
this.http.setRequestHeader(key, value);
158259
}
159260

261+
// Handle the response when it loads.
160262
this.http.onload = function () {
161-
callback(null, this.http.responseText);
263+
callback(null, this.http.responseText); // Invoke the callback with no errors and the response text.
162264
}.bind(this);
163265

266+
// Convert the data object to a request body string.
164267
const requestBody = Object.entries(data)
165268
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
166269
.join('&');
167270

168-
this.http.send(requestBody);
271+
this.http.send(requestBody); // Send the request with the request body.
169272
}
170273

274+
// Make a DELETE request.
171275
delete(url = '', callback = () => { }) {
172-
this.http.open('DELETE', url, true);
276+
this.http.open('DELETE', url, true); // Open a DELETE request to the provided URL.
173277

278+
// Set any headers provided.
174279
for (const [key, value] of Object.entries(this.headers)) {
175280
this.http.setRequestHeader(key, value);
176281
}
177282

283+
// Handle the response when it loads.
178284
this.http.onload = function () {
179285
if (this.http.status === 200) {
180-
callback(null, 'Post Deleted!');
286+
callback(null, 'Post Deleted!'); // Invoke the callback with no errors and a success message.
181287
} else {
182-
callback(`Error: ${this.http.status}`);
288+
callback(`Error: ${this.http.status}`); // Invoke the callback with an error message.
183289
}
184290
}.bind(this);
185291

186-
this.http.send();
292+
this.http.send(); // Send the request.
187293
}
188294
}

0 commit comments

Comments
 (0)