Harry Xue
Harry Xue

Reputation: 13

Knockout: observableArray of arrays of observable inputs

I am having problems with route.html's foreach binding in the project Flight Management Computer.


Problem 1: value bindings in observableArray all update simultaneously

For the route in javascript, I set up a ko.observableArray of an Array of ko.observable (sounds very confusing, but code attached below nonetheless):

/* All bindings applied to viewmodel already */

var route = ko.observableArray();
var DEFAULT_ROUTE = [
    ko.observable(), // Waypoint Name
    ko.observable(), // Lat.
    ko.observable(), // Lon.
    ko.observable(), // Altitude Restriction
    ko.observable(false), // Waypoint isValid
    ko.observable('') // Waypoint information
];

Clicking a specific button adds the DEFAULT_ROUTE with no problem, as it calls

route.push(DEFAULT_ROUTE);

The HTML Code looks roughly like this and have no UI issues:

<tbody data-bind="foreach: route">
    <tr class="wpt-row">
        <td><input data-bind="value: $data[0]"></td> <!--waypoint input-->
        <td><input data-bind="value: $data[1]"></td> <!--lat. input-->
        <td><input data-bind="value: $data[2]"></td> <!--lon. input-->
        <td><input data-bind="value: $data[3]"></td> <!--alt. input-->
    </tr>
</tbody>

However, problems arise when there are multiple arrays in the outer ko.observableArray, as changing one input value both in the UI and in javascript will update ALL values in each array. Example:

var route = ko.observableArray([DEFAULT_ROUTE, DEFAULT_ROUTE, DEFAULT_ROUTE]);

// Then, outside viewmodel (in javascript console)
route()[0][0]('WPT'); // Sets the waypoint of the first input field to be 'WPT'

// Later
route()[0][0](); // 'WPT', correct
route()[1][0](); // 'WPT', incorrect, should be undefined
route()[2][0](); // 'WPT', incorrect, should be undefined

I set up a similar foreach in a different file, but with <input> simply as <span>, and data-bind as text: $data[x] instead of value. That different file works fine with no problems. The different file is log.html


Problem 2 (or rather, a Question)

After the route problem is fixed, I wish to update some specific values in a single array (one waypoint input field) when another value in that same array changes. I.E.

// Scenario 1, waypoint is a valid waypoint with proper coords
var waypoint = 'WAATR';
var coords = getWaypoint(waypoint); // [42.1234, -70.9876]
route()[0][0](waypoint); 
// route()[0][0]() is now 'WAATR'
// route()[0][1] and route()[0][2] should automatically update with value `coords[0]` and `coords[1]`
// route()[0][4] should be set to true (valid waypoint)


// Scenario 2, waypoint is NOT a valid waypoint
var waypoint = 'IDK';
var coords = getWaypoint(waypoint); // []
route()[0][0](waypoint);
// route()[0][0]() is now 'IDK'
// route()[0][1] and route()[0][2] should remain undefined, waiting for users to manually input coordinates
// route()[0][4] should be false (invalid waypoint)

I read the documentation and there is an extend function, but I don't really understand it. The challenge right now is how to limit those automatic fill-in functions to a specific array (waypoint input field) instead of (like Problem #1) to the entire data table of input.

I would greatly appreciate if anybody could help, since the route is the most important feature of the entire project.

Upvotes: 1

Views: 761

Answers (3)

Harry Xue
Harry Xue

Reputation: 13

Hi user3297291, thank you for your kind help! Based on your suggestion, I was able to complete the function:

var Route = function () {

    var self = this;

    // Waypoint name
    var _fix = ko.observable();
    self.fix = ko.pureComputed({
        read: function () {
            return _fix();
        },
        write: function (val) {
            _fix(val);

            var coords = get.waypoint(val);
            var isValid = coords[0] && coords[1];

            self.lat(coords[0], isValid);
            self.lon(coords[1], isValid);
            self.info(coords[2]);
        }
    });

    // Latitude
    var _lat = ko.observable();
    self.lat = ko.pureComputed({
        read: function () {
            return _lat();
        },
        write: function (val, isValid) {
            _lat(val);
            self.valid(isValid ? true : false);
        }
    });

    // longitude
    var _lon = ko.observable();
    self.lon = ko.pureComputed({
        read: function () {
            return _lon();
        },
        write: function (val, isValid) {
            _lon(val);
            self.valid(isValid ? true : false);
        }
    });

    // Is waypoint valid
    self.valid = ko.observable(false);

    // Waypoint info
    self.info = ko.observable();

};

Upvotes: 0

user3297291
user3297291

Reputation: 23372

You should really use objects rather than arrays. It makes everything much easier to read and understand, and will greatly help debugging.

var Route = function() {
  this.waypointName = ko.observable();

  this.lat = ko.observable();
  this.lon = ko.observable();

  this.altitudeRestriction = ko.observable();
  this.isValid = ko.observable(false);
  this.waypointInfo = ko.observable('');
};

Like you already figured out, you can now use this by calling new Route(). You'll solve issue 1 and have code that's easier to read and mantain. The right foundation to solve issue 2:

Because you now have a clearly defined model, you can start defining relations between the properties by using subscribe or computed. You want to change the waypointName property and have other properties automatically update:

var Route = function() {
  this.waypointName = ko.observable();

  // Automatically updates when you set a new waypoint name
  var coords = ko.pureComputed(function() {
    return getWaypoint(this.waypointName());
  }, this);

  // Check if we got correct coords
  this.isValid = ko.pureComputed(function() {
    return coords().length === 2;
  }, this);

  // Auto-extract lat from coords, null if invalid
  this.lat = ko.pureComputed(function() {
    return this.isValid() 
      ? coords()[0]
      : null;
  }, this);

  // Auto-extract lat from coords, null if invalid
  this.lon = ko.pureComputed(function() {
    return this.isValid() 
      ? coords()[1]
      : null;
  }, this);
};

You now have a default Route with isValid: false, lat: null, lon:null and when you set the waypointName to a string value, like route.waypointName("WAATR"), all properties will automatically update.

Upvotes: 1

Jesper Jensen
Jesper Jensen

Reputation: 895

Question 1: This is rather an javascript related problem, than a knockoutjs. You are push-ing a reference to the same object again and again, and thereby making your observableArray contain multiple references to same object. You should change your code, to use a factory function instead:

var DEFAULT_ROUTE = function(){
  return [
    ko.observable(), // Waypoint Name
    ko.observable(), // Lat.
    ko.observable(), // Lon.
    ko.observable(), // Altitude Restriction
    ko.observable(false), // Waypoint isValid
    ko.observable('') // Waypoint information
  ];
};

And then pushing:

route.push(DEFAULT_ROUTE());

This way you add a new object each time.

Upvotes: 0

Related Questions