1
\$\begingroup\$

I tend to feel the need in my Knockout applications to pass along a reference of the root view model down to the sub view models. However, I don't like the fact that I then have a dependency that lies both ways: root has a list of children, and each child a reference to the root.

The Scenario

Here's a typical example, a simplified version of real code I tend to get:

var PersonViewModel = function(dto) {
    var self = this;

    self.name = ko.observable(dto.name);
    self.department = ko.observable(); // This is the culprit; how to fill this?

    // Etc: several more properties would live here.
};

var RootViewModel = function(dto) {
    var self = this;

    self.title = ko.observable(dto.title);

    // For simplicity's sake array items are not observable, but they could be.
    self.departments = ko.observableArray(dto.departments);

    self.people = ko.observableArray([]);

    for (var i=0; i < dto.people.length; i++) {
        var newPerson = new PersonViewModel(dto.people[i]);
        self.people.push(newPerson);
    }
};

This can be constructed as follows:

var data = {
    title: "My organization",
    people: [
        { name: "John", departmentId: 1 },
        { name: "Mary", departmentId: 1 },
        { name: "Barthus", departmentId: 2 },
        { name: "Goldie", departmentId: 3 }
    ],
    departments: [
        { id: 1, name: "IT" }, 
        { id: 2, name: "Sales" }, 
        { id: 3, name: "Finance" }
    ]
};

var root = new RootViewModel(data);
ko.applyBindings(root);

See also this jsfiddle.

The culprit lies with the self.department observable in PersonViewModel. The input for that constructor function is a dto, which only has a departmentId. However, I want to set the actual department into that observable, but the PersonViewModel has no access to the list of available departments.

It is important to note that there will be 5 to 10 more parts like "department", which will become a problem for some of the solutions I've considered (see below).

Considered Options

Here are some things I've considered to make this work.

1. Have the Root View Model amend the PersonViewModel after creation

I can have the root model set the department for the created Person, for example by updating the for loop like this:

    for (var i=0; i < dto.people.length; i++) {
        var newPerson = new PersonViewModel(dto.people[i]);
        var dept = self.departments().filter(function(x) { 
            return x.id === dto.people[i].departmentId;
        })[0];
        newPerson.department(dept);
        self.people.push(newPerson);
    }

Please don't mind the rather crude and unsafe way of finding the actual department, production code would be a bit more robust (and longer).

Some things I don't like about this is that a unit test to check if the dto's department value leads to a correctly filled PersonViewModel.department observable requires me to actually construct a different unit: the root view model.

2. Make PersonViewModel have the Root as a dependency

Since it's the Root that knows about available departments, I could also justify that PersonViewModel is dependent on it:

var PersonViewModel = function(dto, root) {
    var self = this;

    self.name = ko.observable(dto.name);

    var dept = root.departments().filter(function(x) { 
        return x.id === dto.departmentId;
    })[0];

    self.department = ko.observable(dept);
};

This feels a bit like overkill though: there will be many things in Root with which a Person view model will have nothing to do. Which brings me to the next option:

3. Make PersonViewModel dependent on a list of departments

All we need really is the list of departments, so a slight variation of the previous option would be like this:

var PersonViewModel = function(dto, departments) {
    var self = this;

    self.name = ko.observable(dto.name);

    var dept = departments.filter(function(x) { 
        return x.id === dto.departmentId;
    })[0];

    self.department = ko.observable(dept);
};

However, this will lead to a problem in more realistic scenarios: the constructor function for PersonViewModel will get a long list of dependencies. Which brings me to my final option:

4. Make PersonViewModel dependent on a facade or repository

We could have either a facade or repository to encapsulate the dependencies for a PersonViewModel, something like this:

var OrganizationRepository = function(departments /* etc */) {
    // For simplicity's sake array items are not observable, but they could be.
    this.departments = ko.observableArray(departments);

    // More "related" entities will be held here, e.g. locations, projects, etc.
};

var PersonViewModel = function(dto, organizationRepository) {
    var self = this;

    self.name = ko.observable(dto.name);

    var dept = organizationRepository.departments().filter(function(x) { 
        return x.id === dto.departmentId;
    })[0];

    self.department = ko.observable(dept);
};

var RootViewModel = function(dto) {
    // Abbreviated   
    self.organizationRepository = new OrganizationRepository(dto.departments);

    for (var i=0; i < dto.people.length; i++) {
        var newPerson = new PersonViewModel(dto.people[i], self.organizationRepository);
        self.people.push(newPerson);
    }
};

For the simple example here it feels like overkill, but in bigger applications this may be worth it? In any case, this brings me to:

The Bottom Line

Which options are better than others? And did I miss any other options?

As a footnote, I'm trying to optimize here for testability, readability, and having my dependencies as light and "correct" as possible.

\$\endgroup\$
3
  • \$\begingroup\$ Why not get the department where you create the PersonViewModel and pass that aswell? \$\endgroup\$ – Roel van Westerop Sep 12 '14 at 11:01
  • \$\begingroup\$ I wanted to highlight the issue, so I left out all the other observable properties the View Models would have. Think I'll edit the question to note that (without adding 100s of lines of code). \$\endgroup\$ – Jeroen Sep 12 '14 at 11:03
  • \$\begingroup\$ Btw, option 2 still has the problem you have in option 1, you need to construct the root view model for a unittest. \$\endgroup\$ – Roel van Westerop Sep 12 '14 at 11:22
1
\$\begingroup\$

Is this a viable option?

var PersonViewModel = function(person, department) {
    var self = this;

    self.name = ko.observable(person.name);
    self.department = ko.observable(department);
};

var RootViewModel = function(dto) {
    var self = this;

    self.title = ko.observable(dto.title);

    // For simplicity's sake array items are not observable, but they could be.
    self.departments = ko.observableArray(dto.departments);

    self.people = ko.observableArray([]);

    for (var i=0; i < dto.people.length; i++) {
        var person = dto.people[i];
        var newPerson = new PersonViewModel(person, dto.departments[person.departmentId]);
        self.people.push(newPerson);
    }
};
\$\endgroup\$
1
  • \$\begingroup\$ Yes, it certainly might help. It would be a definitive improvement over version 3 I had, though it would still suffer from some constructor over injection as in reality there are more instances of this "departments" problem. (Apologies, this wasn't entirely clear from the question, I edited and tried to clarify that bit after you commented on the question.) \$\endgroup\$ – Jeroen Sep 12 '14 at 11:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.