Skip to content

Commit

Permalink
fix: improve and speed up field types
Browse files Browse the repository at this point in the history
  • Loading branch information
EdieLemoine authored and myparcel-bot[bot] committed Dec 6, 2023
1 parent ca7b56b commit 97fc7fd
Show file tree
Hide file tree
Showing 21 changed files with 455 additions and 539 deletions.
8 changes: 4 additions & 4 deletions libs/core/src/components/FormElement.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
</template>

<script lang="ts" setup>
import {computed, toRefs} from 'vue';
import {computed, toRefs, type Ref, unref} from 'vue';
import {type AnyElementInstance} from '../types';
import {type InteractiveElementInstance} from '../form';
import {createElementHooks} from '../composables';
const props = defineProps<{element: AnyElementInstance}>();
Expand All @@ -39,12 +40,11 @@ const attributes = computed(() => {
const model = computed({
get() {
return props.element.ref;
return unref((props.element as InteractiveElementInstance).ref);
},
set(value) {
// eslint-disable-next-line vue/no-mutating-props
props.element.ref = value;
(props.element as InteractiveElementInstance).ref = value as Ref;
},
});
</script>
17 changes: 11 additions & 6 deletions libs/core/src/components/FormElementWrapper.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {type Component, defineComponent, h, type PropType, provide, Teleport} from 'vue';
import {type Component, defineComponent, h, type PropType, provide, Teleport, type VNode} from 'vue';
import {isOfType} from '@myparcel/ts-utils';
import {type AnyElementInstance} from '../types';
import {INJECT_ELEMENT} from '../symbols';
import {type FormInstance} from '../form';
Expand Down Expand Up @@ -32,11 +33,15 @@ export default defineComponent({
element: this.element,
},
{
...(Array.isArray(this.element.component.children)
? this.element.component.children.map((child: unknown) => {
return typeof child === 'function' ? child : () => child;
})
: this.element.component.children),
...(isOfType<VNode>(this.element.component, 'children')
? [
Array.isArray(this.element.component.children)
? this.element.component.children.map((child: unknown) => {
return typeof child === 'function' ? child : () => child;
})
: this.element.component.children,
]
: []),
...this.element.slots,
...this.$slots,
},
Expand Down
75 changes: 42 additions & 33 deletions libs/core/src/form/Form.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {computed, ref, watch, reactive, unref} from 'vue';
import {get} from '@vueuse/core';
import {get, isDefined} from '@vueuse/core';
import {createHookManager} from '@myparcel-vfb/hook-manager';
import {isOfType} from '@myparcel/ts-utils';
import {markComponentAsRaw} from '../utils';
import {type ToRecord} from '../types/common.types';
import {type AnyElementConfiguration, type AnyElementInstance, type ComponentOrHtmlElement} from '../types';
import {type AnyElementConfiguration, type AnyElementInstance, type ElementName} from '../types';
import {FORM_HOOKS, FormHook} from '../data';
import {PlainElement, type PlainElementInstance} from './plain-element';
import {PlainElement} from './plain-element';
import {
InteractiveElement,
type InteractiveElementConfiguration,
Expand All @@ -17,25 +17,22 @@ import {type FormHooks, type FormInstance, type InstanceFormConfiguration, type
// noinspection JSUnusedGlobalSymbols
export class Form<
V extends FormValues = FormValues,
FN extends string = string,
FC extends InstanceFormConfiguration<V> & FormHooks = InstanceFormConfiguration<V> & FormHooks,
> {
public readonly config: Omit<FC, 'fields'>;
// @ts-expect-error This is initialized this on render.
public element: FormInstance<V>['element'];
public readonly config: FormInstance<V>['config'];
public readonly fields: FormInstance<V>['fields'] = ref([]);
public readonly hooks: FormInstance<V>['hooks'];
public readonly interactiveFields: FormInstance<V>['interactiveFields'];
public isDirty: FormInstance<V>['isDirty'];
public isValid: FormInstance<V>['isValid'] = ref(true);
public readonly model = {} as FormInstance<V>['model'];
public readonly name: FN;
public readonly name: FormInstance<V>['name'];
public readonly off: FormInstance<V>['off'];
public readonly on: FormInstance<V>['on'];
public readonly stable: FormInstance<V>['stable'] = ref(false);
public readonly values: FormInstance<V>['values'];

public constructor(name: FN, formConfig: ToRecord<FC>) {
public constructor(name: FormInstance<V>['name'], formConfig: ToRecord<FC>) {
const {fields, ...config} = formConfig;

formConfig.hookNames = [...FORM_HOOKS, ...(formConfig.hookNames ?? [])];
Expand All @@ -52,7 +49,7 @@ export class Form<
markComponentAsRaw(this.config.fieldDefaults.wrapper);

fields.forEach((field) => {
const instance = this.createFieldInstance(field, this as unknown as FormInstance<V>);
const instance = this.createFieldInstance(field as AnyElementConfiguration, this as unknown as FormInstance<V>);

get(this.fields).push(instance);
});
Expand Down Expand Up @@ -102,9 +99,9 @@ export class Form<
}

public getValue<T = unknown>(fieldName: string): T {
const fieldInstance = this.ensureGetField(fieldName);
const fieldInstance = this.ensureGetField<InteractiveElementInstance>(fieldName);

return get(fieldInstance.ref);
return get(fieldInstance.ref) as T;
}

public getValues(): V {
Expand All @@ -124,7 +121,7 @@ export class Form<
}

public setValue(fieldName: string, value: unknown): void {
const fieldInstance = this.ensureGetField(fieldName);
const fieldInstance = this.ensureGetField<InteractiveElementInstance>(fieldName);

fieldInstance.ref.value = value;
}
Expand Down Expand Up @@ -160,8 +157,8 @@ export class Form<
return get(this.isValid);
}

protected ensureGetField(name: string): AnyElementInstance {
const field = this.getField(name);
protected ensureGetField<I extends AnyElementInstance>(name: string): I {
const field = this.getField<I>(name);

if (!field) {
throw new Error(`Field ${name} not found in form ${this.name}`);
Expand All @@ -171,8 +168,6 @@ export class Form<
}

private createFieldInstance(field: AnyElementConfiguration, form: FormInstance<V>): AnyElementInstance {
let instance: InteractiveElementInstance | PlainElementInstance;

const elementConfig = {
...form.config.fieldDefaults,
...field,
Expand All @@ -185,32 +180,46 @@ export class Form<
markComponentAsRaw(elementConfig.component);
markComponentAsRaw(elementConfig.wrapper);

if (isOfType<InteractiveElementConfiguration<ComponentOrHtmlElement, string>>(elementConfig, 'ref')) {
instance = new InteractiveElement(form, elementConfig.name, elementConfig);
if (!isOfType<InteractiveElementConfiguration>(elementConfig, 'ref')) {
const instance = new PlainElement(form as FormInstance, elementConfig);

watch(instance.ref, async (value: unknown) => {
await this.hooks.execute(FormHook.ElementChange, this, instance, value);
return this.registerElement(elementConfig.name, instance);
}

if (!get(instance.isDisabled)) {
// @ts-expect-error todo
this.values[elementConfig.name] = value as V[keyof V];
}
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const instance: InteractiveElementInstance<any> = new InteractiveElement<
FormInstance,
string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
InteractiveElementConfiguration<any>
>(form, elementConfig.name, elementConfig);

watch(instance.ref, async (value: unknown) => {
await this.hooks.execute(FormHook.ElementChange, this, instance, value);

if (!get(instance.isDisabled)) {
// @ts-expect-error todo
this.values[elementConfig.name] = unref(instance.ref) as unknown as V[keyof V];
this.values[elementConfig.name] = value as V[keyof V];
}
} else {
instance = new PlainElement(form, elementConfig);
});

if (!get(instance.isDisabled)) {
// @ts-expect-error todo
this.values[elementConfig.name] = unref(instance.ref) as V[keyof V];
}

if (isOfType<AnyElementConfiguration<ComponentOrHtmlElement, string>>(elementConfig, 'name')) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
this.model[elementConfig.name] = instance;
return this.registerElement(elementConfig.name, instance);
}

private registerElement<I extends AnyElementInstance>(name: ElementName, instance: I): I {
if (!isDefined(name)) {
return instance;
}

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
this.model[name] = instance;

return instance;
}
}
31 changes: 17 additions & 14 deletions libs/core/src/form/Form.types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/unified-signatures */
import {type ComputedRef, type Ref, type UnwrapNestedRefs} from 'vue';
import {type AnyAttributes, type FunctionOr} from '@myparcel-vfb/utils';
import {type AnyAttributes, type FunctionOr, type ComponentProps} from '@myparcel-vfb/utils';
import {type HookManagerInstance, type HookUnregisterHandler} from '@myparcel-vfb/hook-manager';
import {type PromiseOr, type ReadonlyOr} from '@myparcel/ts-utils';
import {type ToRecord} from '../types/common.types';
Expand Down Expand Up @@ -37,8 +37,9 @@ export interface FormConfiguration<V extends FormValues = FormValues> extends Fo

/**
* Fields in the form.
* @TODO find a way to type this without having it cause a billion errors.
*/
fields: ReadonlyOr<AnyElementConfiguration[]>;
fields: unknown[];

/**
* Configuration for the form element.
Expand All @@ -63,7 +64,7 @@ export interface FormConfiguration<V extends FormValues = FormValues> extends Fo
/**
* Names of hooks to register.
*/
hookNames?: readonly string[] | string[];
hookNames?: ReadonlyOr<string[]>;

/**
* Values to initialize the form with.
Expand Down Expand Up @@ -94,19 +95,19 @@ export interface FormHooks<V extends FormValues = FormValues> {

[FormHook.AfterValidate]?(form: FormInstance<V>): PromiseOr<void>;

[FormHook.BeforeAddElement]?<T extends keyof V, N extends string>(
[FormHook.BeforeAddElement]?<T extends keyof V, Props extends ComponentProps = ComponentProps>(
form: FormInstance<V>,
field: AnyElementInstance<ComponentOrHtmlElement, N, V[T]>,
field: AnyElementInstance<V[T], Props>,
): PromiseOr<void>;

[FormHook.AfterAddElement]?<T extends keyof V, N extends string>(
[FormHook.AfterAddElement]?<T extends keyof V, Props extends ComponentProps = ComponentProps>(
form: FormInstance<V>,
field: AnyElementInstance<ComponentOrHtmlElement, N, V[T]>,
field: AnyElementInstance<V[T], Props>,
): PromiseOr<void>;

[FormHook.ElementChange]?<T extends keyof V, N extends string>(
[FormHook.ElementChange]?<T extends keyof V, Props extends ComponentProps = ComponentProps>(
form: FormInstance<V>,
field: AnyElementInstance<ComponentOrHtmlElement, N, V[T]>,
field: AnyElementInstance<V[T], Props>,
value: V[T],
): PromiseOr<void>;
}
Expand Down Expand Up @@ -170,13 +171,15 @@ export interface BaseFormInstance<Values extends FormValues = FormValues> {
/**
* Add a new element to the form at the end, or before or after an existing element.
*/
addElement<EC extends AnyElementConfiguration = AnyElementConfiguration>(element: EC): Promise<AnyElementInstance>;
addElement<Type = unknown, Props extends ComponentProps = ComponentProps>(
element: AnyElementConfiguration<Type, Props>,
): Promise<AnyElementInstance<Type, Props>>;

addElement(
element: AnyElementConfiguration,
addElement<Type = unknown, Props extends ComponentProps = ComponentProps>(
element: AnyElementConfiguration<Type, Props>,
sibling: string,
position?: 'before' | 'after',
): Promise<undefined | AnyElementInstance>;
): Promise<undefined | AnyElementInstance<Type, Props>>;

/**
* Get a field by name.
Expand Down Expand Up @@ -264,5 +267,5 @@ export interface InstanceFormConfiguration<V extends FormValues = FormValues> ex
export type FormInstance<V extends FormValues = any> = BaseFormInstance<V>;

export type FieldsToModel<V extends FormValues> = {
[K in keyof V]: K extends string ? InteractiveElementInstance<ComponentOrHtmlElement, K, V[K]> : never;
[K in keyof V]: K extends string ? InteractiveElementInstance<ComponentProps, V[K]> : never;
};
Loading

0 comments on commit 97fc7fd

Please sign in to comment.