Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: add a new implementation of @vaadin/hilla-lit-form #2620

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,697 changes: 127 additions & 3,570 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion packages/ts/models/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
"access": "public"
},
"dependencies": {
"@vaadin/hilla-lit-form": "24.5.0-alpha5"
"@vaadin/hilla-lit-form": "24.5.0-alpha5",
"type-fest": "^4.21.0"
},
"peerDependencies": {
"react": "^18"
Expand Down
78 changes: 60 additions & 18 deletions packages/ts/models/src/builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import {
$defaultValue,
$key,
$meta,
$model,
$name,
$owner,
$validators,
type AnyObject,
type DefaultValueProvider,
type Model,
type ModelMetadata,
} from './model.js';
import type { Validator } from './validators.js';

const { create, defineProperty } = Object;

Expand All @@ -21,12 +24,10 @@ export type ModelBuilderPropertyOptions = Readonly<{
meta?: ModelMetadata;
}>;

const $model = Symbol('model');

/**
* The flags for the model constructor that allow to determine specific characteristics of the model.
*/
export type Flags = {
export type Flags = Readonly<{
/**
* Defines if the model is named.
*/
Expand All @@ -43,7 +44,7 @@ export type Flags = {
* safely set it in the end of building using this flag.
*/
selfRefKeys: keyof any;
};
}>;

/**
* A builder class for creating all basic models.
Expand All @@ -58,14 +59,25 @@ export class CoreModelBuilder<
EX extends AnyObject = EmptyObject,
F extends Flags = { named: false; selfRefKeys: never },
> {
protected readonly [$model]: Model<V, EX>;
static create<V, EX extends AnyObject, R extends keyof any>(
base: Model<V, EX, R>,
): CoreModelBuilder<V, EX, { named: false; selfRefKeys: R }>;
static create<V, EX extends AnyObject, R extends keyof any>(
base: Model<unknown, EX, R>,
defaultValueProvider: (model: Model<unknown, EX, R>) => V,
): CoreModelBuilder<V, EX, { named: false; selfRefKeys: R }>;
static create(base: Model, defaultValueProvider?: (model: Model) => unknown): CoreModelBuilder<unknown> {
return new CoreModelBuilder(base, defaultValueProvider);
}

protected readonly [$model]: Model<V, EX, F['selfRefKeys']>;

/**
* @param base - The base model to extend.
* @param defaultValueProvider - The function that provides the default value
* for the model.
*/
constructor(base: Model, defaultValueProvider?: (model: Model<V, EX>) => V) {
protected constructor(base: Model, defaultValueProvider?: (model: Model) => V) {
this[$model] = create(base);

if (defaultValueProvider) {
Expand Down Expand Up @@ -100,7 +112,7 @@ export class CoreModelBuilder<
define<DK extends symbol, DV>(
key: DK,
value: TypedPropertyDescriptor<DV>,
): CoreModelBuilder<V, EX & Readonly<Record<DK, DV>>, F> {
): CoreModelBuilder<V, DK extends keyof Model ? EX : EX & Readonly<Record<DK, DV>>, F> {
defineProperty(this[$model], key, value);
return this as any;
}
Expand All @@ -114,9 +126,9 @@ export class CoreModelBuilder<
* for the model.
* @returns The current builder instance.
*/
defaultValueProvider(defaultValueProvider: DefaultValueProvider<V, EX>): this {
defaultValueProvider(defaultValueProvider: DefaultValueProvider<V, EX, F['selfRefKeys']>): this {
this.define($defaultValue, {
get(this: Model<V, EX>) {
get(this: Model<V, EX, F['selfRefKeys']>) {
return defaultValueProvider(this);
},
});
Expand All @@ -131,17 +143,31 @@ export class CoreModelBuilder<
* @param name - The name of the model.
* @returns The current builder instance.
*/
name(name: string): CoreModelBuilder<V, EX, { named: true; selfRefKeys: F['selfRefKeys'] }> {
name<NV extends V = V>(
this: F['named'] extends false ? this : never,
name: string,
): CoreModelBuilder<NV, EX, { named: true; selfRefKeys: F['selfRefKeys'] }> {
return this.define($name, { value: name }) as any;
}

/**
* Adds a validator to the model. The validator is a function that checks if
* the value is valid according to some criteria.
*/
validator(validator: Validator<V>): this {
this.define($validators, {
value: [...this[$model][$validators], validator],
});
return this;
}

/**
* Builds the model. On the typing level, it checks if all the model parts are
* set correctly, and raises an error if not.
*
* @returns The model.
*/
build(this: F['named'] extends true ? this : never): Model<V, EX> {
build(this: F['named'] extends true ? this : never): Model<V, EX, F['selfRefKeys']> {
return this[$model];
}
}
Expand Down Expand Up @@ -172,15 +198,21 @@ export class ObjectModelBuilder<
EX extends AnyObject = EmptyObject,
F extends Flags = { named: false; selfRefKeys: never },
> extends CoreModelBuilder<V, EX, F> {
constructor(base: Model) {
static override create<V extends AnyObject, EX extends AnyObject, R extends keyof any>(
base: Model<V, EX, R>,
): ObjectModelBuilder<V, V, EX, { named: false; selfRefKeys: R }> {
return new ObjectModelBuilder(base);
}

protected constructor(base: Model) {
super(base, (m) => {
const result = create(null);

// eslint-disable-next-line no-restricted-syntax
for (const key in m) {
defineProperty(result, key, {
enumerable: true,
get: () => (m[key as keyof Model<V, EX>] as Model)[$defaultValue],
get: () => (m[key as keyof Model] as Model)[$defaultValue],
});
}

Expand All @@ -194,20 +226,26 @@ export class ObjectModelBuilder<
*
* @param name - The name of the model.
*/
object<NV extends AnyObject>(
object<NV extends V = V>(
this: F['named'] extends false ? this : never,
name: string,
): ObjectModelBuilder<NV & V, CV, EX, { named: true; selfRefKeys: F['selfRefKeys'] }> {
return this.name(name) as any;
// @ts-expect-error: too generic
return this.name(name);
}

declare ['name']: <NV extends V = V>(
this: F['named'] extends false ? this : never,
name: string,
) => ObjectModelBuilder<NV, CV, EX, { named: true; selfRefKeys: F['selfRefKeys'] }>;

/**
* {@inheritDoc CoreModelBuilder.define}
*/
declare ['define']: <DK extends symbol, DV>(
key: DK,
value: TypedPropertyDescriptor<DV>,
) => ObjectModelBuilder<V, CV, EX & Readonly<Record<DK, DV>>, F>;
) => ObjectModelBuilder<V, CV, DK extends keyof Model ? EX : EX & Readonly<Record<DK, DV>>, F>;

/**
* {@inheritDoc CoreModelBuilder.meta}
Expand Down Expand Up @@ -262,8 +300,12 @@ export class ObjectModelBuilder<

const props = propertyRegistry.get(this)!;

props[key] ??= new CoreModelBuilder<V[PK], EXK, { named: true; selfRefKeys: never }>(
typeof model === 'function' ? model(this) : model,
props[key] ??= (
CoreModelBuilder.create(typeof model === 'function' ? model(this) : model) as CoreModelBuilder<
V[PK],
EXK,
{ named: true; selfRefKeys: never }
>
)
.define($key, { value: key })
.define($owner, { value: this })
Expand Down
60 changes: 47 additions & 13 deletions packages/ts/models/src/core.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,86 @@
import type { EmptyObject } from 'type-fest';
import { CoreModelBuilder } from './builders.js';
import { $enum, $itemModel, type $members, type AnyObject, type Enum, Model, type Value } from './model.js';
import { ValidationError } from './validators.js';

/* eslint-disable tsdoc/syntax */

/**
* The symbol that represents the {@link PrimitiveModel[$parse]} property.
*/
export const $parse = Symbol('parse');

/* eslint-enable tsdoc/syntax */

/**
* The model of a primitive value, like `string`, `number` or `boolean`.
*/
export type PrimitiveModel<V = unknown> = Model<V>;
export const PrimitiveModel = new CoreModelBuilder(Model, (): unknown => undefined).name('primitive').build();
export type PrimitiveModel<V = unknown> = Model<V, Readonly<{ [$parse](value: string): V }>>;
export const PrimitiveModel = CoreModelBuilder.create(Model, (): unknown => undefined)
.name('primitive')
.define($parse, {
value: (value: string) => value,
})
.build();

/**
* The model of a string value.
*/
export type StringModel = PrimitiveModel<string>;
export const StringModel = new CoreModelBuilder(PrimitiveModel, () => '').name('string').build();
export const StringModel = CoreModelBuilder.create(PrimitiveModel, () => '')
.name('string')
.build();

/**
* The model of a number value.
*/
export type NumberModel = PrimitiveModel<number>;
export const NumberModel = new CoreModelBuilder(PrimitiveModel, () => 0).name('number').build();
export const NumberModel = CoreModelBuilder.create(PrimitiveModel, () => 0)
.name('number')
.define($parse, {
value: (value: string) => Number(value),
})
.validator((value) => !isFinite(value) && new ValidationError(value, 'Must be a number'))
.build();

/**
* The model of a boolean value.
*/
export type BooleanModel = PrimitiveModel<boolean>;
export const BooleanModel = new CoreModelBuilder(PrimitiveModel, () => false).name('boolean').build();
export const BooleanModel = CoreModelBuilder.create(PrimitiveModel, () => false)
.name('boolean')
.define($parse, {
value: (value: string) => value !== '',
})
.build();

/**
* The model of an array data.
*/
export type ArrayModel<M extends Model = Model> = Model<
Array<Value<M>>,
export type ArrayModel<V = unknown, EX extends AnyObject = EmptyObject, R extends keyof any = never> = Model<
V[],
Readonly<{
[$itemModel]: M;
[$itemModel]: Model<V, EX, R>;
}>
>;

export const ArrayModel = new CoreModelBuilder(Model, (): unknown[] => [])
export const ArrayModel = CoreModelBuilder.create(Model, (): unknown[] => [])
.name('Array')
.define($itemModel, { value: Model })
.build();

/**
* The model of an object data.
*/
export type ObjectModel<V, EX extends AnyObject = EmptyObject, R extends keyof any = never> = Model<V, EX, R>;
export type ObjectModel<V extends AnyObject, EX extends AnyObject = EmptyObject, R extends keyof any = never> = Model<
V,
EX,
R
>;

export const ObjectModel = new CoreModelBuilder(Model, (): AnyObject => ({})).name('Object').build();
export const ObjectModel = CoreModelBuilder.create(Model, (): AnyObject => ({}))
.name('Object')
.build();

/**
* The model of an enum data.
Expand All @@ -58,8 +92,8 @@ export type EnumModel<T extends typeof Enum> = Model<
}>
>;

export const EnumModel = new CoreModelBuilder<(typeof Enum)[keyof typeof Enum]>(Model)
.name('Enum')
export const EnumModel = CoreModelBuilder.create(Model)
.name<number | string>('Enum')
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
.define($enum, { value: {} as typeof Enum })
.defaultValueProvider((self) => Object.values(self[$enum])[0])
Expand Down
Loading
Loading