Here follows my model:
<!-- ko foreach: teamMembers -->
<tr>
<!-- ko foreach: days -->
<td>
<!-- ko foreach: shifts -->
<input type="text" data-bind="value: startTime">
<input type="text" data-bind="value: endTime">
<!-- /ko -->
</td>
<!-- /ko -->
</tr>
<!-- /ko -->
and my viewmodel:
function TimeCardViewModel() {
var self = this;
self.teamMembers=ko.observableArray();
}
function TeamMemberViewModel(data){
var self=this;
self.days=ko.observableArray();
for (var i=0; i<7; i++) {
self.days.push(new DayViewModel(...);
}
}
function DayViewModel(shifts){
var self=this;
self.shifts=ko.observableArray();
for (var i=0; i<shifts.length; i++) {
self.shifts.push(new ShiftElementsViewModel(...);
}
}
function ShiftElementsViewModel(a,b,c,d) {
var self=this;
self.startTime=ko.observable(a);
self.endTime=ko.observable(b);
}
var timeCardViewModel=new TimeCardViewModel();
ko.applyBindings(timeCardViewModel);
For each member, we have (for each day of the seven days of the week) a number of shifts. For each shift, we have pairs of startTime-endTime inputs.
As far as the visual result is concerned, there are rows which include all the weekly shifts of a member and it might be the case of multiple shifts per day per member. If we look at the columns, these include all the shifts for all the members for a certain day.
My great problem is that I want, whenever there is a blur event on the DOM element of endTime, to focus on the DOM element of startTime vertically. For example, if we are on Monday and the first member has two shifts I want to focus on the startTime of the second shift of the first member when blur is occurring to the endTime of the first shift and then on the startTime of the first shift on Monday of the second member when blur is occurring to the endTime of the second shift of the first member. The same for Tuesday etc. How may I achieve that? For the time being, the cursor is travelling horizontally.
Here i am showing you one of the approaches by using all knockout. It shows you the logic how to implement yours since I don't have your data sample and you might need to modify it based on your data
Working example : https://jsfiddle.net/kyr6w2x3/48/
HTML:
<table>
<tbody>
<!-- ko foreach: teamMembers -->
<tr>
<td data-bind="text:name"></td>
<!-- ko foreach: days -->
<td>
<h4 data-bind="text:name"></h4>
<input type="text" data-bind="value: startTime ,hasFocus :getFocus">
<input type="text" data-bind="value: endTime ,event:{blur: $root.endTimeBlur}">
</td>
<!-- /ko -->
</tr>
<!-- /ko -->
</tbody>
</table>
JS:
var data ={
teamMembers: [{
Name: "Member A",
id:1,
days: [{startTime: '8:00',endTime: "4:00" ,name:'Monday',parentId:1},{startTime: '8:00',endTime: "4:00",name:'Tuesday',parentId:1},{startTime: '8:00',endTime: "4:00",name:'Wednesday',parentId:1},{startTime: '8:00',endTime: "4:00",name:'Thursday',parentId:1},{startTime: '8:00',endTime: "4:00",name:'Friday',parentId:1},{startTime: '8:00',endTime: "4:00",name:'Saturday',parentId:1},{startTime: '8:00',endTime: "4:00",name:'Sunday',parentId:1}]
},
{
Name: "Member B",
id:2,
days: [{startTime: '8:00',endTime: "4:00" ,name:'Monday' ,parentId:2},{startTime: '8:00',endTime: "4:00",name:'Tuesday' ,parentId:2},{startTime: '8:00',endTime: "4:00",name:'Wednesday',parentId:2},{startTime: '8:00',endTime: "4:00",name:'Thursday',parentId:2},{startTime: '8:00',endTime: "4:00",name:'Friday',parentId:2},{startTime: '8:00',endTime: "4:00",name:'Saturday',parentId:2},{startTime: '8:00',endTime: "4:00",name:'Sunday',parentId:2}]
},
{
Name: "Member C",
id:3,
days: [{startTime: '8:00',endTime: "4:00" ,name:'Monday',parentId:3},{startTime: '8:00',endTime: "4:00",name:'Tuesday',parentId:3},{startTime: '8:00',endTime: "4:00",name:'Wednesday',parentId:3},{startTime: '8:00',endTime: "4:00",name:'Thursday',parentId:3},{startTime: '8:00',endTime: "4:00",name:'Friday',parentId:3},{startTime: '8:00',endTime: "4:00",name:'Saturday',parentId:3},{startTime: '8:00',endTime: "4:00",name:'Sunday',parentId:3}]
},]
}
var memberViewModel = function(data) {
var self = this ;
self.days = ko.observableArray([]);
self.name = ko.observable(data.Name);
self.id = ko.observable(data.id);
self.days($.map(data.days, function (item) {
return new daysViewModel(item);
}));
}
var daysViewModel = function (data){
var self = this ;
self.getFocus = ko.observable(false);
self.startTime = ko.observable(data.startTime);
self.endTime = ko.observable(data.endTime);
self.name = ko.observable(data.name)
self.parentId = ko.observable(data.parentId);
}
function ViewModel() {
var self = this;
self.teamMembers = ko.observableArray([]);
self.teamMembers($.map(data.teamMembers, function (item) {
return new memberViewModel(item);
}));
self.endTimeBlur = function(data){
ko.utils.arrayFirst(self.teamMembers(), function (item,i) {
if (item.id() == data.parentId() && self.teamMembers()[i+1] ) {
//here you set getFocus to true to make next member 's monday gets focus
self.teamMembers()[i+1].days()[0].getFocus(true);
return;
}
});
}
}
ko.applyBindings(new ViewModel());
This should work for you...
jQuery(function($) {
$('body').on("blur", "input[data-bind*='value: endTime']", function() {
var
$t = $(this), // Current input
$td = $t.closest('td'), // Current input's parent td
i = $td.find('input[data-bind*="value: endTime"]').index($t), // Index of current input = current shift index
$target = $td.find('input[data-bind*="value: startTime"]').eq(i + 1); // Target is current shift + 1
if ($target.length) {
$target.focus();
}
});
});
The idea is to bind an event handler to blur event of every input that contains value: endTime in data-bind attribute.
If this handler, we find out the index of endTime input in day, add 1 to it and focus the startTime input with that index in the same td (day) 😉
Since the elements dont exist right after document is loaded (but rendered by knockout), we bind the handler to body for inputs selected by input[data-bind*='value: endTime'].
My advice would be to use a computed tabindex attribute.
In your $root viewmodel, we'll compute an array of shift.start and end observables ordered like you want them to be in terms of focus.
We'll also make a factory method that returns a computed index for each <input> bind.
Each input gets an attr: { 'tabindex': getTabIndex() } binding that makes sure the index stays up to date
This approach will work for people that use the tab key to navigate through the form. Most importantly; now that you have a computed list of sorted input observables, you can easily bind to events to select a previous/next one.
Here's an example:
var Root = function() {
this.members = ko.observableArray([]);
for (var i = 0; i < 5; i += 1) {
this.members.push(new Member());
}
// Note: you could do this in less lines of code, but I wanted
// to be extra transparent to show the structure of the data
// transform.
var orderedShiftInputs = ko.pureComputed(function() {
// We want the days to be leading, so we create a
// two-dimensional array: [[meber1day1, member2day1], [member1day2], etc]
// Note: members _cannot_ skip days
var mergedDays = [];
this.members().forEach(function(member) {
member.days().forEach(function(day, index) {
if (!mergedDays[index]) {
mergedDays[index] = [];
}
mergedDays[index] = mergedDays[index].concat(day);
});
});
// We flatten the 2d-array of days to a list of shifts:
// [member1day1shift1, member1day1shift2, member2day1shift1, etc]
var mergedShifts = mergedDays.reduce(function(shifts, days) {
var allShifts = days.reduce(function(all, day) {
return all.concat(day.shifts());
}, []);
return shifts.concat(allShifts);
}, []);
// We flatten the shifts into an array of start and end observables:
// [member1day1shift1start, member1day1shift1end, etc.]
return mergedShifts.reduce(function(inputs, shift) {
return inputs.concat([shift.start, shift.end]);
}, []);
}, this);
this.getTabIndex = function(data) {
return ko.computed(function() {
// Find the start or end data in our sorted array.
// In this example, we can start with index 1. In your app,
// there might be other input elements before the shifts...
var START_TAB_INDEX = 1;
return orderedShiftInputs().indexOf(data) + START_TAB_INDEX;
}, this);
}.bind(this);
}
var Member = function() {
this.days = ko.observableArray([]);
for (var i = 0; i < 2; i += 1) {
this.days.push(new Day());
}
}
var Day = function() {
this.shifts = ko.observableArray([]);
for (var i = 0; i < 3; i += 1) {
this.shifts.push(new Shift());
}
}
var Shift = function() {
this.start = ko.observable(1);
this.end = ko.observable(2);
}
ko.applyBindings(new Root());
td {
border: 1px solid black
}
input {
display: inline;
width: 30px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<table>
<tbody>
<!-- ko foreach: members -->
<tr>
<td data-bind="text: 'Member-' + $index()"></td>
<!-- ko foreach: days -->
<td>
<span data-bind="text: 'Day ' + $index()"></span>
<br/>
<!-- ko foreach: shifts -->
<input type="text" data-bind="value: start,
attr: { 'tabindex': $root.getTabIndex(start) }">
<input type="text" data-bind="value: end,
attr: { 'tabindex': $root.getTabIndex(end) }">
<!-- /ko -->
</td>
<!-- /ko -->
</tr>
<!-- /ko -->
</tbody>
</table>
I'm working on a project at the moment using angular JS.
I've used a directive to switch from advanced search so that the results can be viewed on its own page. basically when submit is clicked, the advanced search is hidden and only results table can be seen. so now, once on results table, I want to be able to go back to that advanced search to the point before I hit that submit button, the data is still there, I have just reversed the directive in a way. any suggestions would be greatly appreciated, this is all fairly new to me.
thanks! (please note, my search controller uses TypeScript)
this is being called just under the submit button on the search_partial.html
<div class="submit">
<button class="btn btn-primary" type="button" name="Submit" ng-click="vm.validateForm()" ng-disabled="!(!!vm.hospitalNumberInput || !!vm.nhsNumberInput || !!vm.fullNameInput || !!vm.sexInput || vm.yearOfBirthInput || !!vm.streetInput || !!vm.cityInput
|| !!vm.postCodeInput || !!vm.chosenCountry)">Search</button>
Clear all
</div>
</form>
<section>
<div id="searchDirective" search-results patients="vm.results" class="ng-hide"></div>
</section>
and I have a directives file called search.results.directive.js
(function () {
angular
.module('app.search.results')
.directive('searchResults', function() {
return {
restrict: 'AE',
templateUrl: 'app/search/Partials/result_partial.html',
scope: {
patients: '='
}
};
});
})();
so what I'm trying to do now that I can see the results-partial.html on the screen in front of me, I want to be able to click a back button on there to take me back to the search-partial.html at the point before the user clicked that submit button so that the data in the boxes can be altered if needs be or more search data added. (at the moment I have a back href going to the home url, it works for now, but that's what im hoping to replace).
results-partial.html:
<main class="container">
<!-- RESULT -->
<section class="result-display">
<form>
<div class="searchresults content table-responsive">
<h2>Search Criteria: </h2>
<h2> Search Results: {{patients.length}} patients found</h2>
<table class="table resultstable table-striped">
<thead>
<tr class="theading">
<th>Hospital number</th>
<th>NHS Number</th>
<th>Full Name</th>
<th>DOB</th>
<th>Sex</th>
</tr>
</thead>
<!--repeated simply for style insight-->
<tbody>
<tr ng-repeat="patient in patients">
<td>{{patient.resource.hospitalNumber}}</td>
<td>{{patient.resource.nhsNumber}}</td>
<td>{{patient.resource.nameString}}</td>
<td>{{patient.resource.birthDate}}</td>
<td>{{patient.resource.gender.toUpperCase()}}</td>
</tr>
</tbody>
</table>
</div>
<a href style="float: right; font-size:120%;" onclick="location.href='http://localhost:3000/';"><i class="close"></i><u>Back to Search</u></a>
</form>
</section>
</main>
If I understand the question right, you want to preserve the values in the input.
You can use a factory or value to store the data.
myApp.factory('DataHolder', function (){
return data;
});
// Or, with value
myApp.value('DataHolder', data);
And in your controller you can access that data anywhere.
myApp.controller('Ctrl', function ($scope, DataHolder){
$scope.data = DataHolder;
});
So when you come back to the back state, if you have data stored you can get it back to show it.
fixed it. managed to get it working my changing the flag to false with "returnToSearch()" function at the bottom.
createDisplayParams(): void {
// add in search params to display params
var paramTypes: string[] = [];
for (var i: number = 0; i < this.searchParams.length; ++i) {
var objectIndex: number = paramTypes.indexOf(this.searchParams[i].ParamName);
if (objectIndex === -1) {
// if param name dosen't exist, add it to the paramTypes
paramTypes.push(this.searchParams[i].ParamName);
}
}
for (var j: number = 0; j < paramTypes.length; ++j) {
var valueParams: core.ISearchParam[] = [];
valueParams =_.filter(this.searchParams, searchParam => { //lodash
return searchParam.ParamName == paramTypes[j];
});
var valueStrings: string[] = [];
valueParams.forEach(v => valueStrings.push(v.ParamValue));
this.displayParams.push({ paramType: paramTypes[j], paramValue: valueStrings.join(", ") });
}
}
obtainPatientInformation(): void {
this.createSearchParams();
if (this.searchParams.length > 0) {
var rawResults: angular.IPromise<core.IPatient> = this.searchDataService.performAdvancedSearch(this.searchParams);
var searchControllerInstance: SearchController = this;
this.createDisplayParams();
rawResults.then(
function success(response: any): void {
if (response.data.entry && response.data.entry.length > 0) {
searchControllerInstance.noResults = false;
searchControllerInstance.results = response.data.entry;
for (var index: number = 0; index < searchControllerInstance.results.length; index++) {
var patient: core.IEntry = searchControllerInstance.results[index];
patient.resource.nameString = '';
if (patient.resource.name) {
var familyNameArray: string[] = patient.resource.name[0].family;
for (var familyIndex: number = 0; familyIndex < familyNameArray.length; familyIndex++) {
var familyName: string = familyNameArray[familyIndex];
patient.resource.nameString = patient.resource.nameString + ' ' + familyName.toUpperCase() + ',';
}
var givenNameArray: string[] = patient.resource.name[0].given;
for (var givenIndex: number = 0; givenIndex < givenNameArray.length; givenIndex++) {
var givenName: string = givenNameArray[givenIndex];
patient.resource.nameString = patient.resource.nameString + ' ' + givenName;
}
}
var identifiers: core.IIdentifier[] = patient.resource.identifier;
for (var indentifierIndex: number = 0; indentifierIndex < identifiers.length; indentifierIndex++) {
var identifier: core.IIdentifier = identifiers[indentifierIndex];
if (identifier.system) {
if (identifier.system === 'nhsNumber') {
patient.resource.nhsNumber = identifier.value;
}
if (identifier.system === 'hospitalNumber') {
patient.resource.hospitalNumber = identifier.value;
}
}
}
}
} else {
searchControllerInstance.noResults = true;
searchControllerInstance.results = null;
}
});
}
this.searchClicked = true;
this.checkSearch();
}
checkSearch(): void {
var resultSectionElements: angular.IAugmentedJQuery = angular.element('[id*="resultSection"]');
var advanceSearchSectionElements: angular.IAugmentedJQuery = angular.element('[id*="advanceSearchSection"]');
if (this.searchClicked) {
resultSectionElements.removeClass('ng-hide');
advanceSearchSectionElements.removeClass('ng-show');
advanceSearchSectionElements.addClass('ng-hide');
} else {
resultSectionElements.addClass('ng-hide');
advanceSearchSectionElements.removeClass('ng-hide');
advanceSearchSectionElements.addClass('ng-show');
}
}
returnToSearch(): void {
this.searchClicked = false;
this.searchParams.splice(0,this.searchParams.length);
this.displayParams.splice(0,this.displayParams.length);
this.checkSearch();
}
We have a situation as mentioned below:
There is a set of data for a search panel, it's called in several pages with different types of components and placement of it. There can be combo boxes, radio buttons, input boxes and buttons.
Knockout has a feature of template binding in which we can have the flexibility to show numerous panels on condition using a template in the html mapped to MOdel.
Below is the code and pattern:
HTML:
<div id="content-wrapper">
<div class="spacer"></div>
<div>
<table class="data-table">
<thead>
<tr>
<th colspan="4"> Search </th>
</tr>
</thead>
<tbody data-bind="foreach: preSearchData" >
<tr>
<!-- ko template: { name: 'label_' + templateName()} -->
<!-- /ko -->
</tr>
</tbody>
</table>
</div>
</div>
<script type="text/html" id="label_Combo">
<td>It is a Combo </td>
</script>
<script type="text/html" id="label_Number">
<td>
It is a Number
</td>
</script>
MODEL:
Models.Components = function(data) {
var self = this;
self.number = data.number;
self.labelCd = data.labelCd;
self.xmlTag = data.xmlTag;
self.Type = new Cobalt.Models.Type(data.Type);
};
Models.Type = function(data) {
var self = this;
self.component = data.component;
self.records = data.records;
self.minLength = data.minLength;
self.maxLength = data.maxLength;
self.defaultValue = data.defaultValue;
self.targetAction = data.targetAction;
};
Models.ComponentType = function (paymentTypeCode, data, actionId) {
var ret;
self.templateName(data.component);
if (!data || (actionId === Cobalt.Constant.Dashboard.copyProfile))
data = {};
if (paymentTypeCode == Cobalt.Constant.Dashboard.creditCard)
ret = new Cobalt.Models.CreditCardPaymentType(data.cardHolderName, data.cardNumber, data.cardExpireDate);
else if (paymentTypeCode == Cobalt.Constant.Dashboard.dd)
ret = new Cobalt.Models.DDPaymentType(data.pinNumber);
else if (Cobalt.Utilities.startsWith(paymentTypeCode, Cobalt.Constant.Dashboard.yahooWallet)) {
if (!data && paymentTypeCode.indexOf('~') > -1) {
data.payCode = paymentTypeCode.substr(paymentTypeCode.indexOf('~') + 1, paymentTypeCode.lastIndexOf('~'));
data.billingAgentId = paymentTypeCode.substr(paymentTypeCode.lastIndexOf('~') + 1);
}
ret = new Cobalt.Models.WalletPaymentType(data.payCode, data.billingAgentId);
}
else if (paymentTypeCode == Cobalt.Constant.Dashboard.ajl) {
ret = new Cobalt.Models.DDPaymentType(data.pinNumber);
}
else
ret = data || {};
return ret;
};
Models.POCModel = function () {
var self = this;
self.templateName = ko.observable();
self.preSearchData = ko.observableArray([]);
self.getResultData = function () {
var data = Cobalt.Data.getResultData();
var componentList = data.componentList;
self.preSearchData(componentList);
};
};
Above code gives me a error saying:
Ajax error: parsererror ( Error: Unable to parse bindings. Message: ReferenceError: templateName is not defined; Bindings value: template:
{ name: 'label_' + templateName()} ) cobalt.init.js:66
This is not a direct answer to your question, but it shows an alternate way of doing this using the ViewModel type to find the view (Template)
http://jsfiddle.net/nmLsL/2
Each type of editor is a ViewModel
MyApp.Editors.BoolViewModel = function(data) {
this.checked = data;
};
MyApp.Editors.BoolViewModel.can = function(data) {
return typeof data === "boolean";
};
And it has a can function that determins if it can edit the value
I then usea library called Knockout.BindingConventions to find the template connected to the ViewModel
https://github.com/AndersMalmgren/Knockout.BindingConventions/wiki/Template-convention
Your foreach binding creates a child binding context, which doesn't include templateName since that's part of the parent. Replace it with
<!-- ko template: { name: 'label_' + $parent.templateName()} -->
In my view I am looping through an observableArray (itemGroup) that has one property that is also an observableArray (item). I have a method to remove an entire itemGroup and one to remove an item from and itemGroup but I would like to add in some logic along the lines of it there is only 1 item left in the group removing that item should also remove the itemGroup.
here is an example of the relevant parts of my view model and view.
my JS
var ItemModel = function(item) {
var self = this;
self.name = ko.observable(item.name);
self.price = ko.observable(item.price);
};
var ItemGroupModel = function(itemGroup) {
var self = this;
self.order = ko.observable(itemGroup.order);
self.items = ko.observableArray(ko.utils.arrayMap(itemGroup.items, function(item){
return new ItemModel(item);
}));
self.type = ko.observable(item.type);
self.removeItem = function(item) {
self.items.remove(item);
}
};
var ViewModel = function(data) {
var self = this;
self.itemGroups = ko.observableArray(ko.utils.arrayMap(data.itemGroups, function(itemGroup) {
return new ItemGroupModel(item);
}));
// some other properties and methods
self.removeItemGroup = function(itemGroup) {
self.itemGroups.remove(itemGroup);
}
};
My View
<ul data-bind="foreach: {data: VM.itemGroups, as: 'itemGroup'}">
<li>
<button data-bind="click: $root.VM.removeItemGroup">X</button>
<ul data-bind="foreach: {data: itemGroup.items, as: 'item'}">
<li>
<!-- ko if: itemGroup.items().length > 1 -->
<button data-bind="click: itemGroup.removeItem">X</button>
<!-- /ko -->
<!-- ko ifnot: itemGroup.items().length > 1 -->
<button data-bind="click: function () { $root.VM.removeItemGroup($parent) }">X</button>
<!-- /ko -->
</li>
</ul>
</li>
</ul>
This works but to me it isnt ideal. It is my understanding that knockout should help me get away from using an anonymous function like "function () { $root.VM.removeItemGroup($parent) }" but I am not sure how to do it another way. Also removing the if and ifnot statements would be good to clean up as well.
I would like to give my solution
send index of itemGroups and items as argument to remove method.
Hope you know how to send index
Then check the length of itemGroups
self.remove(itemGroupsIndex,itemsIndex) {
var itemGroupsLength = self.itemGroups()[itemGroupsIndex].items().length;
if(itemGroupsLength = 1) {
self.itemGroups.remove(itemGroupsIndex);
}
else {
self.itemGroups()[itemGroupsIndex].items.remove(itemsIndex);
}
};
I have an array that I'm removing items from but I'm keeping count of the number of items to do UI formatting. I need to be able to have the bind update.
ko.applyBindings(viewModel);
getFoos();
var viewModel = {
foos: ko.observableArray([]),
reloadFoos: function () {
getFoos();
},
removeFoo: function () {
remove(this);
}
};
var foo = function () {
this.Id = ko.observable();
this.Name = ko.observable();
this.Count = ko.observable();
};
function remove(foo) {
viewModel.foos.splice(viewModel.foos.indexOf(foo), 1);
viewModel.foos.each(function(index) {
viewModel.foos[index].Count = index%10 == 0;
});
}
function getFoos() {
viewModel.foos([]);
$.get("/myroute/", "", function (data) {
for (var i = 0; i < data.length; i++) {
var f = new foo();
f.Id = data[i];
f.Name = data[i];
f.Count = i%10 == 0;
viewModel.foos.push(f);
}
});
}
<div data-bind="foreach: foos">
<div style="float: left">
<a href="javascript:void(0);" data-bind="click : $parent.removeFoo, attr: { id: Id }">
<label data-bind="value: Name"></label>
</a>
</div>
<!-- ko if: Count -->
<div style="clear: left"></div>
<!-- /ko -->
</div>
When the click event fires the item is removed from the array but the if bind doesn't get updated and the ui formatting is off. I'm trying to keep from reloading the data because the ui block bounces as it removes and reloads.
Your UI is not being updated because when you do your assignment to Count, you aren't assigning as an observable. You are replacing the observable with a straight boolean value. So, your assignment calls like this one:
viewModel.foos[index].Count = index%10 == 0;
Will cause viewModel.foos[index].Count to be equal to true or false and the value won't be stored in the observable.
That line should be this instead:
viewModel.foos[index].Count(index%10 == 0);
That will set the observable correctly. Note that you must change all of your assignments to observables to be set this way. See the "Reading and Writing Observables" section of this page: Knockout Observables.