How to load a KnockoutJS component programmatically - javascript

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>

Related

Vue js issue with jquery get

Hi I'm trying to get data via ajax and using vue wrapper component. Here is my code.
<html>
<head>
<title>title</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
html, body {
font: 13px/18px sans-serif;
}
select {
min-width: 300px;
}
</style>
</head>
<body>
<div id="el"></div>
<!-- using string template here to work around HTML <option> placement restriction -->
<script type="text/x-template" id="demo-template">
<div>
<p>Selected: {{ input.selected }}</p>
<select2 :options="options" v-model="input.selected">
<option disabled value="0">Select one</option>
</select2>
</div>
</script>
<script type="text/x-template" id="select2-template">
<select>
<slot></slot>
</select>
</script>
<script src="http://themestarz.net/html/craigs/assets/js/jquery-3.3.1.min.js"></script>
<script src="https://unpkg.com/vue#2.5.17/dist/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.min.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
Vue.component('select2', {
props: ['options', 'value'],
template: '#select2-template',
mounted: function () {
var vm = this;
$(this.$el)
// init select2
.select2({data: this.options})
.val(this.value)
.trigger('change')
// emit event on change.
.on('change', function () {
vm.$emit('input', this.value)
});
},
watch: {
value: function (value) {
// update value
$(this.$el)
.val(value)
.trigger('change')
},
options: function (options) {
// update options
$(this.$el).empty().select2({data: options})
}
},
destroyed: function () {
$(this.$el).off().select2('destroy')
}
})
var vm = new Vue({
el: '#el',
template: '#demo-template',
data: {
input: {
selected: "all"
},
options: []
},
created: function () {
this.mymethod();
},
methods: {
mymethod: function () {
var vm = this;
$.get("https://api.coindesk.com/v1/bpi/currentprice.json", function (data) {
vm.options = [
{id: 'all', text: 'All'},
{id: 1, text: 'Hello'},
{id: 2, text: 'World'},
{id: 3, text: 'Bye'}
];
vm.input.selected = 2;
});
}
}
});
</script>
</body>
</html>
After items loaded to drop down I need to change the selected item like this
vm.input.selected = 2;
But unfortunately this is not happening after ajax request. If I added the array before ajax it happens as expected but I need data from an ajax request. And I have rduced the complexity of code for better visibility.
Here is a jsfiddle for issue. I think the issue is with vue component.
I did a few tests and it seems like you're essentially changing the select2's value before its options, and since option 2 doesn't exist, the change fizzles.
Like I mentioned in my comment, changing the order of options and value in the component's watch fixes this, probably because that way the options are changed right before the new value is set.
Working example:
Vue.component('select2', {
props: ['options', 'value'],
template: '#select2-template',
mounted: function() {
var vm = this;
$(this.$el)
// init select2
.select2({
data: this.options
})
.val(this.value)
.trigger('change')
// emit event on change.
.on('change', function() {
vm.$emit('input', this.value)
});
},
watch: {
options: function(options) {
// update options
$(this.$el).empty().select2({
data: options
})
},
value: function(value) {
// update value
$(this.$el)
.val(value)
.trigger('change')
}
},
destroyed: function() {
$(this.$el).off().select2('destroy')
}
})
var vm = new Vue({
el: '#el',
template: '#demo-template',
data: {
input: {
selected: "all"
},
options: []
},
created: function() {
this.mymethod();
},
methods: {
mymethod: function() {
var vm = this;
$.get("https://api.coindesk.com/v1/bpi/currentprice.json", function(data) {
vm.options = [{
id: 'all',
text: 'All'
},
{
id: 1,
text: 'Hello'
},
{
id: 2,
text: 'World'
},
{
id: 3,
text: 'Bye'
}
];
vm.input.selected = 2;
// setTimeout(() => { vm.input.selected = 2; }, 0);
});
}
}
});
html,
body {
font: 13px/18px sans-serif;
}
select {
min-width: 300px;
}
<script type="text/javascript" src="https://code.jquery.com/jquery-3.3.1.js"></script>
<script type="text/javascript" src="https://unpkg.com/vue#2.5.17/dist/vue.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.min.js"></script>
<script type="text/javascript" src="https://unpkg.com/axios/dist/axios.min.js"></script>
<div id="el"></div>
<!-- using string template here to work around HTML <option> placement restriction -->
<script type="text/x-template" id="demo-template">
<div>
<p>Selected: {{ input.selected }}</p>
<select2 :options="options" v-model="input.selected">
<option disabled value="0">Select one</option>
</select2>
</div>
</script>
<script type="text/x-template" id="select2-template">
<select>
<slot></slot>
</select>
</script>

knockout template from string must create a custom binding?

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)?

Allow user to self define the selected value in a select using Select2 and Knockout

Select2 allows to improve standard <select> by applying on some style and feature.
I have to display Select2 styled dropdown list, custom binded with Knockout.
It is a phone number list, and user could add some entries.
I want it to be editable.
The user should be able to select an entry or type a new one wich can be used in the application (binded with manager.selectedPhoneNumber)
So, in the HTML:
<body>
<select id="list" data-bind="
options: manager.getMyPhoneNumbers(),
optionsValue: 'id',
optionsText: 'text',
customBinding_phoneNumbersEditableList: manager.selectedPhoneNumber
">
</select>
</body>
And in Javascript:
ko.bindingHandlers.customBinding_phoneNumbersEditableList = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
// initialize select2 with binded element
$(element).select2({
//createSearchChoice: function (term, data) {
// if ($(data).filter(function () {
// return this.text.localeCompare(term) === 0;
// }).length === 0) {
// return {
// id: term,
// text: term
// };
// }
//},
formatResult: function (item) {
return buildSomePrettyString();
},
formatSelection: function (item) {
return buildSomePrettyString();
},
sortResults: function (results, container, query) {
return results;
}
});
},
update: function (element, valueAccessor, allBindings, viewModel, bindingContext) { }
};
Uncomment the function createSearchCoice does not work. It causes an exception Error: Option 'createSearchChoice' is not allowed for Select2 when attached to a <select> element.
Someone could help me ?

How to get observable property name KnockoutJS

function Employee() {
var self = this;
self.name = ko.observable("John");
}
ko.bindingHandlers.test = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
// implementation
}
}
<div data-bind="test: name"></div>
Is there a way to get the observable name? not the value of the observable.
TIA.
Update:
This is the code snippet.
function ViewModel() {
var self = this;
$.ajax({
type: type,
url: url,
success: function (data) {
ko.mapping.fromJS(data, {} , self);
}
});
self.item = ko.observable(self.my_item);
self.selectedItem = ko.observable();
}
ko.bindingHandlers.item = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
$el = $(element);
var propName = allBindings().name;
var val = ko.unwrap(valueAccessor());
$el.attr("src", val);
$el.click(function () {
viewModel.selectedItem(propName);
});
},
update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
$el = $(element);
var ops = allBindings().name;
var val = ko.unwrap(valueAccessor());
$el.attr("src", val);
}
};
ko.bindingHandlers.selectItem = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
$el = $(element);
$el.attr("src", valueAccessor());
$el.click(function () {
bindingContext.$data.item()[ko.unwrap(bindingContext.$data.selectedItem)](valueAccessor());
});
}
};
<img height="25" width="25" data-bind="item: item().img1, name: 'img1'" />
<img height="20" width="20" data-bind="selectItem: '/images/myimage1.png'" />
<img height="20" width="20" data-bind="selectItem: '/images/myimage2.png'" />
<img height="20" width="20" data-bind="selectItem: '/images/myimage3.png'" />
When you click images that has selectItem the first image should replaced its src attribute. If you have better way to do this please suggest.
FYI, the properties inside items observables are link of images.
TIA.
You are getting ahead of yourself with the custom binding.
The bottom line is: You don't need a custom binding for what you want to do. It's easy - if you don't make it complicated:
function loadImages() {
// return $.get(url);
// mockup Ajax response, in place of a real $.get call
return $.Deferred().resolve({
items: [
{height: 20, width: 20, src: 'http://placehold.it/150/30ac17', title: 'image 1'},
{height: 20, width: 20, src: 'http://placehold.it/150/412ffd', title: 'image 2'},
{height: 20, width: 20, src: 'http://placehold.it/150/c672a0', title: 'image 3'}
]
}).promise();
}
function ImageList() {
var self = this;
// data
self.items = ko.observableArray();
self.selectedItem = ko.observable();
// init
loadImages().done(function (data) {
ko.mapping.fromJS(data, {}, self);
});
}
ko.applyBindings(new ImageList())
<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 src="https://cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.min.js"></script>
<div data-bind="with: selectedItem">
<img data-bind="attr: {height: 25, width: 25, src: src}">
</div>
<div data-bind="foreach: items">
<img data-bind="attr: {height: height, width: width, src: src}, click: $root.selectedItem" />
</div>
<hr>
<pre data-bind="text: ko.toJSON($root, null, 2)"></pre>
Note how I use selectedItem as a click event handler. This is possible because in event handlers, knockout passes the relevant object (in this case, an image object from the array) as the first argument. Conveniently, observables set their value to the first argument you call them with. And presto: You have click event handler that sets the last clicked object.
EDIT
"I need multiple selected item then my items are just my context menu not just one selected item."
function loadImages() {
// return $.get(url);
// mockup Ajax response, in place of a real $.get call
return $.Deferred().resolve({
items: [
{height: 20, width: 20, src: 'http://placehold.it/150/30ac17', title: 'image 1'},
{height: 20, width: 20, src: 'http://placehold.it/150/412ffd', title: 'image 2'},
{height: 20, width: 20, src: 'http://placehold.it/150/c672a0', title: 'image 3'}
]
}).promise();
}
function ImageList() {
var self = this;
// data
self.items = ko.observableArray();
self.selectedItems = ko.observableArray();
// init
loadImages().done(function (data) {
ko.mapping.fromJS(data, {}, self);
});
self.selectItem = function (item) {
var pos = ko.utils.arrayIndexOf(self.selectedItems(), item);
if (pos === -1) self.selectedItems.push(item);
};
self.deselectItem = function (item) {
var pos = ko.utils.arrayIndexOf(self.selectedItems(), item);
if (pos !== -1) self.selectedItems.remove(item);
};
}
ko.applyBindings(new ImageList())
<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 src="https://cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.min.js"></script>
<div data-bind="foreach: selectedItems">
<img data-bind="attr: {height: 25, width: 25, src: src}, click: $root.deselectItem">
</div>
<div data-bind="foreach: items">
<img data-bind="attr: {height: height, width: width, src: src}, click: $root.selectItem" />
</div>
<hr>
<pre data-bind="text: ko.toJSON($root, null, 2)"></pre>
<div data-bind="foreach: {data: skills, as: 'skill'}" >
<div data-bind="foreach: Object.keys(skill)" >
<a data-bind="text: $data"></a> : <a data-bind="text: skill[$data]"></a>
</div>
</div>
v = new function AppViewModel() {
this.skills = [{rates:"sdfdcvxcsd", cat: 2, car:55}, {color:"sdfdcvxcsd", zoo: 2,boat:55}];
}
ko.applyBindings(v);
Try something like this
view:
<div data-bind="test:name,propertyName:'name'"></div>
viewModel:
ko.bindingHandlers.test = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
var propertyName = allBindings().propertyName; //property name here
ko.bindingHandlers.text.update(element, valueAccessor);
alert(propertyName)
}
};
function Employee() {
var self = this;
self.name = ko.observable("John");
}
ko.applyBindings(new Employee());
working fiddle here
This will get you the observable name.
This converts the valueAccessor (function(){return name }) to a string which is then split to remove the observable name.
ko.bindingHandlers.GetObservableName = {
init: function (element, valueAccessor) {
var observableName = String(valueAccessor).split(' ')[1];
alert(observableName);
}
};
function Employee() {
var self = this;
self.name = ko.observable("John");
}
<div data-bind="GetObservableName: name"></div>
Here is a JSFiddle
The index of the split method in the fiddle is different to the one in the example above. The above works for me in Visual Studio 2013.
Thanks

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.

Categories

Resources