Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Svelte store not firing onchange when nested components update

Let's say I want to create a multi-color picker with svelte, maybe to let the user choose a foreground color and a background color. My data model that looks like this:

{
  foreground: {
    r: 100,g:100,b:100
  },
  background: {
    r: 200,g:200,b:200
  }
};

So my app.js is

    import AppUI from './App.html';
import { Store } from 'svelte/store.js';

const defaultData = {
  foreground: {
    r: 100,g:100,b:100
  },
  background: {
    r: 200,g:200,b:200
  }
};

const store = new Store(defaultData);

window.store = store; // useful for debugging!

store.onchange(() => console.log('something changed'));

var app = new AppUI({
  target: document.querySelector( '#main' ),
  store
});

export default app;

Then I can build an RGBSelector component to reuse:

  <input type="range" min=0 max=255 step=1 bind:value=data.r/>{{data.r}}
  <input type="range" min=0 max=255 step=1 bind:value=data.g/>{{data.g}}
  <input type="range" min=0 max=255 step=1 bind:value=data.b/>{{data.b}}

And my App.html is pretty simple:

foreground:
<RGBSelector bind:data=$foreground/>

background:
<RGBSelector bind:data=$background/>

<script>
  import RGBSelector from './RGBSelector.html';

  export default {
    components: {
      RGBSelector
    }
  };
</script>

This seems to work, mostly. The two-way binding in the range inputs is working (the labels update), and the store is even being updated (verified by inspecting store._state in the console). So I believe the bind keywords in the RGBSelector are passing the change up to where they're declared in the App, which in turn is binding them to the store.

Trouble is, the store.onchange handler is not firing. Can anyone see what I'm doing wrong?

Full example: https://glitch.com/edit/#!/nonstop-hourglass

like image 671
nvioli Avatar asked Jan 09 '18 22:01

nvioli


1 Answers

This is a bug in Svelte, not your app! It turns out component bindings don't play nicely with store — the bind:data=$foreground is just updating $foreground in your <App> component rather than updating foreground in your store.

Tracking the issue here: https://github.com/sveltejs/svelte/issues/1100

There isn't a great workaround sadly — until we get this fixed you would need to do something like this:

foreground: <RGBSelector bind:data=foreground/>
background: <RGBSelector bind:data=background/>
text: <Textinput bind:value=text/>

<script>
  import RGBSelector from './RGBSelector.html';
  import Textinput from './Textinput.html';

  export default {
    components: {
      RGBSelector, Textinput
    },

    oncreate() {
      this.observe('foreground', foreground => {
        this.store.set({ foreground });
      });

      this.observe('background', background => {
        this.store.set({ background });
      });

      this.observe('text', text => {
        this.store.set({ text });
      });
    }
  };
</script>

And in your JS file, this:

var app = new App({
  target: document.body,
  data: defaultData,
  store
});

If changes can go both ways, you'd also need to observe the store properties, taking care to prevent infinite update loops:

// inside `oncreate` — would also need to do this
// for `background` and `text`
let foregroundUpdating = false;

this.observe('foreground', foreground => {
  if (foregroundUpdating) return;
  foregroundUpdating = true;
  this.store.set({ foreground });
  foregroundUpdating = false;
});

this.store.observe('foreground', foreground => {
  if (foregroundUpdating) return;
  foregroundUpdating = true;
  this.set({ foreground });
  foregroundUpdating = false;
});

Recreating the functionality of bindings like this is obviously a bit cumbersome, so we'll try and get this bug fixed soon.

like image 85
Rich Harris Avatar answered Oct 23 '22 02:10

Rich Harris