Here is the fiddle:https: https://jsfiddle.net/t5v7fmoq/1/
What I want to achieve:
I want to be able to update checkbox view automatically depending on the recieved state variable (which can have true or false value)
state variables (with initial states) for three checkboxes are:
self.state1 = ko.observable(true);
self.state2 = ko.observable(false);
self.state3 = ko.observable(true);
In the init function I populate observablearray:
self.init = function() {
self.availableItems([
new Item(1, "item1", self.state1(), self.onItemStateChange),
new Item(2, "item2", self.state2(), self.onItemStateChange),
new Item(3, "item3", self.state3(), self.onItemStateChange)
]);
In Item function I set the observable properties and onChnage method:
function Item(id, name, state, onChange) {
var self = this;
self.id = ko.observable(id);
self.name = ko.observable(name);
self.state = ko.observable(state);
self.state.subscribe(function(newValue) {
onChange(self, newValue);
});
}
With setTimeout I fake an one-time ajax call, which sets new states:
setTimeout(()=>{
self.state1(false)
self.state2(true)
self.state3(false)
self.availableItems()[0].state(self.state1())
self.availableItems()[1].state(self.state2())
self.availableItems()[2].state(self.state3())
},1000)
But, what I want to achieve, is that I want to avoid typing the following:
self.availableItems()[0].state(self.state1())
self.availableItems()[1].state(self.state2())
self.availableItems()[2].state(self.state3())
I want to code this behaviour and track this statuses using common practice and optimal coding...
I don't have the idea how to approach this problem differently.
I tried using arrays like this (so that later I can use forach and indexing):
setTimeout(()=>{
self.state1(false)
self.state2(true)
self.state3(false)
self.availableItems()[0].state(self.itemStatus()[0])
self.availableItems()[1].state(self.itemStatus()[1])
self.availableItems()[2].state(self.itemStatus()[2])
},1000)
But this does not work as expected.
In Short I would like to learn what coding approach to take to code the behaviour, so that when a new state is recieved from server, the proper state is applied to proper checkbox, and proper checkbox view is updated properly.
General truth: If you create numbered variables (item1, item2, item3), you are doing something wrong. Use arrays.
Depending on how you're getting state updates from the server, the implementation of updateState needs to be changed. My implementation below assumes you're getting an array of Boolean values, e.g. [true, true, false].
It's a good idea to make viewmodels that accept a params object and initialize themselves with it, so that's what the code below does.
function Item(params) {
var self = this;
self.id = ko.observable(params.id);
self.name = ko.observable(params.name);
self.state = ko.observable(params.state);
}
function ItemList(params) {
var self = this;
self.items = ko.observableArray(params.items.map(item => new Item(item)));
self.updateState = function () {
var items = self.items(),
randomStates = items.map(item => Math.random() < 0.5);
randomStates.forEach((state, i) => items[i].state(state));
};
}
var viewModel = new ItemList({
items: [
{id: "item1", name: "Item 1", state: false},
{id: "item2", name: "Item 2", state: false},
{id: "item3", name: "Item 3", state: true},
]
});
ko.applyBindings(viewModel);
.switchName {
font-weight: bold;
}
pre {
position: absolute;
right: 0;
top: 0;
left: 50%;
font-size: smaller;
]
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div data-bind="foreach: items">
<div class="switchBox">
<input type="checkbox" data-bind="checked: state, attr: {id: id}">
<label class="switchName" data-bind="text: name, attr: {for: id}"></label>
</div>
</div>
<button data-bind="click: updateState">Simulate Random Update</button>
<pre data-bind="text: ko.toJSON($root, null, 2)"></pre>
Related
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.
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
I'm building a simple app with AngularJS. The app make a async AJAX call to the server and the server returns an array like this:
{
paragraphs: [
{content: "content one"},
{content: "cnt two"},
{content: "random three"},
{content: "last one yeeaah"}
]
}
So I'm setting this content to the StorageService factory via my set method. Everything is fine here.
I'm using ng-repeat to render the results and JQuery UI sortable to be able to change the order of the elements. When an item is swapped my script is calling the StorageService.swap method and the element order in StorageService is updated, BUT ng-repeat isn't rerendering the change, but if I remove/add or change the content it's working. How I can force the angular to rerender the ng-repeat?
= JSFIDDLE =
http://jsfiddle.net/6Jzx4/3/
= Example =
When a swap occurs the ng-repeat should be rerendered, so the IDs are consecutive
= Code =
HTML
<div ng-controller="Test" sortable>
<div ng-repeat="item in data().paragraphs" class="box slide_content" id="{{$index}}">
{{item.content}}, ID: {{$index}}
</div>
<input type="button" ng-click="add()" value="Add">
</div>
JS
var App = angular.module("MyApp", []);
App.controller("Test", function($scope, StorageService) {
StorageService.set({
paragraphs: [
{content: "content one"},
{content: "cnt two"},
{content: "random three"},
{content: "last one yeeaah"}
]
});
$scope.data = StorageService.get;
$scope.add = StorageService.add;
});
App.directive("sortable", function(StorageService) {
return {
link: function(scope, element, attrs) {
$(element[0]).sortable({
cancel: ".disabled",
items: "> .slide_content:not(.disabled)",
start: function(e, t) {
t.item.data("start_pos", t.item.index());
},
stop: function(e, t) {
var r = t.item.data("start_pos");
if (r != t.item.index()) {
StorageService.sort($(this).sortable("toArray"));
}
}
});
}
};
});
App.factory('StorageService', function() {
var output = {};
return {
set: function(data) {
angular.copy(data, output);
return output;
},
get: function() {
return output;
},
add: function() {
output.paragraphs.push({
content: 'Content'
});
},
sort: function(order) {
var localOutput = [];
for (var j in order) {
var id = parseInt(order[j]);
localOutput.push(output.paragraphs[id]);
}
console.log('new order', localOutput);
output.paragraphs = localOutput;
return output;
}
};
});
Angular doesn't know you've changed the array. Executing your sort inside a scope.$apply() will address that.
Note that I've added a that variable since this changes meaning inside the apply.
var that = this;
scope.$apply(function() {
StorageService.sort($(that).sortable("toArray"));
}
But that fix uncovers other problems that appear to be caused by the interaction between the jQuery sortable and Angular (here's a fiddle that shows an attempt at resolving the problems but still has issues). These issues have been solved in Angular UI Sortable. So a good path forward may be to switch to this.
I'm trying to figure out why the text entry field is not active when the checkbox changes?
<form data-bind="foreach: editables">
<input type="checkbox" name="edit" data-bind=" checked: active" />
<input type="text" name="edit" data-bind="value: name, disable: !active" />
<br/>
</form>
var viewModel = function () {
this.editables = ko.observableArray(
[{
active: true,
name: "mi"
}, {
active: false,
name: "yo"
}, {
active: true,
name: "cel"
}]);
};
ko.applyBindings(new viewModel());
http://jsfiddle.net/legolito/2FAJN/2/
I hope that someone can helpme. (english isn't my native language, so i'm sorry if something is bad with my grammar )
Have you considered making the active property an observable?
http://jsfiddle.net/tzG3t/
var viewModel = function () {
this.editables = ko.observableArray(
[{
active: ko.observable(true),
name: "mi"
}, {
active: ko.observable(false),
name: "yo"
}, {
active: ko.observable(true),
name: "cel"
}]);
};
ko.applyBindings(new viewModel());
That because you are not using ko.observable in ko.observableArray
See the knockout documentation on observableArrays
Key point: An observableArray tracks which objects are in the array, not the state of those objects
Simply putting an object into an observableArray doesn’t make all of
that object’s properties themselves observable. Of course, you can
make those properties observable if you wish, but that’s an
independent choice. An observableArray just tracks which objects it
holds, and notifies listeners when objects are added or removed.
So make it observable and problem solved.
Fiddle: http://jsfiddle.net/2FAJN/4/
I am building a web app and am looking to convert the UI to use Knockout JS. I am a total noob in Knockout so please be kind!
Normally I would load an employee list (using PHP) and then if an employee is selected I would find the ID of that employee using JQuery and then make and AJAX call to my backend, fill in the result box and slide it down.
Is there a way to replicate this behavior in Knockout?
An boilerplate for you to start, uses jQuery and Knockout.
http://jsfiddle.net/5BHrc/6/
HTML
<ul data-bind="foreach: employees">
<li data-bind="css: {current: $data == $root.selected()}, click: $root.selected">
#<span data-bind="text: id"></span> - <span data-bind="text: name"></span>
</li>
</ul>
<div data-bind="slideVisible: ! loading(), html: employee_detail"></div>
CSS
.current {
background: blue;
color: white;
}
ul>li {
list-style: none;
}
JS
$(function() {
// for your requirment on sliding animation, this slideVisible is copied from http://knockoutjs.com/documentation/custom-bindings.html
ko.bindingHandlers.slideVisible = {
update: function(element, valueAccessor, allBindings) {
var value = valueAccessor();
var valueUnwrapped = ko.unwrap(value);
var duration = allBindings.get('slideDuration') || 400;
if (valueUnwrapped == true)
$(element).slideDown(duration); // Make the element visible
else
$(element).slideUp(duration); // Make the element invisible
}
};
var vm = {
employees: ko.observableArray([
// build your initial data in php
{id: 1, name: 'John'},
{id: 2, name: 'Tom'},
{id: 3, name: 'Lily'},
{id: 4, name: 'Bob'}
]),
selected: ko.observable(), // a placeholder for current selected
loading: ko.observable(false), // an indicator for ajax in progress
employee_detail: ko.observable() // holder for content from ajax return
};
// when user selects an employee, fire ajax
vm.selected.subscribe(function(emp) {
var emp_id = emp.id;
// ajax starts
vm.loading(true);
$.ajax('/echo/html/?emp_id='+emp_id, {
// just a simulated return from jsfiddle
type: 'POST',
data: {
html: "<b>Employee #" + emp_id + "</b><p>Details, bla bla...</p>",
delay: 0.2
},
success: function (content) {
// update employee_detail
vm.employee_detail(content);
},
complete: function() {
// ajax finished
vm.loading(false);
}
});
});
ko.applyBindings(vm);
});
This sounds similar to the drill down that happens with folders and emails in this knockout tutorial.