Drill down Ajax style in knockout js - javascript

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.

Related

How to track state change for multiple checkboxes in ko.js

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>

Why aren't the inputs changing color?

I'm on a angularjs application with jquery and can't seem to change the background color of some inputs. In short, when the user clicks "hide stage" text or "unhide stage" text, the input boxes background should change to white/lightgrey respectively. But the only thing that changes is the text between "hide stage" to "unhide stage" which is fine. Here is the angularjs file:
function stages($scope,$rootScope,$apiSrvc,$compile){
// defintions
// setting up stages statuses to angular array.
$scope.stages_visibilities = ['unhidden','unhidden','unhidden','unhidden','unhidden','unhidden','unhidden','unhidden'];
// changes the stage's status and it's visibility settings
$scope.setStageStatus = function(stageN,status){
$scope.stages_visibilities[stageN] = 'hidden';
if(status === 'unhide'){
$scope.stages_visibilities[stageN] = 'unhidden';
}
$scope.showStages();
};
// shows the stage rows.
$scope.showStages = function() {
// update(populate) the caption/value rows
$('#stages_rows').empty();
$nStages = parseInt($scope.nStages);
var stage_row = "";
for(var i=0; i < $nStages; i++){
stage_row += '<div class="stage_row">'+
'<input type="text" id="stage_caption_'+i+'" class="stg_caption" />'+
'<input type="number" id="stage_value_'+i+'" class="stg_val" />';
// show hide/unhide for this stage.
if($scope.stages_visibilities[i] === 'unhidden'){
stage_row += '<span id="stage_hide_unhide_'+i+'" class="hide_unhide_stg" ng-click="setStageStatus('+i+',\'hide\')">hide stage</span>';
// set row color to white.
$("#stage_caption_"+i).css("background-color","white");
$("#stage_value_"+i).css("background-color","white");
$("#stage_value_"+i).prop('disabled', false);
$("#stage_caption_"+i).prop('disabled', false);
}
else {
stage_row += '<span id="stage_hide_unhide_'+i+'" class="hide_unhide_stg" ng-click="setStageStatus('+i+',\'unhide\')">un-hide stage</span>';
// set row color to lightgrey.
$("#stage_caption_"+i).css("background-color","lightgrey");
$("#stage_value_"+i).css("background-color","lightgrey");
$("#stage_value_"+i).prop('disabled', true);
$("#stage_caption_"+i).prop('disabled', true);
}
stage_row += '</div>';
}
$("#stages_rows").append(stage_row);
// register new directives to angularjs
$compile($("#stages_rows").contents())($scope);
}
}
All the angular js pre-liminary stuff work fine, it's just that section of the code where it doesn't change the background colors of the input(#stage_caption_i and #stage_value_i) which is in the $scope.showStages function. I have no idea what is wrong.
Here is a more "Angular" approach to what you're trying to do. I would recommend setting aside your jQuery knowledge as it really should only be used in a very limited fashion in directives. As georgeawg mentioned you shouldn't do DOM manipulation in the controller. Rather you should modify the values of your model and then put all the view-specific stuff in the HTML.
A preferred method would be to make the hidden/visible status a property on the model, rather than maintaining a separate array with that data. Of course, you can still use an array, but then you have to write a method to check the value of the visibility in the corresponding array when you want to change a class or toggle the disabled status. Using a method like that will cause a lot of traffic between the controller and the view as the view calls back for each item. Probably not noticeable on a small sample like this, but could lead to performance issues on a larger view. Even if you are getting this data back from a database or some other storage on the back end you can still extend the model to include a visibility property on the client.
The id values aren't needed, but I've included them to show how to use $index in case you decide to go the route of storing visibility in a separate array. You could use the $index value from the ng-repeat to reference the proper element in the visibility array.
This separation of controller and view will help with long term maintainability and keep Angular and jQuery from stepping on each other as they both manipulate elements of the DOM.
angular.module('app', [])
.controller('ctrl', ($scope) => {
$scope.stages = [{
caption: 'Stage 0',
value: 0,
hidden: false
}, {
caption: 'Stage 1',
value: 1,
hidden: false
}, {
caption: 'Stage 2',
value: 2,
hidden: false
}, {
caption: 'Stage 3',
value: 3,
hidden: false
}, {
caption: 'Stage 4',
value: 4,
hidden: false
}, {
caption: 'Stage 5',
value: 5,
hidden: false
}, {
caption: 'Stage 6',
value: 6,
hidden: false
}, {
caption: 'Stage 7',
value: 7,
hidden: false
}];
// changes the stage's status and its visibility settings
$scope.setStageStatus = (stage) => {
stage.hidden = !stage.hidden;
};
});
.hidden {
background-color: lightgray;
}
.unhidden {
background-color: white;
}
.hide_unhide_stg {
cursor: pointer;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.7.2/angular.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
<div class="stage_row" ng-repeat="stage in stages">
<input type="text"
id="stage_caption_{{$index}}"
class="stg_caption"
ng-model="stage.caption"
ng-disabled="stage.hidden"
ng-class="{hidden: stage.hidden, unhidden: !stage.hidden}" />
<input type="number"
id="stage_value_{{$index}}"
class="stg_val"
ng-model="stage.value"
ng-disabled="stage.hidden"
ng-class="{hidden: stage.hidden, unhidden: !stage.hidden}" />
<span class="hide_unhide_stg"
ng-click="setStageStatus(stage)"><span ng-if="stage.hidden">un-</span>hide stage</span>
</div>
</div>

Polymer iron-scroll-threshold not triggering

I want to make a feed that automatically loads items when the bottom of the current page is reached, however the iron-scroll-threshold doesn't trigger. I'm using my api call to fill the items in the template and the restaurants load just fine. Also when I bind a load function to a button it works just fine. It seems that the iron-scroll-threshold never triggers. Can anyone explain to me what I'm missing/doing wrong?
Code:
<iron-scroll-threshold id="threshold" lower-threshold="100" on-lower-threshold="loadMoreData">
<div id="scroller" class="vertical layout center" fill>
<template is="dom-repeat" items="[[restaurants]]" filter="{{computeFilter(searchInput)}}" scroll-target="threshold" on-scroll="_scrollHandler">
<!-- Items -->
</template>
<div class="loadingIndicator" hidden$="[[!loadingRestaurants]]">
<paper-spinner active$="[[loadingRestaurants]]"></paper-spinner> Fetching restaurants</b>
</div>
</div>
</iron-scroll-threshold>
<script>
Polymer({
is: 'my-view2',
properties: {
restaurants:{
type: Array,
value: []
},
offset:{
type: Number,
value: 0
},
limit:{
type: Number,
value: 50
},
loadingRestaurants: Boolean
},
ready: function () {
this.$.requestRestaurants.generateRequest();
},
handleResponse: function (data) {
var self = this;
var response = data.detail.response;
response.forEach(function(restaurant){
self.push('restaurants', restaurant);
});
console.log(this.restaurants);
this.$.threshold.clearTriggers();
},
toggle: function(e) {
console.log(this.$.threshold.top);
var index = "#collapse" + e.model.__data.index;
this.$$(index).toggle();
this.loadMore();
},
loadMore: function() {
console.log("test");
this.offset+=50;
this.limit+=50;
this.$.requestRestaurants.generateRequest();
this.$.threshold.clearLower();
this.$.threshold.clearTriggers();
}
});
The naming was inconsistent
on-lower-threshold="loadMoreData"
loadMore: function()

knockout subscription not working

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.

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

Categories