error: 'type' attribute cannot be dynamic if input uses two-way binding
Asked Answered
P

9

47

I was trying to create an Input component for my project. I want to set type attribute dyamically on input element

But when i set type attribute dynamically on input i get error saying 'type' attribute cannot be dynamic if input uses two-way binding

So is there any workaround for this such that i can set type attribute dynamically without loosing two way binding

Input.svelte

<script>
  export let placeholder = "";
  export let label = "";
  export let description = "";
  export let value = "";
  export let type = "text";
</script>

<div class="container">
    <label>{label}</label>
    <input {type} bind:value {placeholder} />
    <p>{description}</p>
</div>
Pederasty answered 7/8, 2019 at 10:53 Comment(0)
M
80

The reason type must be static with two-way binding is that the code Svelte generates is different for different kinds of input. For example, number and range inputs must have their values coerced to a number, some inputs need change event listeners instead of input events or vice versa, and so on.

But you can manually do the same thing the generated code would have been doing — adding an event listener that reflects state:

<script>
  export let placeholder = "";
  export let label = "";
  export let description = "";
  export let value = "";
  export let type = "text";

  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;
  };
</script>

<div class="container">
    <label>{label}</label>
    <input {type} {value} {placeholder} on:input={handleInput} />
    <p>{description}</p>
</div>
Maillot answered 7/8, 2019 at 11:47 Comment(5)
Hi Rich, value coercion not working - I still get string type instead of number for [type=number] inputs. I used console.log{ value: new FormData(form).get('inputname') } inside <form on:submit> event. Thanks for this wonderful Svelte framework btw!Weide
UPDATE: Rich, actually your solution seems to work. Looks like it's the form's behavior that converts all form data to string types on submit.Weide
multiple select is not working by this way - on:input always has single value onlyWaxbill
Which type are you using to type the event argument of handleInput(e: ???)?Epiboly
handleInput(e: Event) but if it's an input field you need to cast it over to an InputEvent to see the data there.Jeseniajesh
M
24

If you want to still use bind:value and SSR, you can do something like this:

<script>
  export let type = "text";
  export let value = "";
</script>

<input bind:value {...{ type }} />
Mellie answered 31/1, 2023 at 14:27 Comment(3)
The most elegant answer of all!Often
I do not understand this one...what is happening here?Jotunheim
@Jotunheim { type } creates an object like { type: "text" }. ... destructures the attributes, that is, applies every property found in the object.Mellie
E
22

use a function in your component to set the node type:

Input.svelte:

<script>
    export let type = 'text'
    export let label
    export let value

    function typeAction(node) {
        node.type = type;
    }
</script>

<div class="space-y-1">
    <label>{label}</label>

    <input use:typeAction bind:value class="rounded-md w-full">

    <p class="text-sm text-red-600">errors</p>
</div>

Form.svelte:

    <form on:submit|preventDefault={login}>
        <Input type="email" label="Email" bind:value={values.email}/>
        <Input type="password" label="Password" bind:value={values.password}/>

        <Button type="submit" label="Login"/>
    </form>
Ecdysiast answered 20/2, 2022 at 10:22 Comment(2)
Very elegant, are there any downsides or considerations to this approach?Cercaria
We stopped doing it this way, beacuse a JS error could prevent typeAction from running which could lead to passwords being rendered in plain text. If you're always expecting strings then I think https://mcmap.net/q/360003/-error-39-type-39-attribute-cannot-be-dynamic-if-input-uses-two-way-binding is much bette, it supports SSR, won't expose passwords and is far more elegant.Jody
L
11

One other possible solution which isn't very elegant or DRY, but doesn't require simulating the core Svelte functionality yourself is to simply branch on type and render different inputs accordingly:

<script>
  export let name;
  export let value;
  export let type = 'text';
</script>


{#if type === 'password'}
  <input
    type="password"
    id={name}
    {name}
    on:change
    on:blur
    bind:value
  />
{:else if type === 'email'}
  <input
    type="email"
    id={name}
    {name}
    on:change
    on:blur
    bind:value
  />
{:else if type === 'number'}
  <input
    type="number"
    id={name}
    {name}
    on:change
    on:blur
    bind:value
  />
{:else if type === 'date'}
  <input
    type="date"
    id={name}
    {name}
    on:change
    on:blur
    bind:value
  />
{:else}
  <input
    type="text"
    id={name}
    {name}
    on:change
    on:blur
    bind:value
  />
{/if}
Leventis answered 24/9, 2021 at 10:49 Comment(0)
E
2

What about something like that?

export let type: 'email' | 'text' | 'password' | 'number' = 'text'

let ref: HTMLInputElement

onMount(() => {
  if (ref) {
    ref.type = type
  }
})

and then

<input bind:this={ref} />

OR

export let type: 'email' | 'text' | 'password' | 'number' = 'text'

const ref = (node: HTMLInputElement) => {
  node.type = type
}

and then

<input use:ref />
Eyepiece answered 3/6, 2021 at 18:5 Comment(1)
I recommend against rhetoric questions in answers. They risk being misunderstood as not an answer at all. You are trying to answer the question at the top of this page, aren't you? Otherwise please delete this post.Saturant
S
1
<!-- InputField.svelte -->
<script>
  export let placeholder = "";
  export let label = "";
  export let description = "";
  export let value = "";
  export let type = "text";

  const handleInputType = (e) => {
    e.target.type = type;
  };
</script>

<div class="container">
    <label>{label}</label>
    <input {value} {placeholder} on:input={handleInputType} />
    <p>{description}</p>
</div>

I came across with this, to simplify, what I did was like above, basically I removed the {type} inside the input and on input I just modify the target type, since this is a component, so on your parent/route svelte file.

<InputField type="email" bind:value={emailValue} />
Silures answered 20/11, 2020 at 2:46 Comment(1)
With this approach the value will always be of type string, when we specify type=number or type=range in svelte we expects the binded value shoud be of type number. In this approach we would also need to handle type changing from string to numberPederasty
A
0

A simple workaround: get the element by id and use element.setAttribute(attributename, attributevalue) to set the type

<script>
  export let id = "";
  export let placeholder = "";
  export let label = "";
  export let description = "";
  export let value = "";
  export let type = "text";
</script>

<div class="container">
    <label>{label}</label>
    <input {type} bind:value {placeholder}
    on:focus="{() => {
       console.log(type)
       let inpt =    document.getElementById(id)
       inpt.setAttribute("type", type)
       console.log(inpt)
    }}"/>
    <p>{description}</p>
</div>

Hope it helps :-)

Aubreir answered 26/3, 2020 at 9:24 Comment(0)
R
0

Export input to a standalone component, and then handle the input manually instead of binding, then you can set dynamic type. and then you can bind the value to the component.

See example here: https://svelte.dev/repl/77f694a1851d464b85b382f4f152cb8e?version=3.46.4

Rafi answered 30/3, 2022 at 14:13 Comment(0)
C
0

One issue with some of these suggestions such as using actions or the onMount method is that it doesn't apply the type during the server-side render, which means if the user has javascript disabled their password field will be a regular text input.

The workaround I found is actually really simple:

<script>
  export let placeholder = "";
  export let label = "";
  export let description = "";
  export let value = "";
  export let type = "text";
  const inputProperties = { placeholder, type };
</script>

<div class="container">
    <label>{label}</label>
    <input {...inputProperties} bind:value />
    <p>{description}</p>
</div>

tbh I'm not 100% sure why this works but I think the destructuring is preventing the type attribute from becoming dynamic, because it isn't linked to the type variable.

Carpentry answered 1/6, 2023 at 3:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.