SolidJS: input field loses focus when typing
Asked Answered
M

3

6

I have a newbie question on SolidJS. I have an array with objects, like a to-do list. I render this as a list with input fields to edit one of the properties in these objects. When typing in one of the input fields, the input directly loses focus though.

How can I prevent the inputs to lose focus when typing?

Here is a CodeSandbox example demonstrating the issue: https://codesandbox.io/s/6s8y2x?file=/src/main.tsx

Here is the source code demonstrating the issue:

import { render } from "solid-js/web";
import { createSignal, For } from 'solid-js'

function App() {
  const [todos, setTodos] = createSignal([
    { id: 1, text: 'cleanup' },
    { id: 2, text: 'groceries' },
  ])

  return (
    <div>
      <div>
        <h2>Todos</h2>
        <p>
          Problem: whilst typing in one of the input fields, they lose focus
        </p>
        <For each={todos()}>
          {(todo, index) => {
            console.log('render', index(), todo)
            return <div>
              <input
                value={todo.text}
                onInput={event => {
                  setTodos(todos => {
                    return replace(todos, index(), {
                      ...todo,
                      text: event.target.value
                    })
                  })
                }}
              />
            </div>
          }}
        </For>
        Data: {JSON.stringify(todos())}
      </div>
    </div>
  );
}

/*
 * Returns a cloned array where the item at the provided index is replaced
 */
function replace<T>(array: Array<T>, index: number, newItem: T) : Array<T> {
  const clone = array.slice(0)
  clone[index] = newItem
  return clone
}

render(() => <App />, document.getElementById("app")!);

UPDATE: I've worked out a CodeSandbox example with the problem and the three proposed solutions (based on two answers): https://codesandbox.io/s/solidjs-input-field-loses-focus-when-typing-itttzy?file=/src/App.tsx

Mountainous answered 18/5, 2022 at 11:28 Comment(0)
K
9

<For> components keys items of the input array by the reference. When you are updating a todo item inside todos with replace, you are creating a brand new object. Solid then treats the new object as a completely unrelated item, and creates a fresh HTML element for it.

You can use createStore instead, and update only the single property of your todo object, without changing the reference to it.

const [todos, setTodos] = createStore([
   { id: 1, text: 'cleanup' },
   { id: 2, text: 'groceries' },
])
const updateTodo = (id, text) => {
   setTodos(o => o.id === id, "text", text)
}

Or use an alternative Control Flow component for mapping the input array, that takes an explicit key property: https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyed#Key

<Key each={todos()} by="id">
   ...
</Key>
Kindle answered 18/5, 2022 at 11:55 Comment(2)
Thanks a lot, this explains it very clearly. I wasn't aware of the solid-primitives library, that looks like a neat solution. I now understand that createStore apparently works different than createSignal in this regard, the todo's example that I was looking at on the website also uses a createStore.Mountainous
awesome! saved me lot of time to dig in deeper. Well explained.Wailful
G
3

While @thetarnav solutions work, I want to propose my own. I would solve it by using <Index>

import { render } from "solid-js/web";
import { createSignal, Index } from "solid-js";

/*
 * Returns a cloned array where the item at the provided index is replaced
 */
function replace<T>(array: Array<T>, index: number, newItem: T): Array<T> {
  const clone = array.slice(0);
  clone[index] = newItem;
  return clone;
}

function App() {
  const [todos, setTodos] = createSignal([
    { id: 1, text: "cleanup" },
    { id: 2, text: "groceries" }
  ]);

  return (
    <div>
      <div>
        <h2>Todos</h2>
        <p>
          Problem: whilst typing in one of the input fields, they lose focus
        </p>
        <Index each={todos()}>
          {(todo, index) => {
            console.log("render", index, todo());
            return (
              <div>
                <input
                  value={todo().text}
                  onInput={(event) => {
                    setTodos((todos) => {
                      return replace(todos, index, {
                        ...todo(),
                        text: event.target.value
                      });
                    });
                  }}
                />
              </div>
            );
          }}
        </Index>
        Dat: {JSON.stringify(todos())}
      </div>
    </div>
  );
}
render(() => <App />, document.getElementById("app")!);

As you can see, instead of the index being a function/signal, now the object is. This allows the framework to replace the value of the textbox inline. To remember how it works: For remembers your objects by reference. If your objects switch places then the same object can be reused. Index remembers your values by index. If the value at a certain index is changed then that is reflected in the signal.

This solution is not more or less correct than the other one proposed, but I feel this is more in line and closer to the core of Solid.

Grous answered 18/5, 2022 at 12:10 Comment(3)
Yup, it fixes the problem just as well, and even without extra dependencies or refactoring the state logic. One thing worth mentioning though: Adding/Removing items in the middle of the array; or swapping them around will cause similar problems with rendering as with the original issue. So go ahead, but use with caution.Kindle
Most likely. createStore is probably most in line with the idea of the language here as it feels like this is what it is made for.Grous
Thanks, both Index and Key will work in my case. Good to have an overview of the options.Mountainous
D
0

With For, whole element will be re-created when the item updates. You lose focus when you update the item because the element (input) with the focus gets destroyed, along with its parent (li), and a new element is created.

You have two options. You can either manually take focus when the new element is created or have a finer reactivity where element is kept while the property is updated. The indexArray provides the latter out of the box.

The indexArray keeps the element references while updating the item. The Index component uses indexArray under the hood.

function App() {
  const [todos, setTodos] = createSignal([
    { id: 1, text: "cleanup" },
    { id: 2, text: "groceries" }
  ]);

  return (
    <ul>
      {indexArray(todos, (todo, index) => (
        <li>
          <input
            value={todo().text}
            onInput={(event) => {
              const text = event.target.value;
              setTodos(todos().map((v, i) => i === index ? { ...v, text } : v))
            }}
          />
        </li>
      ))}
    </ul>
  );
}

Note: For component caches the items internally to avoid unnecessary re-renders. Unchanged items will be re-used but updated ones will be re-created.

Dogleg answered 21/5, 2022 at 19:34 Comment(2)
Thanks. Reading in the docs I see indexArray is used by <Index> under the hood, so I think this solution is effectively the same as using <Index>.Mountainous
@JosdeJong Yes but Index wraps children in createMemo for extra layer of caching.Dogleg

© 2022 - 2024 — McMap. All rights reserved.