Knockout JS ObservableArray with many-to-many relationships

javascript, knockout-mapping-plugin, knockout.js, mvvm

Solution

There are probably other ways to do this, but this method has pretty minimal duplication, without sacrificing proper modeling. A server should have no trouble generating the data in this format.

Here it is in a (crudely styled) fiddle. Note, clicking a tag or guest will cause the selections below it to update (the first is selected by default).

Basically, store the relationships by id, and use `computed` array's to represent associations. Here is a basic viewmodel:

var ViewModel = function(guests, tags) {
    var self = this;
    self.guests = ko.observableArray(
        ko.utils.arrayMap(guests, function(i){ return new Guest(i); }
    ));
    self.tags= ko.observableArray(
        ko.utils.arrayMap(tags, function(i){ return new Tag(i); }
    ));

    self.selectedGuest = ko.observable(self.guests()[0]);
    self.selectedTag = ko.observable(self.tags()[0]);

    self.guestTags = ko.computed(function() {
        return ko.utils.arrayFilter(self.tags(), function(t) {
            return self.selectedGuest().tags().indexOf(t.id()) > -1;
        });        
    });

    self.tagGuests = ko.computed(function() {
        return ko.utils.arrayFilter(self.guests (), function(g) {
            return self.selectedTag().guests().indexOf(g.id()) > -1;
        });        
    });
};

UPDATE

So I have made a new fiddle to demonstrate a different kind of mapping, but this code could easily co-exist with the above viewmodel; its only seperate for demonstration. Instead of working off selection, it offers a general lookup, so that any code can consume it. Below is the HTML from Tags (guests is symmetrical), and the `guestMap` function that was added to the viewmodel.

You will note that the names are `input`s now, so you can change the names and watch all the bindings stay up to date. Let me know what you think:

<div>Tags
    <ul data-bind="foreach: tags">
        <li>
            <input data-bind="value: name, valueUpdate: 'afterkeydown'" />
            </br><span>Tags</span>
            <ul data-bind="foreach: guests">
                <li><span data-bind="text: $parents[1].guestMap($data).name()"></span></li>
            </ul>
        </li>
    </ul>
</div>
self.guestMap = function(id) {
        return ko.utils.arrayFirst(self.guests(), function(g) {
            return id == g.id();
        });
    };   

Problem

I am creating a guest list app using Knockout.js, and so far things are going swimmingly. However I have a best-practices question. My app has several different types of objects: guests and tags among them. Guests can have multiple tags, and tags can have multiple guests. At different points in the app, I need to display both arrays individually. For example, I have a "Guests" view where one can see all the guests along with their associated tags, and I also have a "Tags" view where one can see all tags and their associated guests. At present, the code for me to add a tag to a guest looks something like this: ``` var tag = function(opts) { this.guests = ko.observableArray() // Other tag code here... } var guest = function(opts) { this.tags = ko.observableArray() // Other guest code here... var self = this this.addTag = function(tag) { self.tags.push(tag) tag.guests.push(self) } } ``` I know there must be a better way of doing this kind of many-to-many relationship in Knockout other than updating each observableArray independently. This also leads to a kind of recursive app structure where a guest has a tags property/array which contains a tag, which has a guest property/array which contains a guest, which has a tags property... you get the picture. Right now, the ViewModel structure is like so: ``` - Parent Object - Guests ObservableArray - Guest Object - Tag Object as property of Guest - Tags ObservableArray - Tag Object - Guest Object as property of Tag ``` So I guess my question is twofold: 1) Is there a better way to structure my ViewModel to avoid recursive arrays? and 2) how can I better use Knockout.js to update my ViewModel in a DRY manner, rather than updating both the tags array AND the guests array individually? Thanks!

Original source