Sekrab Garage

Svelte Input Component

Programmatic attachment in Svelte

Svelte October 25, 26
Subscribe to Sekrab Parts newsletter.
Series:

Form Validation in Svelte

This series is dedicated to Svelte Input component, which I will attempt to automate the validation without being obtrusive to the native HTML input elements, using native JavaScript validation.
  1. 6 days ago
    Programmatic attachment in Svelte

Let's build an in-house input component with validation for Svelte: the target is to have a simple, unobtrusive solution for validation, an binding to the native input fields, using native HTML input and native Svelte binding. (this is very important).

Find this first episode on StackBlitz: Svelte Input

The outcome should be something like this

<MyComponent ...someprops someValidationRule>
<input type="text" id="something" ...extraprops required={true} bind:value={formState.value} />
</MyComponent>

So let's start with a library component, and let's name it properly. OlInput, short for Olive! Why not?

anchorThe Input component

The initial input is at least a wrapper for the text field, with at least placeholder property and a unique ID generated by Svelte.

<script lang="ts">
  interface IProps { placeholder?: string; input?: any; help?: any  };
  const { placeholder }: IProps = $props();
  
  // unique id
  const id = $props.id();

</script>
<div class="ol-field" {id}>

  {#if placeholder}
  <label class="ol-label">{placeholder}</label>
  {/if}
  <!--snippet for real input here -->
  {@render input()}

  <span class="ol-help">
	<!-- may be a snippet for help text? -->
    {@render help?.()}
  </span>

</div>

Using it:

<script lang="ts">
import OlInput from '#src/lib/input/OlInput.svelte';
let formState = $state{ address: null };
</script>

  <OlInput placeholder="Address">
      {#snippet input()}
        <input type="text" bind:value={formState.address} id="address"  />
      {/snippet}
      {#snippet help()}
        Tip about this field is optional
      {/snippet}
  </OlInput>

This does nothing but contain the look and feel. First step is passing up the id, and passing down the placeholder. To do that, we need to reference the input itself. How?

anchorFinding the projected content

This is one of the shortcomings of Svelte, the projected content is not easily found and accessible through the component. The usual way of thought is a simple DOM track:

<script lang="ts">

	let element = document.querySelectorAll('someselector');
	// probably need to be watched
	
</script>

Another way is to declare an attach, define it in the component itself:

<script lang="ts">

  const refElement = (n) => {
	  // even thoughh this is an attachment, but it actually holds the HTML element
	 console.log(n);
  }
</script>
<!-- somehere in component -->
{@render input(refElement)}

To use it:

<OlInput placeholder="Address">
	{#snippet input(ref)}
	  <input type="text" bind:value={v}  {@attach ref} />
	{/snippet}
</OlInput>

Tidying up, we can create a destroy function in refElement, we can also go as far as creating a programmatic attachment, that conceals the details in a props spread, as follows:

// create a programmatic attachment in lib/olinput
  const refElement = {
    [createAttachmentKey()]: (node: HTMLInputElement) => {
      // node is available, but be careful when referencing it
      console.log(node);
      return () => {
        // destroy
      }
    }
  };

To use

<OlInput placeholder="Address">
	{#snippet input(ref)}
	  <input type="text" bind:value={v}  {...ref} />
	{/snippet}
</OlInput>

This is the most powerful way because it will come in handy as we pass other props back to our child snippet. Let's try to place the element in a state for later:

// OlInpute.svelte
let el = $state();
const refElement = {
	[createAttachmentKey()]: (node: HTMLInputElement) => {
	  console.log(node);
	  el = node;
	  return () => {
		// destroy
		el = undefined;
	  }
	}
};

In order to stop cyclic attachments (because the body of the attachment is like an effect, it watches the changes in everything inside), we need to contain it in with "untrack"

// OlInout.svelte
let el = $state();
const refElement = {
	[createAttachmentKey()]: (node: HTMLInputElement) => {
	  // good measure, not to keep tracking
	  untrack(() => {
		el = node;
	  });
	  return () => {
		// destroy
		el = undefined;
	  };
	},
};

Now we can define placeholder directly in refElement (for now, though, it might need a bit of work to make it reactive)

// OlInpute.svelte
let el = $state();
const refElement = {
	placeholder: placeholder, // that easy
	[createAttachmentKey()]: (node: HTMLInputElement) => {
	 // ...
	},
};

But applying placeholder isn't actually a target, what we want is pass the id of the input, back to the label:

// OnInput.svelte

  let forLabel = $state('');
  const refElement = {
    [createAttachmentKey()]: (node: HTMLInputElement) => {
      untrack(() => {
        el = node;
        forLabel = node.id;
      });
      return () => {
        // destroy
        el = undefined;
      };
    },

  };
  ` in the html template:
   {#if placeholder}
	  <label class="ol-label" for={forLabel}>{placeholder}</label>
    {/if}
  `

To use

Pass id to input.
<OlInput placeholder="Address">
  {#snippet input(ref)}
    <input id="address" type="text"  {...ref} />
   {/snippet}
</OlInput>

We can make it more automated by creating a random ID in svelte component and use it. My experience dictates that this is bad practice. Keep the outside world explicitly responsible for the input ID.

anchorDefining style

One thing we can add to the refElement is the css class name:

// OlInput.svelte
const refElement = {
    class: 'ol-input',
    // ..
}

But that overwrites our original classes passed to our native input element. So we need to find a way to addClass instead of assign class. (If you try to be smart about having the css load on server, and use what Svelte recommends, go ahead, go down that rabbit hole of being overly protective for a silly thing that is just "style". I'd rather be on the simple side.)

The recommended Svelte way—by the way, is to apply this anytime you want to apply classes:

 {...ref} class={[ref.class, 'w100']}

I find this repulsive beyond endurance

// OnInput.svelte

// just add the ** class in the Attachment body
node.classList.add('ol-input');

anchorThe Attachment getters

When defining the refElement you can assign getters as properties as follows:

// OnInput.svelte
	  const refElement = {
	     get 'data-something'() { return 'another thing'; },

This is reactive, meaning it will react immediately to changes applied to the component. This will come in handy later as we apply different validation statuses to the input. Let's begin.

anchorValidation

First, the easiest validation: required. This, and the different field types is coming next episode.😴

How'd you find the "Olive" name?

  1. 6 days ago
    Programmatic attachment in Svelte
Have a comment?
Tweet sekrabbin.
Subscribe to Sekrab Parts newsletter.