Creating an inline editable component in Svelte and publishing to NPM

Uk Chukundah
7 min readJan 13, 2020

--

We previously created this component in Vue here; now we’ll explore creating it in Svelte — a nice and tiny framework which compiles components and does not require a runtime.

While squashing some bugs on and making some updates to my address book, I decided to change the input fields to the inline editable style found on Trello (show below), Google Sheets, etc.

I’d like to use this component in other apps so we’ll be making it a standalone component in this tutorial.

Since this is a single component, it’s easier testing in an online sandbox like this one on Sandbox.io instead of creating a full-fledged app.

Svelte provides a base for building shareable components and we’ll make use of it.

To start, I’ll run:

$ npx degit sveltejs/component-template svelte-inline-input
$ cd svelte-inline-input
$ npm install

In package.json, we change the name to svelte-inline-inputand rename src/Component.svelte to src/InlineInput.svelte. We also update the reference in src/index.js.

Initialize Git and create a commit.

The default state of an inline editable component is to display a static element. When the component is clicked, it transforms into an input field that accepts user input. When focus goes away from the element, it toggles back to a static element.

Our component will handle text, number, select and textarea inputs.

We’ll leave styling up to users. At the least, you should show a pointer cursor when the mouse hovers over the static element.

Number and text

<script>
import { tick } from 'svelte';
// Props
export let value = '';
export let type = 'text';
export let placeholder = '';
export let labelClasses = '';
export let inputClasses = '';
let editing = false;
let inputEl;
let label;
// Computed
$: isText = type === 'text';
$: isNumber = type === 'number';
$: if (isNumber) {
label = value === '' ? placeholder : value;
} else if (isText) {
label = value ? value : placeholder;
}
const toggle = async (_) => {
editing = !editing;
if (editing) {
await tick();
inputEl.focus();
}
};
const handleInput = (e) => {
value = isNumber ? +e.target.value : e.target.value;
};
const handleEnter = (e) => {
if (e.keyCode === 13) inputEl.blur();
};
const handleBlur = (_) => {
toggle();
};
</script>
{#if editing && (isText || isNumber)}
<input
class={inputClasses}
bind:this={inputEl}
{type}
{value}
{placeholder}
on:input={handleInput}
on:keyup={handleEnter}
on:blur={handleBlur}>
{:else}
<span
class={labelClasses}
on:click={toggle}>
{label}
</span>
{/if}

Our default state is a static element. We’ll use a span as our static element. When the span is clicked, we hide the span and show our input element.

At lines 5–9, we declare our props (i.e. variables we expect from parent components) and set their default values. Lines 11–13 declare our internal variables. Lines 16–22 contain our reactive declarations (i.e. Svelte equivalent of Vue’s computed properties).

When the span is clicked, toggle() is called which does two things. It hides the span by setting editing to true and it places the focus on our input so it can accept user input without the user needed to click on the input again. That's one of the reasons we're holding a ref to the input via inputEl; the other is to take the focus away from the input.

When focus leaves the input, we handle the blur event fired via handleBlur(). This function calls toggle() to hide the input and show the span.

We also want to toggle the component when the user presses the Enter key. We handle that via handleEnter() which just takes away focus from the input element which will cause handleBlur() to be called.

When the user changes the input value, we call handleInput(). It checks if the type prop is a number and if it is, converts the event value to a number and updates value.

If you’ve seen the Vue version, you might be wondering where emitValue() is. Since Svelte supports two-way data binding with the bind directive, emitValue() is not needed. We just need to bind to value in our parent components like so:

<InlineInput bind:value={yourVariable} />

If your variable is named value, then you could do:

<InlineInput bind:value />

The inputClasses and labelClasses props are to give the user full control over styling as we're not going to do any styling.

Our label computed prop controls what is displayed when the static element is shown. For number inputs, we only display placeholder when nothing has been input; otherwise we display the user input (this ensures that 0 is displayed). For text inputs, we display placeholder if value is empty.

On line 50, notice the {type} binding. This is shorthand for type={type}, which is equivalent to :type="type" in Vue.

That’s all that’s needed for text and number input. We’ll go ahead and create a commit here.

Textarea

<script>
// ...
// Props
// ...
export let rows = 2;
export let cols = 20;
// ...// Computed
// ...
$: isTextArea = type === 'textarea';
$: if (isNumber) {
label = value === '' ? placeholder : value;
} else if (isText || isTextArea) {
label = value ? value : placeholder;
}
// ...
</script>
{#if editing && (isText || isNumber)}
<!-- -->
{:else if editing && isTextArea}
<textarea
class={inputClasses}
bind:this={inputEl}
{placeholder}
{value}
{rows}
{cols}
on:input={handleInput}
on:blur={handleBlur} />
{:else}
<!-- -->
{/if}

In textareas, Enter is a normal input and it would be bad UX to toggle the component when Enter is pressed. That's the reason we handled Enter separately from other inputs. This way, we only have to not add the handler to the textarea, instead of modifying handleInput() to differentiate Enters from other input and textareas for number and text. This would make our function more complex and more error-prone.

The only addition is the rows and cols props to give the parent component control over sizing. They are initialized to their HTML default values if the parent component does not supply values.

That’s all that’s needed to support textareas. Let’s make a commit here.

Select

<script>
// ...
// Props
// ...
export let options = [];
// ...
let selectedIndex = options.findIndex(o => o.value === value);
// Computed
// ...
$: isSelect = type === 'select';
$: if (isNumber) {
label = value === '' ? placeholder : value;
} else if (isText || isTextArea) {
label = value ? value : placeholder;
} else { // Select
label = selectedIndex === -1 ? placeholder : options[selectedIndex].label;
}
// ...const handleChange = (e) => {
selectedIndex = placeholder ? e.target.selectedIndex - 1 : e.target.selectedIndex;
value = options[selectedIndex].value;
};
</script>
{#if editing && (isText || isNumber)}
<!-- -->
{:else if editing && isTextArea}
<!-- -->
{:else if editing && isSelect}
<select
class={inputClasses}
bind:this={inputEl}
{value}
on:blur={handleBlur}>
{#if placeholder}
<option selected value disabled>{placeholder}</option>
{/if}
{#each options as { label, value }, i}
<option
key={i}
{value}>
{label}
</option>
{/each}
</select>
{:else}
<span
class={labelClasses}
on:click={toggle}>
{label}
<slot name="selectCaret">
{#if isSelect}
<span>&#9660;</span>
{/if}
</slot>
</span>
{/if}

For selects we need an array of options which is supplied through the options prop, which is an array of objects that have label and value keys.

In the span element we show a caret to indicate that it’s a select. We added the caret in a named slot to enable the user change the default.

In our label computed prop, for selects we display the label of the currently selected value, if any or the supplied placeholder.

When a selection is made, we get the index of the selected option (which is one less if a placeholder was supplied). We then update value.

With that, we have added support for select inputs. As we've done previously, we make a commit here.

Publishing to NPM

Before we publish, we need to a README with helpful text and build the component.

To build the component, run:

$ npm run build

In this case, we only build when publishing the component using the prepublishOnly hook in our package.json scripts declaration.

Our README content:

# svelte-inline-input> An inline editable input component for Svelte.Displays as text and becomes editable by clicking or tapping.## Installation```js
npm install svelte-inline-input
```
### Browser```html
<script type="text/javascript" src="https://unpkg.com/svelte-inline-input/dist/index.mjs"></script>
```
### Module```js
import InlineInput from 'svelte-inline-input';
```
## UsageOnce installed, it can be used in a template as:```html
<InlineInput bind:value />
```
See the props table below for the available options.### Props| Property | Type | Description | Default |
|:--|:--|:--|:--|
| type | string | The input type. Could be text, number, textarea or select | text |
| placeholder | string | Text to be shown as a placeholder while there is no input | empty string |
| labelClasses | string | CSS classes for the label element | empty string |
| inputClasses | string | CSS classes for the input element | empty string |
| rows | integer | Textarea rows | 2 |
| cols | integer | Textarea columns | 20 |
| options | array | Provides the options for selects. Each object should have the format `{label: x, value: x}` | [] |
## License[MIT](http://opensource.org/licenses/MIT)

We then add some more information in our package.json that are needed to publish the component. After that, we commit the changes.

To publish, I first login to my NPM account with:

$ npm login

Then, from the root directory I run:

$ npm publish

After successfully publishing, we can use our component in Node.js projects by installing it with:

$ npm install svelte-inline-input

or in the browser using https://unpkg.com/svelte-inline-input.

The GitHub repository can be found here.

--

--

Uk Chukundah

Software engineer & accountant. I ruminate and regurgitate here.