alteryx / Optimization

Alteryx Optimization Tool
GNU General Public License v3.0
0 stars 1 forks source link

Syncing dataItems with the Mobx store #11

Open ramnathv opened 8 years ago

ramnathv commented 8 years ago
import { extendObservable, observable, computed, autorun } from 'mobx'; //eslint-disable-line

const dataItems = [
  {obsName: 'editorValue', dataName: 'FormulaFields',  mode: 'read'},
  {obsName: 'varList', dataName: 'fieldNames', mode: 'read'},
  {obsName: 'constraints', dataName: 'constraints', mode: 'write'},
  {obsName: 'objective', dataName: 'objective', mode: 'write'},
  {obsName: 'fieldList', dataName: 'fieldList', mode: 'write'}
]

class AyxStore {
  constructor(manager, dataItems){
    dataItems.forEach(d => {
      const item = manager.GetDataItemByDataName(d.dataName)
      if (d.mode === 'read'){
        this[d.obsName] = item.getValue()
        item.BindUserDataChanged(v => { this[d.obsName] = v; })
      } else {
        this[d.obsName] = JSON.parse(item.getValue())
        autorun(() => item.setValue(JSON.stringify(this[d.obsName].toJSON())))
      }
    })
  }
}
ramnathv commented 8 years ago

Here is an implementation of the idea that actually seems to work

import { extendObservable, observable, computed, autorun, toJSON } from 'mobx';
class AyxStore {
  constructor(manager, dataItems){
    dataItems.forEach(d => {
      const item = manager.GetDataItemByDataName(d.dataName)
      if (d.mode === 'read'){
        this[d.obsName] = item.getValue()
        item.BindUserDataChanged(v => { this[d.obsName] = v; })
      } else {
        this[d.obsName] = JSON.parse(item.getValue())
      }
    })
    extendObservable(this, this)
    dataItems
      .filter(d => d.mode === 'write')
      .forEach(d => {
        autorun(() => {
          console.log("I am autorunning...")
          let item = manager.GetDataItemByDataName(d.dataName)
          item.setValue(JSON.stringify(toJSON(this[d.obsName])))
        })
      })
  }
}

export default AyxStore;
ramnathv commented 8 years ago

I was able to test this using the following code

Alteryx.Gui.AfterLoad = (manager) => {
  const testStore = new AyxStore(manager, [
    {obsName: 'editorValue', dataName: 'editorValue',  mode: 'read'},
    {obsName: 'dummy', dataName: 'dummy',  mode: 'write'}
  ])
  // setting the observable dummy on the store should update the value of the data item
  testStore.dummy = {"x": 3, "y": 4}
  console.log(manager.GetDataItemByDataName("dummy").value === '{"x": 3, "y": 4}')

  //  setting the data item editorValue should update the value of the observable in the store
  manager.GetDataItemByDataName("editorValue").setValue("2x1 + 3x2")
  console.log(testStore.editorValue = "2x1 + 3x2")
}
ramnathv commented 8 years ago

This should not create any circular logic, since the auto updates are only set one way.

  1. In read mode, the value of the observable is always updated on change in value of the data item.
  2. In write mode, the value of the data item is always updated on change in value of the observable.

In some cases (e.g. editorValue), we need both.

  1. An observable that constantly tracks the editorValue data item.
  2. Update editorValue data item from an observable value (e.g. constraint being edited)
ramnathv commented 8 years ago

I was able to get true two-way syncing working. The only drawback seems to be that more events are getting triggered than should be because of the nature of the updates. For example, updating a dataitem value from the UI should ONLY trigger an autorun that updates the value of the observable. But due to the syncing mechanism this triggers an additional update of the dataitem which triggers another update of the observable. The recursion seems to stop there, so the fear of an infinite loop does not seem to be justified. I have some ideas on how to kill these extra events.

class AyxStore2 {
  constructor(manager, dataItems){
    dataItems.forEach(d => {
      const item = manager.GetDataItemByDataName(d.dataName)
      this[d.obsName] = item.getValue()
      item.BindUserDataChanged(v => { 
        console.log("Bind User Data Changed...")
        this[d.obsName] = v; 
      })
    })
    extendObservable(this, this)
    dataItems.forEach(d => {
      autorun(() => {
        console.log("Autorun...")
        let item = manager.GetDataItemByDataName(d.dataName);
        item.setValue(this[d.obsName]);
      })
    })
  }
}
cafreeman commented 8 years ago

@ramnathv here's the implementation I came up with based on your work here. This solves the recursion problem by diffing the values before triggering updates.

import { autorun, extendObservable } from 'mobx';

class NewStore {
  constructor(ayx, dataItems) {
    const { Gui: { manager, renderer } } = ayx;
    this.manager = manager;
    this.renderer = renderer;

    dataItems.forEach(d => {
      // Obtain a reference to the data item
      const item = this.manager.GetDataItemByDataName(d);

      // Assign the data item value as a property on the store
      extendObservable(this, { [`${d}`]: item.getValue() });

      // Bind an event listener to the data item that will diff the values and update accordingly
      // Not needed for read-only attributes
      item.BindUserDataChanged(v => {
        if (this[d] !== v) {
          this[d] = v;
        }
      });

      // Set up an autorun on the observable property that will change the underlying data item only
      // if the values differ
      autorun(() => {
        if (this[d] !== item.getValue()) {
          item.setValue(this[d]);
        }
      });
    });
  }
}

export default NewStore;
ramnathv commented 8 years ago

Thanks @cafreeman. This is really cool!