makeTwoWay on arrays using the empty binding

Hello,

My data contains a list, which I would like to be user-editable, but I am failing to make the two-way binding work. E.g. the data for a node looks this:

{ "text":"hello", "list":[1,2,3] }

I am displaying such a node like this:

$(go.Node, "Auto", $(go.Shape, "Rectangle"),
  $(go.Panel, "Vertical",
    $(go.TextBlock, new go.Binding("text","text")),
    $(go.Panel, "Table", new go.Binding("itemArray","list"),
      { itemTemplate: $(go.TextBlock, new go.Binding("text","").makeTwoWay(), {editable:true}) }
    )
  )
)
-----------
|  hello  |
|    1    |
|    2    |
|    3    |
-----------

The user editing any item 1,2,3 in the list does not update the model. I see from Binding | GoJS API that

Because in this example the source property name is the empty string, and because one cannot replace the whole source data object, any return value from the conversion function is ignored.

I see from e.g. Binding to structured node data that there is sometimes a way to bind the data anyway, using a backConverter. I tried passing such a function, bC, to .makeTwoWay(bC):

function bC(target,dataobj) {
  dataobj = target
}

This (probably as expected …) has no effect. It seems that target is just the new value and dataobj the old value.

How can you make the two-way binding work for elements of an array?

Thanks,

First, are you using the debug library and checking for errors or warnings in the console? That is an invalid item template, because it is not a Panel.

Second, because the data for each item is a number, one cannot modify any of the properties of a number. But you are hoping that one can completely replace the number because the Array is mutable. Sorry, but that is not supported.

Yes, I am using the debug library in general – sorry, I just whipped that example up to illustrate my question. (By the way, I was wondering if there’s a sort of gojs-fiddle to make it easy to share minimal working examples?)

Yes, that’s indeed my hope. Do you think there may be a way of achieving that goal?

To go into a bit more detail about my implementation, my node data looks more like this:

{ "text":"hello",
  "priors":["A","B","C"],
  "states":[
    { "text":"x", "conditionals":[1,2,3] }
    { "text":"y", "conditionals":[4,5,6] }
  ]
}

The conditionals are dependent on (the ordering of) the priors. Because there can be many priors and many states, while it would be possible to change the data format to zip the lists together (so that the data format for elements of states becomes {"text":"x", "conditionals":[{"prior":"A","value":1},{"prior":"B","value":2},{"prior":"C","value":3}]}, and similarly for state y etc.) it seems more elegant to keep them separate. Since the user needs to be able to edit the conditionals, this would require finding a way to implement a two-way binding on the elements of the array.

Well, right now you could change the structure of your “conditionals” to be:
"conditionals": [{"v": 1}, {"v": 2}, {"v": 3}]

That would allow something like:
$(go.TextBlock, new go.Binding("text", "v").makeTwoWay(), ...)

Ha. I used exactly that when narrowing down the issue – but don’t want to introduce this convolution into the data structure, which i.a. will be exposed to other software later. Is it perhaps possible to achieve the result by using a converter and backConverter in the go.Binding("itemArray","list", converter).makeTwoWay(backConverter), mapping between [1,2,3] and [{"v":1},{"v":2},{"v":3}]? I’ll play around with this later.

If you have “load” and “store” phases in your app, I would do it then.

That seems like a reasonable approach. I also tried the following approach, using a binding on the itemArray to turn [1,2,3] into [{"":1},{"":2},{"":3}].

function enc(i) { return {"":i} } // int to object
function dec(o) { return o[""] } // object to int
go.Binding("itemArray","list",function(a) { return a.map(enc) } }).makeTwoWay(function(b) { return b.map(dec) })
function write(i,d) { d[""] = i }
go.Binding("text","",dec).makeTwoWay(write)

I thought now the text binding on the elements of the list should update the model. Unfortunately, this does not seem to be the case. The advantage of this approach could be that the data-convulsion is only localised, and the bindings sytax much the same as before. I’m going to play with this a bit further later, but leaving this here in the meantime.

That won’t work because although the item panel’s data is bound to an Object such as {"":2}, the node.data.list will continue to be [1,2,3]. Because the Panel.itemArray isn’t replaced, the back-conversion function won’t be run.

Ah, ok. Thanks. Do you see any possibility of back-end changes to enable two-way bindings to lists of immutables, perhaps in one of the ways discussed here?

Certainly not in v1.6, since we’re just doing bug fixes in that branch.

I have added this topic to the list of new features to consider, but I have to admit that I think it’s relatively low priority. If it were easy to implement it would have higher priority.

I got it to work using your suggestion, thanks for the accurate advice, but I’m unhappy about the solution.

The code that goes through my data structure and turns a list [1,2,3] into [{"v":1},{"v":2},{"v":3}], and vice-versa, is a silly, unmeaningful thing. Whenever I access any of this data directly, I have to convert back and forth, so this interference is not localised in the codebase. Even when I define the go.Binding I have to hard-code the “v” because using a binding on the "" property for lists like [{"":1},{"":2},{"":3}] does not work, meaning code that basically just binds to list elements does not look like other code that binds to list elements, and there are now two different ways and places in which the abstraction breaks down. It’s not neat.

Please add an abstraction that allows write modifications of lists of immutables using the "" binding natively.