@alepha/react 0.13.6 → 0.13.8

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 (37) hide show
  1. package/dist/auth/index.browser.js +5 -5
  2. package/dist/auth/index.browser.js.map +1 -1
  3. package/dist/auth/index.d.ts +105 -103
  4. package/dist/auth/index.js +5 -5
  5. package/dist/auth/index.js.map +1 -1
  6. package/dist/core/index.browser.js +407 -142
  7. package/dist/core/index.browser.js.map +1 -1
  8. package/dist/core/index.d.ts +144 -116
  9. package/dist/core/index.js +409 -145
  10. package/dist/core/index.js.map +1 -1
  11. package/dist/core/index.native.js +24 -2
  12. package/dist/core/index.native.js.map +1 -1
  13. package/dist/form/index.d.ts +14 -6
  14. package/dist/form/index.js +32 -12
  15. package/dist/form/index.js.map +1 -1
  16. package/dist/head/index.d.ts +18 -18
  17. package/dist/head/index.js +5 -1
  18. package/dist/head/index.js.map +1 -1
  19. package/dist/i18n/index.d.ts +25 -25
  20. package/dist/i18n/index.js +4 -3
  21. package/dist/i18n/index.js.map +1 -1
  22. package/dist/websocket/index.d.ts +1 -1
  23. package/package.json +22 -23
  24. package/src/auth/hooks/useAuth.ts +1 -0
  25. package/src/auth/services/ReactAuth.ts +6 -4
  26. package/src/core/components/ErrorViewer.tsx +378 -130
  27. package/src/core/components/NestedView.tsx +16 -11
  28. package/src/core/contexts/AlephaProvider.tsx +41 -0
  29. package/src/core/contexts/RouterLayerContext.ts +2 -0
  30. package/src/core/hooks/useAction.ts +4 -1
  31. package/src/core/index.shared.ts +1 -0
  32. package/src/core/primitives/$page.ts +15 -2
  33. package/src/core/providers/ReactPageProvider.ts +6 -7
  34. package/src/core/providers/ReactServerProvider.ts +2 -6
  35. package/src/form/services/FormModel.ts +81 -26
  36. package/src/head/index.ts +2 -1
  37. package/src/i18n/providers/I18nProvider.ts +4 -2
@@ -150,10 +150,10 @@ export interface PagePrimitiveOptions<
150
150
  * Load data before rendering the page.
151
151
  *
152
152
  * This function receives
153
- * - the request context and
153
+ * - the request context (params, query, etc.)
154
154
  * - the parent props (if page has a parent)
155
155
  *
156
- * In SSR, the returned data will be serialized and sent to the client, then reused during the client-side hydration.
156
+ * > In SSR, the returned data will be serialized and sent to the client, then reused during the client-side hydration.
157
157
  *
158
158
  * Resolve can be stopped by throwing an error, which will be handled by the `errorHandler` function.
159
159
  * It's common to throw a `NotFoundError` to display a 404 page.
@@ -162,6 +162,13 @@ export interface PagePrimitiveOptions<
162
162
  */
163
163
  resolve?: (context: PageResolve<TConfig, TPropsParent>) => Async<TProps>;
164
164
 
165
+ /**
166
+ * Default props to pass to the component when rendering the page.
167
+ *
168
+ * Resolved props from the `resolve` function will override these default props.
169
+ */
170
+ props?: () => Partial<TProps>;
171
+
165
172
  /**
166
173
  * The component to render when the page is loaded.
167
174
  *
@@ -189,6 +196,12 @@ export interface PagePrimitiveOptions<
189
196
  */
190
197
  parent?: PagePrimitive<PageConfigSchema, TPropsParent, any>;
191
198
 
199
+ /**
200
+ * Function to determine if the page can be accessed.
201
+ *
202
+ * If it returns false, the page will not be accessible and a 403 Forbidden error will be returned.
203
+ * This function can be used to implement permission-based access control.
204
+ */
192
205
  can?: () => boolean;
193
206
 
194
207
  /**
@@ -314,7 +314,11 @@ export class ReactPageProvider {
314
314
  if (!it.error) {
315
315
  try {
316
316
  const element = await this.createElement(it.route, {
317
+ // default props attached to page
318
+ ...(it.route.props ? it.route.props() : {}),
319
+ // resolved props
317
320
  ...props,
321
+ // context props (from previous layers)
318
322
  ...context,
319
323
  });
320
324
 
@@ -380,12 +384,6 @@ export class ReactPageProvider {
380
384
  return { state };
381
385
  }
382
386
 
383
- protected createRedirectionLayer(redirect: string): CreateLayersResult {
384
- return {
385
- redirect,
386
- };
387
- }
388
-
389
387
  protected getErrorHandler(route: PageRoute): ErrorHandler | undefined {
390
388
  if (route.errorHandler) return route.errorHandler;
391
389
  let parent = route.parent;
@@ -431,7 +429,7 @@ export class ReactPageProvider {
431
429
  ): string {
432
430
  const found = this.pages.find((it) => it.name === page.options.name);
433
431
  if (!found) {
434
- throw new Error(`Page ${page.options.name} not found`);
432
+ throw new AlephaError(`Page ${page.options.name} not found`);
435
433
  }
436
434
 
437
435
  let url = found.path ?? "";
@@ -475,6 +473,7 @@ export class ReactPageProvider {
475
473
  value: {
476
474
  index,
477
475
  path,
476
+ onError: this.getErrorHandler(page) ?? ((error) => this.renderError(error)),
478
477
  },
479
478
  },
480
479
  element,
@@ -39,17 +39,13 @@ import {
39
39
  const envSchema = t.object({
40
40
  REACT_SSR_ENABLED: t.optional(t.boolean()),
41
41
  REACT_ROOT_ID: t.text({ default: "root" }), // TODO: move to ReactPageProvider.options?
42
- REACT_SERVER_TEMPLATE: t.optional(
43
- t.text({
44
- size: "rich",
45
- }),
46
- ),
47
42
  });
48
43
 
49
44
  declare module "alepha" {
50
45
  interface Env extends Partial<Static<typeof envSchema>> {}
51
46
  interface State {
52
47
  "alepha.react.server.ssr"?: boolean;
48
+ "alepha.react.server.template"?: string;
53
49
  }
54
50
  }
55
51
 
@@ -171,7 +167,7 @@ export class ReactServerProvider {
171
167
 
172
168
  public get template() {
173
169
  return (
174
- this.alepha.env.REACT_SERVER_TEMPLATE ??
170
+ this.alepha.store.get("alepha.react.server.template") ??
175
171
  "<!DOCTYPE html><html lang='en'><head></head><body></body></html>"
176
172
  );
177
173
  }
@@ -1,11 +1,5 @@
1
- import {
2
- $inject,
3
- Alepha,
4
- type Static,
5
- type TObject,
6
- type TSchema,
7
- t,
8
- } from "alepha";
1
+ import type { TArray } from "alepha";
2
+ import { $inject, Alepha, type Static, t, type TObject, type TSchema, } from "alepha";
9
3
  import { $logger } from "alepha/logger";
10
4
  import type { ChangeEvent, InputHTMLAttributes } from "react";
11
5
 
@@ -221,17 +215,21 @@ export class FormModel<T extends TObject> {
221
215
  if (!options.schema || !t.schema.isObject(schema)) {
222
216
  return {};
223
217
  }
218
+
224
219
  if (prop in schema.properties) {
225
- if (t.schema.isObject(schema.properties[prop])) {
226
- return this.createProxyFromSchema(
227
- options,
228
- schema.properties[prop],
229
- {
230
- parent: parent ? `${parent}.${prop}` : prop,
231
- store: context.store,
232
- },
233
- );
234
- }
220
+
221
+ // // it's a nested object, create another proxy
222
+ // if (t.schema.isObject(schema.properties[prop])) {
223
+ // return this.createProxyFromSchema(
224
+ // options,
225
+ // schema.properties[prop],
226
+ // {
227
+ // parent: parent ? `${parent}.${prop}` : prop,
228
+ // store: context.store,
229
+ // },
230
+ // );
231
+ // }
232
+
235
233
  return this.createInputFromSchema<T>(
236
234
  prop as keyof Static<T> & string,
237
235
  options,
@@ -253,7 +251,7 @@ export class FormModel<T extends TObject> {
253
251
  parent: string;
254
252
  store: Record<string, any>;
255
253
  },
256
- ): InputField {
254
+ ): BaseInputField {
257
255
  const parent = context.parent || "";
258
256
  const field = schema.properties?.[name];
259
257
  if (!field) {
@@ -268,7 +266,6 @@ export class FormModel<T extends TObject> {
268
266
  }
269
267
 
270
268
  const isRequired = schema.required?.includes(name) ?? false;
271
-
272
269
  const key = parent ? `${parent}.${name}` : name;
273
270
  const path = `/${key.replaceAll(".", "/")}`;
274
271
 
@@ -278,7 +275,7 @@ export class FormModel<T extends TObject> {
278
275
 
279
276
  if (context.store[key] === typedValue) {
280
277
  // no change, do not update
281
- // return;
278
+ // return; <- disabled for now, as some inputs may need to sync even if value is same
282
279
  }
283
280
 
284
281
  context.store[key] = typedValue;
@@ -291,6 +288,8 @@ export class FormModel<T extends TObject> {
291
288
  id: this.id,
292
289
  path: path,
293
290
  value: typedValue,
291
+ }, {
292
+ catch: true
294
293
  });
295
294
 
296
295
  if (sync) {
@@ -299,6 +298,7 @@ export class FormModel<T extends TObject> {
299
298
  );
300
299
  if (inputElement instanceof HTMLInputElement) {
301
300
  if (t.schema.isBoolean(field)) {
301
+ inputElement.value = value;
302
302
  inputElement.checked = Boolean(value);
303
303
  } else {
304
304
  inputElement.value = value;
@@ -324,7 +324,15 @@ export class FormModel<T extends TObject> {
324
324
  }
325
325
 
326
326
  if (t.schema.isBoolean(field)) {
327
- set(event.target.checked, false);
327
+ if (event.target.value === "true") {
328
+ set(true, false);
329
+ } else if (event.target.value === "false") {
330
+ set(false, false);
331
+ } else if (event.target.value === "") {
332
+ set(undefined, false);
333
+ } else {
334
+ set(event.target.checked, false);
335
+ }
328
336
  } else {
329
337
  set(event.target.value, false);
330
338
  }
@@ -391,6 +399,39 @@ export class FormModel<T extends TObject> {
391
399
  Object.assign(attr, customAttr);
392
400
  }
393
401
 
402
+ // if type = object, add items: { [key: string]: InputField }
403
+ if (t.schema.isObject(field)) {
404
+ return {
405
+ path,
406
+ props: attr,
407
+ schema: field,
408
+ set,
409
+ form: this,
410
+ required,
411
+ items: this.createProxyFromSchema(
412
+ options,
413
+ field,
414
+ {
415
+ parent: key,
416
+ store: context.store,
417
+ },
418
+ )
419
+ } as ObjectInputField<any>;
420
+ }
421
+
422
+ // if type = array, add items: InputField[]
423
+ if (t.schema.isArray(field)) {
424
+ return {
425
+ path,
426
+ props: attr,
427
+ schema: field,
428
+ set,
429
+ form: this,
430
+ required,
431
+ items: [], // <- will be populated dynamically in the UI
432
+ } as ArrayInputField<any>;
433
+ }
434
+
394
435
  return {
395
436
  path,
396
437
  props: attr,
@@ -466,9 +507,7 @@ export class FormModel<T extends TObject> {
466
507
  }
467
508
 
468
509
  export type SchemaToInput<T extends TObject> = {
469
- [K in keyof T["properties"]]: T["properties"][K] extends TObject
470
- ? SchemaToInput<T["properties"][K]>
471
- : InputField;
510
+ [K in keyof T["properties"]]: InputField<T["properties"][K]>;
472
511
  };
473
512
 
474
513
  export interface FormEventLike {
@@ -476,13 +515,29 @@ export interface FormEventLike {
476
515
  stopPropagation?: () => void;
477
516
  }
478
517
 
479
- export interface InputField {
518
+ export type InputField<T extends TSchema> =
519
+ T extends TObject
520
+ ? ObjectInputField<T>
521
+ : T extends TArray<infer U>
522
+ ? ArrayInputField<U>
523
+ : BaseInputField;
524
+
525
+ export interface BaseInputField {
480
526
  path: string;
481
527
  required: boolean;
482
528
  props: InputHTMLAttributesLike;
483
529
  schema: TSchema;
484
530
  set: (value: any) => void;
485
531
  form: FormModel<any>;
532
+ items?: any;
533
+ }
534
+
535
+ export interface ObjectInputField<T extends TObject> extends BaseInputField {
536
+ items: SchemaToInput<T>;
537
+ }
538
+
539
+ export interface ArrayInputField<T extends TSchema> extends BaseInputField {
540
+ items: Array<InputField<T>>
486
541
  }
487
542
 
488
543
  export type InputHTMLAttributesLike = Pick<
package/src/head/index.ts CHANGED
@@ -8,6 +8,7 @@ import { $module } from "alepha";
8
8
  import { $head } from "./primitives/$head.ts";
9
9
  import type { Head } from "./interfaces/Head.ts";
10
10
  import { ServerHeadProvider } from "./providers/ServerHeadProvider.ts";
11
+ import { HeadProvider } from "./providers/HeadProvider.ts";
11
12
 
12
13
  // ---------------------------------------------------------------------------------------------------------------------
13
14
 
@@ -43,5 +44,5 @@ declare module "@alepha/react" {
43
44
  export const AlephaReactHead = $module({
44
45
  name: "alepha.react.head",
45
46
  primitives: [$head],
46
- services: [AlephaReact, ServerHeadProvider],
47
+ services: [AlephaReact, ServerHeadProvider, HeadProvider],
47
48
  });
@@ -15,6 +15,7 @@ export class I18nProvider<
15
15
  protected cookie = $cookie({
16
16
  name: "lang",
17
17
  schema: t.text(),
18
+ ttl: [1, "year"]
18
19
  });
19
20
 
20
21
  public readonly registry: Array<{
@@ -170,7 +171,7 @@ export class I18nProvider<
170
171
  options: I18nLocalizeOptions = {},
171
172
  ) => {
172
173
  // Handle numbers
173
- if (typeof value === "number") {
174
+ if (typeof value === "number" && !options.date) {
174
175
  return new Intl.NumberFormat(this.lang, options.number).format(value);
175
176
  }
176
177
 
@@ -178,7 +179,8 @@ export class I18nProvider<
178
179
  if (
179
180
  value instanceof Date ||
180
181
  this.dateTimeProvider.isDateTime(value) ||
181
- (typeof value === "string" && options.date)
182
+ (typeof value === "string" && options.date) ||
183
+ (typeof value === "number" && options.date)
182
184
  ) {
183
185
  // convert to DateTime with locale applied
184
186
  let dt = this.dateTimeProvider.of(value);