Using knockout.js with custom template engine - javascript

I'm trying to get the timeline component of vis.js work together with knockout.js.
The timeline has a templates option which allows you to write custom HTML for each event on the timeline. In my case, it looks like this:
var options = {
... // other options
template: function(item) {
var html = '<b>' + item.subject + '</b>'+
'<p>' + item.owner.username + ' (' + item.format.name + ' for ' + item.channel.name + ')</p>' +
'<p><input type="checkbox"';
if (item.done !== null) {
html += "checked"
};
html += '></p>';
html += '<pre data-bind="text: $root"></pre>'; // http://www.knockmeout.net/2013/06/knockout-debugging-strategies-plugin.html
return html;
}
}
All data bindings are tested and OK, but I cannot think of a way to attach knockout behaviour to the template generated by the vis.js timeline library. As you see, even trying to print out the $root data doesn't do a thing.
How can I attach observables to this template?

The template property of the options for the timeline component allows you to provide an HTML element instead of an HTML string if you so wish. This could be one way to achieve what you are after. This way you could create the element, use knockout to apply bindings on the element (and its children) using the provided item as context and return that element to vis.js.
An example of doing this could use code similar to the following:
var templateHtml = '<div data-bind="text: content"></div>'
//Set the template to a custom template which lets knokcout bind the items
options.template = function(item){
//Create a div wrapper element to easily create elements from the template HTML
var element = document.createElement('div');
element.innerHTML = templateHtml;
//Let knockout apply bindings on the element with the template, using the item as data context
ko.applyBindings(item, element);
//Return the bound element to vis.js, for adding in the component
return element;
};
In a knockout world, it would of course be better to create a custom bindingHandler for the vis.js timeline component.
Therefore, here you also have a sample of doing similar using a custom bindingHandler for knockout (which is a very simple sample bindingHandler, not really supporting observable options or observableArray for data, but it does support observable values in the items).
ko.bindingHandlers.visTimeline = {
init: function(element, valueAccessor){
var unwrappedValue = ko.unwrap(valueAccessor());
var data = ko.unwrap(unwrappedValue.data);
var options = ko.unwrap(unwrappedValue.options);
if (options.templateId){
var templateId = ko.unwrap(options.templateId);
var templateHtml = document.getElementById(templateId).innerHTML;
//Set the template to a custom template which lets knokcout bind the items
options.template = function(item){
//Create a div wrapper element to easily create elements from the template HTML
var element = document.createElement('div');
element.innerHTML = templateHtml;
//Let knockout apply bindings on the element with the template, using the item as data context
ko.applyBindings(item, element);
//Return the bound element to vis.js, for adding in the component
return element;
};
}
//Apply the vis.js timeline component
new vis.Timeline(element, data, options);
//Let knockout know that we want to handle bindings for child items manually
return { controlsDescendantBindings: true };
}
};
var items = [
{ id: 1, content: 'item 1', counter: ko.observable(0), start: '2014-04-20'},
{ id: 2, content: 'item 2', counter: ko.observable(0), start: '2014-04-14'},
{ id: 3, content: 'item 3', counter: ko.observable(0), start: '2014-04-18'},
{ id: 4, content: 'item 4', counter: ko.observable(0), start: '2014-04-16', end: '2014-04-19'}
];
var viewModel = {
items: items
};
//Randomly increment the counters of the items, to see that the data is bound
setInterval(function(){
var randomItem = items[Math.floor(Math.random() * items.length)];
randomItem.counter(randomItem.counter() + 1);
}, 500);
ko.applyBindings(viewModel);
<!-- First, let's include vis.js and knockout -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/vis/3.12.0/vis.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/vis/3.12.0/vis.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<!-- This is the template we want to use for our items -->
<script type="text/html" id="myCustomTemplate">
<strong data-bind="text: id"></strong>. <span data-bind="text: content"></span>
<div data-bind="text: counter"></div>
</script>
<!-- And this is where we use our bindingHandler -->
<div data-bind="visTimeline: { data: items, options: { templateId: 'myCustomTemplate' } }"></div>
You can also see this snippet at http://jsbin.com/lecidaxobo/1/edit?html,js,output

Related

Show specified data from KnockoutJS on mouseover event jCanvas

I am bumped into another problem connected with KnockoutJS and jCanvas. My model and ViewModel:
eventsModel = function () {
var self = this;
self.events = ko.observableArray([]);
}
eventItemViewModel = function(o) {
var self = this;
self.BeginInMinutes = ko.observable(o.BeginInMinutes);
self.EventDuration = ko.observable(o.EventDuration);
self.Type = ko.observable(o.Type);
self.ReferenceNumber = ko.observable(o.ReferenceNumber);
self.FullDescription = ko.computed(function () {
var eventType = self.Type() == '0' ? 'Driving' : 'Resting';
var hour = Math.floor(self.BeginInMinutes() / 60);
var minutes = Math.floor(self.BeginInMinutes() % 60) < 10 ? '0' + Math.floor(self.BeginInMinutes() % 60) : Math.floor(self.BeginInMinutes() % 60);
return hour + ':' + minutes + " " + eventType + " " + self.EventDuration();
}, this);
};
var events = new eventsModel();
ko.applyBindings(events);
I think this should be enough for now. Basically, I want to show this FullDescription above my canvas, but the problem is that it is included in an array. In canvas I have some drawings and this all properties are connected with rectangles in canvas. I want to do something like: on mouseover event of rectangle in jCanvas I want to show fullDescription for example in plain text above the canvas.
I show some information in table using knockout data-bind foreach etc., but for now I want to show this one specified information from whole collection. I tried if binding but it wasn't working.
My canvas:
<canvas id="myCanvas" width="1000" height="300"></canvas>
And maybe my previous question have some valueable information: Knockout observablearray of observables with arrayMap function
I'm sure that it should be some simple way to get only specified field from an array.
Thank you.
This is actually pretty straightforward with Knockout. You simply need to put your canvas in a Knockout foreach, and then all the usual Javascript events are available as Knockout bindings (like mouseover). A simple example is this:
HTML:
<div data-bind="foreach: { data: items, afterRender: itemRendered}">
<span data-bind="text: description"></span><br />
<canvas data-bind="event: {mouseover: $parent.doSomething}, attr: { id: itemId }" style="background-color: blue"></canvas><br />
</div>
Javascript:
var MyViewModel = function () {
var self = this;
self.items = ko.observableArray(
[
{ itemId: 1, description: "Item #1" },
{ itemId: 2, description: "Item #2" },
{ itemId: 3, description: "Item #3" }
]
);
self.doSomething = function (selectedItem) {
alert("You hovered over " + selectedItem.description);
};
self.itemRendered = function (o, renderedItem) {
console.log("Initialize your jCanvas here for canvas id: MyCanvas"
+ renderedItem.itemId);
};
};
As you can see, the act of iterating through your data items in the observableArray actually attaches that item as a data context to each canvas, so that when you do some event on one of the rendered canvases it can be received in the handler function and you have access to all the properties and functions of that particular item. In this case I called the passed item "selectedItem".
Now, as far as hooking up jCanvas to your canvas tags, you can use the afterRender callback of the foreach binding, which will pass an array of DOM elements in the rendered item (which we can ignore for now), and the data item itself. We can take the id of that data item using the "attr" binding to attach it to the canvas, and then programmatically initialize each individual jCanvas in our itemRendered handler function.
This now gives you all the flexibility in the world to define how each canvas will be rendered (shape, color, etc.) and that can all be driven by the data in each individual item.
Here is a JSFiddle to try it out:
https://jsfiddle.net/snydercoder/wkcqr76L/
Also, reference the Knockout docs for "foreach" and "attr" bindings.

Render angular directives in Selectize.js items

I am using angular-selectize to use Selectize.js in my angular project.
To use custom items in Selectize.js selector, I am using Selectize.js' render option:
render: {
item: function(item, escape) {
var avatar = '<div>' +
'<span avatars="\'' + escape(item._id) +'\'" class="avatars">' +
'</span>' +
escape(item.nick) +
'</div>';
var compiledAvatar = $compile(avatar)($rootScope);
$timeout();
return compiledAvatar.html();
},
where avatars is a custom directive with asychronous behaviour
The problem is that the render.item function expects an HTML string as an output but:
There is no way of returning a rendered or "$compileed" HTML string in a synchronous way as expected by render.item method.
I do not know how to render that item's elements afterwards when they have already been added to the DOM.
Note that although $compile is called, returned string would not be the expected compiled result but the string before compilation due to the asynchronous nature of $compile.
One idea is to use DOM manipulation which is not the most recommended Angular way, but I got it working on this plunker. and a second one with custom directive and randomized data to simulate your compiled avatar.
To simulate your asynchronous call, I use ngResource. My render function returns a string "<div class='compiledavatar'>Temporary Avatar</div>" with a special class markup compiledavatar. For a second or two, you will see Temporary Avatar as you select an element. When the ngResource calls finishes I look for the element with class compiledavatar and then replace the html with what I downloaded. Here is the full code:
var app = angular.module('plunker', ['selectize', 'ngResource']);
app.controller('MainCtrl', function($scope, $resource, $document) {
var vm = this;
vm.name = 'World';
vm.$resource = $resource;
vm.myModel = 1;
vm.$document = $document;
vm.myOptions = [{
id: 1,
title: 'Spectrometer'
}, {
id: 2,
title: 'Star Chart'
}, {
id: 3,
title: 'Laser Pointer'
}];
vm.myConfig = {
create: true,
valueField: 'id',
labelField: 'title',
delimiter: '|',
placeholder: 'Pick something',
onInitialize: function(selectize) {
// receives the selectize object as an argument
},
render: {
item: function(item, escape) {
var label = item.title;
var caption = item.id;
var Stub = vm.$resource('mydata', {});
// This simulates your asynchronous call
Stub.get().$promise.then(function(s) {
var result = document.getElementsByClassName("compiledavatar")
angular.element(result).html(s.compiledAvatar);
// Once the work is done, remove the class so next time this element wont be changed
// Remove class
var elems = document.querySelectorAll(".compiledavatar");
[].forEach.call(elems, function(el) {
el.className = el.className.replace(/compiledavatar/, "");
});
});
return "<div class='compiledavatar'>Temporary Avatar</div>"
}
},
// maxItems: 1
};
});
To simulate the JSON API I just created a file in plunker mydata:
{
"compiledAvatar": "<div><span style='display: block; color: black; font-size: 14px;'>an avatar</span></div>"
}
Of course your compiled function should return you something different every calls. Me it gives me the same to demonstrate the principle.
In addition, if your dynamic code is an Agular directive, here is a second plunker with a custom directive and randomized data so you can better see the solution:
The data include a custom directive my-customer:
[{
"compiledAvatar": "<div><span style='display: block; color: black; font-size: 14px;'>an avatar #1 <my-customer></my-customer></span></div>"
},
{
"compiledAvatar": "<div><span style='display: block; color: black; font-size: 14px;'>an avatar #2 <my-customer></my-customer></span></div>"
},
(...)
The directive is defined as:
app.directive('myCustomer', function() {
return {
template: '<div>and a custom directive</div>'
};
});
And the main difference in the app is that you have to add $compile when replacing the HTML and the text should show An avatar #(number) and a custom directive. I get an array of json value and use a simple random to pick a value. Once the HTML is replaced I remove the class, so next time only the last added element will be changed.
Stub.query().$promise.then(function(s) {
var index = Math.floor(Math.random() * 10);
var result = document.getElementsByClassName("compiledavatar")
angular.element(result).html($compile(s[index].compiledAvatar)($scope));
// Remove class
var elems = document.querySelectorAll(".compiledavatar");
[].forEach.call(elems, function(el) {
el.className = el.className.replace(/compiledavatar/, "");
});
});
Also, I looked at selectize library and you cant return a promise... as it does a html.replace on the value returned by render. This is why I went to the route of a temporary string with a class to retrieve later and update.
Let me know if that helps.
This answer is based in the helpful answer by #gregori with the following differences:
Take into account Selectize.js' Render Cache. The standard behaviour of Selectize.js is that the items are cached as returned by the render function, and not with the modifications we have done to them. After adding and deleting some elements, the cached and not the modified version would be displayed if we do not update the render cache acordingly.
Using random id's to identify the elements to select to be manipulated from DOM.
Using watchers to know when the compilation has been done
First, we define a method to modify the selectize.js render cache:
scope.selectorCacheUpdate = function(key, value, type){
var cached = selectize.renderCache[type][key];
// update cached element
var newValue = angular.element(cached).html(value);
selectize.renderCache[type][key] = newValue[0].outerHTML;
return newValue.html();
};
Then, the render function is defined as follows:
function renderAvatar(item, escape, type){
// Random id used to identify the element
var randomId = Math.floor(Math.random() * 0x10000000).toString(16);
var avatar =
'<div id="' + randomId + '">' +
'<span customAvatarTemplate ...></span>' +
...
'</div>';
var compiled = $compile(avatar)($rootScope);
// watcher to see when the element has been compiled
var destroyWatch = $rootScope.$watch(
function (){
return compiled[0].outerHTML;
},
function (newValue, oldValue){
if(newValue !== oldValue){
var elem = angular.element(document.getElementById(randomId));
var rendered = elem.scope().selectorCacheUpdate(item._id, compiled.html(), type);
// Update DOM element
elem.html(rendered);
destroyWatch();
}
}
);
});
return avatar;
}
Note: The key for the render cache is the valueField of the selectize items, in this case, _id
Finally, we add this function as a selectize render function in the selectize configuration object:
config = {
...
render: {
item: function(i,e){
return renderAvatar(i, e, 'item');
},
option: function(i,e){
return renderAvatar(i, e, 'option');
}
},
...
}
For more details, see how this solution has been added to the application that motivated this question: https://github.com/P2Pvalue/teem/commit/968a437e58c5f1e70e80cc6aa77f5aefd76ba8e3.

How to jQuery bindings with a Backbone View

I have a Backbone view, Inside that view there's an input that uses twitters typeahead plugin. My code works, but I'm currently doing the typeahead initialisation inside the html code and not in my Backbone code.
TypeAhead.init('#findProcedure', 'search/procedures', 'procedure', 'name', function(suggestion){
var source = $("#procedure-row-template").html();
var template = Handlebars.compile(source);
var context = {
name: suggestion.name,
created_at: suggestion.created_at,
frequency: suggestion.frequency.frequency,
urgency: suggestion.urgency.urgency,
id: suggestion.id
};
var html = template(context);
AddRow.init("#procedure-table", html);
});
The code above is what works inside script tag.
But when I try taking to backbone it doesn't work.
What I'm trying in my BB code is:
initialize: function(ob) {
var url = ob.route;
this.render(url);
this.initTypeahead();
this.delegateEvents();
},
And obviously inside the init function there's the code that works, but not in BB. What could be wrong ? Is this a correcto approach ?
Thanks.

WHY does it initialize this Knockout.js component in random order?

I am beyond confused...
I am creating a list using Knockout.js components, templates, and custom elements. For some reason, the steps I create in my Viewmodel are being initialized in random order within the custom element definition! And it is completely randomized so that it is different each time!
To help better illustrate this, it is best to look at the JSFiddle. I put alert("break") after each step initialization. Load it once, and then click "run" again to see the demo properly. Look in the output window and you can see that other than step 1 being written first, the steps always appear randomly (though they maintain their order in the end).
https://jsfiddle.net/uu4hzc41/8/
I need to have these in the correct order because I will add certain attributes from my model into an array. When they are random I can't access the array elements properly.
HTML:
<ul>
<sidebar-step params="vm: sidebarStepModel0"></sidebar-step>
<sidebar-step params="vm: sidebarStepModel1"></sidebar-step>
<sidebar-step params="vm: sidebarStepModel2"></sidebar-step>
<sidebar-step params="vm: sidebarStepModel3"></sidebar-step>
<sidebar-step params="vm: sidebarStepModel4"></sidebar-step>
</ul>
JS/Knockout:
//custom element <sidebar-step>
ko.components.register("sidebar-step", {
viewModel: function (params) {
this.vm = params.vm;
alert("break");
},
template: "<li data-bind='text: vm.message'>vm.onChangeElement</li>"
});
// model
var SidebarStepModel = function () {
this.message = ko.observable("step description");
};
// viewmodel
var OrderGuideViewModel = function () {
this.sidebarStepModel0 = new SidebarStepModel();
this.sidebarStepModel0.message("step 1");
this.sidebarStepModel1 = new SidebarStepModel();
this.sidebarStepModel1.message("step 2");
this.sidebarStepModel2 = new SidebarStepModel();
this.sidebarStepModel2.message("step 3");
this.sidebarStepModel3 = new SidebarStepModel();
this.sidebarStepModel3.message("step 4");
this.sidebarStepModel4 = new SidebarStepModel();
this.sidebarStepModel4.message("step 5");
};
ko.applyBindings(new OrderGuideViewModel());
By default knockout components load asynchronously. In version 3.3 an option was added to allow the component to load synchronously.
Add synchronous:true when registering to get the behavior you want.
Example:
ko.components.register("sidebar-step", {
viewModel: function (params) {
this.vm = params.vm;
alert("break");
},
template: "<li data-bind='text: vm.message'>vm.onChangeElement</li>",
synchronous: true
});

Kendo UI Web - MultiSelect: select an option more than once

I'm currently facing a problem with the Kendo UI MultiSelect widget for selecting an option more than once. For example, in the image below I want to select Schindler's List again after selecting The Dark Knight, but unfortunately the MultiSelect widget behaves more like a set than an ordered list, i.e. repetitive selection is not allowed. Is there actually a proper way to achieve this? Any workarounds?
That's the intended behavior of the multi-select control and there is no simple way to make it do what you want using the available configuration options. Possible workarounds are ...
Creating a custom multi-select widget
Something like this should work (note that I haven't tested this much - it lets you add multiples and keeps the filter intact though):
(function ($) {
var ui = kendo.ui,
MultiSelect = ui.MultiSelect;
var originalRender = MultiSelect.fn._render;
var originalSelected = MultiSelect.fn._selected;
var CustomMultiSelect = MultiSelect.extend({
init: function (element, options) {
var that = this;
MultiSelect.fn.init.call(that, element, options);
},
options: {
name: 'CustomMultiSelect'
},
_select: function (li) {
var that = this,
values = that._values,
dataItem,
idx;
if (!that._allowSelection()) {
return;
}
if (!isNaN(li)) {
idx = li;
} else {
idx = li.data("idx");
}
that.element[0].children[idx].selected = true;
dataItem = that.dataSource.view()[idx];
that.tagList.append(that.tagTemplate(dataItem));
that._dataItems.push(dataItem);
values.push(that._dataValue(dataItem));
that.currentTag(null);
that._placeholder();
if (that._state === "filter") {
that._state = "accept";
}
console.log(this.dataSource.view());
},
_render: function (data) {
// swapping out this._selected keeps filtering functional
this._selected = dummy;
return originalRender.call(this, data);
this._selected = originalSelected;
}
});
function dummy() { return null; }
ui.plugin(CustomMultiSelect);
})(jQuery);
Demo here.
Using a dropdown list
Use a simple dropdown list (or ComboBox) and bind the select event to append to your list (which you have to create manually).
For example:
var mySelectedList = [];
$("#dropdownlist").kendoDropDownList({
select: function (e) {
var item = e.item;
var text = item.text();
// store your selected item in the list
mySelectedList.push({
text: text
});
// update the displayed list
$("#myOrderedList").append("<li>" + text + "</li>");
}
});
Then you could bind clicks on those list elements to remove elements from the list. The disadvantage of that is that it requires more work to make it look "pretty" (you have to create and combine your own HTML, css, images etc.).

Categories

Resources