knockout data-bind html with having inner data-bind - javascript

Here is my html looks like:
<div class="xyz-template" data-bind="html:myViewModel().htmlTemplate"></div>
And I would like to have data-bind in htmlTemplate itself:
here is my knockout:
function myViewModel() {
self.htmlTemplate = ko.observable();
self.initialize = function ()
{
self.htmlTemplate(`
<p data-deId="de-box-5" data-bind="html: myViewModel().getDynamicContent()">
Initial content
</p>
`);
};
self.getDynamicContent = function()
{
return "dynamic content";
};
};
Well the return value is
Initial content
How can I have inner bind in binding html?

Whole trick is around rebinding your view. ko does not allow to call applyBindings twice on the same node, so you need to cleanNode from bindings and apply it to element.
Here is the working scenario:
function myViewModel() {
let self = this
self.htmlTemplate = ko.observable();
self.initialize = function ()
{
self.htmlTemplate(`
<p data-deId="de-box-5" data-bind="html: myViewModel().getDynamicContent()">
Initial content
</p>
`);
};
self.getDynamicContent = function()
{
return "dynamic content";
};
return self;
};
const vm = myViewModel();
const root = $('.xyz-template');
ko.applyBindings(vm, root[0]);
vm.initialize();
const templateHolder = root.find('p');
ko.cleanNode(templateHolder[0]);
ko.applyBindings(vm, templateHolder[0]);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div class="xyz-template" data-bind="html: htmlTemplate"></div>
Keep in mind that in your particular case you cannot rebind root because everytime you initialize binding html binding from xyz div kicks in, so again <p> is somehow detached
Here you will also find better ways to solve that problem knockout custom html binding
Code which I posted is just to show the concept

Related

Validate javascript inside attributes

I am using Knockout a lot and often times I have to write scripts inside the data-bind attributes. Is there any validation tools that I can use on these markup files to validate the javascript inside data-bind attributes? Would be nice if there is a grunt plugin.
There probably isn't (a prominent) one, because it's not common to have a lot of complex logic inside your view. With MVVM-like approaches it works best if you keep the View rather plain, and write out logic in your ViewModel where you can unit test it.
So do not do this:
var ViewModel = function() {
var self = this;
self.isCurrent = ko.observable(false);
self.item = ko.observable({ id: 42 });
}
ko.applyBindings(new ViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<!-- Don't do this! -->
<div data-bind="visible: !isCurrent() && !!item()">
Showing something!
</div>
Instead, do this:
var ViewModel = function() {
var self = this;
self.isCurrent = ko.observable(false);
self.item = ko.observable({ id: 42 });
self.shouldShowItem = ko.computed(function() {
return !self.isCurrent() && !!self.item();
});
}
ko.applyBindings(new ViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<!-- Don't do this! -->
<div data-bind="visible: shouldShowItem">
Showing something!
</div>
Because that would allow you to unit test the shouldShowItem logic, e.g. with QUnit:
QUnit.test("shouldShowItem is false if not isCurrent and item not set", function(assert) {
var vm = new ViewModel();
vm.isCurrent(false);
vm.item(null);
assert.strictEqual(vm.shouldShowItem(), false);
});
Bottom line, if you find yourself writing out lots of logic inside your view, you probably need to move some of it to your view models and make it testable.

how to update ko.observable text on click event?

This should be simple but certainly I'm getting it wrong.
How to update ko.observable text on the click event?
I could do this using "afterkeydown" or "keypress" but not in case click event.http://knockoutjs.com/documentation/value-binding.html
<span data-bind="text: Count"></span>
<button data-bind="click: update">Update</button>
function MyViewModel() {
var self = this;
self.Count = ko.observable("0");
self.update = function() {
self.Count = ko.observable("1");
}
}
http://jsfiddle.net/EBsj5/
You should change it like a function.
self.update = function() {
self.Count("1");
}
Demo: http://jsfiddle.net/EBsj5/1/
Any basic tutorial will explain this to you, so I recommend watching a few.
When you are setting the value of a Knockout observable, you need to use parans like a function and pass in the new value.
<span data-bind="text: Count"></span>
<button data-bind="click: update">Update</button>
function MyViewModel() {
var self = this;
self.Count = ko.observable("0");
self.update = function() {
self.Count("1");
}
}
This will update the observable to "1" in this case. You don't need to call ko.observable() again because you have already created the observable, you are simply trying to 'set' the value with the setter function.

Remove knockout js bindings on cloned element

I am using the knockout js template binding functionality to render a collection of items to an element:
<script type="text/javascript">
ko.applyBindings(new function () {
this.childItems = [{ Text: "Test", ImageUrl: "Images/Image.png" }];
});
</script>
<script type="text/html" id="template">
<div class="childItem" data-bind="attr: { title: Text }">
<img data-bind="attr: { src: ImageUrl }" />
</div>
</script>
<div class="childSelector" data-bind="template: { name: 'template', foreach: childItems }">
</div>
When clicked, the child items are cloned and placed into another element:
$(".childSelector").on("click", ".childItem", function () {
var clone = $(this).clone()[0];
ko.cleanNode(clone);
$(".targetNode").append(clone);
});
The problem is that when the source data changes and the template is re-bound to the new data, the following error is thrown:
Uncaught Error: Unable to parse bindings. Message: ReferenceError:
Text is not defined; Bindings value: attr: { title: Text }
I had found another post that suggested using ko.cleanNode(element) to remove knockout's bindings, however this has not resolved the issue in my case.
Is there a way to remove knockout's bindings on a cloned element to prevent this error when re-binding? If not I'll just "manually" clone the element by extracting the required data from the clicked element.
Here is a simple example of what I'm doing
You can remove all knockout bindings from an element by traversing the DOM and removing the data-bind attributes and knockout comments.
Use removeDataBindings(clone); but first clean the node with ko.cleanNode(clone) to clear any event handlers.
var commentNodesHaveTextProperty = document.createComment("test").text === "<!--test-->";
var startCommentRegex = commentNodesHaveTextProperty ? /^<!--\s*ko(?:\s+(.+\s*\:[\s\S]*))?\s*-->$/ : /^\s*ko(?:\s+(.+\s*\:[\s\S]*))?\s*$/;
var endCommentRegex = commentNodesHaveTextProperty ? /^<!--\s*\/ko\s*-->$/ : /^\s*\/ko\s*$/;
function isStartComment(node) {
return (node.nodeType == 8) && (commentNodesHaveTextProperty ? node.text : node.nodeValue).match(startCommentRegex);
}
function isEndComment(node) {
return (node.nodeType == 8) && (commentNodesHaveTextProperty ? node.text : node.nodeValue).match(endCommentRegex);
}
function traverseNode(node, func) {
func(node);
node = node.firstChild;
while (node) {
traverseNode(node, func);
node = node.nextSibling;
}
}
function removeDataBindings(element) {
var koComments = [];
traverseNode(element, function (node) {
if (isStartComment(node) || isEndComment(node)) {
koComments.push(node);
return;
}
//remove the 'data-bind' attributes
if (node.nodeType === 1) { //ELEMENT_NODE
node.removeAttribute('data-bind');
}
});
//remove Knockout binding comments
for (i = 0; i < koComments.length; i++) {
node = koComments[i];
if (node.parentNode) {
node.parentNode.removeChild(node);
}
}
}
Oliver, using jQuery to clone elements bound to knockout like this is not a good idea. You should be using data-binding for the targetNode. If you haven't yet done so, its a good idea to go through the Knockout Tutorials to get a good understanding of the basic uses.
If you are trying to keep a list of items, with a clone button, here is a dead simple fiddle using nothing but Knockout to do so. If you are trying to do something else, let me know; your question isn't entirely clear on your goal.
HTML:
<div data-bind="foreach: items">
<span data-bind="text: $data"></span>
<button data-bind="click: $parent.clone">Clone</button></br>
</div>
JS:
var ViewModel = function(data) {
var self = this;
self.items = ko.observableArray(data);
self.clone = function(item) {
//The ko.toJS here is a handy copy tool for viewModels
//It isn't necessary for simple arrays like this one
//But I included it because for an array of objects, you will want to use it
self.items.push(ko.toJS(item));
};
};

Is there a way to set the page title by data-binding using Knockout.js?

I have a viewModel with a Title property. I'd like to set the page title using that property. Here's what I tried already, which didn't work:
<html>
<head>
<title data-bind="text: Title"></title>
</head>
<body>
<span data-bind="text: Title"/> <!-- this displays the title properly -->
</body>
The browser title is blank/default instead of the value of my Title property.
Try giving your html element an id
<html id="htmlTop" xmlns="http://www.w3.org/1999/xhtml">
and applying your viewModel to it
ko.applyBindings(viewModel, document.getElementById("htmlTop"));
EDIT
This works for me; I just ran this page and the title said "Hello". Double check your code for typos.
<html id="htmlTop">
<head>
<title data-bind="text: title"></title>
<script type='text/javascript' src='jquery.min.js'></script>
<script type='text/javascript' src='knockout-1.2.1.js'></script>
<script type="text/javascript">
$(function () {
var viewModel = { title: "Hello" };
ko.applyBindings(viewModel, document.getElementById("htmlTop"));
});
</script>
</head>
<body>
</body>
</html>
Screenshot:
In my eyes, this situations begs for an observable subscription.
...
<title>{FALL BACK TEXT}</title>
...
View Model
ViewModel = function() {
var self = this;
self.PageTitle = ko.observable(null);
self.PageTitle.subscribe(function(newValue){ document.title = self.PageTitle() });
//change PageTitle to see your handy work in action
self.PageTitle("Hello World!");
};
EDIT: As an amendment to my previous answer, I'd like to put forth the following. Will my previous suggestion work? Yes, it works very nicely. However, DOM manipulation within the view model itself does not "exactly" follow the MVVM paradigm. The actual "best" approach would be to create a custom binding, which sets the document title on update of a particular observable.
...
<title data-bind="htmlDocumentTitle: PageTitle()">{FALLBACK TEXT}</title>
...
View Model
ViewModel = function() {
var self = this;
self.PageTitle = ko.observable(null);
self.init = function(){
self.PageTitle("My page title from an obersvable");
};
//init the viewmodel
self.init();
};
And finally our shiny custom binding to "listen" for changes to the observable (note the use of ONLY the update action)
ko.bindingHandlers.htmlDocumentTitle = {
update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
var fallbackTitle = "My document title";
var title = ko.unwrap(valueAccessor());
if(!title || title == null && title == "")
title = fallbackTitle;
document.title = title;
}
};
At first glance this solution may appear less scalable, but do keep in mind that we can create "pseudo-inheritance" with Knockout View Models using "call()".
You could create a custom binding handler which sets document.title on update, then add the binding to the body element.
You can use knockout subscribe:
function viewModel() {
var self = this;
self.title = ko.observable(null);
self.title.subscribe(function(newTitle) {
document.title = newTitle;
})
}
var vm = new viewModel();
ko.applyBindings(vm);
vm.title('Hello page');
As per #Douglas's suggestion, my solution was to add a hidden div somewhere in the body bound to a computed value:
<div data-bind="text: currentPageTitle()"></div>
Then, in the value computation, I set the document.title:
self.currentPageTitle = ko.computed(function() {
document.title = self.Title();
return self.Title();
}, this);
This works perfectly for me

View Events not firing on created elements?

Trying to create a todo example app to mess around with backbone. I cannot figure out why the click event for the checkbox of a task is not firing. Here is my code for the TaskCollection, TaskView, and TaskListView:
$(document).ready(function() {
Task = Backbone.Model.extend({});
TaskCollection = Backbone.Collection.extend({
model: 'Task'
});
TaskView = Backbone.View.extend({
tagName: "li",
className: "task",
template: $("#task-template").html(),
initialize: function(options) {
if(options.model) {
this.model = options.model
}
this.model.bind('change',this.render,this);
this.render();
},
events: {
"click .task-complete" : "toggleComplete"
},
render: function(){
model_data = this.model.toJSON();
return $(_.template(this.template, model_data));
},
toggleComplete: function() {
//not calling this function
console.log("toggling task completeness");
}
});
TaskListView = Backbone.View.extend({
el: $("#task-list"),
task_views: [],
initialize: function(options) {
task_collection.bind('add',this.addTask,this);
},
addTask: function(task){
task_li = new TaskView({'model' : task});
this.el.append(task_li.render());
this.task_views.push(task_li);
},
});
});
Template for the task:
<script type='text/template' id='task-template'>
<li class="task">
<input type='checkbox' title='mark complete' class='task-check' />
<span class='task-name'><%= name %></span>
</li>
</script>
I can't seem to figure out why the toggleComplete event will not fire for the tasks. how can I fix this?
The problem here is that the backbone events only set to the element of the view (this.el) when you create a new view. But in your case the element isn't used. So you have the tagName:li attribute in your view, which let backbone create a new li element, but you doesn't use it. All you return is a new list element created from your template but not the element backbone is creating, which you can access by this.el
So you have to add your events manually to your element created by your template using jQuery or add your template as innerHtml to your element:
(this.el.html($(_.template(this.template, model_data)))
Try changing the lines where you set your listeners using .bind() to use .live(). The important difference is .live() should be used when you want to bind listeners to elements that will be created after page load.
The newest version of jQuery does away with this bit of ugliness and simplifies the methods used to set event listeners.
Your event is binding to a class of .task-complete but the class on your checkbox is .task-check
Try modifying your render function to call delegateEvents() like so:
render: function(){
model_data = this.model.toJSON();
this.el = $(_.template(this.template, model_data));
this.delegateEvents();
return this.el;
},
You'd really be better off changing your template to not include the li and then return this.el instead of replacing it, but if you want the events to work you need to have this.el be the root element one way or another; delegateEvents() re-attaches the event stuff, so when you change this.el that should fix the issue.
#Andreas Köberle answers it correctly. You need to assign something to this.elto make events work.
I changed your template and your TaskView#render() function.
This JSFiddle has the changes applied.
New render function:
render: function(){
var model_data = this.model.toJSON();
var rendered_data = _.template(this.template, model_data);
$(this.el).html(rendered_data);
return this;
}
It is recommended that the render() returns this.
One line in your TaskListView#addTask function changes from this.el.append(task_li.render()); to this.el.append(task_li.render().el);.
Template change
Since we are using this.el in the render() function, we have to remove the <li> tag from the template.
<script type='text/template' id='task-template'>
<input type='checkbox' title='mark complete' class='task-complete' />
<span class='task-name'><%= name %></span>
</script>

Categories