0

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/

2 Answers 2

4

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.

Sign up to request clarification or add additional context in comments.

2 Comments

I usually do !!$data.Value, but a good solution to OP problem
IMO, this is not the best option. this looks like "procedural code", but @TSV's answer looks more like "declarative code" which is what I expect to find in a KO view. OTOH, the other answer can be expanded with the use of components to allow not only to change the view, but also the scripting (javascript for view model) using a similar technique
2

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.

2 Comments

Two suggestions: 1) I'd give the templates more significant names. Why don't call them "textbox", "checkbox", "something"? 2) If your question has different related functionality, you could extend this idea and use components instead of templates. Templates only defint the "view" part. But components can also add some scripting.
@JotaBe I see and share your point. I used first names came into my mind. It can be beautified in the production code.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.