user3743222
user3743222

Reputation: 18665

how to force rendering in svelte 3?

I have the case of a form with input fields. One of those fields must be cleared when I type (keyup) the Enter key. I know I can handle this field as a controlled field (meaning listening on keyup and maintain a copy of the field), or alternatively use two-way binding, but in my use case I can't do the latter, and I'd rather not do the former. My question is thus, how can I force rendering of a Svelte Component?

Typically, the form is displayed with <Component field="value" />, the user modifies the field and clicks the Enter key, and I would like app.set(field, value) to reset the field to value, even and when, to Svelte's eyes, the field property has not changed. Is that possible?

A turn around I tried unsucessfully consists in updating the property inside the Svelte component, with the hope that when app.set(field, value), Svelte will see two different values for the field property and update the component. But that did not seem to work:

<script>

  const watchForEnter = ev => {
    if (ev.keyCode === 13) {
      ev.preventDefault();
      const formData = new FormData(ev.target.closest("form"));
      const tag = formData.get("tag");
      dispatch({ [ADDED_TAG]: tag });
    }
  };

  const updateCurrentTag = ev => {
    currentTag = new FormData(ev.target.closest("form")).get("tag");
    console.log(`currentTag!`, currentTag)
  }

</script>

        <form>
            <fieldset class="form-group">
              <input
                name="tag"
                class="form-control"
                type="text"
                placeholder="Enter tags"
                on:input={updateCurrentTag}
                value={currentTag}
                on:keyup={watchForEnter} />
            </fieldset>
        </form>

Upvotes: 6

Views: 6711

Answers (2)

Campbeln
Campbeln

Reputation: 2990

I believe I've found an elegant solution to the problem of forcing a component to update when its data has been changed outside of it's scope.

main.js; vanilla from the examples online, no special changes:

import App from './App.svelte';

var app = new App({
    target: document.body
});

export default app;

index.html; Note window.neek = {...}:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Svelte app</title>
    <script>
        window.neek = { nick: true, camp: { bell: "Neek" }, counter: 0 };
    </script>
    <script defer src='/build/bundle.js'></script>
</head>
<body>
</body>
</html>

App.svelte; Note $: notneek = window.neek and window.neek.update = ...:

<script>
    let name = 'world';
    $: notneek = window.neek;

    function handleClick() {
        notneek.counter += 1;
    }

    window.neek.update = function () {
        notneek = notneek;
    }
</script>

<h1>Hello { notneek.camp.bell }!</h1>

<button on:click={handleClick}>
    Clicked {notneek.counter} {notneek.counter === 1 ? 'time' : 'times'}
</button>

Since the update function is within the scope of App.svelte, it is able to force the re-render when called via window.neek.update(). This setup uses window.neek.counter for the internal data utilized by the button (via notneek.counter) and allows for the deep properties (e.g. neek.camp.bell = "ish") to be updated outside of the component and reflected once neek.update() is called.

In the console, type window.neek.camp.bell = "Bill" and note that Hello Neek! has not been updated. Now, type window.neek.update() in the console and the UI will update to Hello Bill!.

Best of all, you can be as granular as you want within the update function so that only the pieces you want to be synchronized will be.

Upvotes: 2

rixo
rixo

Reputation: 25001

I don't understand everything in the example you give, but the general answer to your question is that, in Svelte, you should try to avoid situations where you'd need to trigger an update yourself.

In most cases, it is possible to make your component more declarative and better leverages reactivity (reactive statements in particular) for Svelte to render just when it is needed.

However, for cases where it is not possible, as a workaround you can abuse the key of an {#each ...} block to force Svelte to recreate a component that would normally have been reused.

For example:

<script>
  export let allItems
  export let currentCategory

  $: items = allItems.filter(x => x.category === currentCategory)
</script>

{#each [items] as todos (currentCategory)}
  <Todos {todos} />
{/each}

See this other answer (from which I copied the above example) for more details.

See also this REPL that shows how to implement this hack into a reusable component, for cleaner syntax in the consumer.

And, for completeness, there's also this issue that has just appeared and asks for precisely this feature.

Upvotes: 0

Related Questions