@relax.js/core 1.0.3 → 1.0.5
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 +194 -188
- package/dist/DependencyInjection.d.ts +45 -27
- package/dist/collections/LinkedList.d.ts +9 -8
- package/dist/collections/index.js +1 -1
- package/dist/collections/index.js.map +3 -3
- package/dist/collections/index.mjs +1 -1
- package/dist/collections/index.mjs.map +3 -3
- package/dist/di/index.js +1 -1
- package/dist/di/index.js.map +3 -3
- package/dist/di/index.mjs +1 -1
- package/dist/di/index.mjs.map +3 -3
- package/dist/elements/index.js +1 -1
- package/dist/elements/index.js.map +1 -1
- package/dist/errors.d.ts +20 -0
- package/dist/forms/FormValidator.d.ts +3 -22
- package/dist/forms/ValidationRules.d.ts +4 -6
- package/dist/forms/index.js +1 -1
- package/dist/forms/index.js.map +4 -4
- package/dist/forms/index.mjs +1 -1
- package/dist/forms/index.mjs.map +4 -4
- package/dist/forms/setFormData.d.ts +39 -1
- package/dist/html/TableRenderer.d.ts +1 -0
- package/dist/html/index.js +1 -1
- package/dist/html/index.js.map +3 -3
- package/dist/html/index.mjs +1 -1
- package/dist/html/index.mjs.map +3 -3
- package/dist/html/template.d.ts +4 -0
- package/dist/http/ServerSentEvents.d.ts +1 -1
- package/dist/http/SimpleWebSocket.d.ts +1 -1
- package/dist/http/http.d.ts +1 -0
- package/dist/http/index.js +1 -1
- package/dist/http/index.js.map +3 -3
- package/dist/http/index.mjs +1 -1
- package/dist/http/index.mjs.map +3 -3
- package/dist/i18n/icu.d.ts +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/index.js.map +2 -2
- package/dist/i18n/index.mjs +1 -1
- package/dist/i18n/index.mjs.map +2 -2
- package/dist/index.js +3 -3
- package/dist/index.js.map +3 -3
- package/dist/index.mjs +3 -3
- package/dist/index.mjs.map +3 -3
- package/dist/routing/NavigateRouteEvent.d.ts +4 -4
- package/dist/routing/index.js +3 -3
- package/dist/routing/index.js.map +3 -3
- package/dist/routing/index.mjs +3 -3
- package/dist/routing/index.mjs.map +3 -3
- package/dist/routing/navigation.d.ts +1 -1
- package/dist/routing/routeTargetRegistry.d.ts +1 -0
- package/dist/routing/types.d.ts +2 -1
- package/dist/templates/NodeTemplate.d.ts +3 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +3 -3
- package/dist/utils/index.mjs +1 -1
- package/dist/utils/index.mjs.map +3 -3
- package/docs/Architecture.md +333 -333
- package/docs/DependencyInjection.md +277 -237
- package/docs/Errors.md +87 -87
- package/docs/GettingStarted.md +238 -231
- package/docs/Pipes.md +5 -5
- package/docs/Translations.md +167 -312
- package/docs/WhyRelaxjs.md +336 -336
- package/docs/api.json +93193 -0
- package/docs/elements/dom.md +102 -102
- package/docs/forms/creating-form-components.md +924 -924
- package/docs/forms/form-api.md +94 -94
- package/docs/forms/forms.md +99 -99
- package/docs/forms/patterns.md +311 -311
- package/docs/forms/reading-writing.md +465 -365
- package/docs/forms/validation.md +351 -351
- package/docs/html/TableRenderer.md +291 -291
- package/docs/html/html.md +175 -175
- package/docs/html/index.md +54 -54
- package/docs/html/template.md +422 -422
- package/docs/http/HttpClient.md +459 -459
- package/docs/http/ServerSentEvents.md +184 -184
- package/docs/http/index.md +109 -109
- package/docs/i18n/i18n.md +49 -4
- package/docs/i18n/intl-standard.md +178 -178
- package/docs/routing/RouteLink.md +98 -98
- package/docs/routing/Routing.md +332 -332
- package/docs/routing/layouts.md +207 -207
- package/docs/setup/bootstrapping.md +154 -0
- package/docs/setup/build-and-deploy.md +183 -0
- package/docs/setup/project-structure.md +170 -0
- package/docs/setup/vite.md +175 -0
- package/docs/utilities.md +143 -143
- package/package.json +4 -2
package/docs/routing/layouts.md
CHANGED
|
@@ -1,207 +1,207 @@
|
|
|
1
|
-
Layouts
|
|
2
|
-
=======
|
|
3
|
-
|
|
4
|
-
Layouts allow different HTML pages to serve as shells for different parts of your application. Common use cases:
|
|
5
|
-
|
|
6
|
-
- Public pages (login, register) with minimal UI
|
|
7
|
-
- Authenticated pages with navigation, sidebar, header
|
|
8
|
-
- Admin pages with different structure
|
|
9
|
-
|
|
10
|
-
## How It Works
|
|
11
|
-
|
|
12
|
-
Each layout is a separate static HTML file. When navigating to a route with a different layout than the current one, the router:
|
|
13
|
-
|
|
14
|
-
1. Stores the target route name and params in `sessionStorage`
|
|
15
|
-
2. Redirects the browser to the new HTML file
|
|
16
|
-
3. On the new page, `startRouting()` reads from `sessionStorage` and navigates to the intended route
|
|
17
|
-
4. The URL is updated to show the route path (not the `.html` file)
|
|
18
|
-
|
|
19
|
-
## File Structure
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
/index.html <- default layout (no layout property needed)
|
|
23
|
-
/public.html <- public layout
|
|
24
|
-
/admin.html <- admin layout
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## Route Configuration
|
|
28
|
-
|
|
29
|
-
```ts
|
|
30
|
-
import { Route, defineRoutes, startRouting } from '
|
|
31
|
-
|
|
32
|
-
const routes: Route[] = [
|
|
33
|
-
// Uses public.html
|
|
34
|
-
{ name: 'login', path: '/login', componentTagName: 'login-page', layout: 'public' },
|
|
35
|
-
{ name: 'register', path: '/register', componentTagName: 'register-page', layout: 'public' },
|
|
36
|
-
|
|
37
|
-
// Uses index.html (default - no layout property needed)
|
|
38
|
-
{ name: 'home', path: '/', componentTagName: 'home-page' },
|
|
39
|
-
{ name: 'settings', path: '/settings', componentTagName: 'settings-page' },
|
|
40
|
-
|
|
41
|
-
// Uses admin.html
|
|
42
|
-
{ name: 'admin', path: '/admin', componentTagName: 'admin-dashboard', layout: 'admin' },
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
defineRoutes(routes);
|
|
46
|
-
startRouting();
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## Layout HTML Files
|
|
50
|
-
|
|
51
|
-
Each layout file must:
|
|
52
|
-
- Include `<r-route-target>` where routed components render
|
|
53
|
-
- Import and call `defineRoutes()` and `startRouting()`
|
|
54
|
-
- Register all components that may be rendered in that layout
|
|
55
|
-
|
|
56
|
-
### index.html (default layout)
|
|
57
|
-
|
|
58
|
-
```html
|
|
59
|
-
<!DOCTYPE html>
|
|
60
|
-
<html>
|
|
61
|
-
<head>
|
|
62
|
-
<title>My App</title>
|
|
63
|
-
<script type="module" src="/src/app.ts"></script>
|
|
64
|
-
</head>
|
|
65
|
-
<body>
|
|
66
|
-
<app-header></app-header>
|
|
67
|
-
<app-sidebar></app-sidebar>
|
|
68
|
-
<main>
|
|
69
|
-
<r-route-target></r-route-target>
|
|
70
|
-
</main>
|
|
71
|
-
</body>
|
|
72
|
-
</html>
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
### public.html
|
|
76
|
-
|
|
77
|
-
```html
|
|
78
|
-
<!DOCTYPE html>
|
|
79
|
-
<html>
|
|
80
|
-
<head>
|
|
81
|
-
<title>My App - Login</title>
|
|
82
|
-
<script type="module" src="/src/app.ts"></script>
|
|
83
|
-
</head>
|
|
84
|
-
<body>
|
|
85
|
-
<div class="auth-container">
|
|
86
|
-
<r-route-target></r-route-target>
|
|
87
|
-
</div>
|
|
88
|
-
</body>
|
|
89
|
-
</html>
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
## Layout Detection
|
|
93
|
-
|
|
94
|
-
The router determines the current layout from the URL:
|
|
95
|
-
|
|
96
|
-
| URL | Detected Layout |
|
|
97
|
-
|-----|-----------------|
|
|
98
|
-
| `/index.html` | `default` |
|
|
99
|
-
| `/public.html` | `public` |
|
|
100
|
-
| `/admin.html` | `admin` |
|
|
101
|
-
| `/` or `/settings` | `default` (inferred) |
|
|
102
|
-
|
|
103
|
-
## Navigation Flow
|
|
104
|
-
|
|
105
|
-
When a user on `/settings` (default layout) navigates to `login` (public layout):
|
|
106
|
-
|
|
107
|
-
1. `navigate('login')` is called
|
|
108
|
-
2. Router detects layout mismatch: `default` !== `public`
|
|
109
|
-
3. Stores `{ routeName: 'login', params: {} }` in `sessionStorage`
|
|
110
|
-
4. Redirects browser to `/public.html#layout`
|
|
111
|
-
5. `public.html` loads and calls `startRouting()`
|
|
112
|
-
6. `startRouting()` reads from `sessionStorage`, calls `navigate('login')`
|
|
113
|
-
7. URL updates to `/login`, `login-page` component renders
|
|
114
|
-
|
|
115
|
-
## Shared Routes Configuration
|
|
116
|
-
|
|
117
|
-
Since all layout files need the same routes, keep route configuration in a shared module:
|
|
118
|
-
|
|
119
|
-
```ts
|
|
120
|
-
// routes.ts
|
|
121
|
-
import { Route } from '
|
|
122
|
-
|
|
123
|
-
export const routes: Route[] = [
|
|
124
|
-
{ name: 'login', path: '/login', componentTagName: 'login-page', layout: 'public' },
|
|
125
|
-
{ name: 'home', path: '/', componentTagName: 'home-page' },
|
|
126
|
-
// ...
|
|
127
|
-
];
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
```ts
|
|
131
|
-
// app.ts (imported by all layout HTML files)
|
|
132
|
-
import { defineRoutes, startRouting } from '
|
|
133
|
-
import { routes } from './routes';
|
|
134
|
-
|
|
135
|
-
defineRoutes(routes);
|
|
136
|
-
startRouting();
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
## Error Handling
|
|
140
|
-
|
|
141
|
-
If a layout redirect fails (e.g., the HTML file doesn't exist), the router throws an error:
|
|
142
|
-
|
|
143
|
-
```
|
|
144
|
-
A redirect failed, does the requested layout exist? "admin"?
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
The `#layout` hash in the redirect URL is used to detect this: if the hash is still present after redirect, something went wrong.
|
|
148
|
-
|
|
149
|
-
## Security Considerations
|
|
150
|
-
|
|
151
|
-
Layout files are static HTML served to the browser. This has security implications:
|
|
152
|
-
|
|
153
|
-
### Route Exposure
|
|
154
|
-
|
|
155
|
-
A file like `admin.html` contains (or imports) route configurations that reveal admin URLs. Anyone requesting the file can see what endpoints exist, even without authentication.
|
|
156
|
-
|
|
157
|
-
### Client-Side Guards Are Not Security
|
|
158
|
-
|
|
159
|
-
Route guards in Relaxjs prevent components from rendering, but they don't protect data. An attacker can:
|
|
160
|
-
- Read the JavaScript bundle to find all routes
|
|
161
|
-
- Call backend APIs directly, bypassing the UI entirely
|
|
162
|
-
|
|
163
|
-
**The real security boundary is always your backend API.** Every API endpoint must validate authentication and authorization.
|
|
164
|
-
|
|
165
|
-
### Server-Side Protection
|
|
166
|
-
|
|
167
|
-
For defense in depth, protect sensitive layout files at the HTTP server level:
|
|
168
|
-
|
|
169
|
-
**Nginx:**
|
|
170
|
-
```nginx
|
|
171
|
-
location /admin.html {
|
|
172
|
-
auth_request /auth/verify;
|
|
173
|
-
error_page 401 = @unauthorized;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
location @unauthorized {
|
|
177
|
-
return 302 /login;
|
|
178
|
-
}
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
**Express:**
|
|
182
|
-
```ts
|
|
183
|
-
app.get('/admin.html', verifyJwt, (req, res) => {
|
|
184
|
-
res.sendFile('admin.html');
|
|
185
|
-
});
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
**Caddy:**
|
|
189
|
-
```
|
|
190
|
-
admin.html {
|
|
191
|
-
forward_auth /auth/verify
|
|
192
|
-
}
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
### Alternatives
|
|
196
|
-
|
|
197
|
-
If hiding routes is important:
|
|
198
|
-
|
|
199
|
-
1. **Single layout with dynamic navigation**: Load admin navigation only after auth verification
|
|
200
|
-
2. **Separate builds**: Build different bundles for public/authenticated/admin with different route configurations
|
|
201
|
-
3. **Server-rendered shells**: Generate layout HTML server-side based on user role
|
|
202
|
-
|
|
203
|
-
### Recommendation
|
|
204
|
-
|
|
205
|
-
1. Always secure your backend APIs with proper authentication/authorization
|
|
206
|
-
2. Protect sensitive layout files at the server level in production
|
|
207
|
-
3. Don't rely on client-side guards for security. They're for UX, not protection
|
|
1
|
+
Layouts
|
|
2
|
+
=======
|
|
3
|
+
|
|
4
|
+
Layouts allow different HTML pages to serve as shells for different parts of your application. Common use cases:
|
|
5
|
+
|
|
6
|
+
- Public pages (login, register) with minimal UI
|
|
7
|
+
- Authenticated pages with navigation, sidebar, header
|
|
8
|
+
- Admin pages with different structure
|
|
9
|
+
|
|
10
|
+
## How It Works
|
|
11
|
+
|
|
12
|
+
Each layout is a separate static HTML file. When navigating to a route with a different layout than the current one, the router:
|
|
13
|
+
|
|
14
|
+
1. Stores the target route name and params in `sessionStorage`
|
|
15
|
+
2. Redirects the browser to the new HTML file
|
|
16
|
+
3. On the new page, `startRouting()` reads from `sessionStorage` and navigates to the intended route
|
|
17
|
+
4. The URL is updated to show the route path (not the `.html` file)
|
|
18
|
+
|
|
19
|
+
## File Structure
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
/index.html <- default layout (no layout property needed)
|
|
23
|
+
/public.html <- public layout
|
|
24
|
+
/admin.html <- admin layout
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Route Configuration
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { Route, defineRoutes, startRouting } from '@relax.js/core/routing';
|
|
31
|
+
|
|
32
|
+
const routes: Route[] = [
|
|
33
|
+
// Uses public.html
|
|
34
|
+
{ name: 'login', path: '/login', componentTagName: 'login-page', layout: 'public' },
|
|
35
|
+
{ name: 'register', path: '/register', componentTagName: 'register-page', layout: 'public' },
|
|
36
|
+
|
|
37
|
+
// Uses index.html (default - no layout property needed)
|
|
38
|
+
{ name: 'home', path: '/', componentTagName: 'home-page' },
|
|
39
|
+
{ name: 'settings', path: '/settings', componentTagName: 'settings-page' },
|
|
40
|
+
|
|
41
|
+
// Uses admin.html
|
|
42
|
+
{ name: 'admin', path: '/admin', componentTagName: 'admin-dashboard', layout: 'admin' },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
defineRoutes(routes);
|
|
46
|
+
startRouting();
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Layout HTML Files
|
|
50
|
+
|
|
51
|
+
Each layout file must:
|
|
52
|
+
- Include `<r-route-target>` where routed components render
|
|
53
|
+
- Import and call `defineRoutes()` and `startRouting()`
|
|
54
|
+
- Register all components that may be rendered in that layout
|
|
55
|
+
|
|
56
|
+
### index.html (default layout)
|
|
57
|
+
|
|
58
|
+
```html
|
|
59
|
+
<!DOCTYPE html>
|
|
60
|
+
<html>
|
|
61
|
+
<head>
|
|
62
|
+
<title>My App</title>
|
|
63
|
+
<script type="module" src="/src/app.ts"></script>
|
|
64
|
+
</head>
|
|
65
|
+
<body>
|
|
66
|
+
<app-header></app-header>
|
|
67
|
+
<app-sidebar></app-sidebar>
|
|
68
|
+
<main>
|
|
69
|
+
<r-route-target></r-route-target>
|
|
70
|
+
</main>
|
|
71
|
+
</body>
|
|
72
|
+
</html>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### public.html
|
|
76
|
+
|
|
77
|
+
```html
|
|
78
|
+
<!DOCTYPE html>
|
|
79
|
+
<html>
|
|
80
|
+
<head>
|
|
81
|
+
<title>My App - Login</title>
|
|
82
|
+
<script type="module" src="/src/app.ts"></script>
|
|
83
|
+
</head>
|
|
84
|
+
<body>
|
|
85
|
+
<div class="auth-container">
|
|
86
|
+
<r-route-target></r-route-target>
|
|
87
|
+
</div>
|
|
88
|
+
</body>
|
|
89
|
+
</html>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Layout Detection
|
|
93
|
+
|
|
94
|
+
The router determines the current layout from the URL:
|
|
95
|
+
|
|
96
|
+
| URL | Detected Layout |
|
|
97
|
+
|-----|-----------------|
|
|
98
|
+
| `/index.html` | `default` |
|
|
99
|
+
| `/public.html` | `public` |
|
|
100
|
+
| `/admin.html` | `admin` |
|
|
101
|
+
| `/` or `/settings` | `default` (inferred) |
|
|
102
|
+
|
|
103
|
+
## Navigation Flow
|
|
104
|
+
|
|
105
|
+
When a user on `/settings` (default layout) navigates to `login` (public layout):
|
|
106
|
+
|
|
107
|
+
1. `navigate('login')` is called
|
|
108
|
+
2. Router detects layout mismatch: `default` !== `public`
|
|
109
|
+
3. Stores `{ routeName: 'login', params: {} }` in `sessionStorage`
|
|
110
|
+
4. Redirects browser to `/public.html#layout`
|
|
111
|
+
5. `public.html` loads and calls `startRouting()`
|
|
112
|
+
6. `startRouting()` reads from `sessionStorage`, calls `navigate('login')`
|
|
113
|
+
7. URL updates to `/login`, `login-page` component renders
|
|
114
|
+
|
|
115
|
+
## Shared Routes Configuration
|
|
116
|
+
|
|
117
|
+
Since all layout files need the same routes, keep route configuration in a shared module:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
// routes.ts
|
|
121
|
+
import { Route } from '@relax.js/core/routing';
|
|
122
|
+
|
|
123
|
+
export const routes: Route[] = [
|
|
124
|
+
{ name: 'login', path: '/login', componentTagName: 'login-page', layout: 'public' },
|
|
125
|
+
{ name: 'home', path: '/', componentTagName: 'home-page' },
|
|
126
|
+
// ...
|
|
127
|
+
];
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
// app.ts (imported by all layout HTML files)
|
|
132
|
+
import { defineRoutes, startRouting } from '@relax.js/core/routing';
|
|
133
|
+
import { routes } from './routes';
|
|
134
|
+
|
|
135
|
+
defineRoutes(routes);
|
|
136
|
+
startRouting();
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Error Handling
|
|
140
|
+
|
|
141
|
+
If a layout redirect fails (e.g., the HTML file doesn't exist), the router throws an error:
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
A redirect failed, does the requested layout exist? "admin"?
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The `#layout` hash in the redirect URL is used to detect this: if the hash is still present after redirect, something went wrong.
|
|
148
|
+
|
|
149
|
+
## Security Considerations
|
|
150
|
+
|
|
151
|
+
Layout files are static HTML served to the browser. This has security implications:
|
|
152
|
+
|
|
153
|
+
### Route Exposure
|
|
154
|
+
|
|
155
|
+
A file like `admin.html` contains (or imports) route configurations that reveal admin URLs. Anyone requesting the file can see what endpoints exist, even without authentication.
|
|
156
|
+
|
|
157
|
+
### Client-Side Guards Are Not Security
|
|
158
|
+
|
|
159
|
+
Route guards in Relaxjs prevent components from rendering, but they don't protect data. An attacker can:
|
|
160
|
+
- Read the JavaScript bundle to find all routes
|
|
161
|
+
- Call backend APIs directly, bypassing the UI entirely
|
|
162
|
+
|
|
163
|
+
**The real security boundary is always your backend API.** Every API endpoint must validate authentication and authorization.
|
|
164
|
+
|
|
165
|
+
### Server-Side Protection
|
|
166
|
+
|
|
167
|
+
For defense in depth, protect sensitive layout files at the HTTP server level:
|
|
168
|
+
|
|
169
|
+
**Nginx:**
|
|
170
|
+
```nginx
|
|
171
|
+
location /admin.html {
|
|
172
|
+
auth_request /auth/verify;
|
|
173
|
+
error_page 401 = @unauthorized;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
location @unauthorized {
|
|
177
|
+
return 302 /login;
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Express:**
|
|
182
|
+
```ts
|
|
183
|
+
app.get('/admin.html', verifyJwt, (req, res) => {
|
|
184
|
+
res.sendFile('admin.html');
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Caddy:**
|
|
189
|
+
```
|
|
190
|
+
admin.html {
|
|
191
|
+
forward_auth /auth/verify
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Alternatives
|
|
196
|
+
|
|
197
|
+
If hiding routes is important:
|
|
198
|
+
|
|
199
|
+
1. **Single layout with dynamic navigation**: Load admin navigation only after auth verification
|
|
200
|
+
2. **Separate builds**: Build different bundles for public/authenticated/admin with different route configurations
|
|
201
|
+
3. **Server-rendered shells**: Generate layout HTML server-side based on user role
|
|
202
|
+
|
|
203
|
+
### Recommendation
|
|
204
|
+
|
|
205
|
+
1. Always secure your backend APIs with proper authentication/authorization
|
|
206
|
+
2. Protect sensitive layout files at the server level in production
|
|
207
|
+
3. Don't rely on client-side guards for security. They're for UX, not protection
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
Bootstrapping
|
|
2
|
+
=============
|
|
3
|
+
|
|
4
|
+
Relaxjs apps need three subsystems started in the right order: **dependency injection**, **i18n**, and **routing** (if used). Getting the order wrong produces "why is my service undefined?" bugs that are hard to trace.
|
|
5
|
+
|
|
6
|
+
This page shows the safe pattern for `src/main.ts`.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## The rule
|
|
11
|
+
|
|
12
|
+
> Register services **before** any component that uses `@Inject` is constructed.
|
|
13
|
+
> Load i18n namespaces **before** you render anything that calls `t()`.
|
|
14
|
+
> Start routing **last**.
|
|
15
|
+
|
|
16
|
+
A custom element is constructed the moment two things line up: the tag is defined with `customElements.define(...)` **and** the tag appears in the DOM. If DI is not ready at that moment, the component's `@Inject` fields become `undefined`.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Minimal `main.ts`
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// 1. Import service modules first so their @ContainerService decorators run.
|
|
24
|
+
import './services/ApiClient';
|
|
25
|
+
import './services/UserService';
|
|
26
|
+
|
|
27
|
+
// 2. Import component modules. Each file ends with customElements.define(...)
|
|
28
|
+
// but the tags don't render yet unless they are in index.html.
|
|
29
|
+
import './components/HomePage';
|
|
30
|
+
import './components/UserPanel';
|
|
31
|
+
|
|
32
|
+
// 3. Relaxjs subsystems
|
|
33
|
+
import { setLocale, loadNamespaces } from '@relax.js/core/i18n';
|
|
34
|
+
import { defineRoutes, startRouting } from '@relax.js/core/routing';
|
|
35
|
+
|
|
36
|
+
async function bootstrap() {
|
|
37
|
+
// i18n before anything calls t()
|
|
38
|
+
await setLocale(navigator.language || 'en');
|
|
39
|
+
await loadNamespaces(['r-validation']);
|
|
40
|
+
|
|
41
|
+
// Routing last — this is when components start mounting
|
|
42
|
+
defineRoutes([
|
|
43
|
+
{ name: 'home', path: '/', componentTagName: 'home-page' },
|
|
44
|
+
{ name: 'profile', path: '/profile/:id', componentTagName: 'user-panel' },
|
|
45
|
+
]);
|
|
46
|
+
startRouting();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
bootstrap();
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The matching `index.html`:
|
|
53
|
+
|
|
54
|
+
```html
|
|
55
|
+
<!DOCTYPE html>
|
|
56
|
+
<html>
|
|
57
|
+
<body>
|
|
58
|
+
<r-route-target></r-route-target>
|
|
59
|
+
<script type="module" src="/src/main.ts"></script>
|
|
60
|
+
</body>
|
|
61
|
+
</html>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Notice the body contains **only** `<r-route-target>`. Page components are created by the router after `startRouting()` runs, so they never construct before DI and i18n are ready.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Why the import order matters
|
|
69
|
+
|
|
70
|
+
TypeScript imports run top-to-bottom. Decorator side effects (registering a service, defining a custom element) happen at import time.
|
|
71
|
+
|
|
72
|
+
If you flip the order:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import './components/UserPanel'; // ❌ custom element defined
|
|
76
|
+
import './services/UserService'; // ❌ service registered AFTER
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
...and `<user-panel>` appears in `index.html`, the browser constructs it before `UserService` is registered. Every `@Inject(UserService)` field resolves to `undefined`.
|
|
80
|
+
|
|
81
|
+
**Always import services before components.** Then import subsystems (i18n, routing) last.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Manual registration (when you can't use decorators)
|
|
86
|
+
|
|
87
|
+
Some services wrap existing instances (config objects, third-party clients). Register them before the `bootstrap()` call:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { serviceCollection } from '@relax.js/core/di';
|
|
91
|
+
|
|
92
|
+
const config = {
|
|
93
|
+
apiUrl: import.meta.env.VITE_API_URL ?? '/api',
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
serviceCollection.register(ConfigService, {
|
|
97
|
+
inject: [],
|
|
98
|
+
instance: config,
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Do this at the top of `main.ts`, before the subsystem imports.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Components that don't use routing
|
|
107
|
+
|
|
108
|
+
If your app uses [Level 1-3 patterns](../GettingStarted.md) (plain components, no router), the same rule applies, just without `startRouting()`:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import './services/ApiClient';
|
|
112
|
+
import './components/AppShell'; // defines <app-shell>
|
|
113
|
+
|
|
114
|
+
import { setLocale, loadNamespaces } from '@relax.js/core/i18n';
|
|
115
|
+
|
|
116
|
+
async function bootstrap() {
|
|
117
|
+
await setLocale('en');
|
|
118
|
+
await loadNamespaces(['r-validation']);
|
|
119
|
+
|
|
120
|
+
// Insert the root component now that everything is ready
|
|
121
|
+
document.body.appendChild(document.createElement('app-shell'));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
bootstrap();
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Key point: the root component is appended **after** async setup, not written directly in `index.html`.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Common mistakes
|
|
132
|
+
|
|
133
|
+
### `@Inject` field is undefined
|
|
134
|
+
|
|
135
|
+
Cause: a component constructed before its service was registered. Fix: move the service import above the component import in `main.ts`.
|
|
136
|
+
|
|
137
|
+
### `t('...')` returns the key unchanged
|
|
138
|
+
|
|
139
|
+
Cause: the namespace wasn't loaded, or the component rendered before `setLocale()` finished. Fix: `await loadNamespaces([...])` before rendering, and re-render on `localechange` if the user can switch locales.
|
|
140
|
+
|
|
141
|
+
### Routing renders a blank page
|
|
142
|
+
|
|
143
|
+
Cause: `startRouting()` ran before `defineRoutes(...)`. Fix: always call `defineRoutes` first.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Summary
|
|
148
|
+
|
|
149
|
+
1. Import service modules (side-effect imports run `@ContainerService`)
|
|
150
|
+
2. Import component modules (`customElements.define`)
|
|
151
|
+
3. `await setLocale(...)` and `await loadNamespaces(...)`
|
|
152
|
+
4. `defineRoutes(...)` then `startRouting()`
|
|
153
|
+
|
|
154
|
+
If you follow this order every time, components never see an undefined service or a missing translation.
|