@alepha/react 0.14.1 → 0.14.3

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.
Files changed (86) hide show
  1. package/dist/auth/index.browser.js +1488 -4
  2. package/dist/auth/index.browser.js.map +1 -1
  3. package/dist/auth/index.d.ts +2 -2
  4. package/dist/auth/index.js +1827 -4
  5. package/dist/auth/index.js.map +1 -1
  6. package/dist/core/index.d.ts +58 -937
  7. package/dist/core/index.d.ts.map +1 -1
  8. package/dist/core/index.js +139 -2014
  9. package/dist/core/index.js.map +1 -1
  10. package/dist/form/index.d.ts.map +1 -1
  11. package/dist/form/index.js +6 -1
  12. package/dist/form/index.js.map +1 -1
  13. package/dist/head/index.browser.js +3 -1
  14. package/dist/head/index.browser.js.map +1 -1
  15. package/dist/head/index.d.ts +552 -8
  16. package/dist/head/index.d.ts.map +1 -1
  17. package/dist/head/index.js +17 -2
  18. package/dist/head/index.js.map +1 -1
  19. package/dist/{core → router}/index.browser.js +126 -516
  20. package/dist/router/index.browser.js.map +1 -0
  21. package/dist/router/index.d.ts +1334 -0
  22. package/dist/router/index.d.ts.map +1 -0
  23. package/dist/router/index.js +1939 -0
  24. package/dist/router/index.js.map +1 -0
  25. package/package.json +12 -6
  26. package/src/auth/__tests__/$auth.spec.ts +188 -0
  27. package/src/auth/index.ts +1 -1
  28. package/src/auth/services/ReactAuth.ts +1 -1
  29. package/src/core/__tests__/Router.spec.tsx +169 -0
  30. package/src/core/components/ClientOnly.tsx +14 -0
  31. package/src/core/components/ErrorBoundary.tsx +3 -2
  32. package/src/core/contexts/AlephaContext.ts +3 -0
  33. package/src/core/contexts/AlephaProvider.tsx +2 -1
  34. package/src/core/hooks/useAction.browser.spec.tsx +569 -0
  35. package/src/core/hooks/useAction.ts +11 -0
  36. package/src/core/index.ts +13 -102
  37. package/src/form/hooks/useForm.browser.spec.tsx +366 -0
  38. package/src/form/services/FormModel.ts +5 -0
  39. package/src/head/__tests__/expandSeo.spec.ts +203 -0
  40. package/src/head/__tests__/page-head.spec.ts +39 -0
  41. package/src/head/__tests__/seo-head.spec.ts +121 -0
  42. package/src/head/hooks/useHead.spec.tsx +288 -0
  43. package/src/head/index.ts +18 -8
  44. package/src/head/interfaces/Head.ts +3 -0
  45. package/src/head/providers/BrowserHeadProvider.browser.spec.ts +271 -0
  46. package/src/head/providers/HeadProvider.ts +6 -1
  47. package/src/head/providers/ServerHeadProvider.spec.ts +163 -0
  48. package/src/head/providers/ServerHeadProvider.ts +20 -0
  49. package/src/i18n/__tests__/integration.spec.tsx +239 -0
  50. package/src/i18n/components/Localize.spec.tsx +357 -0
  51. package/src/i18n/hooks/useI18n.browser.spec.tsx +438 -0
  52. package/src/i18n/providers/I18nProvider.spec.ts +389 -0
  53. package/src/{core → router}/components/ErrorViewer.tsx +2 -0
  54. package/src/router/components/Link.tsx +21 -0
  55. package/src/{core → router}/components/NestedView.tsx +3 -5
  56. package/src/router/components/NotFound.tsx +30 -0
  57. package/src/router/errors/Redirection.ts +28 -0
  58. package/src/{core → router}/hooks/useActive.ts +6 -2
  59. package/src/{core → router}/hooks/useQueryParams.ts +2 -2
  60. package/src/{core → router}/hooks/useRouter.ts +1 -1
  61. package/src/{core → router}/hooks/useRouterState.ts +1 -1
  62. package/src/{core → router}/index.browser.ts +14 -12
  63. package/src/{core/index.shared-router.ts → router/index.shared.ts} +6 -3
  64. package/src/router/index.ts +125 -0
  65. package/src/router/primitives/$page.browser.spec.tsx +702 -0
  66. package/src/router/primitives/$page.spec.tsx +702 -0
  67. package/src/{core → router}/primitives/$page.ts +1 -1
  68. package/src/{core → router}/providers/ReactBrowserProvider.ts +3 -13
  69. package/src/{core → router}/providers/ReactBrowserRendererProvider.ts +3 -0
  70. package/src/{core → router}/providers/ReactBrowserRouterProvider.ts +3 -0
  71. package/src/{core → router}/providers/ReactPageProvider.ts +5 -3
  72. package/src/router/providers/ReactServerProvider.spec.tsx +316 -0
  73. package/src/{core → router}/providers/ReactServerProvider.ts +12 -30
  74. package/src/{core → router}/services/ReactPageServerService.ts +3 -0
  75. package/src/{core → router}/services/ReactPageService.ts +5 -5
  76. package/src/{core → router}/services/ReactRouter.ts +26 -5
  77. package/dist/core/index.browser.js.map +0 -1
  78. package/dist/core/index.native.js +0 -403
  79. package/dist/core/index.native.js.map +0 -1
  80. package/src/core/components/Link.tsx +0 -18
  81. package/src/core/components/NotFound.tsx +0 -27
  82. package/src/core/errors/Redirection.ts +0 -13
  83. package/src/core/hooks/useSchema.ts +0 -88
  84. package/src/core/index.native.ts +0 -21
  85. package/src/core/index.shared.ts +0 -9
  86. /package/src/{core → router}/contexts/RouterLayerContext.ts +0 -0
package/src/core/index.ts CHANGED
@@ -1,63 +1,24 @@
1
1
  import { $module } from "alepha";
2
- import { AlephaDateTime } from "alepha/datetime";
3
- import { AlephaServer, type ServerRequest } from "alepha/server";
4
- import { AlephaServerCache } from "alepha/server/cache";
5
- import { AlephaServerLinks } from "alepha/server/links";
6
- import type { ReactNode } from "react";
7
- import { $page, type PageAnimation } from "./primitives/$page.ts";
8
- import type { ReactHydrationState } from "./providers/ReactBrowserProvider.ts";
9
- import {
10
- ReactPageProvider,
11
- type ReactRouterState,
12
- } from "./providers/ReactPageProvider.ts";
13
- import { ReactServerProvider } from "./providers/ReactServerProvider.ts";
14
- import { ReactPageServerService } from "./services/ReactPageServerService.ts";
15
- import { ReactPageService } from "./services/ReactPageService.ts";
16
- import { ReactRouter } from "./services/ReactRouter.ts";
17
2
 
18
3
  // ---------------------------------------------------------------------------------------------------------------------
19
4
 
20
- export * from "./index.shared.ts";
21
- export * from "./index.shared-router.ts";
22
- export * from "./providers/ReactBrowserProvider.ts";
23
- export * from "./providers/ReactPageProvider.ts";
24
- export * from "./providers/ReactServerProvider.ts";
5
+ export { default as ClientOnly } from "./components/ClientOnly.tsx";
6
+ export type * from "./components/ClientOnly.tsx";
7
+ export { default as ErrorBoundary } from "./components/ErrorBoundary.tsx";
8
+ export type * from "./components/ErrorBoundary.tsx";
9
+ export * from "./contexts/AlephaProvider.tsx";
10
+ export * from "./contexts/AlephaContext.ts";
11
+ export * from "./hooks/useAction.ts";
12
+ export * from "./hooks/useAlepha.ts";
13
+ export * from "./hooks/useEvents.ts";
14
+ export * from "./hooks/useInject.ts";
15
+ export * from "./hooks/useClient.ts";
16
+ export * from "./hooks/useStore.ts";
25
17
 
26
18
  // ---------------------------------------------------------------------------------------------------------------------
27
19
 
28
20
  declare module "alepha" {
29
- interface State {
30
- "alepha.react.router.state"?: ReactRouterState;
31
- }
32
-
33
21
  interface Hooks {
34
- /**
35
- * Fires when the React application is starting to be rendered on the server.
36
- */
37
- "react:server:render:begin": {
38
- request?: ServerRequest;
39
- state: ReactRouterState;
40
- };
41
- /**
42
- * Fires when the React application has been rendered on the server.
43
- */
44
- "react:server:render:end": {
45
- request?: ServerRequest;
46
- state: ReactRouterState;
47
- html: string;
48
- };
49
- // -----------------------------------------------------------------------------------------------------------------
50
- /**
51
- * Fires when the React application is being rendered on the browser.
52
- */
53
- "react:browser:render": {
54
- root: HTMLElement;
55
- element: ReactNode;
56
- state: ReactRouterState;
57
- hydration?: ReactHydrationState;
58
- };
59
- // -----------------------------------------------------------------------------------------------------------------
60
- // TOP LEVEL: All user actions (forms, transitions, custom actions)
61
22
  /**
62
23
  * Fires when a user action is starting.
63
24
  * Action can be a form submission, a route transition, or a custom action.
@@ -91,35 +52,6 @@ declare module "alepha" {
91
52
  type: string;
92
53
  id?: string;
93
54
  };
94
- // -----------------------------------------------------------------------------------------------------------------
95
- // SPECIFIC: Route transitions
96
- /**
97
- * Fires when a route transition is starting.
98
- */
99
- "react:transition:begin": {
100
- previous: ReactRouterState;
101
- state: ReactRouterState;
102
- animation?: PageAnimation;
103
- };
104
- /**
105
- * Fires when a route transition has succeeded.
106
- */
107
- "react:transition:success": {
108
- state: ReactRouterState;
109
- };
110
- /**
111
- * Fires when a route transition has failed.
112
- */
113
- "react:transition:error": {
114
- state: ReactRouterState;
115
- error: Error;
116
- };
117
- /**
118
- * Fires when a route transition has completed, regardless of success or failure.
119
- */
120
- "react:transition:end": {
121
- state: ReactRouterState;
122
- };
123
55
  }
124
56
  }
125
57
 
@@ -136,26 +68,5 @@ declare module "alepha" {
136
68
  * @module alepha.react
137
69
  */
138
70
  export const AlephaReact = $module({
139
- name: "alepha.react",
140
- primitives: [$page],
141
- services: [
142
- ReactServerProvider,
143
- ReactPageProvider,
144
- ReactRouter,
145
- ReactPageService,
146
- ReactPageServerService,
147
- ],
148
- register: (alepha) =>
149
- alepha
150
- .with(AlephaDateTime)
151
- .with(AlephaServer)
152
- .with(AlephaServerCache)
153
- .with(AlephaServerLinks)
154
- .with({
155
- provide: ReactPageService,
156
- use: ReactPageServerService,
157
- })
158
- .with(ReactServerProvider)
159
- .with(ReactPageProvider)
160
- .with(ReactRouter),
71
+ name: "alepha.react.core",
161
72
  });
@@ -0,0 +1,366 @@
1
+ import { AlephaContext } from "@alepha/react";
2
+ import { fireEvent, render } from "@testing-library/react";
3
+ import { Alepha, t } from "alepha";
4
+ import { AlephaLogger } from "alepha/logger";
5
+ import type { ReactNode } from "react";
6
+ import { describe, it } from "vitest";
7
+ import { useForm } from "../index.ts";
8
+
9
+ describe("useForm", () => {
10
+ const renderWithAlepha = (alepha: Alepha, element: ReactNode) => {
11
+ return render(
12
+ <AlephaContext.Provider value={alepha}>{element}</AlephaContext.Provider>,
13
+ );
14
+ };
15
+
16
+ it("should run handler on submit", async ({ expect }) => {
17
+ const alepha = Alepha.create().with(AlephaLogger);
18
+ const calls: Array<any> = [];
19
+ const Form = () => {
20
+ const form = useForm({
21
+ id: "test",
22
+ schema: t.object({
23
+ str: t.text(),
24
+ int: t.integer(),
25
+ nested: t.object({
26
+ str: t.text(),
27
+ another: t.object({
28
+ level: t.text(),
29
+ }),
30
+ }),
31
+ }),
32
+ handler: (values, args) => {
33
+ calls.push(values);
34
+ },
35
+ });
36
+
37
+ return (
38
+ <form {...form.props} data-testid="test-form">
39
+ <input {...form.input.str.props} />
40
+ <input {...form.input.int.props} />
41
+ <input {...form.input.nested.items.str.props} />
42
+ <input {...form.input.nested.items.another.items.level.props} />
43
+ <button type="submit">Submit</button>
44
+ </form>
45
+ );
46
+ };
47
+
48
+ await alepha.start();
49
+
50
+ const ui = renderWithAlepha(alepha, <Form />);
51
+
52
+ fireEvent.change(ui.getByTestId("test-str"), {
53
+ target: { value: "testuser" },
54
+ });
55
+
56
+ fireEvent.change(ui.getByTestId("test-int"), {
57
+ target: { value: "123" },
58
+ });
59
+
60
+ fireEvent.change(ui.getByTestId("test-nested.str"), {
61
+ target: { value: "nestedvalue" },
62
+ });
63
+
64
+ fireEvent.change(ui.getByTestId("test-nested.another.level"), {
65
+ target: { value: "anothervalue" },
66
+ });
67
+
68
+ fireEvent.submit(ui.getByText("Submit"));
69
+
70
+ await new Promise((resolve) => setTimeout(resolve, 100));
71
+
72
+ expect(calls[0]).toEqual({
73
+ str: "testuser",
74
+ int: 123,
75
+ nested: {
76
+ str: "nestedvalue",
77
+ another: {
78
+ level: "anothervalue",
79
+ },
80
+ },
81
+ });
82
+ });
83
+
84
+ it("should provide correct InputField types for nested objects", async ({
85
+ expect,
86
+ }) => {
87
+ const alepha = Alepha.create().with(AlephaLogger);
88
+
89
+ const Form = () => {
90
+ const form = useForm({
91
+ id: "types-test",
92
+ schema: t.object({
93
+ name: t.text(),
94
+ address: t.object({
95
+ street: t.text(),
96
+ city: t.text(),
97
+ country: t.object({
98
+ code: t.text(),
99
+ name: t.text(),
100
+ }),
101
+ }),
102
+ }),
103
+ handler: () => {},
104
+ });
105
+
106
+ // Verify nested object InputFields have items property
107
+ const addressInput = form.input.address;
108
+ const streetInput = addressInput.items.street;
109
+ const countryInput = addressInput.items.country;
110
+ const countryCodeInput = countryInput.items.code;
111
+
112
+ return (
113
+ <div data-testid="type-check">
114
+ <span data-testid="has-items">
115
+ {addressInput.items ? "true" : "false"}
116
+ </span>
117
+ <span data-testid="street-path">{streetInput.path}</span>
118
+ <span data-testid="country-code-path">{countryCodeInput.path}</span>
119
+ </div>
120
+ );
121
+ };
122
+
123
+ await alepha.start();
124
+ const ui = renderWithAlepha(alepha, <Form />);
125
+
126
+ expect(ui.getByTestId("has-items").textContent).toBe("true");
127
+ expect(ui.getByTestId("street-path").textContent).toBe("/address/street");
128
+ expect(ui.getByTestId("country-code-path").textContent).toBe(
129
+ "/address/country/code",
130
+ );
131
+ });
132
+
133
+ it("should provide ArrayInputField with items array for arrays", async ({
134
+ expect,
135
+ }) => {
136
+ const alepha = Alepha.create().with(AlephaLogger);
137
+
138
+ const Form = () => {
139
+ const form = useForm({
140
+ id: "array-test",
141
+ schema: t.object({
142
+ tags: t.array(t.text()),
143
+ contacts: t.array(
144
+ t.object({
145
+ name: t.text(),
146
+ email: t.text(),
147
+ }),
148
+ ),
149
+ }),
150
+ handler: () => {},
151
+ });
152
+
153
+ // Verify array InputFields have items property (initially empty)
154
+ const tagsInput = form.input.tags;
155
+ const contactsInput = form.input.contacts;
156
+
157
+ return (
158
+ <div data-testid="array-check">
159
+ <span data-testid="tags-has-items">
160
+ {Array.isArray(tagsInput.items) ? "true" : "false"}
161
+ </span>
162
+ <span data-testid="tags-items-length">{tagsInput.items.length}</span>
163
+ <span data-testid="contacts-has-items">
164
+ {Array.isArray(contactsInput.items) ? "true" : "false"}
165
+ </span>
166
+ <span data-testid="contacts-items-length">
167
+ {contactsInput.items.length}
168
+ </span>
169
+ <span data-testid="tags-path">{tagsInput.path}</span>
170
+ <span data-testid="contacts-path">{contactsInput.path}</span>
171
+ </div>
172
+ );
173
+ };
174
+
175
+ await alepha.start();
176
+ const ui = renderWithAlepha(alepha, <Form />);
177
+
178
+ // Arrays have items property as array (initially empty)
179
+ expect(ui.getByTestId("tags-has-items").textContent).toBe("true");
180
+ expect(ui.getByTestId("tags-items-length").textContent).toBe("0");
181
+ expect(ui.getByTestId("contacts-has-items").textContent).toBe("true");
182
+ expect(ui.getByTestId("contacts-items-length").textContent).toBe("0");
183
+ expect(ui.getByTestId("tags-path").textContent).toBe("/tags");
184
+ expect(ui.getByTestId("contacts-path").textContent).toBe("/contacts");
185
+ });
186
+
187
+ it("should update array values via set method", async ({ expect }) => {
188
+ const alepha = Alepha.create().with(AlephaLogger);
189
+ const calls: Array<any> = [];
190
+
191
+ const Form = () => {
192
+ const form = useForm({
193
+ id: "array-set-test",
194
+ schema: t.object({
195
+ tags: t.array(t.text()),
196
+ }),
197
+ handler: (values) => {
198
+ calls.push(values);
199
+ },
200
+ });
201
+
202
+ return (
203
+ <form {...form.props} data-testid="array-form">
204
+ <button
205
+ type="button"
206
+ data-testid="set-tags"
207
+ onClick={() => form.input.tags.set(["tag1", "tag2", "tag3"])}
208
+ >
209
+ Set Tags
210
+ </button>
211
+ <button type="submit">Submit</button>
212
+ </form>
213
+ );
214
+ };
215
+
216
+ await alepha.start();
217
+ const ui = renderWithAlepha(alepha, <Form />);
218
+
219
+ fireEvent.click(ui.getByTestId("set-tags"));
220
+ fireEvent.submit(ui.getByText("Submit"));
221
+
222
+ await new Promise((resolve) => setTimeout(resolve, 100));
223
+
224
+ expect(calls[0]).toEqual({
225
+ tags: ["tag1", "tag2", "tag3"],
226
+ });
227
+ });
228
+
229
+ it("should update array of objects via set method", async ({ expect }) => {
230
+ const alepha = Alepha.create().with(AlephaLogger);
231
+ const calls: Array<any> = [];
232
+
233
+ const Form = () => {
234
+ const form = useForm({
235
+ id: "array-objects-test",
236
+ schema: t.object({
237
+ contacts: t.array(
238
+ t.object({
239
+ name: t.text(),
240
+ email: t.text(),
241
+ }),
242
+ ),
243
+ }),
244
+ handler: (values) => {
245
+ calls.push(values);
246
+ },
247
+ });
248
+
249
+ return (
250
+ <form {...form.props} data-testid="array-objects-form">
251
+ <button
252
+ type="button"
253
+ data-testid="set-contacts"
254
+ onClick={() =>
255
+ form.input.contacts.set([
256
+ { name: "Alice", email: "alice@example.com" },
257
+ { name: "Bob", email: "bob@example.com" },
258
+ ])
259
+ }
260
+ >
261
+ Set Contacts
262
+ </button>
263
+ <button type="submit">Submit</button>
264
+ </form>
265
+ );
266
+ };
267
+
268
+ await alepha.start();
269
+ const ui = renderWithAlepha(alepha, <Form />);
270
+
271
+ fireEvent.click(ui.getByTestId("set-contacts"));
272
+ fireEvent.submit(ui.getByText("Submit"));
273
+
274
+ await new Promise((resolve) => setTimeout(resolve, 100));
275
+
276
+ expect(calls[0]).toEqual({
277
+ contacts: [
278
+ { name: "Alice", email: "alice@example.com" },
279
+ { name: "Bob", email: "bob@example.com" },
280
+ ],
281
+ });
282
+ });
283
+
284
+ it("should handle complex nested structures with objects and arrays", async ({
285
+ expect,
286
+ }) => {
287
+ const alepha = Alepha.create().with(AlephaLogger);
288
+ const calls: Array<any> = [];
289
+
290
+ const Form = () => {
291
+ const form = useForm({
292
+ id: "complex-test",
293
+ schema: t.object({
294
+ company: t.object({
295
+ name: t.text(),
296
+ address: t.object({
297
+ street: t.text(),
298
+ city: t.text(),
299
+ }),
300
+ }),
301
+ employees: t.array(
302
+ t.object({
303
+ name: t.text(),
304
+ role: t.text(),
305
+ }),
306
+ ),
307
+ }),
308
+ handler: (values) => {
309
+ calls.push(values);
310
+ },
311
+ });
312
+
313
+ return (
314
+ <form {...form.props} data-testid="complex-form">
315
+ <input {...form.input.company.items.name.props} />
316
+ <input {...form.input.company.items.address.items.street.props} />
317
+ <input {...form.input.company.items.address.items.city.props} />
318
+ <button
319
+ type="button"
320
+ data-testid="set-employees"
321
+ onClick={() =>
322
+ form.input.employees.set([
323
+ { name: "Alice", role: "Engineer" },
324
+ { name: "Bob", role: "Designer" },
325
+ ])
326
+ }
327
+ >
328
+ Set Employees
329
+ </button>
330
+ <button type="submit">Submit</button>
331
+ </form>
332
+ );
333
+ };
334
+
335
+ await alepha.start();
336
+ const ui = renderWithAlepha(alepha, <Form />);
337
+
338
+ fireEvent.change(ui.getByTestId("complex-test-company.name"), {
339
+ target: { value: "Acme Corp" },
340
+ });
341
+ fireEvent.change(ui.getByTestId("complex-test-company.address.street"), {
342
+ target: { value: "123 Main St" },
343
+ });
344
+ fireEvent.change(ui.getByTestId("complex-test-company.address.city"), {
345
+ target: { value: "New York" },
346
+ });
347
+ fireEvent.click(ui.getByTestId("set-employees"));
348
+ fireEvent.submit(ui.getByText("Submit"));
349
+
350
+ await new Promise((resolve) => setTimeout(resolve, 100));
351
+
352
+ expect(calls[0]).toEqual({
353
+ company: {
354
+ name: "Acme Corp",
355
+ address: {
356
+ street: "123 Main St",
357
+ city: "New York",
358
+ },
359
+ },
360
+ employees: [
361
+ { name: "Alice", role: "Engineer" },
362
+ { name: "Bob", role: "Designer" },
363
+ ],
364
+ });
365
+ });
366
+ });
@@ -457,6 +457,11 @@ export class FormModel<T extends TObject> {
457
457
  }
458
458
 
459
459
  if (t.schema.isBoolean(schema)) {
460
+ // Handle string representations from Select components (Yes/No dropdown)
461
+ if (input === "true") return true;
462
+ if (input === "false") return false;
463
+ if (input === "" || input === null || input === undefined) return undefined;
464
+ // Handle actual boolean values
460
465
  return !!input;
461
466
  }
462
467