Knockout.js: combining "visible" and multiple view models - javascript

I have spent some time trying to unscramble this issue. I have a page that I would like to be used by a beauty salon owner to manage two things:
Appointments
Days with special schedule
To select a 'view' I am using a toggle that is based on Knockout's visible binding:
So when a radio button is clicked a div is being rendered and another hidden:
<div id="selectingScopes" class="col-xs-6 col-md-4">
<fieldset>
<legend>View:</legend>
<div id="radioControls" class="switch-toggle well">
<input jsf:id="appointmentsRadio" name="appointmentsRadio" type="radio"
data-bind="checked: selectedScope" value="#{bigcopy.appointmentsLabel}"/>
<label for="appointmentsRadio" onclick="">#{bigcopy.appointmentsLabel}</label>
<input jsf:id="specialdaysRadio" name="specialdaysRadio" type="radio"
data-bind="checked: selectedScope" value="#{bigcopy.specialDaysLabel}"/>
<label for="specialdaysRadio" onclick="">#{bigcopy.specialDaysLabel}</label>
<a class="btn btn-primary"></a>
</div>
</fieldset>
</div>
<div id="appointmentsModel" class="row" data-bind="fadeVisible: selectedScope()==='#{bigcopy.appointmentsLabel}'">
<ui:include src="includes/appointmentsManagement.xhtml"/>
</div>
<div class="row" data-bind="fadeVisible: selectedScope()==='#{bigcopy.specialDaysLabel}'">
<ui:include src="includes/specdaysManagement.html"/>
</div>
In the same time I want to bind the "appointmentsModel" div above to a different view model that will be responsible only for managing the table of appointments that have to be uploaded from the server. This is the js file sifted of irrelevant clutter:
var restServiceRoot = "localhost:8080/RimmaNew/rest";
var scopeSelector = {
selectedScope: ko.observable()
};
ko.applyBindings(scopeSelector);
//appointments part
//Initial load
function appointmentsModel() {
var self = this;
self.serviceURL = restServiceRoot + "/appointments";
self.Appointments = ko.observableArray([]);
$.ajax({
url: self.serviceURL,
type: 'get',
data: null,
dataType: 'json',
success: function (appointments) {
var parsed = JSON.parse(appointments);
var appointmentsArray = parsed.current;
var mappedAppointments = $.map(appointmentsArray, function(item){
return new Appointment(item);
});
self.Appointments(mappedAppointments);
},
error: function (xhr, ajaxOptions, thrownError) {
var err = xhr.responseText;
alert(err);
}
});
}
var appointmentsModelImpl = new appointmentsModel();
ko.applyBindings(appointmentsModelImpl, document.getElementById('appointmentsModel'));
My code is inspired of two books: Knockout.js (O'Reilly - Jamie Munro) and Java EE and HTML 5 Enterprise Application Development (Oracle Press) that barely touch upon the topic of having cross-bindings in KO. I equally didn't find official KO documentation to be helpful in this endeavor.
There is a stackoverflow ticket that dealt with a similar issue, but in one it was the author who answered himself - his explanation made perfect sense to himself, but the mentions of 'stopBinding' and 'controlsDescendantBindings' seemed taken out of context to the two bindings that he uses below...I can't see how to apply this to my problem...
So you would make me a huuge favor if you could either
direct me to a resource where I could educate myself on how to use cross-bindings in KO
Or guide me in how can I mend my code to be able to control the visibility of these two divs and apply a viewmodel to each of them
Doing both would be fantastic.
1 The full code for the original page (xhtml) is here
2The js file
3The js 'classes' file
4The enclosed page with the 'appointments' table

Your best bet will probably be to think of your models a little bit differently. Instead of doing a separate model binding, it may make more sense to do a default appointmentsModel, then update the observables inside that.
What I would do is have a single parent binding to the body, or a containing div in the body, then submodels for the various children.
Sample JS:
var MyParentModel = function(){
var self = this;
self.selectingScopes = {
scope: ko.observable("");
};
self.appointmentModel = {
param1: ko.observable(),
param2: ko.observable()
}
};
ko.applyBindings(new MyParentModel());
Sample HTML:
<div id="appointmentsModel" data-bind="with: appointmentModel"></div>

Related

Tracking many dynamic forms with ng-model

I'm generating dozens of forms on my page. Each form has several parameters (not the same for each form). I'm generating my forms as such (simplified):
<div ng-repeat='module in modules'>
<form ng-submit='submitModule(module)'>
<div ng-repeat='arg in module.args'>
<input ng-model='models[module.name][arg.name]' id="{{ arg.name }}">
</div>
</form>
</div>
You can see I'm trying to assign a unique ng-model to each input parameter by using a two dimensional array models[module.name][arg.name].
Because I am planning on submitting this as JSON, the idea was that I could just do models[some_module] in my controller to get the full JSON, and then just post along.
Unfortunately this isn't working, when trying models['test_module'] I get undefined, instead of my object. There are no errors elsewhere in the code, I've tested extensively. The problem comes from the use of multi-dimensional arrays here which is apparently a big no-no.
How should I handle my situation? IE: several forms, several inconsistent parameters, and a need to POST every param together as JSON.
EDIT: For info, my controller looks like:
angular.module('app')
.controller('InputCtrl', function($scope, InputSvc) {
$scope.models = {};
InputSvc.list().success(function(modules) {
$scope.modules = modules;
$scope.models['test_module'] = {}
});
$scope.submitModule = function(module) {
console.log($scope.models['test_module']);
};
});
Perhaps you could give each form a controller so the model is scoped to the form instance rather than the parent:
<div ng-repeat='module in modules'>
<form ng-controller="FormCtrl" ng-submit='submitModule(module)'>
<div ng-repeat='arg in module.args'>
<input ng-model='formData[arg.name]' id="{{ arg.name }}">
</div>
</form>
</div>
Then your FormCtrl would have the submit method and the model:
angular.module('app')
.controller('FormCtrl', function($scope) {
$scope.formData = {};
$scope.submitModule = function(module) {
console.log($scope.formData);
};
});
Here is a Codepen

ng-repeat populating only after window click - AngularJS & PouchDB

I have the following function reading from a service:
var db = pouchService.db;
db.allDocs({startkey: 'move_', endkey: 'move_\uffff', include_docs: true})
.then(function (data) {
$scope.recordlist = data;
console.log($scope.recordlist);
});
Service:
angular.module('msfLogger').service('pouchService', PouchService);
function PouchService() {
var self = this;
self.db= new PouchDB('FleetDB');
};
On the front end, I'm trying to populate an ng-repeat from $scope.recordlist, but right now, when I load the page, the ng-repeat will be empty, and when I click anywhere on the page, it will get populated.
Also, when I add a new item in the DB, it will only show after page reload and click on window, not automatically.
<div class="row msf-row" ng-repeat="record in recordlist.rows">
<div class="col-md-1">{{record.doc.time}}</div>
<div class="col-md-1"><strong>{{record.doc.car}}</strong></div>
<div class="col-md-2">{{record.doc.driver}}</div>
<div class="col-md-2">{{record.doc.from}}</div>
...
</div>
What am I missing?
I think even if you have angular-pouchdb as a library, you need to inject it in the service, if not, you are using global PouchDB lib as normal and should be managing the $scope.$apply manually.
angular.module('msfLogger', ['pouchdb']).service('pouchService', PouchService);
function PouchService(pouchDB) {
var self = this;
self.db= pouchDB('FleetDB');
};

Adding more, another loop to my Underscore template with a new backbone model?

UPDATE :
I have got my code working, of sorts, but I have two issues and one problem I am not sure how to fix. I will post my current code below. One The Clients append to the right section within the TimesheetData template. But it wraps the option tag within the ClientData template within another option .
So I get :
<select>
<option> <option value="XX"> XXX </option> </option>
</select>
Now I know this is what it is designed to do, to have a root element but I can not seem to find a solution to this issue, although it still works.
Now the other issue is that I need to select a default client, which is loaded into the Timesheet model, Timesheetrow.client_id holds what the database as saved for that row. I just not sure how to access this in an if statement or some other way within the client template.
Now the problem I have is that the Client data does not always load? So when I reload / refresh the page it sometimes lists all my clients in option tags, sometimes it loads nothing, just giving me an empty select tag. However when it does not load anything, I don't have any console log errors or anything?
All help most welcome :)
So this is my current Backbone code:
Client Data Code :
var ClientModel = Backbone.Model.extend({
defaults: {
Client: "",
}
}); //End of ClientModel
var ClientCollection = Backbone.Collection.extend({
model: ClientModel,
url: '/dashboard/json/clients'
}); //End of ClientCollection
var ClientView = Backbone.View.extend({
tagName: 'option',
template: _.template($('#ClientData').html()),
render: function() {
this.$el.append(this.template(this.model.toJSON()));
return this.$el;
}
});
Timesheet Data Code :
var TimeSheetModel = Backbone.Model.extend({
defaults: {
Timesheetrow: "",
}
}); //End of TimeSheetModel
var TimeSheetCollection = Backbone.Collection.extend({
model: TimeSheetModel,
url: '/dashboard/json/row/' + TimesheetID()
}); //End of TimeSheetCollection
var TimeSheetRowView = Backbone.View.extend({
className: 'TimesheetRowLine',
template: _.template($('#TimesheetData').html()),
render: function() {
this.$el.append(this.template(this.model.toJSON()));
return this.$el;
}
}); //End of TimeSheetRowView
Timesheet & Client Code Section:
var TimeSheetCollectionView = Backbone.View.extend({
el:'#MasterContainer',
template: _.template($('#TimesheetForm').html()),
events: {
"click .actionSubmit": "handleSubmit"
},
initialize: function() {
//Get Client Data & Add To Template
this.clientcollection = new ClientCollection();
this.listenTo(this.clientcollection, "add", this.AddClient);
this.clientcollection.fetch();
//Get Main Timesheet Data & Add To Template
this.collection = new TimeSheetCollection();
this.listenTo(this.collection, "add", this.AddTimesheetRow);
this.collection.fetch();
this.$el.append(this.template());
this.submitButton = this.$(".actionSubmit");
},
AddTimesheetRow: function(model) {
var view = new TimeSheetRowView({model: model});
view.render().insertBefore(this.submitButton);
},
AddClient: function(model) {
var clients = new ClientView({model: model});
$("#TimesheetDataList .TimesheetRowLine #clienttemp").append( clients.render() );
},
handleSubmit: function(){
//in real life, you would validate and save some model
alert("form submit");
return false;
}
}); //End of TimeSheetCollectionView
var collectionView = new TimeSheetCollectionView();
This is my Underscore Template code:
<script type="text/template" id="TimesheetForm">
<form action="#" method="post" id="TimesheetDataList" style="width: auto; padding-left: 50px;">
<input type="submit" class="actionSubmit" value="Send"/>
</form>
</script>
<script type="text/template" id="TimesheetData">
<%= console.log( Timesheetrow.client_id ) %>
<input type="hidden" name="data[Timesheetrow][<%= Timesheetrow.id %>][id]" value="<%= Timesheetrow.id %>">
<input type="type" name="data[Timesheetrow][<%= Timesheetrow.id %>][jobtitle]" value="<%= Timesheetrow.twistjob %>">
<select name="data[Timesheetrow][<%= Timesheetrow.id %>][client_id]" id="clienttemp"></select>
</script>
<script type="text/template" id="ClientData">
<option value="<%= Client.id %>"><%= Client.clientname %></option>
</script>
OLD POST
Ok, I am having an issue with my Backbone view rendering into my Underscore template, again. I thought it would be best to ask it as a new question.
My last question, Underscore Template Looping, Without A Loop?, the guy on there help me very well but I am now trying, with little success to edit this code and extend it a little more.
CODE REMOVE - SEE UPDATE
So I was trying to follow the same methods. I know I have to look up more training to expand my knowledge.
With this code, I have a list of clients, I need to load into a select / option pull down form element. But it only seems to loop around 17 times, which is the number of rows I have in my timesheet. I am console logging 'Client' in the 'ClientData' template, this is what displays my client data but only the client data that is logged in my timesheet rows, not all the clients from the JSON data that the model / collection is pointing to? I am also getting a ref. Error for Client? Even though it is in my model as a default?
I am understanding backbone (a bit), but not Underscore so much.
All help most welcome.
Thanks,
:: EDIT ::
I thought I would post my Underscore templates.
CODE REMOVED - SEE UPDATE
So what I am trying to do is loop around in ClientData template for all the clients, with 'option' tags, then add this to the select elements on the TimesheetData template. Then it is complete by this template being added to the TimesheetForm template, which it already does.
This might need a different method and might have been my fault that it don't work as I forgot to explain on the other question a out my client list.
Thanks,

Read content from DOM element with KnockoutJS

The goal
Read content from DOM element with KnockoutJS.
The problem
I have a list of products in my HTML. The code is something like this:
<li>
<div class="introduction">
<h3>#Model["ProductName"]</h3>
</div>
<form data-bind="submit: addToSummary">
<input type="number"
placeholder="How much #Model["ProductName"] do you want?" />
<button>Add product</button>
</form>
</li>
When I click on <button>Add Product</button>, I want to send to KnockoutJS the text inside <h3></h3> of the element that was submitted.
The file to work with KnockoutJS is external and independent of HTML. It name is summary.js and the code is below:
function ProductReservation(id, name, unity, quantity) {
var self = this;
self.id = id;
self.name = name;
self.unity = unity;
self.quantity = quantity;
}
function SummaryViewModel() {
var self = this;
self.products = ko.observableArray([
new ProductReservation(1, "Testing", "kgs", 1)
]);
self.addToSummary = function () {
// Do Something
}
}
What I'm thinking about
HTML:
<li>
<div class="introduction">
<h3 data-bind="text: productName">#Model["ProductName"]</h3>
</div>
[...]
</li>
JS:
productName = ko.observable("text: productName");
But, of course, no success — this is not the correct context or syntax, was just to illustrate.
So I ask: what I need to do?
You're binding addToSummary via the submit binding. By default KO sends the form element to submit-bound functions.
So addToSummary should have a parameter -
self.addToSummary = function (formElement) {
// Do Something
}
You can pass additional parameters to this function (as described in KO's click binding documentation under 'Note 2'), or you could just add a hidden field to your form element and pull it from there.
<li>
<div class="introduction">
<h3>#Model["ProductName"]</h3>
</div>
<form data-bind="submit: addToSummary">
<input type="number" name="quantity"
placeholder="How much #Model["ProductName"] do you want?" />
<input type="hidden" name="productName" value="#Model["ProductName"]" />
<button>Add product</button>
</form>
</li>
Then in your knockout code you could use jQuery to process the form element to pull out the data -
self.addToSummary = function (formElement) {
var productName = $(formElement).children('[name="productName"]')[0].val();
var quantity= $(formElement).children('[name="quantity"]')[0].val();
// ...
self.products.push(new ProductReservation(?, productName, ?, quantity));
}
One strategy that has worked well in my experience is to implement a toJSON extension method that serializes your model (if you use a library like JSON.NET you can have a lot of control over what gets serialized and what does not).
Inside of the view where you initialize KnockoutJS, you could serialize your model and pass it into the KnockoutJS ViewModel you're creating (assuming :
Main view:
<script type="text/javascript">
var viewModel = new MyViewModel(#Model.ToJSON());
</script>
ViewModel:
function MyViewModel(options) {
this.productName = ko.observable(options.ProductName);
}

Using knockoutJS, how to bind list items to same view?

I am new to Knockout and I am building a Simple POC for using knockout to build SPA(Single Page Application).
What I want to do is to show "Business Units" when the app loads and on selection of a business unit show all "Front End Units" under that business unit and on selection of a front end unit, show all "Sales Segments" under that front end unit.
All this will happen in a single page using the same view and the viewmodel will bind the model based on selected business unit or front end unit.
The issue I am facing is that, I have 5 business units that get bound properly first on document ready, but on selection of business unit, the front end units get repeated 5 times each. In this case, I have 2 front end units and each is shown 5 times. Same issue on selection of front end unit.
You can see this issue mimicked in the following jsFiddle sample - jsFiddle Link
Let me know if you can't access the jsfiddle link. In this sample, I have used arrays, but in actual I will be getting the data through async call to the oData service.
This is the view HTML:
<div id="divbu">
<h4 data-bind="text: Heading"></h4>
<ul data-role="listview" data-inset="true" data-bind="foreach: Collection">
<li data-role="list-divider" data-bind="text: EntityName"></li>
<li>
<a href="#" data-bind="click: $root.fnNextLevel">
<table border="0">
<tr>
<td>
<label style="font-size: 12px;">Bus. Plan: </label>
</td>
<td>
<label style="font-size: 12px;" data-bind="text: BusinessPlan"></label>
</td>
<td>
<label style="font-size: 12px;">Forecast: </label>
</td>
<td>
<label style="font-size: 12px;" data-bind="text: Forecast"></label>
</td>
</tr>
<tr>
<td>
<label style="font-size: 12px;">Gross Sales: </label>
</td>
<td colspan="3">
<label style="font-size: 12px;" data-bind="text: GrossSales"></label>
</td>
</tr>
</table>
</a>
</li>
</ul>
</div>
This is the model and view model:
function CommonModel(model, viewType) {
var self = this;
if (viewType == 'BU') {
self.EntityName = model[0];
self.BusinessUnit = model[0];
self.BusinessPlan = model[1];
self.Forecast = model[2];
self.GrossSales = model[3];
} else if (viewType == 'FEU') {
self.EntityName = model[1];
self.BusinessUnit = model[0];
self.FrontEndUnit = model[1];
self.BusinessPlan = model[2];
self.Forecast = model[3];
self.GrossSales = model[4];
} else if (viewType == 'SS') {
self.EntityName = model[2];
self.BusinessPlan = model[3];
self.Forecast = model[4];
self.GrossSales = model[5];
}
}
function ShipmentReportsViewModel(results, viewType) {
var self = this;
self.Collection = ko.observableArray([]);
for (var i = 0; i < results.length; i++) {
self.Collection.push(new CommonModel(results[i], viewType));
}
if (viewType == 'BU') {
self.Heading = "Business Units";
self.fnNextLevel = function (businessUnit) {
FetchFrontEndUnits(businessUnit);
};
self.Home = function () {
FetchBusinessUnits();
};
} else if (viewType == 'FEU') {
self.Heading = results[0][0];
self.fnNextLevel = function (frontEndUnit) {
FetchSalesSegments(frontEndUnit);
};
self.Home = function () {
FetchBusinessUnits();
};
} else if (viewType == 'SS') {
self.fnNextLevel = function () {
alert('No activity zone');
};
self.Heading = results[0][0] + ' - ' + results[0][1];
self.Home = function () {
FetchBusinessUnits();
};
}
}
You can see the complete code in the jsFiddle link.
I have also tried this with multiple views and multiple view models, where I apply bindings by giving the element ID. In this case, one flow from business unit -> sales segment is fine, but when I click on home or back button and I do binding again to that element, I face the same issue. (home and back button features are not done in jsFiddle example).
Let me know if more details are required. I did look into lot of other links in stack overflow, but nothing addressing this particular problem.
Any help is deeply appreciated. Thanks in advance.
The problem here is that you call your ko.applybindings TWICE and there is a foreach binding that iterate within 5 items, therefore the data are duplicated five times.
you should not call a ko.applybindings more than once on the same model.
Your model is always the same even if it's parametrized.
I had the same problem here: Data coming from an ObservableArray are displayed twice in my table
the fact that you have you business logic inside your viewModel is something that could be discussed, and it makes it not easy to fix this.
Make 3 classes, put them in a common model without logic inside. Then once you have applyed the ko.applyBindings once, you just have to modify the array like this:
viewModel.myArray(newValues)
Here is the fiddle with the amended code: http://jsfiddle.net/MaurizioPiccini/5B9Fd/17/
it does not do exaclty what you need but if remove the multiple bindings by moving the Collection object scope outside of your model.
As you can see the problem IS that you are calling the ko.applybindings twice on the same model.
Finally, I got this working. Thanks to #MaurizioIndenmark.
Though I have removed multiple call for ko.applybindings, I was still calling the view model multiple times. This was causing the issue.
Now, I have cleaner view model and I have different function calls for different actions and modify all the data required to be modified within these functions(events). Now, everything is working as expected.
This is how the view model looks now -
function ShipmentReportsViewModel(results) {
var self = this;
self.Heading = ko.observable();
self.BusinessUnits = ko.observableArray();
self.FrontEndUnits = ko.observableArray();
self.SalesSegments = ko.observableArray();
self.Home = function () {
var bu = FetchBusinessUnits();
self.Heading("Business Units");
self.BusinessUnits(bu);
self.FrontEndUnits(null);
self.SalesSegments(null);
};
self.fnFeu = function (businessUnit) {
var feu = FetchFrontEndUnits(businessUnit);
self.Heading(feu[0].BusinessUnit);
self.FrontEndUnits(feu);
self.BusinessUnits(null);
self.SalesSegments(null);
};
self.fnSalesSeg = function (frontEndUnit) {
var ss = FetchSalesSegments(frontEndUnit);
self.Heading(ss[0].BusinessUnit + ' - ' + ss[0].FrontEndUnit);
self.SalesSegments(ss);
self.BusinessUnits(null);
self.FrontEndUnits(null);
};
self.Home();
}
To see the entire working solution, please refer this jsFiddle
Thanks for all the valuable suggestions in getting this work.

Categories

Resources