Aditya Jamuar
Aditya Jamuar

Reputation: 127

How to refresh DOM when props change in Svelte?

I have a grid-like structure with a pointer object coming from a parent. Now with some operation, the pointer is getting updated but the change is not getting reflected in the child component, nor is DOM getting updated.

Parent:

<script>
 import Boxes from "./Boxes.svelte";

 let boxes = [
   ["", "", "", "", "", "", "", ""],
   ["", "", "", "", "", "", "", ""],
   ["", "", "", "", "", "", "", ""],
   ["", "", "", "", "", "", "", ""],
   ["", "", "", "", "", "", "", ""],
   ["", "", "", "", "", "", "", ""],
   ["", "", "", "", "", "", "", ""],
   ["", "", "", "", "", "", "", ""]
 ];

 let activeBox = {
   x: 0,
   y: 0
 };

 function handleKeydown(keyEvent) {
   let i = activeBox.x;
   let j = activeBox.y;

   const width = boxes[i].length,
     height = boxes.length,
     left = 37,
     up = 38,
     right = 39,
     down = 40,
     tab = 9,
     backspace = 8;



   // Loop around single row with right and left arrows
   if (keyEvent.keyCode == right) {
     activeBox.x = activeBox.x + 1;
     if(activeBox.x === boxes[i].length) activeBox.x = 0;
     return;
   }

   $: console.log("^^^ activeBox &&", activeBox);
</script>

<style>
 main {
   display: flex;
   align-items: center;
   justify-content: center;
 }

 @media (min-width: 640px) {
   main {
     max-width: none;
   }
 }
</style>

<svelte:window on:keydown="{handleKeydown}" />
<main>
 <Boxes boxes={boxes} activeBox={activeBox} />
</main>

The child component is using the activeBox variable to render the selected box. But this doesn't seem to work as while the first render works perfectly, the box doesn't get updated with it.

Child:

<script>
  export let boxes;
  export let activeBox;

  $: console.log("^^^ active Box updated");

  function getSelectedClass(i, j) {
    if (activeBox.x === j && activeBox.y === i) {
      return "selected";
    }
    return "";
  }
</script>

<style>
  .grid-container {
    display: grid;
    grid-template-columns: auto auto auto auto auto auto auto auto;
    background-color: #2196f3;
    padding: 0px;
  }
  .grid-item {
    background-color: rgba(255, 255, 255, 0.8);
    border: 1px solid rgba(0, 0, 0, 0.8);
    width: 40px;
    height: 40px;
    padding: 20px;
    font-size: 30px;
    text-align: center;
  }
  .selected {
    border: 1px solid red;
  }
</style>

<main>
  <div class="grid-container">
    {#each boxes as row, i}
      {#each row as column, j}
        <div class=" grid-item {getSelectedClass(i, j)}">{boxes[i][j]}</div>
      {/each}
    {/each}
  </div>
</main>

I could really use some insight as I still haven't properly grasped the concept of Svelte. Any help would be much appreciated.

Upvotes: 3

Views: 6800

Answers (2)

rixo
rixo

Reputation: 25001

With your code, the value of activeBox is in fact propagated to the child, but you don't see it because there is no reactive code that use it in the child.

Reactive code is re-executed and re-evaluated when the value of the variables it depends upon change, but only for the variables immediately present in the reactive code.

So, in this code:

  $: console.log("^^^ active Box updated");

... there is no variables at all. So this block will run once initially, then never again.

This:

    {#each boxes as row, i}
      ...
    {/each}

would get re-evaluated any time the boxes variable change.

And in this:

        <div class=" grid-item {getSelectedClass(i, j)}">...</div>

... the reactive value {getSelectedClass(i, j)} only "sees" the variables getSelectedClass, i and j. i and j are local variables, so they are not tracked by Svelte. getSelectedClass is a top level variable, so it would get tracked and the block re-evaluated if its value changed (i.e. getSelectedClass = 'foo'), but it's never the case here.

So... How to fix?

Just by adding the activeBox in the reactive expression will make the compiler recompute it when its value changes (even if it's not actually used in the function). So just this would fix your code as it is:

        <div class=" grid-item {getSelectedClass(i, j, activeBox)}">...</div>

Another option would be to use another construct based on an expression that actually contains the activeBox variable:

        <div class="grid-item" class:selected={activeBox.x === j && activeBox.y === i}>{boxes[i][j]}</div>

And here's a middle ground:

<script>
  // NOTE this block contains activeBox variable, so isSelected is recreated when
  // activeBox changes
  $: isSelected = (i, j) => activeBox.x === j && activeBox.y === i
</script>

<main>
  <div class="grid-container">
    {#each boxes as row, i}
      {#each row as column, j}
        <div class="grid-item" class:selected={isSelected(i, j)}>{boxes[i][j]}</div>
      {/each}
    {/each}
  </div>
</main>

Upvotes: 3

JHeth
JHeth

Reputation: 8366

So a few things to be aware of when using Svelte:

  1. Don't console log in reactive declarations $: console.log() will run as soon as the component is created but has no other meaning or use.
  2. When your exported variable is the same as the variable name in the parent component just use {varName} in the tag, no need for varName={varName}

As far as updating activeBox in the child component, it is being updated but in your code there's no way for it to know that it needs to run the function that selects class again. That function only runs once, which is why it works on initial render.

One way you can keep the class updated is to have a ternary operator directly in your conditional class statement in the child:

<div class=" grid-item {activeBox.x === j && activeBox.y === i? 'selected' : ''}">{boxes[i][j]}</div>

And just drop the function. Like this

Upvotes: 1

Related Questions