Creating a themeable Vue component with readonly mode and keyboard support
Tags: vue
Published: 2019-12-16 10:27:29

I have this project I'm working on that needed a checkbox.
I didn't use any 3rd party library as I wanted this component to be styled and created from scratch.

# Requirements
So, here are the requirements I've drafted for this component:
1. Created from scratch - no 3rd party
2. Have a readonly mode where clicks are ignored
3. Can be color themed - with different look for readonly mode.
4. Keyboard support - the user must be able to *tab* to this component (unless it's in readonly mode) and use *space* to toggle it (again, unless in readonly mode)
5. Simple usage - replace the native checkbox with minimal adaptations
6. Support a `label` element pointing to this component. When the label is clicked, the checkbox should be toggled (this is the behavior of the native checkbox)

# Design
Let's look at the requirements and figure out a solution.

Our component's template will be constructed of a span that contains an svg element. 
We use svg for two reasons:
1. It is color-neutral, so we can adapt it to any theme easily.
2. It is embedded in our source, no need for an extra png file.

This covers requirements **#1** and **#3**.

Native checkboxes actually *don't have* a readonly state. So here we're free to define any API we want. I've decided to go with this:
* If a `v-model` attribute is defined - the checkbox will behave normally
* If a `checked` attribute is used instead - the checkbox will be readonly.

This also goes hand in hand with the Vue philosophy of not changing component props from the inside (as `checked` is provided as prop).

This covers requirements **#2** and **#5**.

For keyboard support we'll need two things:
1. Specify a `tabindex=0` attribute on the template, but only if we're not in readonly mode.
2. Handle spacebar keypress to toggle our state

This covers requirement **#4**

The last requirement is a bit more tricky, but doable.

When mounted, we'll look for a label targeting our component's id and add an onclick handler on it.

Of course we'll also need to remove the handler before destroy is called.

And that takes care of requirement **#6** and we're done with the design.

# Implementation

First, let's take a look at the usage of such a component.

## Read/write usage
```html
 <label for="test1">click this text to activate the checkbox </label>
 <CheckBox v-model="cb1" id="test1"/>
```

## Read-only usage
```html
<CheckBox :checked="cb2"/>
```

## Styling/Theming
```scss
.check-box {
  border: 1px solid darkgray;
  &.readonly {
    border: none;
  }
  .check-mark {
    fill: $primary;
  }
  &:focus {
    outline: black auto 2px;
  }
}
```

Now let's look at the different parts of our component:

## Template

We'll look at important parts of the template.
First up is the root element:
Here we handle the interaction, add keyboard support and add theme support for the readonly state.
```jsx
<span
    class="check-box"
    @click="notifyToggle()"
    :tabindex="isReadOnly ? '': 0"
    @keypress.space="notifyToggle()"
    :class="{readonly: isReadOnly}"
  >
...

```
Inside this span, we'll have two `v-if`'d spans, the first displays the checkmark and contains the checkmark svg, the second is a placeholder with an empty svg, to maintain the size of the outer span:
```html
    <span v-if="value || checked">
      <svg
        class="check-mark" xmlns="http://www.w3.org/2000/svg"
        preserveAspectRatio="xMidYMid meet" viewBox="0 0 40 40"
        :width="size" :height="size"
      >
        <!--created with vectr.com (it's their built-in check-mark shape)-->
        <path
          d="M21.15 31.19L21.15 31.19L13.74 40L0 23.69L7.42 14.88L13.73 22.39L32.58 0L40 8.81L21.15 31.19Z"
        ></path>
      </svg>
    </span>
    <span v-else>
      <svg xmlns="http://www.w3.org/2000/svg" :width="size" :height="size">
        <!--an empty placeholder for theming purposes -->
      </svg>
    </span>

```

## Toggle and read-only support
```javascript
props: {
    checked: {
      // for readonly access
      type: Boolean,
      default: undefined // so that we can check if this property was provided
    },
    value: Boolean, // for v-model usage
...
  methods: {
    notifyToggle() {
      this.$emit("input", !this.value);
    }
  },
  computed: {
    isReadOnly() {
      return this.checked !== undefined;
    }
  },
...
```

## External label support
```javascript
props: {
...
    id: String, // for tracking down the label that activates this checkbox
...
},
...
  mounted() {
    if (this.id) {
      this.label = document.querySelector(`label[for="${this.id}"]`);
      if (this.label && this.label.onclick === null) {
        this.label.onclick = () => this.notifyToggle();
      } else {
        this.label = null;
      }
    }
  },
  beforeDestroy() {
    if (this.label) {
      this.label.onclick = null;
    }
  }
```
# Summary

That's it. We've got a fully functional, theme-able checkbox with readonly option and keyboard support.

The full code, alongside a demo, can be found here:
https://codesandbox.io/s/vue-checkbox-vkssq

*Code with love,*
*Lilo*