Knockout.js dynamically selecting template error: "Cannot find template with ID''" - javascript

My target is to show "noSets" template if observableArray length less 0 and render template with item details - "showSets", if observableArray length greater 0. I'd like to use templates for this purpose, but FireBug show error: Cannot find template with ID "templatename".
Here is ViewModel:
function SetsViewModel() {
var self = this;
self.usersets = ko.observableArray();
self.getTemplate = function () {
return self.usersets().length > 0 ? "showSets" : "noSets";
}
}
$(document).ready(function () {
ko.applyBindings(new SetsViewModel(), document.getElementById('user_sets'));
});
And here is HTML markup:
<div data-bind="template: { name: $root.getTemplate, foreach: usersets }" id="user_sets">
<script type="text/html" id="noSets">
<p>You do not have items yet.</p>
</script>
<script type="text/html" id="showSets">
<div class="block">
<input type="hidden" data-bind="value: $data.SetId" />
<div class="fav" data-bind="css: { fullop: $data.IsFavorite == true }">
<img alt="" src="img/fav.png" data-bind="click: $root.setFavorite">
</div>
<div>
<img alt="" data-bind="attr: { src: $data.SetImg }">
</div>
<div class="txt">
<h3 data-bind="text: $data.SetName, click: $root.go"></h3>
<p><span data-bind="text: $data.ItemsNumber + ' вещей,'"></span><span data-bind=" text: ' общая цена ' + $data.SetPrice + ' руб'"></span></p>
</div>
</div>
</script>
</div>
How can I fix it?

You can declare the templates outside the div that you're binding to as a work around. As #JeffMercado states:
The actual problem was that since the user_sets uses a template binding, the body is discarded (and the templates along with it)
function SetsViewModel() {
var self = this;
self.usersets = ko.observableArray([{SetId: 1, SetName: 'Name 1'}]);
self.getTemplate = function () {
return self.usersets().length > 0 ? "showSets" : "noSets";
}
}
$(document).ready(function () {
ko.applyBindings(new SetsViewModel(), document.getElementById('user_sets'));
});
<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="noSets">
<p>You do not have items yet.</p>
</script>
<script type="text/html" id="showSets">
<div class="block">
<input data-bind="value: $data.SetId" />
<div class="txt">
<h3 data-bind="text: 'Set name:' + $data.SetName"></h3>
</div>
</div>
</script>
<div data-bind="template: { name: getTemplate, foreach: usersets }" id="user_sets">
</div>

I think named templates are potentially the wrong approach to this specific issue - they're more designed for when you want a different template for each item, which isn't the case here. Instead, it would be more appropriate to show an entirely different div when there are 0 items:
<div data-bind="visible: usersets().length == 0">
You have no sets
</div>
<div data-bind="visible: usersets().length > 0, foreach: usersets" id="user_sets">
<div class="block">
<input type="hidden" data-bind="value: $data.SetId" />
<div class="fav" data-bind="css: { fullop: $data.IsFavorite == true }">
<img alt="" src="img/fav.png" data-bind="click: $root.setFavorite">
</div>
<div>
<img alt="" data-bind="attr: { src: $data.SetImg }">
</div>
<div class="txt">
<h3 data-bind="text: $data.SetName, click: $root.go"></h3>
<p><span data-bind="text: $data.ItemsNumber + ' вещей,'"></span><span data-bind=" text: ' общая цена ' + $data.SetPrice + ' руб'"></span></p>
</div>
</div>
</div>

Related

Knockout JS show hide div based on page URL

I want to split two different section based on page URL. I'm not aware how to do it using knockout.
The below example I have tried so far.
<!-- ko if: attr: { href: https://stackoverflow.com } -->
<p>Div 1</p>
<!-- /ko -->
<!-- ko if: attr: { href: https://getbootstrap.com } -->
<p>Div 2</p>
<!-- /ko -->
Any clarification please drop a comment. Thanks in Advance.
You can use the template system to perform this kind of switch :
ko.applyBindings({
stackoverflowData : {
totalQuestions : 321
},
bootstrapData : {
version : '5.0.2'
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.1/knockout-latest.js"></script>
<div data-bind="template: { name: 'stackoverflow-template', data: stackoverflowData }"></div>
<hr />
<div data-bind="template: { name: 'bootstrapData-template', data: bootstrapData }"></div>
<script type="text/html" id="stackoverflow-template">
stackoverflow specific view <br />
Questions: <span data-bind="text: totalQuestions"></span>
</script>
<script type="text/html" id="bootstrapData-template">
bootstrapData specific view <br />
Version <span data-bind="text: version"></span>
</script>
If you want something dynamic you can create a function that returns the template to use based on the viewmodel.
function getTemplate(url) {
// use reg ex
if (url == 'https://stackoverflow.com')
return 'stackoverflow';
if (url == 'https://getbootstrap.com')
return 'bootstrap';
return null;
}
var websites = [{
url: 'https://stackoverflow.com',
specificData: {
totalQuestions: 321
}
},
{
url: 'https://getbootstrap.com',
specificData: {
version: '5.0.2'
}
}
];
websites.map(function(website) {
website.template = ko.computed(function() {
return getTemplate(this.url);
}, website);
return website;
})
ko.applyBindings({
websites: websites
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.1/knockout-latest.js"></script>
<div data-bind="foreach: websites">
<div data-bind="template: { name: (template()+'-template'), data: specificData }">
<hr />
</div>
</div>
<script type="text/html" id="stackoverflow-template">
stackoverflow specific view <br /> Questions: <span data-bind="text: totalQuestions"></span>
</script>
<script type="text/html" id="bootstrap-template">
bootstrapData specific view <br /> Version <span data-bind="text: version"></span>
</script>
To make this work, you'll need to bring the url into your viewmodel. You could do this with a simple custom url bindinghandler. When initialized, it stores the current url in the bound observable (in this case myUrl) and adds an event listener for storing a changed url (in this case I use hash change to be able to make the example work).
Now, in HTML you can show or hide divs based on this observable value. Have a look at the example below.
ko.bindingHandlers.url = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
valueAccessor()(location.href);
window.addEventListener('hashchange', function(ev) {
valueAccessor()(ev.newURL);
});
}
};
ko.applyBindings({
myUrl: ko.observable(),
showDiv1: function() {
window.location.hash = 'div1'
},
showDiv2: function() {
window.location.hash = 'div2'
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<p>
Current url: <em data-bind="text: myUrl"></em>
</p>
<button data-bind="click: showDiv1">Show Div 1</button>
<button data-bind="click: showDiv2">Show Div 2</button>
<div data-bind="url: myUrl">
<!-- ko if: myUrl() == "https://stacksnippets.net/js#div1" -->
<p>Div 1</p>
<p data-bind="text: myUrl"></p>
<!-- /ko -->
<!-- ko if: myUrl() == "https://stacksnippets.net/js#div2" -->
<p>Div 2</p>
<p data-bind="text: myUrl"></p>
<!-- /ko -->
</div>

How can I fetch two consecutive array objects simultaneously with ng-repeat

Let's say we have an array in our controller as follows:
$scope.elements = [
{
title: title-1,
desc: description-1
},
{
title: title-2,
desc: description-2
},
{
title: title-3,
desc: description-3
},
{
title: title-4,
desc: description-4
}
]
I wish to loop through the array so I can place the elements as follows:
<div class="row">
<div class="col-sm-6">
{{ elements[0].title }}
{{ elements[0].desc }}
</div>
<div class="col-sm-6">
{{ elements[1].title }}
{{ elements[1].desc }}
</div>
</div>
<div class="row">
<div class="col-sm-6">
{{ elements[2].title }}
{{ elements[2].desc }}
</div>
<div class="col-sm-6">
{{ elements[3].title }}
{{ elements[3].desc }}
</div>
</div>
...and so on.
This could be achieved if we could fetch two consecutive elements simultaneously via an ng-repeat and pass it to the directive. Can this be done? Also if so, how would the fetched array objects be handled inside the directive?
I'd use the $index property to do this. The hiding with ng-if is a little inefficient, if this is a huge repeat, you might want a more elegant solution.
<div class="row" ng-repeat="element in elements" ng-if="$index <= elements.length /2">
<div class="col-sm-6">
{{ elements[$index*2].title }}
{{ elements[$index*2].desc }}
</div>
<div class="col-sm-6" ng-if="elements[$index*2 + 1]"> //ng-if for if you want to remove last element if odd array.
{{ elements[$index*2 + 1].title }}
{{ elements[$index*2 + 1].desc }}
</div>
</div>
Seems like this would be as simple as creating another array to iterate over
$scope.TemplateArray = [
[0,1],
[2,3],
etc...
]
Then use this for your ng-repeat
array in TemplateArray
<div class="row">
<div class="col-sm-6">
{{ elements[array[0]].title }}
{{ elements[array[0]].desc }}
</div>
<div class="col-sm-6">
{{ elements[array[1]].title }}
{{ elements[array[1]].desc }}
</div>
</div>
Probably the best way to do this is going to be to pre-process your array into groups of however many columns you have and then ng-repeat over that array. So pre-process your elements into something that looks like:
$scope.processedElements = [
[{title:'1',desc:'1d'},{title:'2',desc:'2d'},{title:'3',desc:'3d'},{title:'4',desc:'4d'}],
[{title:'5',desc:'5d},....... ]
];
and then
<div ng-repeat="item in processedElements">
{{item[0].title}} {{item[1].title}} // etc. etc
</div>
I think this can solve your problem to a limit
var app = angular.module('plunker', []);
app.controller('example', function($scope) {
$scope.limit = 2;
$scope.elements = [];
$scope.$watch('limit', function() {
$scope.elements = elements;
});
elements = [{
title: 'title-1',
desc: 'description-1'
}, {
title: 'title-2',
desc: 'description-2'
}, {
title: ' title-3',
desc: 'description-3'
}, {
title: 'title-4',
desc: 'description-4'
}];
});
<!DOCTYPE html>
<html ng-app="plunker">
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<script>
document.write('<base href="' + document.location + '" />');
</script>
<link rel="stylesheet" href="style.css" />
<script data-require="angular.js#1.4.x" src="https://code.angularjs.org/1.4.8/angular.js" data-semver="1.4.8"></script>
<script src="app.js"></script>
</head>
<body>
<div ng-controller="example">
<input type="number" ng-model="limit">
<div class="row">
<div class="col-sm-6" ng-repeat="el in elements" ng-if="$index < limit">
{{el.title}} {{el.desc}}
</div>
</div>
<p>seperator</p>
<div class="row">
<div class="col-sm-6" ng-repeat="el in elements" ng-if="$index >= limit">
{{el.title}} {{el.desc}}
</div>
</div>
</div>
</body>
</html>

Undefined property knockout.js + mvc

I have a masterDetails view that I populate with some data from a db (it populates fine). I added a button to the master details view, to add a step to my workflow.
My Viewmodel:
/// <reference path="_references.js" />
var viewModel = function (data) {
var self = this;
self.SelectedWorkflow = ko.observable({
Steps: ko.observableArray([]),
Name: ko.observable("")
});
self.Workflows = ko.observableArray(data);
self.addStep = function() {
self.Steps.push(new Step(SelectedWorkflow, "Assignment here", "01/01/2014", "dd:mm:ss", "mail"));
};
};
function Step(workflow,assignment, enddate, reminder, mailaddresses, type) {
var self = this;
self.Workflow = workflow;
self.StepNumber = 0;
self.Assignment = assignment;
self.Enddate = enddate;
self.Reminder = reminder;
self.MailAddresses = mailaddresses;
self.Type = type;
};
/// <reference path="workflowdetails-vm.js" />
$(document).ready(function () {
$.ajax({
url: "/WorkflowDetails/Index/",
type: "POST",
data: {},
success: function (data) {
var workflowlist = ko.mapping.fromJS(data.Workflows);
vm = new viewModel(workflowlist);
ko.applyBindings(vm);
}
});
$(".right-aligned-section").hide();
});
$(document).delegate(".show-details", "click", function () {
$(".right-aligned-section").fadeIn();
var workflow = ko.dataFor(this);
vm.SelectedWorkflow(workflow);
});
My View:
<div class="left-aligned-section">
<ul data-bind="foreach: Workflows()">
<li>
<div class="workflow-item-border">
<div>
<label data-bind="text: Name"></label>
</div>
<div>
<label data-bind="text: StartDate"></label>
</div>
<div>
Show Details
</div>
</div>
</li>
</ul>
</div>
<div class="right-aligned-section" data-bind="with: SelectedWorkflow">
<div class="steps-header">
<div class="left-aligned-div"><strong>Steps for </strong></div>
<div class="left-aligned-div" data-bind="text: Name"></div>
</div>
<button data-bind="click: addStep">add step</button>
<ul data-bind="foreach: Steps">
<li>
<div class="step-item-border">
<div>
<div class="step-label">Stepnumber: </div>
<div style="font-weight: bold" data-bind="text: StepNumber"></div>
</div>
<div>
<div class="step-label">Assignment: </div>
<div style="font-weight: bold" data-bind="text: Assignment"></div>
</div>
<div>
<div class="step-label">Mails: </div>
<div style="font-weight: bold" data-bind="text: MailAddresses"></div>
</div>
<div>
<div class="step-label">End Date: </div>
<div style="font-weight: bold" data-bind="text: Enddate"></div>
</div>
<div>
<div class="step-label">Type: </div>
<div style="font-weight: bold" data-bind="text: Type"></div>
</div>
</div>
</li>
</ul>
</div>
When I press the button - nothing happens. The error I receive is:
ReferenceError: Steps is not defined
I know what it means, but I'm not proficient enough in web development to actually fix it. Please help.
Probably this would work:
var viewModel = {};
viewModel.SelectedWorkflow = {
Steps: ko.observableArray([]),
Name: ko.observable("")
};
viewModel.Workflows = ko.observableArray(data);
viewModel.addStep = function () {
viewModel.SelectedWorkflow.Steps.push(
new Step(SelectedWorkflow, "Assignment here", "01/01/2014", "dd:mm:ss", "mail"));
}
}
You have forgotten about SelectedWorkflow.Steps I think...
Looks like you forgot a this before the Steps.push....
Edit: I have made some further changes to the click handler function
var viewModel = function (data) {
this.SelectedWorkflow = ko.observable({
Steps: ko.observableArray([]),
Name: ko.observable("")
});
this.Workflows = ko.observableArray(data);
this.addStep = function (selectedWorkflow) { // the current context is passed in from the click data-bind
selectedWorkflow.Steps.push(new Step(selectedWorkflow, "Assignment here", "01/01/2014", "dd:mm:ss", "mail"));
}
}
Edit: Fixed code

As binding knockout undefined nested foreach

So I have html:
<div class="body" data-bind="foreach: { data: Sections }">
<span data-bind="text: '(' + OrderQualifier + ') ' + Text">
</span>
<p data-bind="foreach: { data: Children, as: 'child' }">
<fieldset class="section-edit" data-bind="visible: IsEditing">
<input type="text" data-bind="attr: {value: child.EditedText}" /><!-- child is undefined here even though I have it as my as binding on the above foreach-->
<button data-bind="event: {click: $root.addEdit}">Submit</button>
</fieldset>
</p>
</div>
I tried to do this without the as binding but it was pulling the value from the parent section which also has an EditedText property and get the same result using $data.
The data (Sections) i'm trying to bind looks like:
[
{
"SectionID":1,
"Text":"Parent text",
"Html":null,
"OrderQualifier":"1",
"IsUserCreated":false,
"Children":[
{
"SectionID":2,
"Text":"Child text",
"Html":null,
"OrderQualifier":"1",
"IsUserCreated":false,
"EditCount":0,
"ExplanationCount":0,
"EvidenceCount":0,
"IsEditing":true,
"EditedText":"Child text"
}
],
"EditCount":0,
"ExplanationCount":0,
"EvidenceCount":0,
"IsEditing":true,
"EditedText":"Parent text"
}
]
Any ideas?
Or use virtual elements if you want to keep using the <p> tags
<!-- ko foreach: { data: Children, as: 'child' } -->
<p>
...
</p>
<!-- /ko -->
http://jsfiddle.net/cvtw3b2h/2/
Pretty weird, but try changing
<p data-bind="foreach: { data: Children, as: 'child' }">
....
</p>
to
<div data-bind="...">...</div>
http://jsfiddle.net/cvtw3b2h/1/

Keep track of tabs with knockoutjs + twitter bootstrap

I'm trying to keep track of the selected tab in the view model but I can't seem to make it work.
In the following code when you click a tab the header will update correctly but the content of the tab is not displayed. If you remove , click: $parent.selectSection then the contents are shown but the header does not update.
Now if you remove the data-bind="css: { active: selected }" from the li then it seems to work when you click the tabs but the button to select the second tab doesn't.
How can I make this work?
See: http://jsfiddle.net/5PgE2/3/
HTML:
<h3>
<span>Selected: </span>
<span data-bind="text: selectedSection().name" />
</h3>
<div class="tabbable">
<ul class="nav nav-tabs" data-bind="foreach: sections">
<li data-bind="css: { active: selected }">
<a data-bind="attr: { href: '#tab' + name }
, click: $parent.selectSection" data-toggle="tab">
<span data-bind="text: name" />
</a>
</li>
</ul>
<div class="tab-content" data-bind="foreach: sections">
<div class="tab-pane" data-bind="attr: { id: 'tab' + name }">
<span data-bind="text: 'In section: ' + name" />
</div>
</div>
</div>
<button data-bind="click: selectTwo">Select tab Two</button>
JS:
var Section = function (name) {
this.name = name;
this.selected = ko.observable(false);
}
var ViewModel = function () {
var self = this;
self.sections = ko.observableArray([new Section('One'),
new Section('Two'),
new Section('Three')]);
self.selectedSection = ko.observable(new Section(''));
self.selectSection = function (s) {
self.selectedSection().selected(false);
self.selectedSection(s);
self.selectedSection().selected(true);
}
self.selectTwo = function() { self.selectSection(self.sections()[1]); }
}
ko.applyBindings(new ViewModel());
There are several ways that you can handle this either using bootstrap's JS or by just having Knockout add/remove the active class.
To do this just with Knockout, here is one solution where the Section itself has a computed to determine if it is currently selected.
var Section = function (name, selected) {
this.name = name;
this.isSelected = ko.computed(function() {
return this === selected();
}, this);
}
var ViewModel = function () {
var self = this;
self.selectedSection = ko.observable();
self.sections = ko.observableArray([
new Section('One', self.selectedSection),
new Section('Two', self.selectedSection),
new Section('Three', self.selectedSection)
]);
//inialize to the first section
self.selectedSection(self.sections()[0]);
}
ko.applyBindings(new ViewModel());
Markup would look like:
<div class="tabbable">
<ul class="nav nav-tabs" data-bind="foreach: sections">
<li data-bind="css: { active: isSelected }">
<a href="#" data-bind="click: $parent.selectedSection">
<span data-bind="text: name" />
</a>
</li>
</ul>
<div class="tab-content" data-bind="foreach: sections">
<div class="tab-pane" data-bind="css: { active: isSelected }">
<span data-bind="text: 'In section: ' + name" />
</div>
</div>
</div>
Sample here: http://jsfiddle.net/rniemeyer/cGMTV/
There are a number of variations that you could use, but I think that this is a simple approach.
Here is a tweak where the active tab used the section name as a template: http://jsfiddle.net/rniemeyer/wbtvM/

Categories

Resources