Render angular directives in Selectize.js items - javascript

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.

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.

doc.addEventListener is not a function

On my page I'm rendering a tree based on jsTree angular directive. I'm trying to achieve a showing tooltip on hover for particularly node which has a description field. I have checked list of API for jsTree and have found on hover event listener hover_node, with help angular directive for jsTree I have setup my scope function to this listener
<js-tree
tree-data="scope"
tree-model="tree"
tree-events="hover_node:nodeHovered"
tree-core="tree_core"
></js-tree>
$scope.nodeHovered = function (e, data) {
var dataNode = data.node;
var original = dataNode.original;
if (typeof original.description !== 'undefined'){
var nodeDescription = original.description;
$(original).tooltip({
title: nodeDescription,
animation: true
});
}
};
My function works well when I hover on any node, I can see the object data in console, so I wrote a piece of code which allows to see the description in console if typeof node description is not an equal undefined. It works well for console but when I added function bootstrap tooltip for this node that to show the description as tooltip, I've got next error
Uncaught TypeError: doc.addEventListener is not a function
As I could understand from some topics here, the nature of this mistake is arrays don't have addEventListener function, but I'm working with an object which is already defined thru the library, so could anybody help to find what I'm missing? I appreciate any help.
Plunker
my full code
var treeFolders = [];
$scope.tree = [];
$http.get("folders.json")
.then(function (res) {
treeFolders = res.data;
angular.forEach(treeFolders, function(obj){
if(!("parent" in obj)){
obj.parent = "#";
}
})
})
$scope.load = function(){
$scope.tree = treeFolders
};
$scope.tree_core = {
multiple: false, // disable multiple node selection
check_callback: function (operation, node, node_parent, node_position, more) {
return true; // allow all other operations
},
themes : {
theme : "default",
dots : true,
icons : true
}
};
$scope.nodeHovered = function (e, data) {
var dataNode = data.node;
var original = dataNode.original;
if (typeof original.description !== 'undefined'){
var nodeDescription = original.description;
$(original).tooltip({
title: nodeDescription,
animation: true
});
}
};
html
<button class="btn btn-default" ng-click="load()">Load Tree</button>
<js-tree
tree-data="scope"
tree-model="tree"
tree-events="hover_node:nodeHovered"
tree-core="tree_core"
></js-tree>
This is because the original object that you are passing to jQuery in the following code is a jsTree node, not a DOM element:
$(original).tooltip({
title: nodeDescription,
animation: true
});
Instead use the get_node method of jsTree to retrieve the jQuery wrapped DOM element from the node:
var liElement = data.instance.get_node(data.node, true);
This will give you the entire li element, but you probably want the tooltip placed relative to the a element:
var aElement = liElement.children('a').first();
Then:
aElement.tooltip({
title: nodeDescription,
animation: true
}).tooltip('show');
Note that I have added an extra call to tooltip at the end. This is for the tooltip to be shown the first time the element is hovered. Without this the tooltip would be created on the first hover, then not shown until the next.
Demo: https://plnkr.co/edit/itMSrsroTVswD3RLbs7O?p=preview

AngularJS + ag-grid: sticky/remembered selections with virtual paging/infinite scrolling

In an AngularJS application, I have an ag-grid that uses virtual paging/infinite scrolling to lazy-load rows from a dataset that is too large to show at once. I have turned on check-box selection in the first column, so that the user should be able to select individual rows for arbitrary application-specific actions.
The AngularJS application uses ui-router to control multiple views. So, building on the virtual-paging example with "sorting & filtering", with constructed data about Olympic winners, from the ag-grid documentation, I've further extended the code a bit. From index.html:
<body ng-controller="MainController" class="container">
<div ui-view="contents"></div>
</body>
and the following ui-router states:
myapp.config(function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise("example.page1")
$stateProvider
.state('example', {
abstract: true,
views: {
contents: {
template: '<div ui-view="example"></div>'
}
}
})
.state('example.page1', {
url: '/page1',
views: {
example: {
templateUrl: 'page1.html'
}
}
})
.state('example.page2', {
url: '/page2',
views: {
example: {
template: 'Go back to the <a ui-sref="example.page1">example grid</a>.'
}
}
});
});
where page1.html looks like the following:
<div ng-controller="GridController">
<div ag-grid="gridOptions" class="ag-fresh" style="height: 250px;"></div>
</div>
<div>
<h3>Selected rows:</h3>
<ul class="list-inline">
<li ng-repeat="row in currentSelection track by row.id">
<a ng-click="remove(row)">
<div class="badge">#{{ row.id }}, {{ row.athlete }}</div>
</a>
</li>
</ul>
</div>
<p>Go to <a ui-sref="example.page2">the other page</a>.</p>
What I want to accomplish:
That selections made in the ag-grid is remembered (sticky) when scrolling a (virtual) page out of view and back again, so that a user can select multiple rows on separate pages.
That the remembered selections are available outside the grid, and support adding and removing selections (as intended by the ng-click="remove(row)" in page1.html, shown above).
That the selections should be remembered when switching from the view with the ag-grid to another one, and back again.
(Optional) That the selections are remembered for the user's session.
How can I accomplish this?
I've created a working example of this can be implemented.
First of all, we'll write a AngularJS service, selectionService to keep track of the selections:
function _emptyArray(array) {
while (array.length) {
array.pop();
}
}
function _updateSharedArray(target, source) {
_emptyArray(target);
_.each(source, function _addActivity(activity) {
target.push(activity);
});
}
myapp.factory('selectionService', function ($rootScope, $window) {
var _collections = {},
_storage = $window.sessionStorage,
_prefix = 'selectionService';
angular.element($window).on('storage', _updateOnStorageChange);
function _persistCollection(collection, data) {
_storage.setItem(_prefix + ':' + collection, angular.toJson(data));
}
function _loadCollection(collection) {
var item = _storage.getItem(_prefix + ':' + collection);
return item !== null ? angular.fromJson(item) : item;
}
function _updateOnStorageChange(event) {
var item = event.originalEvent.newValue;
var keyParts = event.originalEvent.key.split(':');
if (keyParts.length < 2 || keyParts[0] !== _prefix) {
return;
}
var collection = keyParts[1];
_updateSharedArray(_getCollection(collection), angular.fromJson(item));
_broadcastUpdate(collection);
}
function _broadcastUpdate(collection) {
$rootScope.$emit(_service.getUpdatedSignal(collection));
}
function _afterUpdate(collection, selected) {
_persistCollection(collection, selected);
_broadcastUpdate(collection);
}
function _getCollection(collection) {
if (!_.has(_collections, collection)) {
var data = _loadCollection(collection);
// Holds reference to a shared array. Only mutate, don't replace it.
_collections[collection] = data !== null ? data : [];
}
return _collections[collection];
}
function _add(item, path, collection) {
// Add `item` to `collection` where item will be identified by `path`.
// For example, path could be 'id', 'row_id', 'data.athlete_id',
// whatever fits the row data being added.
var selected = _getCollection(collection);
if (!_.any(selected, path, _.get(item, path))) {
selected.push(item);
}
_afterUpdate(collection, selected);
}
function _remove(item, path, collection) {
// Remove `item` from `collection`, where item is identified by `path`,
// just like in _add().
var selected = _getCollection(collection);
_.remove(selected, path, _.get(item, path));
_afterUpdate(collection, selected);
}
function _getUpdatedSignal(collection) {
return 'selectionService:updated:' + collection;
}
function _updateInGridSelections(gridApi, path, collection) {
var selectedInGrid = gridApi.getSelectedNodes(),
currentlySelected = _getCollection(collection),
gridPath = 'data.' + path;
_.each(selectedInGrid, function (node) {
if (!_.any(currentlySelected, path, _.get(node, gridPath))) {
// The following suppressEvents=true flag is ignored for now, but a
// fixing pull request is waiting at ag-grid GitHub.
gridApi.deselectNode(node, true);
}
});
var selectedIdsInGrid = _.pluck(selectedInGrid, gridPath),
currentlySelectedIds = _.pluck(currentlySelected, path),
missingIdsInGrid = _.difference(currentlySelectedIds, selectedIdsInGrid);
if (missingIdsInGrid.length > 0) {
// We're trying to avoid the following loop, since it seems horrible to
// have to loop through all the nodes only to select some. I wish there
// was a way to select nodes/rows based on an id.
var i;
gridApi.forEachNode(function (node) {
i = _.indexOf(missingIdsInGrid, _.get(node, gridPath));
if (i >= 0) {
// multi=true, suppressEvents=true:
gridApi.selectNode(node, true, true);
missingIdsInGrid.splice(i, 1); // Reduce haystack.
if (!missingIdsInGrid.length) {
// I'd love for `forEachNode` to support breaking the loop here.
}
}
});
}
}
var _service = {
getCollection: _getCollection,
add: _add,
remove: _remove,
getUpdatedSignal: _getUpdatedSignal,
updateInGridSelections: _updateInGridSelections
};
return _service;
});
The selectionService service allows adding and removing arbitrary objects to separate collections, identified by collection, a name you find suitable. This way the same service can be used for remembering selections in multiple ag-grid instances. Each object will be identified using a path parameter. The path is used to retrieve the unique identifier using lodash's get function.
Furthermore, the service uses sessionStorage to persist the selections during the user's whole tab/browser session. This might be overkill; we could have just relied on the service to keep track of the selections since it will only get instantiated once. This can of course be modified to your needs.
Then there were the changes that had to be done to the GridController. First of all the columnDefs entry for the first column had to be changed slightly
var columnDefs = [
{
headerName: "#",
width: 60,
field: 'id', // <-- Now we use a generated row ID.
checkboxSelection: true,
suppressSorting: true,
suppressMenu: true
}, …
where the new, generated row ID is generated once the data has been retrieved from the remote server
// Add row ids.
for (var i = 0; i < allOfTheData.length; i++) {
var item = allOfTheData[i];
item.id = 'm' + i;
}
(The 'm' in the ID was included just to make sure I didn't confused that ID with other IDs used by ag-grid.)
Next, the necessary changes to gridOptions were to add
{
…,
onRowSelected: rowSelected,
onRowDeselected: rowDeselected,
onBeforeFilterChanged: clearSelections,
onBeforeSortChanged: clearSelections,
…
}
Were the different handlers are quite straight forward, communicating with the selectionService
function rowSelected(event) {
selectionService.add(event.node.data, 'id', 'page-1');
}
function rowDeselected(event) {
selectionService.remove(event.node.data, 'id', 'page-1');
}
function clearSelections(event) {
$scope.gridOptions.api.deselectAll();
}
Now, the GridController needs to handle updates signalled by the selectionService too
$scope.$on('$destroy',
$rootScope.$on(selectionService.getUpdatedSignal('page-1'),
updateSelections));
and
function updateSelections() {
selectionService.updateInGridSelections($scope.gridOptions.api, 'id', 'page-1');
}
calls selectionService.updateInGridSelections which will update the in-grid selections of the grid in question. That was the most cumbersome function to write. For example, if a selection has been added externally (outside the grid), then we'll have to perform a forEachNode run, even if we know all the necessary nodes have already been selected in-grid; there's no way to exit that loop early.
Finally, another crux was to clear and reapply the selections before and after, respectively, when the filters or sort orders are changed, or when new data is retrieved from the server (which is only simulated in the demo). The solution was to include a call to updateSelections after the params.successCallback inside the getRows handler
params.successCallback(rowsThisPage, lastRow);
updateSelections();
Now, the most puzzling findings during the implementation of this solution was that the ag-grid API grid options onAfterFilterChanged and onAfterSortChanged couldn't be used for reapplying the selections because they trigger before the (remote) data has finished loading.

Using knockout.js with custom template engine

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

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