Data-sync on deeply-nested structures


#1

I’m building a rather complex, nested UI, and have a similar question to this one which remains unanswered.

  • The UI can be created from no data (create) or be populated (update)
  • The user can add set of fields, etc, etc and again, some will generate others
  • Various levels can be reordered via drag and drop

I’ve chosen:

  • to pass data down as props
  • to deep clone the data for each new level
  • not to use Vuex (building a 1D API for a 5D structure sounds like hell)

I have all this working, but am getting some feedback loops when feeding values back up. The only way to get round this I have found is to set an updating flag on each level that is updated on $nextTick so that when the props come back down again, the watch does not re-fire.

My worry is, after the relief of this working, that it’s a bit of a hack (for example, the top level will update 13 times on some changes - I think this might be because of a deep watch; next step - disable in place of handlers!

I’m considering various approaches, though, perhaps no passing of events up, just a recursive read on submit.

Has anyone else solved similar problems?

Cheers,
Dave


Framework/strategy for deeply nested data input
#2

I can tell you right now from this description it sounds like you want vuex, but maybe I’m not understanding the problem. Can you give a more specific example of what you are trying to do with this app?

Lets start here. What is a basic version of the “data” and what is the desired UI?


#3

I can pretty much say hand on heart, I don’t think Vuex is right for this!

Unfortunately, I can’t reveal the data as it’s confidential (banking), but imagine a master object with multiple tiers of things each of which has multiple items which have multiple properties and can be reordered, dragged into different lists, added to, etc. And some of those properties have a recursive structure, so the nesting can get quite deep.

This kind of thing:

+- thing
    +- id
    +- name
    +- foo
    +- groups
        +- group
        |   +- id
        |   +- name
        |   +- foo
        |       +- options
        |           +- option
        |           |   +- id
        |           |   +- name
        |           |   +- options
        |           |       +- option
        |           |       |   +- id
        |           |       |   +- name
        |           |       +- option
        |           |       |   +- id
        |           |       |   +- name
        |           |       +- option
        |           |           +- id
        |           |           +- name
        |           +- option
        |               +- id
        |               +- name
        |               +- options
        |                   +- option
        |                   |   +- id
        |                   |   +- name
        |                   +- option
        |                       +- id
        |                       +- name
        +- group
        |   +- id
        |   +- name 
        |   +- foo
        |       +- options
        |           +- option
        |               +- id
        |               +- name
        |               +- options
        |                   +- option
        |                   |   +- id
        |                   |   +- name
        |                   +- option
        |                   |   +- id
        |                   |   +- name
        |                   +- option
        |                       +- id
        |                       +- name
        +- group
            +- id
            +- name
            +- foo
                +- options
                    +- option
                    |   +- id
                    |   +- name
                    |   +- options
                    |       +- option
                    |       |   +- id
                    |       |   +- name
                    |       +- option
                    |       |   +- id
                    |       |   +- name
                    |       +- option
                    |           +- id
                    |           +- name
                    +- option
                    |   +- id
                    |   +- name
                    |   +- options
                    +- option
                    |   +- id
                    |   +- name
                    |   +- options
                    |       +- option
                    |       |   +- id
                    |       |   +- name
                    |       +- option
                    |       |   +- id
                    |       |   +- name
                    |       +- option
                    |           +- id
                    |           +- name
                    +- option
                        +- id
                        +- name
                        +- options
                            +- option
                            |   +- id
                            |   +- name
                            +- option
                                +- id
                                +- name


And depending on a top level property, the 2nd level options will be different types of objects with different structures.

The UI has to reflect this, in a manner similar to the diagram in the other post I posted in the OP.

Each tier has “Add item” and “Remove Item”

What were you thinking?


#4

Ok, so imagine I have a select input, and depending on the selected option the next select input shown will have a different set of items?


#5

I can’t offer you answers, but I’ve implemented something that looks somewhat similar: a form interface dynamically based on a complex and deeply nested json-schema.

While I’m not unhappy with Vue, handling deeply nested data in Javascript has been very frustrating. The most common issue is making sure that you create the whole hierarchy (parents) if they don’t exist before Vue is showing anything, and to do it in such a way that the whole thing stays reactive so Vue notices changes. The other problem is addressing deeply nested data; I’m using Vuex, but as you mention, Vuex isn’t very helpful at all here.

I’m sending both the schema and the associated data down the nested components as props, and have Vuex mutations against these props. It mostly works, but it doesn’t feel clean. The other option I’ve experimented with is to flatten the data and indexing it by path (using slash or dot path syntax like json-pointer or lodash get/set/unset). That means all mutations have to run deep getter and setter functions, because Vuex is not smart enough to cache functional getters so it re-runs both the getter and setter every time something gets set. Perhaps there are some possible tricks with Vue getters that are also a closure over a reactive object that gets cached, but I guess you’d need to know a lot more about Vue internals than I do.

Building a dynamic interface with nested components works well enough though, it’s linking up the data in a fool-proof manner that is the difficulty.

I’m not a Javascript/frontend expert and I’m somewhat surprised the Javascript community doesn’t seem to have solved the problem with addressing nested data yet, although I honestly wouldn’t know myself how to solve this problem and keep the concept of “reactivity”. I’m somewhat hopeful store implementations based on Javascript proxies might come up with something better for nested data in the near future, but I’ll guess we’ll have to wait and see…


#6

I actually solved this extremely elegantly in the end; I’ve started writing about it, but have yet to publish.

The trick was to first plan your data structure in the form of composable classes (we used TypeScript). You should add any specialist methods, such as add, remove, check, reset, etc. Also, make sure the constructor takes a data object, and you re-assign any POJOs to sub-classes.

The class structure allows you to modify the data structure without an interface, and passing a data object into the constructor allows you to easily deserialize JSON from the server.

The following is fairly meaningless pseudocode, but you get the idea:

class Foo {
  bar: Bar
  constructor (data) {
    this.bar = new Bar(data.bar)
  }
}

class Bar {
  items:Baz[]
  constructor (data) {
    data.items.forEach(item => this.addBaz(item))
  }
  addBaz (baz: any|Baz) {
    this.items.push(new Baz(baz))
  }
}

Once you have this structure down (as I mentioned, ours was much more complex than this) you can look at the UI.

The trick here is to model all the sub-classes as 1:1 components, that is, if you have a Foo class with a Bar class with an array of Baz items, build 3 components, each which should provide access to, and therefore update, its properties, by way of dropdowns, form inputs, etc with basic v-models:

<input v-model="baz.value" />

You then pass in the entire structure to the top-level component, as a class:

props: {
  data: Foo
}
<foo-component :data="foo" />

The component then renders itself, and inside you pass foo’s bar to <bar-component :bar="foo.bar" /> which does the array of <baz-component v-for="baz in bar.items" :item="baz" />.

And this is where it gets interesting. Each component delegates any logic to the original class (addBar(), removeBar()) etc so the component contains no manipulation logic (but may contains some user checks, such as checking 0-length arrays or what have you):

<p v-if="bar.items.length === 0">Click to add items</p>
<baz-component v-else v-for="baz in bar.items" :item="baz" />
<button @click="bar.addBaz()">Add baz</button>

The beauty is that as the instances update their own content, the reactive UI updates in response, yet all the logic and data is in the class structure NOT the component structure. This makes it really easy to build, test and update. You can even manipulate the classes via the console and the view will update.

When you need to grab the entire data structure, you simply JSON.stringify(foo) to turn the top class into data.

This mainly only works with tightly coupled structures, and you have to be careful NOT to alter original instance outside of the structure. A developer here hadn’t quite grasped the concept and started modifying properties of the original reference / sub-references when exporting to the server, which ended up re-building the component structure (even though he wasn’t looking at it) and then giving us all kinds of unexpected validation errors, so always make sure to deep clone or an an export() method to the top-level parent structure.

As I said, we’ve got an extremely complex setup working, with drag and drop, multi-tiered volume and date pricing setups, and it’s rock solid. We didn’t need any deep watches, hacks, or anything like that, and we get type safety when passing classes via props. Additionally, because Vuex isn’t involved, it’s simply expressive JavaScript bar.addBaz() and the code is the same in the console as it is in the component, no complex paired Vuex setups or clumsily-named actions / commits, etc.

Try it this way, you’ll be laughing in no time!