felte icon indicating copy to clipboard operation
felte copied to clipboard

Reset doesn't update the value prop on a custom component

Open akkie opened this issue 3 years ago • 4 comments

Describe the bug

I have a custom input field which uses the same logic for the input field to handle the label as: https://smeltejs.com/components/text-fields

The issue that I have is, that when I call the reset function on the context, it will reset the value, but the value prop on the component itself is not changed. The component needs to react on value changes, to set the label back. But instead the label is still minimized at the top of the field. It's a little bit strange, because the value itself is reset in the text field. The problem is only that the value prop gets not updated.

A workaround is to use the form data directly and bind it to the value of the field:

const { form: passwordForm, data: passwordData } = createForm<PasswordForm>({
  ...
});

$: password = getValue($passwordData, (d) => d.password);

<form use:passwordForm>        
    <PasswordField
        id="password"
        name="password"
        label={$LL.Form.Fields.Password()}
        value={password}
    />
</form>

With this the value prop on the input component gets updated.

Which package/s are you using?

felte (Svelte), @felte/reporter-svelte, @felte/validator-yup

Environment

  • OS: MacOs
  • Browser: Brave
  • Version: 1.2.6

To reproduce

No response

Small reproduction example

No response

Screenshots

No response

Additional context

No response

akkie avatar Oct 23 '22 10:10 akkie

Hey! When you say "custom component" do you mean something built with createField? If so, it does accept an onFormReset function to handle situations like this.

pablo-abc avatar Oct 24 '22 21:10 pablo-abc

Hi, with custom component I mean a svelte component like:

<script lang="ts">
    import { fade } from 'svelte/transition';
    import { ValidationMessage } from '@felte/reporter-svelte';
    import { AlertCircle } from 'lucide-svelte';

    export let id: string;
    export let name: string;
    export let type = 'text';
    export let label: string;
    export let value = null;
    export let maxlength: number = undefined;
    export let disabled = false;

    let focused = false;
    function toggleFocused() {
        focused = !focused;
    }

    // https://stackoverflow.com/a/57393751/2153190
    const handleInput = (e) => {
        // in here, you can switch on type and implement
        // whatever behaviour you need
        value = type.match(/^(number|range)$/) ? +e.target.value : e.target.value;
    };

    $: labelOnTop = focused || value || value === 0;
</script>

<div class="flex relative w-full h-12">
    <input
        aria-label={label}
        on:focus={toggleFocused}
        on:blur={toggleFocused}
        on:input={handleInput}
        {id}
        {name}
        {type}
        {maxlength}
        {disabled}
        {value}
        class="w-full input input-bordered focus:outline-none pt-4"
        class:pr-12={$$slots.rightIcon != null}
    />
    <label
        for={id}
        class="label absolute left-3 top-1 right-3 cursor-text pointer-events-none label-transition text-base-content/70"
        class:label-top={labelOnTop}
        class:text-xs={labelOnTop}
    >
        <span class:truncate={!labelOnTop}>{label}</span>
    </label>
    {#if $$slots.rightIcon != null}
        <div class="absolute right-0 h-12 w-12"><slot name="rightIcon" /></div>
    {/if}
</div>

<ValidationMessage for={id} let:messages={message}>
    {#if message}
        <div transition:fade|local class="flex text-error text-sm">
            <span class="pt-[0.4rem] pr-1"><AlertCircle size={15} /></span>
            <p class="pt-1">{message}</p>
        </div>
    {/if}
</ValidationMessage>

<style>
    /* Disable background color added by WebKit for autofilled fields */
    input:-webkit-autofill,
    input:-webkit-autofill:focus {
        transition: background-color 600000s 0s, color 600000s 0s;
    }

    /*
    Override default style for hidden fields so that they have the same background as the normal field.
    The default DaisyUI style uses the base-200 style. To distinguish between the normal field we use a
    border that has a opacity of 0.1 instead of 0.2. The font color uses also an opacity of 0.6.
    */
    input[disabled] {
        --tw-border-opacity: 0.1;
        --tw-bg-opacity: 1;
        --tw-text-opacity: 0.6;
        border-color: hsl(var(--bc, var(--bc)) / var(--tw-border-opacity));
        background-color: hsl(var(--b1, var(--b1)) / var(--tw-bg-opacity));
        color: hsl(var(--bc, var(--bc)) / var(--tw-text-opacity));
    }

    .label-top {
        line-height: 0.05;
    }
    .label-transition {
        transition: font-size 0.05s, line-height 0.1s;
    }
    :global(label.text-xs) {
        font-size: 0.7rem;
    }
</style>

The form is created with the createForm helper.

akkie avatar Oct 25 '22 09:10 akkie

Ah. I think I see where the issue comes from. Yeah, while Felte does reset the value of the input itself (in HTML), browser's don't dispatch any event when a value changes programatically. So there's no way for Svelte to know the value has changed.

Assigning the value as you mentioned before works, but for ergonomics maybe creating the component with createField and implementing onFormReset solves your issue?

pablo-abc avatar Oct 31 '22 22:10 pablo-abc

I tried your suggestion, but it doesn't work. Maybe I have done something wrong.

Here is the update component with createField:

<script lang="ts">
    import { fade } from 'svelte/transition';
    import { ValidationMessage } from '@felte/reporter-svelte';
    import { AlertCircle } from 'lucide-svelte';
    import { createField } from 'felte';

    export let id: string;
    export let name: string;
    export let type = 'text';
    export let label: string;
    export let value = null;
    export let maxlength: number = undefined;
    export let disabled = false;

    let focused = false;

    const { field, onInput, onBlur } = createField(name, {
        onFormReset: () => {
            console.log('onFormReset');
            focused = false;
        }
    });

    function toggleFocused() {
        focused = !focused;
    }

    // https://stackoverflow.com/a/57393751/2153190
    const handleInput = (e) => {
        // in here, you can switch on type and implement
        // whatever behaviour you need
        value = type.match(/^(number|range)$/) ? +e.target.value : e.target.value;

        onInput(value);
    };

    const handleBlur = () => {
        toggleFocused();
        onBlur();
    };

    $: labelOnTop = focused || value || value === 0;
</script>

<div class="flex relative w-full h-12">
    <input
        use:field
        on:focus={toggleFocused}
        on:blur={handleBlur}
        on:input={handleInput}
        aria-label={label}
        {id}
        {name}
        {type}
        {maxlength}
        {disabled}
        {value}
        class="w-full input input-bordered focus:outline-none pt-4"
        class:pr-12={$$slots.rightIcon != null}
    />
    <label
        for={id}
        class="label absolute left-3 top-1 right-3 cursor-text pointer-events-none label-transition text-base-content/70"
        class:label-top={labelOnTop}
        class:text-xs={labelOnTop}
    >
        <span class:truncate={!labelOnTop}>{label}</span>
    </label>
    {#if $$slots.rightIcon != null}
        <div class="absolute right-0 h-12 w-12"><slot name="rightIcon" /></div>
    {/if}
</div>

<ValidationMessage for={id} let:messages={message}>
    {#if message}
        <div transition:fade|local class="flex text-error text-sm">
            <span class="pt-[0.4rem] pr-1"><AlertCircle size={15} /></span>
            <p class="pt-1">{message}</p>
        </div>
    {/if}
</ValidationMessage>

<style>
    /* Disable background color added by WebKit for autofilled fields */
    input:-webkit-autofill,
    input:-webkit-autofill:focus {
        transition: background-color 600000s 0s, color 600000s 0s;
    }

    /*
    Override default style for hidden fields so that they have the same background as the normal field.
    The default DaisyUI style uses the base-200 style. To distinguish between the normal field we use a
    border that has a opacity of 0.1 instead of 0.2. The font color uses also an opacity of 0.6.
    */
    input[disabled] {
        --tw-border-opacity: 0.1;
        --tw-bg-opacity: 1;
        --tw-text-opacity: 0.6;
        border-color: hsl(var(--bc, var(--bc)) / var(--tw-border-opacity));
        background-color: hsl(var(--b1, var(--b1)) / var(--tw-bg-opacity));
        color: hsl(var(--bc, var(--bc)) / var(--tw-text-opacity));
    }

    .label-top {
        line-height: 0.05;
    }
    .label-transition {
        transition: font-size 0.05s, line-height 0.1s;
    }
    :global(label.text-xs) {
        font-size: 0.7rem;
    }
</style>

With this code I get the following error:

create-field.ts:71 Uncaught RangeError: Maximum call stack size exceeded.
    at dispatchEvent (create-field.ts:71:13)
    at onInput (create-field.ts:144:5)
    at HTMLInputElement.handleInput (InputField.svelte:34:9)
    at dispatchEvent (create-field.ts:71:13)
    at onInput (create-field.ts:144:5)
    at HTMLInputElement.handleInput (InputField.svelte:34:9)
    at dispatchEvent (create-field.ts:71:13)
    at onInput (create-field.ts:144:5)
    at HTMLInputElement.handleInput (InputField.svelte:34:9)
    at dispatchEvent (create-field.ts:71:13)

I can fix that by removing onInput(value);. But after removing the part, the onFormReset function will not be called.

Do you have an idea?

akkie avatar Nov 02 '22 17:11 akkie