knockout subscription not working - javascript

I have been trying to subscribe to when a dropdown value changes. I have the following logic however I cannot seem to get it working.
HTML
<div id="case-pin-#modelItem.CaseID" data-caseid="#modelItem.CaseID" class="row hidden popovercontainer pinBinding">
<select data-bind="options:userPins,
value:selectedPin,
optionsCaption:'-- please select --',
optionsText: 'Name',
optionsValue: 'Id'"></select>
</div>
JS
function UserPinViewModel(caseId) {
var self = this;
self.selectedPin = ko.observable();
self.userPins = ko.observableArray([]);
self.caseId = caseId;
self.selectedPin.subscribe(function (newValue) {
console.log(newValue);
//addCaseToPin(newValue, self.caseId);
});
}
var pinObjs = [];
$(function () {
pinObjs = [];
$(".pinBinding").each(function () {
var caseId = this.getAttribute("data-caseid");
var view = new UserPinViewModel(caseId);
pinObjs.push(view);
ko.cleanNode(this);
ko.applyBindings(view, this);
});
})
The userPins array is populated by an AJAX call to the server as the values in the dropdown are dependent upon another section of the website which can change the values in the dropdown - here the logic I have used to populate the array.
function getPins() {
$.ajax({
type: 'POST',
url: '/Home/GetPins',
success: function (data) {
for (var i = 0; i < pinObjs.length; i++) {
pinObjs[i].userPins(data);
}
},
error: function (request, status, error) {
alert("Oooopppppsss! Something went wrong - " + error);
}
});
}
The actual values in the dropdowns all change to match what is returned from the server however whenever I manually change the dropdown, the subscription event is not fired.

You're using both jQuery and Knockout to manipulate the DOM, which is not a good idea. The whole idea of Knockout is that you don't manipulate the DOM, it does. You manipulate your viewModel.
Using cleanNode is also a code smell, indicating that you're doing things the wrong way. Knockout will handle that if you use the tools Knockout provides.
In this case, I was going to suggest a custom binding handler, but it looks like all you really want is to have a UserPinViewModel object created and applied to each instance of your .pinBinding element in the HTML. You can do that using the with binding, if you expose the UserPinViewModel constructor in your viewModel.
function UserPinViewModel(caseId) {
var self = this;
self.selectedPin = ko.observable();
self.userPins = ko.observableArray([]);
self.caseId = caseId;
self.selectedPin.subscribe(function(newValue) {
console.log(newValue);
//addCaseToPin(newValue, self.caseId);
});
// Pretend Ajax call to set pins
setTimeout(() => {
self.userPins([{
Name: 'option1',
Id: 1
}, {
Name: 'option2',
Id: 2
}, {
Name: 'option3',
Id: 3
}])
}, 800);
// Later, the options change
setTimeout(() => {
self.userPins([{
Name: 'animal1',
Id: 'Elephant'
}, {
Name: 'animal2',
Id: 'Pony'
}, {
Name: 'animal3',
Id: 'Donkey'
}])
}, 4000);
}
ko.bindingHandlers.pin = {
init: () => null,
update: () => null
};
ko.applyBindings({
pinVm: UserPinViewModel
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div id="case-pin-#modelItem.CaseID" data-bind="with: new pinVm('someCaseId')" class="row hidden popovercontainer pinBinding">
<select data-bind="options:userPins,
value:selectedPin,
optionsCaption:'-- please select --',
optionsText: 'Name',
optionsValue: 'Id'"></select>
</div>
Your getPins function suggests that the .pinBinding elements should correspond to the data being received. In that case, pinObjs should really be a part of your viewModel, and the elements should be generated (perhaps in a foreach) from the data, rather than being hard-coded. I don't know how that works with what I presume is the server-side #modelItem.CaseID, though.

Related

Call ng-click function from ng-change function

I'm new in angularjs and please clear me if I'm wrong.
Im using single select and want to fetch the value and based on value I want to perform action. Eg: If I will get "DELETE" then I want to send it to ng-click function with name delete. Below is my code :-
html.jsp
<body ng-app ng-controller="AppCtrl">
<select ng-model="some_action.type" ng-change="actionchange(some_action.type)" ng-options="type.value as type.displayName for type in types">
</select>
</body>
script.js
function AppCtrl($scope) {
$scope.some_action={
type: 'Select'
}
$scope.types = [
{value: 'DELETE', displayName: 'Delete'},
{value: 'SUSPEND', displayName: 'Suspend'}
]
}
$scope.actionchange= function(value) {
console.log('change action is -'+ value);
//here I will fetch some ids which will sent to some other's click function
};
So i'm using ng-click function for both action seperately and want to be use from ng-change which will call ng-click.
function AppCtrl($scope) {
$scope.some_action = {
type: 'Select'
}
$scope.types = [{
value: 'DELETE',
displayName: 'Delete'
},
{
value: 'SUSPEND',
displayName: 'Suspend'
}
]
}
$scope.actionchange = function(value) {
console.log('change action is -' + value);
var fetchIds= [];
// based on the condition push all ids to fetchIDS
$scope.method_Delete(fetchIds);
};
$scope.method_Delete=function(fetchIds){
// do something using fetchIds
}

Vuejs directive binding a model

I'm using a Vuejs 1.0 directive to turn a select field (single and multiple) into a Select2 jQuery plugin field.
Vue.directive('select2', {
twoWay: true,
priority: 1000,
params: ['max-items'],
bind: function () {
var self = this;
console.log(self.params);
$(this.el)
.select2({
maximumSelectionLength: self.params.maxItems,
theme: 'bootstrap',
closeOnSelect: true
})
.on('change', function () {
var i, len, option, ref, values;
if (self.el.hasAttribute('multiple')) {
values = [];
ref = self.el.selectedOptions;
for (i = 0, len = ref.length; i < len; i++) {
option = ref[i];
values.push(option.value);
}
return self.set(values);
} else {
return self.set(self.el.value);
}
})
},
update: function (value, oldValue) {
$(this.el).val(value).trigger('change')
},
unbind: function () {
$(this.el).off().select2('destroy')
}
});
This all works fine. I'm also trying to bind a model to the value of the field but can't seem to get it to bind properly.
<select class="form-control" name="genre" v-model="upload.genre" v-select2="">
<option value="50">Abstract</option>
<option value="159">Acapella</option>
<option value="80">Acid</option>
...
</select>
The upload.genre property does not update automatically.
v-model is actually syntactic sugar on passing a prop and on change event setting the value, so following:
<input v-model="something">
is equivalent of
<input v-bind:value="something" v-on:input="something = $event.target.value">
You have to also make similar changes, You can see this type code in the select2 example provided by vue team.
.on('change', function () {
vm.$emit('input', this.value)
})
With Vue 1.0
As you are using vue 1.0, there is a two-way options for directives which helps to write data back to the Vue instance, you need to pass in twoWay: true. This option allows the use of this.set(value) inside the directive:
Vue.directive('select2', {
twoWay: true,
bind: function () {
this.handler = function () {
// set data back to the vm.
// If the directive is bound as v-select2="upload.genre",
// this will attempt to set `vm.upload.genre` with the
// given value.
this.set(this.el.value)
}.bind(this)
this.el.addEventListener('input', this.handler)
},
unbind: function () {
this.el.removeEventListener('input', this.handler)
}
})
and in HTML:
<select class="form-control" name="genre" v-select2="upload.genre">

Cascading selects with default values

I have a 3 tier cascading select list that I have implemented using knockout.js / jQuery / json.
There may be instances where a select box only has 1 choice within it - in which case I'd like to not force the user to have to manually select it and instead default it to the single value, and cascade down to the next box automatically. Can this be done?
My select lists (the first is currently slightly different because it's generated by MVC Razor view with values supplied direct from view model):
<!--Variants-->
<select class="dim" data-bind="value: selectedDim1" id="Dim1" name="Dim1" onchange="FetchDim2();"><option selected="selected" value="">Select Colour</option>
<option value="Black">Colour:Black</option>
<option value="NAVYBLUE">Colour:Navy Blue</option>
</select>
<select id="Dim2" data-bind="value: selectedDim2, options: ddlDim2, optionsText: 'Text', optionsValue: 'Value', optionsCaption: 'Select Waist Size'" class="dim"></select>
<select id="Dim3" data-bind="value: selectedDim3, options: ddlDim3, optionsText: 'Text', optionsValue: 'Value', optionsCaption: 'Select Leg Length'" class="dim"></select>
My knockout code:
function DDLViewModel() {
this.ddlDim1 = ko.observableArray([]);
this.ddlDim2 = ko.observableArray([]);
this.ddlDim3 = ko.observableArray([]);
this.selectedDim1 = ko.observable();
this.selectedDim1.subscribe(FetchDim2, this);
this.selectedDim2 = ko.observable();
this.selectedDim2.subscribe(FetchDim3, this);
this.selectedDim3 = ko.observable();
this.selectedDim3.subscribe(FetchVariant, this);
}
var objVM = new DDLViewModel();
// Activates knockout.js
ko.applyBindings(objVM);
function FetchDim2() {
$.ajax({
type: 'POST',
url: '/product/getdims/', // we are calling json method
dataType: 'json',
// here we get value of selected dim.
data: { id: 20408,
level: 2,
head: 'Waist Size',
code: $("#Dim1").val()
},
success: function (dims) {
// dims contains the JSON formatted list of dims passed from the controller
objVM.ddlDim2(dims);
objVM.ddlDim3.removeAll();
},
error: function (ex) {
alert('Failed to retrieve dims.' + ex);
}
});
}
function FetchDim3() {
$.ajax({
type: 'POST',
url: '/product/getdims/', // we are calling json method
dataType: 'json',
// here we get value of selected dim.
data: { id: 20408,
level: 3,
head: 'Leg Length',
code: $("#Dim2").val()
},
success: function (dims) {
// dims contains the JSON formatted list of dims passed from the controller
objVM.ddlDim3(dims);
},
error: function (ex) {
alert('Failed to retrieve dims.' + ex);
}
});
}
I guess I (a) need to specify a default value if there is only on choice and (b) force the call of the code that populates the next level down? Not sure how to do either without breaking it all though!
Yikes! Don't mix jQuery and Knockout like that. Don't let jQuery do DOM manipulation (e.g. val(...)) but instead get the selected value from your view model.
This will also greatly simplify your view, as things like those id attributes become irrelevant.
Furthermore, I recommend making the Fetch... methods a dependency for your view model. In my example below I just inline those functions inside the view model constructor function, but you could also wrap them in a service and have that service as a dependency (you'd still have to provide input and success handlers to that service of course).
Another thing, quite needed / useful if you follow the above advice: use the var self = this idiom, instead of reiterating this everywhere / providing the this argument everywhere.
With all those things changed, it becomes trivial to fix your original question. Triggering cascaded updates can be done inside the success functions. Before I show the full snippet, here's the nitty gritty for your actual question:
success: function(dims) {
self.ddlDim3(dims);
if (dims.length === 1) {
self.selectedDim3(dims[0].Value);
}
}
Simply put, this selects the first option if there is only one, and lets Knockout handle updating the DOM (and cascading, if needed).
Here's a full demo based off your original code:
// Fake the Ajax requests:
var $ = {
ajax: function(options) {
if (options.data.level === 2 && options.data.code === "Black") {
options.success([{
Text: "Waist size S",
Value: "S"
}, {
Text: "Waist size M",
Value: "M"
}, {
Text: "Waist size L",
Value: "L"
}]);
}
if (options.data.level === 2 && options.data.code === "NAVYBLUE") {
options.success([{
Text: "Waist size M",
Value: "M"
}]);
}
// Not faking lvl 3 as extensively, but the same would hold as above.
if (options.data.level === 3) {
options.success([{
Text: "Legs 40",
Value: "40"
}]);
}
}
};
function DDLViewModel() {
var self = this;
self.ddlDim1 = ko.observableArray([]);
self.ddlDim2 = ko.observableArray([]);
self.ddlDim3 = ko.observableArray([]);
self.selectedDim1 = ko.observable();
self.selectedDim1.subscribe(FetchDim2);
self.selectedDim2 = ko.observable();
self.selectedDim2.subscribe(FetchDim3);
self.selectedDim3 = ko.observable();
self.selectedDim3.subscribe(FetchVariant);
function FetchDim2() {
console.log(2);
$.ajax({
url: '/product/getdims/',
data: {
id: 20408,
level: 2,
head: 'Waist Size',
code: self.selectedDim1()
},
success: function(dims) {
self.ddlDim2(dims);
self.ddlDim3.removeAll();
if (dims.length === 1) {
self.selectedDim2(dims[0].Value);
} else {
self.selectedDim2(null);
}
}
});
}
function FetchDim3() {
if (!self.selectedDim2()) {
self.ddlDim3.removeAll();
} else {
$.ajax({
data: {
id: 20408,
level: 3,
head: 'Leg Length',
code: self.selectedDim2()
},
success: function(dims) {
self.ddlDim3(dims);
if (dims.length === 1) {
self.selectedDim3(dims[0].Value);
}
}
});
}
}
function FetchVariant() {
// Noop / not provided in question
}
}
ko.applyBindings(new DDLViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<select data-bind="value: selectedDim1">
<option selected="selected" value="">Select Colour</option>
<option value="Black">Colour:Black</option>
<option value="NAVYBLUE">Colour:Navy Blue</option>
</select>
<select data-bind="value: selectedDim2, options: ddlDim2, optionsText: 'Text', optionsValue: 'Value', optionsCaption: 'Select Waist Size'"></select>
<select data-bind="value: selectedDim3, options: ddlDim3, optionsText: 'Text', optionsValue: 'Value', optionsCaption: 'Select Leg Length'"></select>

Knockout bind after select render

So I have a Select that has its options from a computed. I want to select a default every time the selects options change.
I have tried several different ways of doing it:
subscribe to list - is called before list has returned so changes the value of the observable alright but it dosnt render right because the list changes AFTER.
afterRender - Does not work with this type of binding.
OptionsafterRender - works, as in the fiddle below, HOWEVER its called for every individual item rather then just once on the whole render so strikes me as the Wrong Way to do this.
var rawData = [{
Type: "1",
Color: "Blue",
Name: "Blue Car"
}, {
Type: "2",
Color: "Blue",
Name: "Blue House"
}, {
Type: "1",
Color: "Red",
Name: "Red Car"
}, {
Type: "2",
Color: "Red",
Name: "Red House"
}];
var viewModel = {
FirstSelectedOption: ko.observable(),
SecondSelectOptions: null,
SecondSelectedOption: ko.observable(),
Load: function() {
var self = viewModel;
self.SecondSelectOptions = ko.computed(function() {
var selected = self.FirstSelectedOption();
var returnValue = new Array({
Type: "*",
Color: "All",
Name: "All"
});
var filteredlist = ko.utils.arrayFilter(rawData, function(item) {
return item.Type == selected;
});
returnValue = returnValue.concat(filteredlist);
return returnValue;
}, self);
self.SecondSelectedOption.SetDefault = function() {
// we want the default to always be blue instead 'all', blue might not be the last option
var self = viewModel;
var defaultoption = ko.utils.arrayFirst(self.SecondSelectOptions(), function(item) {
return item.Color == "Blue";
});
self.SecondSelectedOption(defaultoption);
};
}
};
viewModel.Load();
ko.applyBindings(viewModel);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<select data-bind="value: FirstSelectedOption">
<option value="1">Car</option>
<option value="2">House</option>
</select>
<br/>
<select data-bind="options: SecondSelectOptions,
optionsText: 'Name',
value: SecondSelectedOption,
optionsAfterRender: SecondSelectedOption.SetDefault"></select>
http://jsfiddle.net/dt627rkp/
The only way I can think off off the top of my head is a custom binding...and im not even sure that would really be possible without reimplemnting the entire options binding.
I can't be the first one to want this, is there a best practice/way that I'm missing?
The optionsAfterRender callback passes 2 parameters: option (element), and item (data bound to the option). The callback already loops over the options, so no need to reiterate:
self.SecondSelectedOption.SetDefault = function (option, item) {
var self = viewModel;
if (item.Color === 'Blue')
self.SecondSelectedOption(item);
};
Updated fiddle
Ref: from the docs
EDIT: That being said, if you don't want the options to re-evaluate every time,
you could also simply bind the change event with the setDefault method on the first <select>. If I were faced with this code 'issue', I would probably preprocess the data into separate arrays, like in this fiddle

ember.js <select> populated and selected by two different objects

I'm new to ember, so maybe I'm doing this wrong.
I'm trying to create a select dropdown, populated with three values supplied from an external datasource. I'd also like to have the correct value in the list selected based on the value stored in a different model.
Most of the examples I've seen deal with a staticly defined dropdown. So far what I have is:
{{#view contentBinding="App.formValues.propertyType" valueBinding="App.property.content.propertyType" tagName="select"}}
{{#each content}}
<option {{bindAttr value="value"}}>{{label}}</option>
{{/each}}
{{/view}}
And in my module:
App.formValues = {};
App.formValues.propertyType = Ember.ArrayProxy.create({
content: [
Ember.Object.create({"label":"App","value":"app", "selected": true}),
Ember.Object.create({"label":"Website","value":"website", "selected": false}),
Ember.Object.create({"label":"Mobile","value":"mobile", "selected": false})
]
});
And finally my object:
App.Property = Ember.Object.extend({
propertyType: "app"
});
App.property = Ember.Object.create({
content: App.Property.create(),
load: function(id) {
...here be AJAX
}
});
The dropdown will populate, but it's selected state won't reflect value of the App.property. I know I'm missing some pieces, and I need someone to just tell me what direction I should go in.
The answer was in using .observes() on the formValues. For some reason .property() would toss an error but .observes() wouldn't.
I've posted the full AJAX solution here for reference and will update it with any further developments.
App.formValues = {};
App.formValues.propertyType = Ember.ArrayProxy.create({
content: [],
load: function() {
var that = this;
$.ajax({
url: "/api/form/propertytype",
dataType: "json",
success: function(data) {
that.set("content", []);
_.each(data, function(item) {
var optionValue = Ember.Object.create(item);
optionValue.set("selected", false);
that.pushObject(optionValue);
});
that.update();
}
});
},
update: function() {
var content = this.get("content");
content.forEach(function(item) {
item.set("selected", (item.get("value") == App.property.content.propertyType));
});
}.observes("App.property.content.propertyType")
});
App.formValues.propertyType.load();

Categories