@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.
- package/dist/auth/index.browser.js +1488 -4
- package/dist/auth/index.browser.js.map +1 -1
- package/dist/auth/index.d.ts +2 -2
- package/dist/auth/index.js +1827 -4
- package/dist/auth/index.js.map +1 -1
- package/dist/core/index.d.ts +58 -937
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +139 -2014
- package/dist/core/index.js.map +1 -1
- package/dist/form/index.d.ts.map +1 -1
- package/dist/form/index.js +6 -1
- package/dist/form/index.js.map +1 -1
- package/dist/head/index.browser.js +3 -1
- package/dist/head/index.browser.js.map +1 -1
- package/dist/head/index.d.ts +552 -8
- package/dist/head/index.d.ts.map +1 -1
- package/dist/head/index.js +17 -2
- package/dist/head/index.js.map +1 -1
- package/dist/{core → router}/index.browser.js +126 -516
- package/dist/router/index.browser.js.map +1 -0
- package/dist/router/index.d.ts +1334 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +1939 -0
- package/dist/router/index.js.map +1 -0
- package/package.json +12 -6
- package/src/auth/__tests__/$auth.spec.ts +188 -0
- package/src/auth/index.ts +1 -1
- package/src/auth/services/ReactAuth.ts +1 -1
- package/src/core/__tests__/Router.spec.tsx +169 -0
- package/src/core/components/ClientOnly.tsx +14 -0
- package/src/core/components/ErrorBoundary.tsx +3 -2
- package/src/core/contexts/AlephaContext.ts +3 -0
- package/src/core/contexts/AlephaProvider.tsx +2 -1
- package/src/core/hooks/useAction.browser.spec.tsx +569 -0
- package/src/core/hooks/useAction.ts +11 -0
- package/src/core/index.ts +13 -102
- package/src/form/hooks/useForm.browser.spec.tsx +366 -0
- package/src/form/services/FormModel.ts +5 -0
- package/src/head/__tests__/expandSeo.spec.ts +203 -0
- package/src/head/__tests__/page-head.spec.ts +39 -0
- package/src/head/__tests__/seo-head.spec.ts +121 -0
- package/src/head/hooks/useHead.spec.tsx +288 -0
- package/src/head/index.ts +18 -8
- package/src/head/interfaces/Head.ts +3 -0
- package/src/head/providers/BrowserHeadProvider.browser.spec.ts +271 -0
- package/src/head/providers/HeadProvider.ts +6 -1
- package/src/head/providers/ServerHeadProvider.spec.ts +163 -0
- package/src/head/providers/ServerHeadProvider.ts +20 -0
- package/src/i18n/__tests__/integration.spec.tsx +239 -0
- package/src/i18n/components/Localize.spec.tsx +357 -0
- package/src/i18n/hooks/useI18n.browser.spec.tsx +438 -0
- package/src/i18n/providers/I18nProvider.spec.ts +389 -0
- package/src/{core → router}/components/ErrorViewer.tsx +2 -0
- package/src/router/components/Link.tsx +21 -0
- package/src/{core → router}/components/NestedView.tsx +3 -5
- package/src/router/components/NotFound.tsx +30 -0
- package/src/router/errors/Redirection.ts +28 -0
- package/src/{core → router}/hooks/useActive.ts +6 -2
- package/src/{core → router}/hooks/useQueryParams.ts +2 -2
- package/src/{core → router}/hooks/useRouter.ts +1 -1
- package/src/{core → router}/hooks/useRouterState.ts +1 -1
- package/src/{core → router}/index.browser.ts +14 -12
- package/src/{core/index.shared-router.ts → router/index.shared.ts} +6 -3
- package/src/router/index.ts +125 -0
- package/src/router/primitives/$page.browser.spec.tsx +702 -0
- package/src/router/primitives/$page.spec.tsx +702 -0
- package/src/{core → router}/primitives/$page.ts +1 -1
- package/src/{core → router}/providers/ReactBrowserProvider.ts +3 -13
- package/src/{core → router}/providers/ReactBrowserRendererProvider.ts +3 -0
- package/src/{core → router}/providers/ReactBrowserRouterProvider.ts +3 -0
- package/src/{core → router}/providers/ReactPageProvider.ts +5 -3
- package/src/router/providers/ReactServerProvider.spec.tsx +316 -0
- package/src/{core → router}/providers/ReactServerProvider.ts +12 -30
- package/src/{core → router}/services/ReactPageServerService.ts +3 -0
- package/src/{core → router}/services/ReactPageService.ts +5 -5
- package/src/{core → router}/services/ReactRouter.ts +26 -5
- package/dist/core/index.browser.js.map +0 -1
- package/dist/core/index.native.js +0 -403
- package/dist/core/index.native.js.map +0 -1
- package/src/core/components/Link.tsx +0 -18
- package/src/core/components/NotFound.tsx +0 -27
- package/src/core/errors/Redirection.ts +0 -13
- package/src/core/hooks/useSchema.ts +0 -88
- package/src/core/index.native.ts +0 -21
- package/src/core/index.shared.ts +0 -9
- /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
|
|
21
|
-
export * from "./
|
|
22
|
-
export
|
|
23
|
-
export * from "./
|
|
24
|
-
export * from "./
|
|
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
|
|