Skip to content

Virtualize value of form controls #188

@lydell

Description

@lydell

Intro

elm/virtual-dom has a _VirtualDom_virtualize function, that looks at the existing HTML (DOM nodes) and turns it into virtual nodes at initialization.

#187 makes the implementation of _VirtualDom_virtualize complete, so that Elm doesn’t need to do much to the existing DOM nodes in a server-rendered application – it basically leaves them be and just attaches event listeners here and there.

However, there is one thing that isn’t handled: Taking care of user input that happened before Elm initialized on the client.

Example

Imagine you have built an e-commerce site with elm-pages. You send an email to a customer: “Please leave a review for the Space Socks you bought last week!” There’s a direct link to the product page. At the bottom, there’s a <textarea> for writing a review.

The customer clicks the link. Since the page is built with elm-pages, they get server-side rendered HTML. The customer is on a train, and they lose the Internet connection for a while, so only the HTML and CSS loads, but the Elm JS is delayed. They start writing their review. What should happen when the Elm JS loads?

Elm initializes, and in init the model is set to have an empty string as default value for the review text. On the first render, Elm will then clear the textarea (to match the model). The customer loses all text they wrote.

Potential solution

One thing we could do, is to keep track of that “this is the first render after virtualization. For form control elements, don’t update .value, instead trigger the input event so that Elm gets to know about the state of the element (assuming the Elm code listens for the input event)”.

What do other frameworks do?

Interestingly, both React and Preact seem to not set value after hydration, but won’t let the app know about the actual values in the form control, leaving the app out of sync until more events happen.

Here’s the React code:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>React Hydration Test</title>
  </head>
  <body>
    <!-- This is server-rendered HTML: -->
    <div id="root">
      <div>
        <h1>Preact</h1>
        <p>Textarea:</p>
        <textarea></textarea>
        <p>Input:</p>
        <input />
        <p>Checkbox: false</p>
        <input type="checkbox" value="false" />
      </div>
    </div>

    <script type="module">
      import { hydrateRoot } from "https://esm.sh/react-dom@18/client?dev";
      import {
        createElement as h,
        useState,
      } from "https://esm.sh/react@18?dev";

      function App() {
        const [textarea, setTextarea] = useState("");
        const [input, setInput] = useState("");
        const [checkbox, setCheckbox] = useState(false);

        return h(
          "div",
          null,
          h("h1", null, "React"),
          h("p", null, "Textarea: " + textarea),
          h("textarea", {
            value: textarea,
            onInput: (e) => setTextarea(e.target.value),
          }),
          h("p", null, "Input: " + input),
          h("input", {
            value: input,
            onInput: (e) => setInput(e.target.value),
          }),
          h("p", null, "Checkbox: " + checkbox),
          h("input", {
            type: "checkbox",
            value: checkbox,
            onInput: (e) => setCheckbox(e.target.checked),
          })
        );
      }

      // Simulate a slow network.
      setTimeout(() => {
        hydrateRoot(document.getElementById("root"), h(App));
        console.log("Hydrated!");
      }, 5000);
    </script>
  </body>
</html>

And the Preact code:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Preact Hydration Test</title>
  </head>
  <body>
    <!-- This is server-rendered HTML: -->
    <div id="root">
      <div>
        <h1>Preact</h1>
        <p>Textarea:</p>
        <textarea></textarea>
        <p>Input:</p>
        <input />
        <p>Checkbox: false</p>
        <input type="checkbox" value="false" />
      </div>
    </div>

    <script type="module">
      import { h, hydrate } from "https://esm.sh/preact@10.19.5?dev";
      import { useState } from "https://esm.sh/preact@10.19.5/hooks?dev";

      function App() {
        const [textarea, setTextarea] = useState("");
        const [input, setInput] = useState("");
        const [checkbox, setCheckbox] = useState(false);

        return h(
          "div",
          null,
          h("h1", null, "Preact"),
          h("p", null, "Textarea: " + textarea),
          h("textarea", {
            value: textarea,
            onInput: (e) => setTextarea(e.target.value),
          }),
          h("p", null, "Input: " + input),
          h("input", {
            value: input,
            onInput: (e) => setInput(e.target.value),
          }),
          h("p", null, "Checkbox: " + checkbox),
          h("input", {
            type: "checkbox",
            value: checkbox,
            onInput: (e) => setCheckbox(e.target.checked),
          })
        );
      }

      // Simulate a slow network.
      setTimeout(() => {
        hydrate(h(App), document.getElementById("root"));
        console.log("Hydrated!");
      }, 5000);
    </script>
  </body>
</html>

And some links:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions