Skip to content

Provide a way to dynamically set input type (with or without two way binding) #3921

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

Closed
soullivaneuh opened this issue Nov 13, 2019 · 18 comments · Fixed by #10608
Closed

Provide a way to dynamically set input type (with or without two way binding) #3921

soullivaneuh opened this issue Nov 13, 2019 · 18 comments · Fixed by #10608

Comments

@soullivaneuh
Copy link

Is your feature request related to a problem? Please describe.

I am currently making a Input.svelte component to manage common logic:

<script>
  export let id;
  export let label;
  export let type = 'text';
  export let value = '';
  export let revealed = false;

  const revealToggle = (event) => {
    const input = event.currentTarget.previousElementSibling;

    if (!input) {
      return;
    }

    input.type = input.type === 'password' ? 'text' : 'password';
    revealed = !revealed;
  }
</script>

<style>
  button:focus {
    outline: 0 !important;
  }
</style>

<label class="block mb-2" for="{id}">{label}</label>
<div class="flex mb-4">
  {#if type === 'password'}
    <input
      {id}
      type="password"
      placeholder="{label}"
      bind:value
      class="rounded-r-none border-r-0"
    >
    <button
      title="Afficher le mot de passe"
      type="button"
      class="border border-gray-400 rounded rounded-l-none px-3 text-gray-500"
      on:click={revealToggle}
    >
      <i class="fa fa-fw {revealed ? 'fa-eye' : 'fa-eye-slash'}"></i>
    </button>
  {:else}
    <input {id} type="text" placeholder="{label}" bind:value>
  {/if}
</div>

It is currently not possible to assign a prop to the type html attribute as you well answered here: https://stackoverflow.com/a/57393751/1731473

Otherwise, it will produce this error:

Error: ValidationError: 'type' attribute cannot be dynamic if input uses two-way binding

But it is still possible to do "dynamic" type attribute with a basic if/else logic as you can see in my example.

So I don't understand why things are not simpler. Nowadays, with html5, we have a ton of "text similar input" like date, email, or tel: https://www.w3schools.com/html/html_form_input_types.asp

I would like to avoid to make a if forest if I only need to change the type attribute.

Also, maybe a good place for an another issue, but I can't find an easier way to pass input related directly to the right DOM element in my component without declaring each of them manually (min, max, required etc...), but I don't know if this is doable.

Describe the solution you'd like

Allow to do something like this:

<input {id} {type} placeholder="{label}" bind:value>

With or without two-way binding. The goal is to quickly define what type I want from my input component:

<script>
  import Input from './Input.svelte';
</script>

<Input type="text" ... />

Describe alternatives you've considered

The only alternative I found is what you have on my example.

Maybe it's a design issue from my own. In that case, some good advices on this issue and the documentation would be very welcome. 👍

How important is this feature to you?

It is not very urgent, but I think this is quite useful in order to avoid code repetition on big projects.

@rise2semi
Copy link

@soullivaneuh I've created an example with your component: https://svelte.dev/repl/b220a2c7855a47fcbb713648d33aba49?version=3.14.1

You can use a dynamic type, but you need to handle two-way data binding by yourself via setting a value {value} and listening to changes on:input.
This is exactly what @Rich-Harris proposed in an answer to your question on Stackoverflow.

Moreover, you can write a component which handles all kind of input types: https://svelte.dev/repl/31ee5896ee5c4364bf6d73538c895bd5?version=3.14.1
But, again, handling two-way data binding will be on your side. Different types emit different events, so you will have to subscribe to all required events to properly retrieve the input's value.

@Conduitry
Copy link
Member

The proposal here, I guess, is to allow dynamic type values on inputs with bind:value, and to assume that the type will always be set to a 'normal' text-like one. This is something I've definitely considered before, but I can't find an issue right now where it was discussed. I suppose the question is whether that gotcha (which would need to be documented) is better or worse than having to manually put a on:input event handler on your input. I don't know the answer to that.

@soullivaneuh
Copy link
Author

Thanks for the samples @rise2semi, I indeed didn't understand that way.

Still, it's kinda workaround to me. It would be great to have this natively managed by Svelte, this is why I opened this issue. 👍

At least, the current possible implementation is a good candidate for the official documentation/tutorial/examples as this seems to be a quite common case.

@TIOvOIT
Copy link

TIOvOIT commented Apr 2, 2020

I also encountered this problem, so I solved it:

Input component:

<script>
  import { onMount } from "svelte";
	
  export let inputType= "text";
  export let value = "";
	
  let inputElement;
  onMount(() => {
    inputElement.type = inputType;
  });
</script>

<input
  on:keyup
  on:change
  bind:value 
  bind:this={inputElement}/>

External call:

<script>
	import Input from "./Input.svelte";
	let value = "";
	let text = "";
	function foo(){
		if(value.length < 4){
			text="Passwod too short";
		}else{
			text="";
		}
	}
</script>

<Input bind:value inputType='password' on:change={foo}/>
<p>{text}</p>

I don't know if this is really correct,superficially works.

@paulschreiber
Copy link

I would also like to see this, for all of the reasons @soullivaneuh listed.

@finnbear
Copy link

finnbear commented Nov 8, 2020

If the only issue is the difference between number-based inputs (range, number, etc.), text-based (text, email, password, etc.), and [insert type here]-based inputs, the API should make it possible to switch between all input types that all produce the same type for value. My use case is to create a <TextInput/> element that can either be a text, email, or password input.

One option would be to drop the compile-time dynamic-input-type check entirely and let the user run into an error if they end up changing the type to something incompatible. A warning about this could be added to the documentation.

Another option would be to generate slightly less efficient code i.e. change the automatically generated handler any time the type changes.

Another option (which I'm not sure is possible) is to make the type input never reactive, instead using the value provided at initialization.

@jhechtf
Copy link

jhechtf commented Nov 20, 2020

I want to thank the svelte team for making it so that numbers-typed inputs return numbers.

But in this particular instance, if it's causing an issue like this then I'm 100% for letting developers deal with the conversion of number-based types, which is what they would have to do using vanilla JS anyway, and I'm pretty sure even React just has you (the developer) handle the numbers yourself.

I'm interested in checking out the code responsible, perhaps there's a way for the numbered returns and also a way to dynamically set the type on inputs without needing any of the above work arounds?

@finnbear
Copy link

@jhechtf the dynamic thing might the perfect solution. If svelte detects that an input's type is two-way bound, it should simply generate an input handler that checks the type each time.

@stale
Copy link

stale bot commented Jun 26, 2021

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@vince83110
Copy link

vince83110 commented Jul 15, 2021

Solution found on youtube for this problem, and works correctly in 2021 :

export let type = 'text';

const setType = (node) => {
  node.type = type;
};

<input
  bind:value
  use:setType
/>

@peterboyer
Copy link

I encountered this same problem yesterday and think I found a reasonable workaround pattern for those who want to implement their own "Input"/"Field" components.

Problem: Having a Field.svelte component that accepts a value and also type to conditionally render different labels/buttons etc, you would need to pass the now "dynamic" type prop to a child <input> component and also attempt to bind:value to the input — but this causes the error 'type' attribute cannot be dynamic if input uses two-way binding.

Solution: rather than have a component like Field receive all it's input props (like value and type) to be forwarded to an internally defined input:

// parent.svelte

// problem: dynamic type attribute error
<Field id="email" label="Email" type="password" bind:value={values.password} />

you could instead move the declaration of the input (with all it's props) into the parent component as a slotted child component that renders into a slot:

// parent.svelte

// solution: no error, no forwarding of props needed etc.
<Field id="password" label="Password">
  <input id="password" type="password" bind:value={values.password} />
</Field>

// field.svelte

<div class="flex flex-col space-y-1">
  {#if label || $$slots.label}
    <div class="flex flex-row items-center justify-between">
      <label for={id}>
        <slot name="label">{label}</slot>
      </label>
      <slot name="aside" />
    </div>
  {/if}
  <slot />
</div>

This allows me to still standardise my fields and how they render, and avoid dealing with cumbersome management of input props and forwarding event handlers etc.

When it comes to styles, rather than pass class names to given slotted input elements, define the styles as css in the Field/Input svelte component (or in the case of tailwindcss, as @applyed styles targeting input for my whole app).

In the original case of wanting to render additional content based on type (such as in the case of adding a toggle visibility button for a password field), rather than relying on type to be based to your Field/Input component, substitute it with another slot, named similar to aside or input-after to target your desired button:

// field.svelte
<div> // flex row
  <slot />
  <slot name="input-after" />
</div>

// parent.svelte
<Field id="password" label="Password">
  <input id="password" type="password" bind:value={values.password} />
  <button slot="input-after" on:click={...}>Show/Hide</button>
</Field>

Or even wrap up such logic into another component like FieldPassword that accepts hidden: boolean prop to control the visibility of its own defined button into the input-after slot.

This has worked for me so far, however I'm still just learning about svelte (and love it so far) and would be open to seeing how others approach what I can presume is a fairly common Input/Field component problem. :)

@kilianso
Copy link

kilianso commented Oct 13, 2021

@vince83110 your answer is a neat trick!

Just one remark: Most likely you have custom stylings on your inputs based on type e.g for type="checkbox" and another one for radio and text and so on. So not having a type on these elements causes a layout glitch with unstyled inputs until setType fires and styling gets applied. One way to solve that would be applying a dynamically created class to the input based on the type and style it accordingly. Like: class:"input--{type}" with the same styles as fortype={type}

@eden-omb
Copy link

eden-omb commented Feb 5, 2022

You can also get around this is just to do <input {...{ type: 'text' }}>.

@lacherogwu
Copy link

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

@risalfajar
Copy link

risalfajar commented Oct 1, 2022

All the solution/workaround proposed above will always return a string. Is there any solution that can return number when type=number?

Because numeric inputs should return a number, like in Svelte Tutorial

Related issue:
themesberg/flowbite-svelte#344

@elitan
Copy link

elitan commented May 10, 2023

You can also get around this is just to do <input {...{ type: 'text' }}>.

What's the downside of using this approach?

@Skinner927
Copy link

You can also get around this is just to do <input {...{ type: 'text' }}>.

What's the downside of using this approach?

If you do <input {...{ type: 'number' }} bind:value={foo} /> then foo will be a string instead of a number

fpmirabile added a commit to uade-psg-trading/psg-trading-app that referenced this issue May 14, 2023
@HappyLDE
Copy link

Is this creating any problems on the compilation side other than returning a string instead of number ? Specially on Svelte 4?

<input {...{ type }} {name} bind:value />

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.