Jack
Jack

Reputation: 15872

Looping over an array of different objects in Knockout - Binding Error

I'm trying to render a different section of a page and apply the appropriate bindings for different items contained within a single array. Each item in the array could have a different structure / properties.

As an example we could have 3 different question types, the data associated with that question could be in a different format.

JSON Data

var QuestionTypes = { Textbox: 0, Checkbox: 1, Something: 2 }

var QuestionData = [
    {
        Title: "Textbox",
        Type: QuestionTypes.Textbox,
        Value: "A"

    },
    {
        Title: "Checkbox",
        Type: QuestionTypes.Checkbox,
        Checked: "true"
    },
    {
        Title: "Custom",
        Type: QuestionTypes.Something,
        Something: { SubTitle : "Something...", Description : "...." }
    }
];

JavaScript

$(document).ready(function(){
    ko.applyBindings(new Model(QuestionData), $("#container")[0]);
})

function QuestionModel(data){
    var self = this;

  self.title = ko.observable(data.Title);  
  self.type = ko.observable(data.Type);

  self.isTextbox = ko.computed(function(){
    return self.type() === QuestionTypes.Textbox;
  });

  self.isCheckbox = ko.computed(function(){
    return self.type() === QuestionTypes.Checkbox;
  });

  self.isSomething = ko.computed(function(){
    return self.type() === QuestionTypes.Something;
  });
}

function Model(data){
    var self = this;

  self.questionData = ko.observableArray(ko.utils.arrayMap(data, function(question){
    return new QuestionModel(question); 
  }));  
}

HTML

<div id="container">  
  <div data-bind="foreach: questionData">
    <h1 data-bind="text: title"></h1>

    <!-- ko:if isTextbox() -->
    <div data-bind="text: Value"></div>
    <!-- /ko -->

    <!-- ko:if isCheckbox() -->
    <div data-bind="text: Checked"></div>
    <!-- /ko -->

    <!-- ko:if isSomething() -->
    <div data-bind="text: Something">
      <h1 data-text: SubTitle></h1>
      <div data-text: Description></div>
    </div>
    <!-- /ko -->

  </div>
</div>

The bindings within the if conditions get applied whether the condition if true / false. Which causes JavaScript errors... as not all of the objects within the collection have a 'Value' property etc.

Uncaught ReferenceError: Unable to process binding "foreach: function (){return questionData }"
Message: Unable to process binding "text: function (){return Value }"
Message: Value is not defined

Is there any way to prevent the bindings from being applied to the wrong objects?

Conceptual JSFiddle: https://jsfiddle.net/n2fucrwh/

Upvotes: 0

Views: 667

Answers (2)

TSV
TSV

Reputation: 7641

First of all your "QuestionModel" has no corresponding properties: you create "type" and "title" fields only from incoming data.

Proposed solution:

You can use different templates for different data types. I've updated your fiddle:

var QuestionTypes = { Textbox: 0, Checkbox: 1, Something: 2 }

var QuestionData = [
    {
        Title: "Textbox",
        Type: QuestionTypes.Textbox,
        templateName: "template1",
        Value: "A"
        
    },
    {
        Title: "Checkbox",
        Type: QuestionTypes.Checkbox,
        templateName: "template2",
        Checked: "true"
    },
    {
        Title: "Custom",
        Type: QuestionTypes.Something,
        templateName: "template3",
        Something: "Something"
    }
];

$(document).ready(function(){
	ko.applyBindings(new Model(QuestionData), $("#container")[0]);
})

function QuestionModel(data){
	var self = this;
  
  self.title = ko.observable(data.Title);  
  self.type = ko.observable(data.Type);
  self.data = data;
}

function Model(data){
	var self = this;
	
  self.questionData = ko.observableArray(ko.utils.arrayMap(data, function(question){
  	return new QuestionModel(question);	
  }));  
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script type="text/html" id="template1">
    <div data-bind="text: Value"></div>
</script>
<script type="text/html" id="template2">
    <div data-bind="text: Checked"></div>
</script>
<script type="text/html" id="template3">
    <div data-bind="text: Something"></div>
</script>


<div id="container">  
  <div data-bind="foreach: questionData">
    <h1 data-bind="text: title"></h1>
    <!-- ko with: data -->
      <!-- ko template: templateName -->
      <!-- /ko -->
    <!-- /ko -->
  </div>
</div>

In the above edition you can get rid of "QuestionTypes".

Update 1

Of course, you can calculate template name from the question type.

Update 2

Explanation of the cause of errors. If you check original view model:

function QuestionModel(data){
    var self = this;

  self.title = ko.observable(data.Title);  
  self.type = ko.observable(data.Type);

  self.isTextbox = ko.computed(function(){
    return self.type() === QuestionTypes.Textbox;
  });

  self.isCheckbox = ko.computed(function(){
    return self.type() === QuestionTypes.Checkbox;
  });

  self.isSomething = ko.computed(function(){
    return self.type() === QuestionTypes.Something;
  });
}

You can see, that "QuestionModel" has following properties: "title", "type", "isTextbox", "isCheckbox" and "isSomething".

So, if you will try bind template to "Value", "Checked" or "Something" you will get an error because view model does not contain such a property.

Changing binding syntax to the

<div data-bind="text: $data.Value"></div>

or something similar eliminates the error, but always will display nothing in this case.

Upvotes: 2

shu
shu

Reputation: 1956

Please check out the Updated Fiddler without changing your code.Only added $data in side the loop

https://jsfiddle.net/n2fucrwh/3/

 <!-- ko:if isTextbox() -->
<div data-bind="text: $data.Value"></div>
<!-- /ko -->

<!-- ko:if isCheckbox() -->
<div data-bind="text: $data.Checked"></div>
<!-- /ko -->

<!-- ko:if isSomething() -->
<div data-bind="text: $data.Something"></div>
<!-- /ko -->

Inside the loop you need provide $data.Value.It seems to Value is the key word in knockout conflicting with the binding.

Upvotes: 4

Related Questions