knockout template from string must create a custom binding? - javascript

I want create a universal table components by knockout.this component must support custom template show if have a params 'templ' :
tableParams = {
...
{
title: 'factory', name: "FAC_NO", isKey: true,
templ: '<span style="color:red" data-bind="text:FAC_NO"></span>',
validation: {
required: true,
maxLength: 4
}
}
...
I want support template by string not by <script type="text/html"... .but knockout.js not support it?
now I write a bind:
ko.bindingHandlers.templ = {
'init': function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var setting = valueAccessor();
var compile = function (html) {
var cell = $(html);
ko.applyBindings(setting.data, cell[0]);
$(element).html(cell);
};
compile(setting.tpl);
return { controlsDescendantBindings: true };
}
};
and use in my table components cell render:
<!-- ko if:$data.templ -->
<td data-bind="templ:{ tpl: $data.templ, data: $parentContext.$data }"></td>
<!-- /ko -->
It looks work.but have any better way? My custom bind is right(have Resource Leak)?

Related

How to load a KnockoutJS component programmatically

I'm starting to create a ko component for select2 called "select-two", but I realize soon after that I sometimes need to load components after bindings have been registered creating the component programmatically.
I've tried apending a new element but of course it doesnt work, I'm guessing it has to be rebinded.
var sel2 = $("<select-two></select-two>") ;
$("#selectList").append(sel2) ;
There's lot of reference on how to rebind the whole viewmodel, but only just the component? I think this guy has the same problem:
Load knockoutjs component using javascript
Just for reference this is the component code:
define([
'knockout',
'text!./select-two.html',
'select2'
], function (ko, templateMarkup, select2) {
ko.components.register('select-two', {
viewModel: function(params) {
var placeholder = params.placeholder;
var value = params.value;
ko.bindingHandlers.select2 = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
$(element).select2({
placeholder: placeholder,
minimumInputLength: 1,
ajax: {
url: function (term, page) {
return '/models/autores/busqueda/' + term['term']
},
dataType: 'json',
quietMillis: 200,
processResults: function (data) {
return data;
}
},
dropdownCssClass: "bigdrop", // apply css that makes the dropdown taller
}
).on('change', function(event){
ds = $(element).select2('data')[0] ;
value['id'](ds['id']) ;
value['text'](ds['text']) ;
});
}
};
return{
}
},
template: templateMarkup
});
});
my template:
<link rel="stylesheet" href="/assets/js/vendor/select2/dist/css/select2.css" />
<select class="form-control" data-bind="select2"></select>
and how I load it:
<select-two id="authorSelect" params="placeholder: 'Pick an Author', value: autorSelectData" ></select-two>
ko.bindingHandlers.select2 = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
var params = ko.unwrap(valueAccessor());
$(element).select2({
placeholder: params.placeholder,
minimumInputLength: 1,
data: params.data
}).on('select2:select', function(e) {
params.value(e.params.data.text);
});
$(element).select2('val', params.value); // set initial value
}
};
ko.components.register('select-two', {
viewModel: function(params) {
this.value = params.value;
this.data = params.data;
},
template: '<select class="form-control" style="width: 200px;" data-bind="select2: ' +
'{placeholder: \'Pick a fruit\', value: value, data: data}">' +
'</select>'
});
var app = {
newSelect: function(){
var cont = $("<p></p>") ;
var sel2 = $("<select-two></select-two>") ;
ko.applyBindings({}, sel2[0]) ;
cont.append(sel2) ;
$("#select-list").append(cont) ;
},
autorSelectData: ko.observable(null),
options: ['apple','pear','peach','mango','grape']
};
ko.applyBindings(app);
<link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.min.js"></script>
Just the binding: <select class="form-control" style="width: 200px;"
data-bind="select2: {placeholder: 'Pick a fruit', value: autorSelectData, data: options}">
</select><br>
With component: <select-two params="value: autorSelectData, data: options"></select-two>
<p>Adding Selects</p>
<div id="select-list">
</div>
<input type="button" data-bind="click: newSelect()" value="New Select" id="new-select" />
I'm guessing it has to be rebinded.
Please remember that when you think of rebinding as a solution, in 99% of the cases you should reconsider your approach.
creating the component programmatically.
If you follow MVVM/ MVC guidelines, you should never have to do this. All data models/ views' structures are pre-defined; only their content can change.
What shouldn't you be doing?
Registering a custom binding inside a component, which means it will be re-registered every time a component instance is created.
Linking a style sheet inside a component template, which means the stylesheet will be loaded every time a component instance is created (instead of once).
Using the change event while the select2 docs show you have to use select2:select.
Below is a stripped down version of your code (eg., data-array instead of AJAX), showing how it works with (1) just the binding, and (2) with an encapsulatin component. Does it still not work, and if so, what does not work?
ko.bindingHandlers.select2 = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
var params = ko.unwrap(valueAccessor());
$(element).select2({
placeholder: params.placeholder,
minimumInputLength: 1,
data: params.data
}).on('select2:select', function(e) {
params.value(e.params.data.text);
});
$(element).select2('val', params.value); // set initial value
}
};
ko.components.register('select-two', {
viewModel: function(params) {
this.value = params.value;
this.data = params.data;
},
template: '<select class="form-control" style="width: 200px;" data-bind="select2: ' +
'{placeholder: \'Pick a fruit\', value: value, data: data}">' +
'</select>'
});
var app = {
autorSelectData: ko.observable(null),
options: ['apple','pear','peach','mango','grape']
};
ko.applyBindings(app);
<link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.min.js"></script>
Just the binding: <select class="form-control" style="width: 200px;"
data-bind="select2: {placeholder: 'Pick a fruit', value: autorSelectData, data: options}">
</select><br>
With component: <select-two params="value: autorSelectData, data: options"></select-two>

jQuery DataTables and Knockout not binding properly

I've got an Ajax call that's returning a "Team". A team is comprised of a TeamName, LeagueName, and a list of FootballPlayers which have a Name, Position, and TeamName. The Ajax call is correctly returning the Team JSON properly and I'm about to make Knockout bindings to the properties directly on the Team. However, I want to bind a jQuery DataTable to the list of players, but have been unsuccessful in doing so. I can tell the DataTables call is being made, as I can see some of the DataTables controls like "Previous 1 Next" but no data is in the table. I can deploy what I have out to a publicly visible site if that'll be helpful. Thank you!
jQuery: Version 2.1.1
Knockout: Version 3.2.0
DataTabes: Version
1.10.4
HTML Table
<table id="playerList">
<thead>
<tr>
<th>Player Name <span class="glyphicon glyphicon-sort-by-alphabet" aria-hidden="true"></span></th>
<th>Position <span class="glyphicon glyphicon-sort-by-alphabet" aria-hidden="true"></span></th>
<th>Team <span class="glyphicon glyphicon-sort-by-alphabet" aria-hidden="true"></span></th>
</tr>
</thead>
<!--<tbody data-bind='foreach: PlayerList'>-->
<tbody>
<tr>
<td data-bind="text: $data.PlayerList.Name"></td>
<td data-bind="text: $data.PlayerList.SelectedPosition"></td>
<td data-bind="text: $data.PlayerList.TeamName"></td>
</tr>
</tbody>
</table>
Controller Javascript
$('.retrieveTeam').click(function () {
_getData('RetrieveTeam', JSON.stringify({ TeamKey: $(this).data("teamkey") }));
});
function _getData(url, postdata) {
var request = $.ajax({
url: url,
type: 'POST',
data: postdata,
datatype: "json",
contentType: "application/json"
});
request.done(_requestDone);
request.fail(_failedRequest)
}
function _requestDone(result)
{
_bindTeam(result);
my.Views.TeamView.showTeamInfo();
}
function _bindTeam(data) {
if (!viewModel) {
viewModel = ko.mapping.fromJS(data, {}, this);
ko.applyBindings(viewModel);
my.Views.TeamView.applyDataTable('#playerList');
} else {
ko.mapping.fromJS(data, viewModel);
}
}
View Javascript
var applyDataTable = function applyDataTable(element) {
$(element).DataTable(
{
responsive: true,
bFilter: false,
bInfo: false,
bLengthChange: false
});
}
Here is how I have done it... this fiddle uses a custom DataTablesForEach binding and also includes a fork of knockout... I have created a pull request with the knockout repository on github. If this solution helps you, please leave a comment on my pull request to get this merged into knockout 3.4.. thanks!
jsfiddle
pull request #1856 knockout/knockout repository on github
ko.bindingHandlers.DataTablesForEach = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var nodes = Array.prototype.slice.call(element.childNodes, 0);
ko.utils.arrayForEach(nodes, function (node) {
if (node && node.nodeType !== 1) {
node.parentNode.removeChild(node);
}
});
return ko.bindingHandlers.foreach.init(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext);
},
update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var value = ko.unwrap(valueAccessor()),
key = "DataTablesForEach_Initialized";
var newValue = function () {
return {
data: value.data || value,
beforeRenderAll: function (el, index, data) {
if (ko.utils.domData.get(element, key)) {
$(element).closest('table').DataTable().clear();
$(element).closest('table').DataTable().destroy();
}
},
afterRenderAll: function (el, index, data) {
$(element).closest('table').DataTable(value.options);
}
};
};
ko.bindingHandlers.foreach.update(element, newValue, allBindingsAccessor, viewModel, bindingContext);
//if we have not previously marked this as initialized and there is currently items in the array, then cache on the element that it has been initialized
if (!ko.utils.domData.get(element, key) && (value.data || value.length)) {
ko.utils.domData.set(element, key, true);
}
return { controlsDescendantBindings: true };
}
};

How to extend the template binding syntax with custom properties accessible by children contexts?

I am trying to find an easy and elegant way to extend the template binding in order to propagate custom properties to children context. By extend, I mean to use the template binding with additional properties and not create another binding that will control descendant bindings (controlsDescendantBindings) because it requires another DOM element or KO virtual element.
This is what I would like to achieve:
<div class="row"
data-bind="template: {
name: 'column-template',
foreach: items1,
properties: {columnType: 'col-md-6'}
}"></div>
<div class="row"
data-bind="template: {
name: 'column-template',
foreach: items2,
properties: {columnType: 'col-md-12'}
}"></div>
or
<div class="row"
data-bind="
template: {name: 'column-template', foreach: items1},
properties: {columnType: 'col-md-6'}
"></div>
<div class="row"
data-bind="
template: { name: 'column-template', foreach: items2},
properties: {columnType: 'col-md-12'
}"></div>
...using whatever variation with:
<script type="text/html" id="column-template">
<div data-bind="css : columnType">...</div>
</script>
Note that the view model is shared by the 2 <div class="row" /> elements, so I am not asking how to change the view model properties/observables. Instead, I would like to know if there is a mechanism to extend the native template binding syntax in order to make custom properties available in children contexts.
I have read some documentation or workarounds on extending contexts but could not find a solution which does not require extra HTML markup:
http://knockoutjs.com/documentation/custom-bindings-controlling-descendant-bindings.html
https://github.com/knockout/knockout/pull/354
https://github.com/knockout/knockout/issues/1002#issuecomment-19361275
For now, I am using a virtual binding handler suggested in a previous link:
ko.bindingHandlers.let = {
init : function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
// Make a modified binding context, with extra properties, and apply it to descendant elements
var innerContext = bindingContext.extend(valueAccessor());
ko.applyBindingsToDescendants(innerContext, element);
return { controlsDescendantBindings: true };
}
};
ko.virtualElements.allowedBindings['let'] = true;
...applied in this way:
<!-- ko let: { displayMode: 'col-md-6' } -->
<div class="row" data-bind="template: { name: 'column-template', foreach: items1}}"></div>
<!-- /ko -->
or
<!-- ko let: { displayMode: 'col-md-12' } -->
<div class="row" data-bind="template: { name: 'column-template', foreach: items2}}"></div>
<!-- /ko -->
It works as expected but looks a little verbose.
Thanks a lot!
This question has been asked originally here:
https://groups.google.com/forum/#!topic/knockoutjs/NvVIq72SeUY
I found a solution if you use the following sintax:
<div class="row" data-bind="propertyTemplate: {
name: 'column-template',
foreach: items1,
properties: { columnType: 'col-md-6' }
}"></div>
The only difference with yours is that I renamed the binding to propertyTemplate.
Here's the binding code and a fiddle to show it's working.
ko.bindingHandlers['propertyTemplate'] = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var value = valueAccessor(),
context = bindingContext.extend(value.properties);
ko.applyBindingsToNode(element, { template: value }, context);
return { 'controlsDescendantBindings': true };
}
};
It's probably too late but I know it may help others so..
The basic idea is to keep the original binding handler and replace it with a new one while using the kept one to invoke the original functionality.
The main idea behind it:
var originalHandler = ko.bindingHandlers['handler'];
var customHandler = {
extendBindingContext: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
// extend here
// bindingContext = bindingContext.extend(...);
return bindingContext;
},
init: originalHandler.init
? function (element, valueAccessor, allBindings, viewModel, bindingContext) {
bindingContext = customHandler
.extendBindingContext(element, valueAccessor, allBindings, viewModel, bindingContext);
return originalHandler.init(element, valueAccessor, allBindings, viewModel, bindingContext);
}
: undefined,
update: originalHandler.update
? function (element, valueAccessor, allBindings, viewModel, bindingContext) {
bindingContext = customHandler
.extendBindingContext(element, valueAccessor, allBindings, viewModel, bindingContext);
return originalHandler.update(element, valueAccessor, allBindings, viewModel, bindingContext);
}
: undefined
};
ko.bindingHandlers['handler'] = customHandler;
Hope this helps - And don't forget to up-vote please or even comment about success or issues - it's just some clicks away ;P

knockoutjs bindings issue

I'm having issues with my knockoutjs implementation. Seems to be working fine on Chrome and Safari IE and FF have thrown a hissy fit.
The message which I encounter is as follows:
Unable to parse bindings. Message: TypeError: 'AccountName' is
undefined; Bindings value: value: AccountName
The issue is happening within a script tag which serves as a knockout template:
<div id="newAccountDialog" class="dialog" data-bind="dialog: { autoOpen: false, resizable: false, modal: true, width: 350, title: 'Exchange Account'}, template: { name: 'dialogFormTemplate', data: CurrentAccount }, openDialog: IsNew"></div>
<script id="dialogFormTemplate" type="text/html">
<form id="dialogForm">
<h1>Exchange Account Manager</h1>
<p>Add new or edit an existing exchange account settings.</p>
<label for="AccountName">
Account
</label>
<input id="AccountName" name="AccountName" type="text" data-bind="value: AccountName, valueUpdate: 'afterkeydown'" class="ui-widget-content ui-corner-all" />
<div class="buttonsContainer floatRight">
<button id="Save" data-bind="click: $root.SaveAccount, dialogcmd: { id: 'newAccountDialog', cmd: 'close'}, jqButton: { icons: { primary: 'ui-icon-disk' } }">Save & Close</button>
</div>
</form>
</script>
I assume some sort of early binding is being triggered on the template
data : CurrentAccount
where an undefined / null is being passed into CurrentAccount. I have seen this issue outside of script tags, but only if the observable is not defined or null.
My viewmodel looks as following:
var AccountModel = function () {
var self = this;
self.Accounts = ko.observableArray([]);
self.CurrentAccount = ko.observable(null);
self.IsNew = ko.observable(false);
self.LoadAccounts = function () {
$account.invoke("GetAccounts", {}, function (data) {
var mapped = $.map(data, function (item) {
var account = new Account(item);
var innerMapped = $.map(item.Mailboxes, function (mailbox) {
return new Mailbox(mailbox);
});
account.Mailboxes(innerMapped);
return account;
});
self.Accounts(mapped);
});
}
self.EditAccount = function (data) {
self.CurrentAccount(data);
self.IsNew(true);
}
self.SaveAccount = function () {
if (self.CurrentAccount().Id() <= 0) {
$account.invoke('AddAccount', ko.toJS(self.CurrentAccount()), function (data) {
self.Accounts.push(new Account(data));
self.CurrentAccount(new Account(data));
self.IsNew(true);
});
} else {
$account.invoke('UpdateAccount', ko.toJS(self.CurrentAccount()), function (data) {
//self.CurrentAccount(new Account(data));
});
}
}
self.CreateAccount = function () {
self.IsNew(true);
var account = { Id: 0, AccountName: '', IsNTLM: -1, Email: '', Password: '', Domain: 'mydomain', ExchangeVersion: 1, Mailboxes: [] };
self.CurrentAccount(new Account(account));
}
};
My dialog bindingHandler is defined as follows:
ko.bindingHandlers.dialog = {
init: function (element, valueAccessor) {
var options = ko.utils.unwrapObservable(valueAccessor()) || {};
ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
$(element).dialog('destroy');
});
$(element).dialog(options);
}
};
I have ommited the Account object, as it is possibly not required in this context.
I would appreciate any help.
Thank you in advance.
There is no "early" binding in Knockout. Everything is bound when you call ko.applyBindings. But certain bindings can stop or delay binding of their descendant elements. template is one of those when you use the if or ifnot options. In your case, you can use the if option like this:
template: { name: 'dialogFormTemplate', data: CurrentAccount, 'if': CurrentAccount }
Note: The quotes around if are required in some older browsers.

knockout click event

I have the following code:
<div class="icon-upload" data-bind="click: button('upload') "> Upload </div>
ko.bindingHandlers.button = {
init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext)
{
//alert('works');
console.log(element);
}
};
But I keep getting a button is not defined error. I'm trying to do an on('click') event with a parameter to determine latter what binding to initialize.
For example, when clicking on button('upload') I want to initialize the following binding
ko.bindingHandlers.image = {
init: function (element, valueAccessor, allBindingsAccessor, context)
{
var value = ko.utils.unwrapObservable(valueAccessor()),
$element = $(element);
console.log($element)
$element.html(value);
/*$element.pluploadQueue({
runtime: 'gears, browserplus, html5, flash, html4',
max_file_size: '10mb',
max_file_count: 10,
chunk_size: '1mb',
unique_names: true,
multiple_queues: true,
drop_element: true,
dragdrop: true,
filters : [
{title : "Image files", extensions : "jpg,gif,png"}
]
});*/
}
};
Do I must wrap the click code in a ViewModel = function() { like in
http://jsfiddle.net/jearles/xSKyR/
http://jsfiddle.net/FpSWb/ ?
Can't I do it the way I'm trying to do it?
You doing it wrong. You should create custom binding button which will be triggered on click event.
ko.bindingHandlers.button = {
init: function (element) {
$(element).click(function() {
// Your logic
});
}
update: function(element, valueAccessor, allBindingsAccessor) {
switch(ko.utils.unwrapObservable(valueAccessor())) {
case 'upload': ...
}
}
}
In view
<div class="icon-upload" data-bind="button: 'upload'"> Upload </div>

Categories

Resources