turbo-web 4.2.10 → 4.4.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 +100 -31
- package/bin/turbo-charge.js +36 -6
- package/dist/turbo.js +17 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,34 +67,48 @@ If you change code inside **examples/** make sure to do a hard reset for changes
|
|
|
67
67
|
---
|
|
68
68
|
|
|
69
69
|
### [CORE] Feature 1: Declarative UI
|
|
70
|
-
Using declarative methods to define final UI instead of manually handling DOM elements. "What the UI looks like vs step-by-step imperative programming"
|
|
70
|
+
**Abstraction** principle: Using declarative methods to define final UI instead of manually handling DOM elements. "What the UI looks like vs step-by-step imperative programming"
|
|
71
|
+
|
|
72
|
+
```component.js```
|
|
71
73
|
|
|
72
74
|
### [CORE] Feature 2: Virtual DOM
|
|
73
|
-
Inversed Control principle - the user doesn't need to manipulate the DOM directly, the framework creates a lightweight
|
|
75
|
+
**Inversed Control** principle - the user doesn't need to manipulate the DOM directly, the framework creates a lightweight
|
|
74
76
|
JavaScript object tree that represents the UI.
|
|
75
77
|
|
|
78
|
+
```patch-dom.js```
|
|
79
|
+
|
|
76
80
|
### [CORE] Feature 3: Reconciliation Algorithm
|
|
77
|
-
Inversed Control principle - optimizing DOM manipulations by the process of diffing and patching only the required nodes (improves DOM re-rendering & re-painting).
|
|
81
|
+
**Inversed Control** principle - optimizing DOM manipulations by the process of diffing and patching only the required nodes (improves DOM re-rendering & re-painting).
|
|
78
82
|
|
|
83
|
+
```patch-dom.js```
|
|
79
84
|
### [CORE] Feature 4: Components
|
|
80
|
-
Encapsulation & Reusability principles: making UI building blocks with better combined hierarchical tree structure.
|
|
85
|
+
**Encapsulation & Reusability** principles: making UI building blocks with better combined hierarchical tree structure.
|
|
86
|
+
|
|
87
|
+
// Events accept additional modificator ```.stop .prevent``` to stop event bubbling and prevent default browser behaviour.
|
|
88
|
+
|
|
89
|
+
```component.js // building scheme: h(tagOrComponent, { attributes, class, style, on: events }, [children])```
|
|
81
90
|
|
|
82
91
|
### Feature 5: Client-Side Router (#hash-based)
|
|
83
92
|
Allows for the **SPA design** (Single Page Application) with route guards (fn: checkNavigation) and a common "catch-all route" (cases: route not found). Based on a hash part of the URL, implemented with regex pattern matching by simulating URL path, parameters and query as part of the hash fragment itself.
|
|
84
|
-
|
|
93
|
+
|
|
94
|
+
```router.js, route-matchers.js```
|
|
85
95
|
|
|
86
96
|
### Feature 6: Centralized Store
|
|
87
|
-
Separation of Concerns & Single Source of Truth principles: Allows for a global state handling and eliminates the need for prop drilling.
|
|
88
|
-
|
|
97
|
+
**Separation of Concerns** & **Single Source of Truth** principles: Allows for a global state handling and eliminates the need for prop drilling.
|
|
98
|
+
```store.js```
|
|
89
99
|
|
|
90
100
|
### Feature 7: Persistent Storage
|
|
91
|
-
Single Source of Truth principle: App's state is persistent between sessions.
|
|
92
|
-
|
|
101
|
+
**Single Source of Truth** principle: App's state is persistent between sessions.
|
|
102
|
+
|
|
103
|
+
```store.js```
|
|
93
104
|
|
|
94
|
-
|
|
105
|
+
### Feature 8: built-in HTTP module.
|
|
106
|
+
**Abstraction** principle: simplifies development by abstracting imperative logic in HTTP request handling.
|
|
107
|
+
|
|
108
|
+
```http.js```
|
|
95
109
|
|
|
96
110
|
### Feature X: Node-based Initialization through CLI
|
|
97
|
-
Scaffolding setup script that creates base structure with required templates for the App
|
|
111
|
+
Scaffolding setup script that creates base structure with required templates for the App
|
|
98
112
|
|
|
99
113
|
Script: ```npx turbo-web ${PROJECT_NAME}``` creates "Project" & src directory + main.js, router.js, store.js + package.json
|
|
100
114
|
|
|
@@ -103,31 +117,86 @@ Script: ```npx turbo-web ${PROJECT_NAME}``` creates "Project" & src directory +
|
|
|
103
117
|
|
|
104
118
|
---
|
|
105
119
|
|
|
106
|
-
###
|
|
120
|
+
### [1] Router
|
|
121
|
+
Define Routes with Guards - Use the beforeEnter guard to protect routes (e.g., authentication).
|
|
122
|
+
>The router supports asynchronous guards that can either return a boolean or redirect to a different path string
|
|
107
123
|
|
|
124
|
+
```
|
|
125
|
+
beforeEnter: async (from, to, params, query) => {
|
|
126
|
+
const isAuthenticated = localStorage.getItem('auth_token');
|
|
127
|
+
if (!isAuthenticated) return '/login'; // Redirects to login
|
|
128
|
+
return true; // allows navigation
|
|
129
|
+
}
|
|
130
|
+
```
|
|
108
131
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
```await router.init()```
|
|
112
|
-
```#isInitialized```
|
|
132
|
+
### [2] RouterLink
|
|
133
|
+
Always use the built-in RouterLink component instead of standard anchor tags ```(<a href="...">)``` for internal navigation.
|
|
113
134
|
|
|
114
|
-
|
|
115
|
-
|
|
135
|
+
```
|
|
136
|
+
export const Navbar = defineComponent({
|
|
137
|
+
render() {
|
|
138
|
+
return h('nav', {}, [
|
|
139
|
+
h(RouterLink, { to: '/' }, ['Home']),
|
|
140
|
+
h(RouterLink, { to: '/dashboard' }, ['Dashboard'])
|
|
141
|
+
]);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
```
|
|
116
145
|
|
|
117
|
-
###
|
|
118
|
-
|
|
146
|
+
### [3] Components
|
|
147
|
+
Never mutate this.state directly. Always use this.updateState() to trigger the virtual DOM patching process.
|
|
148
|
+
> State initialization should be done via the state() function
|
|
119
149
|
```
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
150
|
+
increment() {
|
|
151
|
+
// Correct: Triggers VDOM diffing and patching
|
|
152
|
+
this.updateState({ count: this.state.count + 1 });
|
|
153
|
+
|
|
154
|
+
// Incorrect: Will not update the UI
|
|
155
|
+
// this.state.count++;
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### [4] Centralized Store
|
|
160
|
+
Mutations must be strictly synchronous as they immediately write to localStorage and trigger state-change events. Use actions for asynchronous operations like HTTP requests, and have the action commit the mutation.
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
mutations: {
|
|
164
|
+
setLoading(state, status) {
|
|
165
|
+
state.isLoading = status; // Synchronous
|
|
124
166
|
},
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
167
|
+
setUsers(state, users) {
|
|
168
|
+
state.users = users; // Synchronous
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
actions: {
|
|
172
|
+
async fetchUsers({ commit, state }) {
|
|
173
|
+
commit('setLoading', true);
|
|
174
|
+
// Async operation
|
|
175
|
+
const response = await fetch('https://api.example.com/users');
|
|
176
|
+
const data = await response.json();
|
|
177
|
+
commit('setUsers', data);
|
|
178
|
+
commit('setLoading', false);
|
|
132
179
|
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### [5] Subscribe to Store Changes in Components
|
|
183
|
+
To make a component react to global state changes, subscribe to the store in onMounted and trigger a local re-render.
|
|
184
|
+
> You must unsubscribe in ```onUnmounted```!!!.
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
onMounted() {
|
|
188
|
+
this.unsubscribe = this.appContext.store.subscribe(() => {
|
|
189
|
+
this.updateState({}); // Force re-render
|
|
190
|
+
});
|
|
191
|
+
this.appContext.store.dispatch('fetchUsers');
|
|
192
|
+
},
|
|
193
|
+
onUnmounted() {
|
|
194
|
+
if (this.unsubscribe) this.unsubscribe();
|
|
195
|
+
},
|
|
196
|
+
```
|
|
197
|
+
### [6] Event bubbling & default browser behaviour override
|
|
198
|
+
It is possible to prevent both by passing additional modificators with functions.
|
|
199
|
+
```
|
|
200
|
+
on: { 'click.stop': () => this.addLog('Button with .stop Clicked') }
|
|
201
|
+
on: { 'click.prevent': () => this.addLog('Link with .prevent Clicked') }
|
|
133
202
|
```
|
package/bin/turbo-charge.js
CHANGED
|
@@ -41,17 +41,47 @@ fs.writeFileSync(
|
|
|
41
41
|
|
|
42
42
|
// defines the boilerplate file contents
|
|
43
43
|
const storeContent = `import { Store } from 'turbo-web';
|
|
44
|
-
export const appStore = new Store({ state: { count: 0 } });`;
|
|
45
44
|
|
|
46
|
-
const
|
|
47
|
-
|
|
45
|
+
export const appStore = new Store({
|
|
46
|
+
state: {
|
|
47
|
+
count: 0
|
|
48
|
+
},
|
|
49
|
+
mutations: {
|
|
50
|
+
increment(state) {
|
|
51
|
+
state.count++;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});`;
|
|
55
|
+
|
|
56
|
+
const routerContent = `import { HashRouter, defineComponent, h } from 'turbo-web';
|
|
57
|
+
|
|
58
|
+
const Home = defineComponent({
|
|
59
|
+
render() {
|
|
60
|
+
return h('h2', {}, ['Welcome to Home Page']);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export const router = new HashRouter([
|
|
65
|
+
{ path: '/', component: Home }
|
|
66
|
+
]);`;
|
|
48
67
|
|
|
49
|
-
const mainContent = `import { createApp, h } from 'turbo-web';
|
|
68
|
+
const mainContent = `import { createApp, defineComponent, h, RouterOutlet } from 'turbo-web';
|
|
50
69
|
import { router } from './router.js';
|
|
51
70
|
import { appStore } from './store.js';
|
|
52
71
|
|
|
53
|
-
const App =
|
|
54
|
-
|
|
72
|
+
const App = defineComponent({
|
|
73
|
+
render() {
|
|
74
|
+
return h('div', { style: { padding: '20px', fontFamily: 'sans-serif' } }, [
|
|
75
|
+
h('h1', {}, ['Turbo Power! 🚀']),
|
|
76
|
+
h('hr'),
|
|
77
|
+
// RouterOutlet renders the components defined in router.js
|
|
78
|
+
h(RouterOutlet)
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const app = createApp(App, {}, { router, store: appStore });
|
|
84
|
+
app.mount(document.body);`;
|
|
55
85
|
|
|
56
86
|
// write the files to the user's new project directory
|
|
57
87
|
fs.writeFileSync(path.join(srcPath, 'store.js'), storeContent);
|
package/dist/turbo.js
CHANGED
|
@@ -194,8 +194,15 @@ function hSlot(name = 'default', children = []) {
|
|
|
194
194
|
return { type: DOM_TYPES.SLOT, name: name, children: children }
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
function addEventListener(
|
|
198
|
-
|
|
197
|
+
function addEventListener(eventNameWithModifiers, handler, el, hostComponent = null) {
|
|
198
|
+
const [eventName, ...modifiers] = eventNameWithModifiers.split('.');
|
|
199
|
+
function boundHandler(event) {
|
|
200
|
+
if (modifiers.includes('stop')) {
|
|
201
|
+
event.stopPropagation();
|
|
202
|
+
}
|
|
203
|
+
if (modifiers.includes('prevent')) {
|
|
204
|
+
event.preventDefault();
|
|
205
|
+
}
|
|
199
206
|
hostComponent
|
|
200
207
|
? handler.apply(hostComponent, arguments)
|
|
201
208
|
: handler(...arguments);
|
|
@@ -212,7 +219,8 @@ function addEventListeners(listeners = {}, el, hostComponent = null ) {
|
|
|
212
219
|
return addedListeners
|
|
213
220
|
}
|
|
214
221
|
function removeEventListeners(listeners = {}, el) {
|
|
215
|
-
Object.entries(listeners).forEach(([
|
|
222
|
+
Object.entries(listeners).forEach(([eventNameWithModifiers, handler]) => {
|
|
223
|
+
const eventName = eventNameWithModifiers.split('.')[0];
|
|
216
224
|
el.removeEventListener(eventName, handler);
|
|
217
225
|
});
|
|
218
226
|
}
|
|
@@ -893,14 +901,14 @@ function patchStyles(el, oldStyle = {}, newStyle = {}) {
|
|
|
893
901
|
}
|
|
894
902
|
function patchEvents(el, oldListeners = {}, oldEvents = {}, newEvents = {}, hostComponent) {
|
|
895
903
|
const { removed, added, updated } = objectsDiff(oldEvents, newEvents);
|
|
896
|
-
for (const
|
|
897
|
-
|
|
904
|
+
for (const eventNameWithModifiers of removed.concat(updated)) {
|
|
905
|
+
const eventName = eventNameWithModifiers.split('.')[0];
|
|
906
|
+
el.removeEventListener(eventName, oldListeners[eventNameWithModifiers]);
|
|
898
907
|
}
|
|
899
908
|
const addedListeners = {};
|
|
900
|
-
for (const
|
|
901
|
-
const listener =
|
|
902
|
-
|
|
903
|
-
addedListeners[eventName] = listener;
|
|
909
|
+
for (const eventNameWithModifiers of added.concat(updated)) {
|
|
910
|
+
const listener = addEventListener(eventNameWithModifiers, newEvents[eventNameWithModifiers], el, hostComponent);
|
|
911
|
+
addedListeners[eventNameWithModifiers] = listener;
|
|
904
912
|
}
|
|
905
913
|
return addedListeners
|
|
906
914
|
}
|