www.fgks.org   »   [go: up one dir, main page]

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

Provided signature and partial type compatibility with InertiaJS (Clone of #65) #71

Closed
wants to merge 5 commits into from

Conversation

juanparati
Copy link
@juanparati juanparati commented May 29, 2024

This is a PR clone of #65.

It was closed because my old precognition repository was removed by mistake and the original PR was automatically closed.

Original description:

I did some rework on the vue-inertia package in order to make it compatible with the original InertiaJS "useForm":

Changes

Precognition "useForm" will accept object type like InertiaJS:

import {useForm} from 'laravel-precognition-vue-inertia'
const form = useForm('post', 'https://example.net', mydata);
// or useForm('post', 'https://example.net', mydata as MyInterface);

Precognition "useForm" will return the original InertiaJS properties and methods patched with the Precognition properties and methods (I added the types).

Original interface properties are also exposed.

Advantages of this change

Original interface properties and Precognition properties and methods are exposed to the linter.
Better Typescript support.
Increased signature compatibility with InertiaJS.
It doesn't require any change in the Precognition core libraries or precognition-vue package.

Disadvantages of this change

It may require to update the return type in case that original InertiaJS "useForm" method change any of their return properties or method.

@taylorotwell taylorotwell marked this pull request as draft May 29, 2024 18:42
@timacdonald
Copy link
Member

Thanks, @juanparati.

I don't want us to be replicating all of Inertia's types in Precognition. Ideally, we would just be extending their existing form to ensure compatibility.

Do you know if this is currently possible? To my knowledge, Inertia does not currently export the types that we need to be able to extend.

touched(name: string): boolean, // Patched
touch(name: string | string[] | NamedInputEvent): this; // Patched
submit(submitMethod: RequestMethod|Config, submitUrl?: string, submitOptions?: Partial<VisitOptions>): void; // Patched
cancel(): void;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an example, we shouldn't be specifying the cancel function, as we don't provide this function. I would want this to come from Inertia's type definition that we extend.

@driesvints
Copy link
Member

@juanparati could you perhaps answer's Tim's questions when you find some time?

@juanparati
Copy link
Author
juanparati commented Jun 17, 2024

@timacdonald : It's possible to extend the existing form. It's not possible to extend the existing form, the only way it's to extend into a different function.

In fact I am currently extending the "useFormPrecognition" as a workaround.

This is the code that I use:

import type {RequestMethod, ValidationConfig} from 'laravel-precognition';
import {watch}                                from 'vue';
import {useForm}                              from 'laravel-precognition-vue-inertia';
import {debounce}                             from 'vue-debounce'
import deepEqual                              from "deep-equal";

export function useFormPrecognition<Data extends object>(
    method: RequestMethod | (() => RequestMethod),
    url: string | (() => string),
    inputs: Data,
    validationTrigger        = {
        autoValidate: true,
        debounceTime: 800
    },
    config: ValidationConfig = {}
) {
    // @ts-ignore
    const form = useForm(method, url, inputs, config);

    const validateChanges = (newVal: any, oldVal: any) => {
        const changedFields: string[] = [];
        for (const [k, v] of Object.entries(newVal)) {
            const type = typeof v;

            if (v === void 0)
                continue;

            if ('object' === type || 'symbol' === type || Array.isArray(v)) {
                deepEqual(v, oldVal[k], {strict: true});
                changedFields.push(k);
            } else if (v !== oldVal[k]) {
                changedFields.push(k);
            }
        }

        changedFields.forEach(form.validate);
    }

    const debounceInstance = debounce(validateChanges, validationTrigger.debounceTime);

    if (validationTrigger.autoValidate) {
        watch(
            () => form.data(),
            (newVal, oldVal) => {
                debounceInstance.cancel();
                debounceInstance(newVal, oldVal);
            },
            {deep: true}
        );
    }

    return form;
}

@juanparati
Copy link
Author
juanparati commented Jun 17, 2024

A possible solution it's just to explain in the documentation how to extend "useForm".

Or see #65 (comment)

@timacdonald
Copy link
Member

It's not possible to extend the existing form

Could you elaborate on why it is not currently possible?

@juanparati
Copy link
Author
juanparati commented Jun 18, 2024

It's not possible to extend the existing form

Could you elaborate on why it is not currently possible?

It's not possible to extend directly "InertiaFormProps" from Inertia because it's not exported however I found a workaround that works ✨.

It seems that I was enable to extend from InertiaForm type.

I just tested and it works:

import { Config, NamedInputEvent, RequestMethod, SimpleValidationErrors, toSimpleValidationErrors, ValidationConfig, ValidationErrors, resolveUrl, resolveMethod, Validator } from 'laravel-precognition'
import { useForm as usePrecognitiveForm, client }      from 'laravel-precognition-vue'
import { InertiaForm, useForm as useInertiaForm }        from '@inertiajs/vue3'
import { Progress } from '@inertiajs/core'
import { watchEffect } from 'vue'

export { client }

type FormDataType = object;

interface FormProps<TForm extends FormDataType> {
    validating: boolean,
    touched(name: string): boolean,
    touch(name: string | string[] | NamedInputEvent): this;
    progress: Progress | null;
    valid(name: string | NamedInputEvent | string[]): boolean,
    invalid(name: string): boolean,
    clearErrors(...fields: (keyof TForm)[]) : this,
    reset(...fields: (keyof TForm)[]) : void,
    setErrors(errors: SimpleValidationErrors|ValidationErrors) : this;
    forgetError(name: string|NamedInputEvent): this;
    setError(field: keyof TForm, value: string): this;
    transform(callback: (data: FormDataType) => Record<string, unknown>) : this;
    validate(name?: string|NamedInputEvent): this;
    setValidationTimeout(duration: number): this;
    validateFiles(): this;
    submit(submitMethod?: RequestMethod|Config, submitUrl?: string, submitOptions?: any): void
    validator(): Validator;
}

export type PrecognitionFormProps<TForm extends FormDataType> = FormProps<TForm> & InertiaForm<TForm>


export const useForm = <Data extends FormDataType>(method: RequestMethod|(() => RequestMethod), url: string|(() => string), inputs: Data, config: ValidationConfig = {}): PrecognitionFormProps<Data> => {
    /**
     * The Inertia form.
     */
    const inertiaForm = useInertiaForm(inputs)

    /**
     * The Precognitive form.
     */
    const precognitiveForm = usePrecognitiveForm(method, url, inputs as Record<string, unknown>, config)

    /**
     * Setup event listeners.
     */
    precognitiveForm.validator().on('errorsChanged', () => {
        inertiaClearErrors()

        inertiaSetError(
            // @ts-expect-error
            toSimpleValidationErrors(precognitiveForm.validator().errors())
        )
    })

    /**
     * The Inertia submit function.
     */
    const inertiaSubmit = inertiaForm.submit.bind(inertiaForm)

    /**
     * The Inertia reset function.
     */
    const inertiaReset = inertiaForm.reset.bind(inertiaForm)

    /**
     * The Inertia clear errors function.
     */
    const inertiaClearErrors = inertiaForm.clearErrors.bind(inertiaForm)

    /**
     * The Inertia set error function.
     */
    const inertiaSetError = inertiaForm.setError.bind(inertiaForm)

    /**
     * The Inertia trasform function.
     */
    const inertiaTransform = inertiaForm.transform.bind(inertiaForm)

    /**
     * The transform function.
     */
    let transformer: (data: Data) => FormDataType = (data) => data

    /**
     * Patch the form.
     */
    const form = Object.assign(inertiaForm, {
        validating: precognitiveForm.validating,
        touched: precognitiveForm.touched,
        touch(name: Array<string>|string|NamedInputEvent) {
            precognitiveForm.touch(name)

            return form
        },
        valid: precognitiveForm.valid,
        invalid: precognitiveForm.invalid,
        clearErrors(...fields: (keyof Data)[]) {
            inertiaClearErrors(...fields)

            if (fields.length === 0) {
                precognitiveForm.setErrors({})
            } else {
                // @ts-expect-error
                fields.forEach(precognitiveForm.forgetError)
            }

            return form
        },
        reset(...fields: (keyof Data)[]) {
            inertiaReset(...fields)

            precognitiveForm.reset(...fields as string[])
        },
        setErrors(errors: SimpleValidationErrors|ValidationErrors) {
            precognitiveForm.setErrors(errors)

            return form
        },
        forgetError(name: string|NamedInputEvent) {
            precognitiveForm.forgetError(name)

            return form
        },
        setError(key: any, value?: any) {
            form.setErrors({
                ...inertiaForm.errors,
                ...typeof value === 'undefined'
                    ? key
                    : { [key]: value },
            })

            return form
        },
        transform(callback: (data: Data) => Record<string, unknown>) {
            inertiaTransform(callback)

            transformer = callback

            return form
        },
        validate(name?: string|NamedInputEvent) {
            precognitiveForm.setData(inertiaForm.data() as Record<string, unknown>)

            precognitiveForm.validate(name)

            return form
        },
        setValidationTimeout(duration: number) {
            precognitiveForm.setValidationTimeout(duration)

            return form
        },
        validateFiles() {
            precognitiveForm.validateFiles()

            return form
        },
        submit(submitMethod: RequestMethod|Config = {}, submitUrl?: string, submitOptions?: any): void {
            const isPatchedCall = typeof submitMethod !== 'string'

            submitOptions = isPatchedCall
                ? submitMethod
                : submitOptions

            submitUrl = isPatchedCall
                ? resolveUrl(url)
                : submitUrl!

            submitMethod = isPatchedCall
                ? resolveMethod(method)
                : submitMethod as RequestMethod

            inertiaSubmit(submitMethod, submitUrl as string, {
                ...submitOptions,
                onError: (errors: SimpleValidationErrors): any => {
                    precognitiveForm.validator().setErrors(errors)

                    if (submitOptions.onError) {
                        return submitOptions.onError(errors)
                    }
                },
            })
        },
        validator: precognitiveForm.validator,
    })

    // Due to the nature of `reactive` elements, reactivity is not inherited by
    // the patched Inertia form as we have to destructure the Precog form. We
    // can handle this by watching for changes and apply the changes manually.
    watchEffect(() => form.validating = precognitiveForm.validating)

    return form
}

I was able to build without any error and I also did some small test with PHPStorm and it seems that it can provide all the hints from Precognition and InertiaJS (See the following example):

Screenshot 2024-06-18 at 14 24 07

Please let me know if this is a good solution so I can finish with some extra tests and then I will send you a PR.

@timacdonald
Copy link
Member

Nice. This looks like a good solution if it works. Thanks!

@juanparati
Copy link
Author

@timacdonald : I created #77

@juanparati juanparati closed this Jun 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants