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 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
- // router.js, route-matchers.js
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
- ### Routing
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
- ```const routes = [{ path: '/', component: Home }, {}, {}]```
110
- ```const router = new HashRouter(routes)```
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
- ### Components
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
- ### Centralized Store
118
- Must be initialized in a separate file (store.js).
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
- export const store = new Store({
121
- state: {
122
- count: 0,
123
- user: null
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
- mutations: {
126
- increment(state, payload = 1) {
127
- state.count += payload
128
- },
129
- setUser(state, user) {
130
- state.user = user
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
  ```
@@ -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 routerContent = `import { HashRouter } from 'turbo-web';
47
- export const router = new HashRouter([{ path: '/', component: null }]);`;
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 = { render: () => h('div', {}, ['Turbo Power!']) };
54
- createApp(App, {}, { router, store: appStore }).mount(document.body);`;
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(eventName, handler, el, hostComponent = null) {
198
- function boundHandler() {
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(([eventName, handler]) => {
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 eventName of removed.concat(updated)) {
897
- el.removeEventListener(eventName, oldListeners[eventName]);
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 eventName of added.concat(updated)) {
901
- const listener =
902
- addEventListener(eventName, newEvents[eventName], el, hostComponent);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "turbo-web",
3
- "version": "4.2.10",
3
+ "version": "4.4.0",
4
4
  "main": "dist/turbo.js",
5
5
  "files": [
6
6
  "dist",