DEV Community

Cover image for Programmatic attachment in Svelte
Ayyash
Ayyash

Posted on Originally published at garage.sekrab.com

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>
Enter fullscreen mode Exit fullscreen mode

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

The 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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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?

Finding 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>
Enter fullscreen mode Exit fullscreen mode

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)}
Enter fullscreen mode Exit fullscreen mode

To use it:

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

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
      }
    }
  };
Enter fullscreen mode Exit fullscreen mode

To use

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

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;
      }
    }
};
Enter fullscreen mode Exit fullscreen mode

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;
      };
    },
};
Enter fullscreen mode Exit fullscreen mode

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) => {
     // ...
    },
};
Enter fullscreen mode Exit fullscreen mode

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}
  `
Enter fullscreen mode Exit fullscreen mode

To use

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

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.

Defining style

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

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

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']}
Enter fullscreen mode Exit fullscreen mode

I find this repulsive beyond endurance

// OnInput.svelte

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

The Attachment getters

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

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

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.

Validation

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

How'd you find the "Olive" name?

Top comments (0)