Skip to content

Using state in templates

Okku edited this page Jun 24, 2021 · 3 revisions

When you insert reactive entities (ReactiveValues, ReactiveArrays) into a template, the template will automatically synchronize with the state. Additionally, when assigning state to value, prop:value, prop:valueAsNumber, or prop:valueAsDate, the state will be updated when user input comes in.

Here's an example:

const username = reactive("");
const initials = computed(() => 
  username.value
  .trim()
  .split(/\s/)
  .filter(Boolean)
  .map(([initial]) => initial + ". ")
  .join("")
);

const template = html`
  <input type="text" placeholder="Type your name…" prop:value=${username} />
  Initials: ${initials}
`;

👉 View on Codepen

In this example, the user can enter their name in the text field, and the initials next to the field will be automatically updated as the user types. Note how the transformation from the source state (username) to derived state (initials) is wrapped in computed(). Whenever you want to compute new reactive values from existing ones, you need to wrap them in computed() — otherwise the library won't have any way of knowing how to track changes. See documentation on computed() for more details.

Binding state

By default, the library has two-way data binding. However, you can opt out of this behavior. reactive.readonly gives you a readonly version of the reactive entity, and the recipient won't be able to update it. You can also unbox the reactive value using reactive.value, and with it, there will be no data binding at all, because the recipient won't know it was ever reactive.

const state = reactive(0);

const template = html`
  <!-- two-way data binding -->
  <input type="number" prop:valueAsNumber=${state} />

  <!-- one-way data binding -->
  <input type="number" prop:valueAsNumber=${state.readonly} />

  <!-- no data binding, pass as value -->
  <input type="number" prop:valueAsNumber=${state.value} />

  <!-- no data binding, pass as reactive -->
  <custom-number-input prop:valueAsNumber=${value.pass} />

  <!-- no data binding, pass as readonly reactive -->
  <custom-number-input prop:valueAsNumber=${value.readonly.pass} />
`;

When you assign a reactive value to a property or attribute, it will be bound to it, and it will receive its contained value. In other words, the first example above will set the valueAsNumber property on the input element to the current numeric value held by the state ReactiveValue. To pass the ReactiveValue to an element as-is, without any binding, you can use the pass property on ReadonlyReactiveValue. This can be useful if the component expects a reactive value as a property.

Computing concatenated strings

As mentioned in the Templating article, partially applied attribute values are not allowed, and you will instead need to compute a new reactive string if you wish to concatenate several reactive strings and/or regular strings.

const favoriteColor = reactive("#ffffff");

const template = html`
  <div style=${computed`background-color: ${favoriteColor}`}>
    What's your favorite color? <br />
    <input type="color" prop:value=${favoriteColor} />
  </div>
`;

👉 View on CodePen

Arrays

In addition to inserting singular values into templates, you may also insert an Array or a ReactiveArray. Each item in the array will be inserted into the template. Naturally, in the case of ReactiveArrays, the DOM will be kept up to date with changes to the array.

Here's an example of a minimal TODO list app utilizing reactive arrays:

const tasks = reactive([
  {
    done: true,
    name: "Pet the cat",
  },
  {
    done: false,
    name: "Travel the world",
  },
  {
    done: false,
    name: "Catch all Pokémon",
  },
]);

const template = html`
  <h1>TODO</h1>
  <ul>
    ${tasks.map((task, i) => html`
      <li style=${computed(() => `color: ${task.done.value ? "green" : "red"};`)}>
        <label>
          <input type="checkbox" prop:checked=${task.done} />
          ${task.name}
        </label>
        <button on:click=${() => tasks.splice(i.value, 1)}>🚮</button>
      </li>
    `)}
  </ul>
  <form on:submit=${event => {
    event.preventDefault();
    tasks.push({
      done: false,
      name: event.target.newTask.value,
    });
  }}>
    <input type="text" name="newTask" />
    <button>Add</button>
  </form>
`;

👉 View on CodePen