KnockoutJS - ViewModel (Grandparent - Parent - Child) Accessing a Parent Function within Child element - javascript

I have a Grandparent, Parent, Child ViewModel relationship setup in knockout and knockout mapping, CustomerViewModel, WorkOrderViewModel, and RepairViewModel.
For each level I flag if the record has been Modified. Then I have a save button that saves the entire Model. The function that Saves the Model is within the Grandparent ViewModel (CustomerViewModel)
Example of a Child level element
<input class="form-control input-sm text-right" name="RepairCost" id="RepairCost" data-bind="value: RepairCost, event: {change: flagRepairAsEdited}" />
Is there a way within the flagRepairAsEdited function I can call the SAVE function within the parent/grandparent?
Thanks so much!
Here is the JS code I'm using (simplified):
var ObjectState = {
Unchanged: 0,
Added: 1,
Modified: 2,
Deleted: 3
};
var workOrderMapping = {
'WorkOrders': {
key: function (workOrders) {
return ko.utils.unwrapObservable(workOrders.WorkOrderId);
},
create: function (options) {
return new WorkOrderViewModel(options.data);
}
},
'Repairs': {
key: function (repairs) {
return ko.utils.unwrapObservable(repairs.RepairId);
},
create: function (options) {
return new RepairViewModel(options.data);
}
}
};
RepairViewModel = function (data) {
var self = this;
ko.mapping.fromJS(data, workOrderMapping, self);
self.flagRepairAsEdited = function () {
if (self.ObjectState() != ObjectState.Added) {
self.ObjectState(ObjectState.Modified);
}
//WOULD LOVE TO CALL SAVE FUNCTION HERE
return true;
}
;
}
WorkOrderViewModel = function (data) {
var self = this;
ko.mapping.fromJS(data, workOrderMapping, self);
self.flagWorkOrderAsEdited = function () {
if (self.ObjectState() != ObjectState.Added) {
self.ObjectState(ObjectState.Modified);
}
//WOULD LOVE TO CALL SAVE FUNCTION HERE
return true;
}
;
}
CustomerViewModel = function (data) {
var self = this;
ko.mapping.fromJS(data, workOrderMapping, self);
self.save = function () {
$.ajax({
url: "/Customers/Save/",
type: "POST",
data: ko.toJSON(self),
contentType: "application/json",
success: function (data) {
ko.mapping.fromJS(data.customerViewModel, workOrderMapping, self);
if (data.newLocation != null)
window.location = data.newLocation;
},
});
},
self.flagCustomerAsEdited = function () {
if (self.ObjectState() != ObjectState.Added) {
self.ObjectState(ObjectState.Modified);
}
return true;
}
;
}

There are 2 ways to do this
a) Pass viewModels as parameters of the child flagRepairAsEdited function:
data-bind="value: RepairCost, event: {change: flagRepairAsEdited.bind($data, $parent, $root)}"
b) Save the link of the parent viewModel inside child viewModel
WorkOrderViewModel = function (data, parent) {
this.parent = parent;
...
}
And use parent.flagWorkOrderAsEdited and parent.parent.flagWorkOrderAsEdited to save parent and grandparent viewmodels

Related

How to make module pattern each function a promise?

I use Angular 1.5 and I made a factory function which is return a literal object like this:
return {
item: null,
get: function() {
return item;
},
create: function() {
if (this.get()){
this.remove();
}
this.item = {};
},
remove: function() {
var item = this.get();
if (item) {
this.item = null;
}
},
add: function() {
if (!this.get()) {
this.create();
}
this.item.newprop = 'value';
}
}
please do not ask me to change to function declaration. I want a object with his own actions(functions) and properties that is working on.
This pattern (like get inside create so on..) I didn't copied from anywhere. so I'm wonder if has a name? It is best way to deal with function-black boxes?
What is the best way to put Promise inside? so every function should return a promise
every then function I need to use bind???
todo like this:
create: function () {
this.get()
.then(remove)
.then(function () {
this.item = {}; // BUT this === undefined!!
});
}
You have to use bind in every then callback function:
var myModule = {
item: null,
get: function() {
return Promise.resolve(this.item);
},
create: function() {
return this.remove().then(function() {
this.item = {};
}.bind(this));
},
remove: function() {
return this.get().then(function(item) {
if (item) {
this.item = null;
}
}.bind(this));
},
add: function() {
return this.get().then(function(item) {
return item || this.create();
}.bind(this)).then(function() {
this.item.newprop = 'value';
}.bind(this));
}
}
// Let see it working:
myModule.create().then(function() {
return myModule.get();
}).then(function(item) {
console.log("After create: ", item);
return myModule.remove();
}).then(function() {
return myModule.get();
}).then(function(item) {
console.log("After remove: ", item);
return myModule.add();
}).then(function() {
return myModule.get();
}).then(function(item) {
console.log("After add: ", item);
});

Angularjs retain local variable

I have a factory like this:
TestFactory= function () {
var objectName=null;
return {
SetName:function(name) {
objectName = name;
},
GetName:function() {
return objectName;
},
Init:function() {
return angular.copy(this);
}
}
}
A controller like:
TestController = function($scope) {
$scope.TestClick = function () {
var tstA = TestFactory.Init();
var tstB = TestFactory.Init();
tstA.SetName('test A')
tstB.SetName('test B')
console.log('A', tstA.GetName());
console.log('B', tstB.GetName());
}
}
In the console I get Test B for both objects.
How can I make a proper instance of this object?
I would like to use the objectName value in other functions of the factory.
Take into account that in Angular, Factories are singletons, so the instance is always the same.
You can do the following:
TestFactory= function () {
var objectName={};
return {
SetName:function(property,name) {
objectName[property] = name;
},
GetName:function(property) {
return objectName[property];
},
Clear:function(property) {
delete objectName[property]
}
}
}
Then in your controller:
TestController = function($scope, TestFactory) {
$scope.TestClick = function () {
TestFactory.SetName('a','test A')
TestFactory.SetName('b','test B')
console.log('A', TestFactory.GetName('a')); // test A
console.log('B', TestFactory.GetName('b')); // test B
}
}
Couple of issues. First your returning an object rather than a function from your factory.
app.factory('TestFactory', function() {
return function() {
var objectName = null;
var setName = function(name) {
objectName = name;
};
var getName = function() {
return objectName;
};
return {
SetName: setName,
GetName: getName
};
};
});
Then you can just instantiate like this:
var tstA = new TestFactory();
var tstB = new TestFactory();
Services and factories are singletons so I think you can achieve what you want with a more appropriate use of the factory by providing an Init function that returns the common code and unique name like so:
angular.module('app')
.factory('ServiceFactory', serviceFactory);
function serviceFactory() {
return {
Init: function (name) {
return {
objectName: name,
setName: function (name) {
this.objectName = name;
},
getName: function () {
return this.objectName;
}
};
}
};
}
This leaves the possibility to use it as a factory that can initialize many types.
You basically need to create a simple getter/setter.
angular.module('app', [])
.controller('TestController', testController)
.service('serviceFactory', serviceFactory);
testController.$inject = ['serviceFactory'];
function testController(serviceFactory) {
serviceFactory.set('A', {
name: 'test A'
});
serviceFactory.set('B', {
name: 'test B'
});
console.log(serviceFactory.getAll());
console.log(serviceFactory.get('A'));
console.log(serviceFactory.get('B'));
}
function serviceFactory() {
var
_model = {
name: ""
},
_data = {};
return {
set: function(key, data) {
_data[key] = angular.extend({}, _model, data);
},
get: function(key) {
return _data[key];
},
getAll: function() {
return _data;
}
}
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.22/angular.min.js"></script>
<body ng-app="app" ng-controller="testController"></body>

Binding view with javascript knockout

Here i have my get method that gets the data that i want to return in order to bind it with the view page. I am having trouble wrapping my head to how i could bind this information to the view.
Get Method:
var getRoster = function () {
Ajax.Get({
Url: ....,
DataToSubmit: {id: properties.Id },
DataType: "json",
OnSuccess: function (roleData, status, jqXHR) {
console.log("roles:", roleData.length);
Ajax.Get({
Url: ...,
DataToSubmit: { pageNumber: 1, id: properties.Id },
DataType: "json",
OnSuccess: function (userData, status, jqXHR) {
for (var x in roleData)
{
var role = roleData[x];
console.log(role);
for (var y in userData)
{
var user = userData[y];
if (user.ContentRole == role.ContentRole)
{
rosterViewModel.PushUser(new userViewModel(user));
console.log(user);
}
}
roleTypesViewModel.PushRole(new roleViewModel(role));
}
}
});
}
});
rosterViewModel.PushUser = function (user) {
viewModel.RosterUsers.push(new userViewModel(user));
};
roleTypesViewModel.PushRole = function (role) {
viewModel.RosterRoleTypes.push(new roleViewModel(role));
}
var userViewModel = function (data) {
var _self = this;
_self.ID = ko.observable(data.ID);
_self.Name = ko.observable(data.Name);
_self.Email = ko.observable(data.Email);
_self.ContentRole = ko.observable(data.ContentRole);
};
var roleViewModel = function (data) {
var _self = this;
_self.ContentRole = ko.observable(data.ContentRole);
_self.RoleName = ko.observable(data.RoleName);
_self.RoleRank = ko.observable(data.RoleRank);
_self.UserCount = ko.observable(data.UserCount);
};
var viewModel = {
RosterRoleTypes: ko.observableArray([]),
RosterUsers: ko.observableArray([])
};
View:
<div id="gridView" data-bind="foreach: RosterRoleTypes">
<h3 class="roleHeader"><span data-bind="text:RoleName"></span>
<span class="userCount">(<span data-bind="text:UserCount"></span>)</span>
</h3>
<div data-bind="template: { name: 'grid', foreach: RosterUsers}">
</div>
</div>
How can i bind my data to display in my view?
If you are trying to bind multiple areas of your page to different view models, that is possible by passing in an additional parameter to your ko.applyBindings() method that you call. Your problem is that you are mixing models and view models and using them improperly. If you want to have one view model adjust your code to include all of the functions of your view model and set your models as models instead of viewmodels -
function rosterViewModel() {
var self = this;
self.RosterRoleTypes = ko.observableArray([]),
self.RosterUsers = ko.observableArray([])
self.PushUser = function (user) {
viewModel.RosterUsers.push(new userModel(user));
};
self.PushRole = function (role) {
viewModel.RosterRoleTypes.push(new roleModel(role));
};
self.getRoster = function () {
Ajax.Get({
Url: ....,
DataToSubmit: {id: properties.Id },
DataType: "json",
OnSuccess: function (roleData, status, jqXHR) {
Ajax.Get({
Url: ...,
DataToSubmit: { pageNumber: 1, id: properties.Id },
DataType: "json",
OnSuccess: function (userData, status, jqXHR) {
for (var x in roleData)
{
var role = roleData[x];
for (var y in userData)
{
var user = userData[y];
if (user.ContentRole == role.ContentRole)
{
self.PushUser(new userModel(user));
}
}
self.PushRole(new roleModel(role));
}
}
});
}
});
}
var userModel = function (data) {
var _self = this;
_self.ID = ko.observable(data.ID);
_self.Name = ko.observable(data.Name);
_self.Email = ko.observable(data.Email);
_self.ContentRole = ko.observable(data.ContentRole);
};
var roleModel = function (data) {
var _self = this;
_self.ContentRole = ko.observable(data.ContentRole);
_self.RoleName = ko.observable(data.RoleName);
_self.RoleRank = ko.observable(data.RoleRank);
_self.UserCount = ko.observable(data.UserCount);
};
ko.applyBindings(new rosterViewModel());
This assumes you want to use a single view model for your view. If you are combining multiple content areas that should be bound separately you can create two view models and merge them as shown in this question - KnockOutJS - Multiple ViewModels in a single View - or you could also bind them separately by passing in an additional parameter to the ko.applyBindings() method as showm here - Example of knockoutjs pattern for multi-view applications
All of the data that you want to bind to UI will be properties of your viewmodel as KO observable or observable arrays. Once the view model is created and its members are assigned with data(callbacks in your case), you need to apply bindings using ko.applyBindinds so that the data is bound to UI. In your case the last AJAX success callback seems to be the appropriate place.
Also your HTML makes using of template bindings however apparently there is no template defined with name 'grid'. Check on this.
Knockout tutorial link http://learn.knockoutjs.com/#/?tutorial=intro
Add
ko.applyBindings(viewModel);
somewhere in your application.

Knockoutjs select value change doesn't update observable

I have a select option that gets its initial value from EmailField and its options from allEmailFields:
<select data-bind="options: $parent.allEmailFields, value: EmailField()"></select>
When I change the value of the select my model doesn't get updated. Isn't this something two way binding should take care of? Or I need to write handler for the change event?
Module is here:
define('mods/fieldmapping', ["knockout", "libs/knockout.mapping", "datacontext", "mods/campaigner", "text!templates/fieldmapping.html", "text!styles/fieldmapping.css"],
function (ko, mapping, datacontext, campaigner, html, css) {
'use strict';
var
fieldMappingItem = function (data) {
var self = this;
self.CrmField = ko.observable(data.CrmField);
self.EmailField = ko.observable(data.EmailField);
},
dataMappingOptions = {
key: function (data) {
return data.PlatformFieldName;
},
create: function (options) {
return new fieldMappingItem(options.data);
}
},
fieldMappingViewModel = {
contentLoading: ko.observable(false)
},
showFieldMapping = function () {
campaigner.addStylesToHead(css);
campaigner.addModalInnerPanel(html);
},
init = function (connectionId, connectionName) {
fieldMappingViewModel.fieldMappings = mapping.fromJS([]);
fieldMappingViewModel.allEmailFields = mapping.fromJS([]);
fieldMappingViewModel.originatorConnectionName = ko.observable();
fieldMappingViewModel.originatorConnectionName(connectionName);
fieldMappingViewModel.saveFieldMappings = function () {
console.log(ko.toJSON(fieldMappingViewModel.fieldMappings));
amplify.request("updateExistingFieldMappings",
{
cid: connectionId,
RequestEntity: ko.toJSON(fieldMappingViewModel.fieldMappings)
},
function (data) {
console.log(data);
});
};
showFieldMapping();
amplify.request('getExistingFieldMappings', { cid: connectionId }, function (data) {
amplify.request("getCampaignerFields", function (breezerData) {
mapping.fromJS(breezerData.ResponseEntity, fieldMappingViewModel.allEmailFields);
});
mapping.fromJS(data.ResponseEntity, dataMappingOptions, fieldMappingViewModel.fieldMappings);
ko.applyBindings(fieldMappingViewModel, $('#fieldMapping')[0]);
});
};
return {
init: init,
fieldMappingViewModel: fieldMappingViewModel,
html: html,
css : css
}
});
Replace:
<select data-bind="options: $parent.allEmailFields, value: EmailField()"></select>
With:
<select data-bind="options: $parent.allEmailFields, value: EmailField"></select>
if you want to create bi-derectinonal dependency, so you should pass into binding observable.
P.S.: http://knockoutjs.com/documentation/observables.html#observables

Set a knockout.js observable without subscriptions firing

How can I set an observable property without any subscriptions firing for it?
I have a scenario were the page loads, an ajax call is made to get some data, the data is looped over and the currently selected item is then set to an observable. I want to be able to set this observable without any subscriptions for it firing because the first time this observable is set is considered its initial sate and the subscriptions should not execute on initial state.
function PlanViewModel() {
var self = this;
self.plans = ko.observableArray();
self.selectedItem = ko.observable();
self.getAllPlans = function () {
$.ajax({
url: "/Backoffice/Home/GetAllPlans",
type: "POST",
data: {},
context: this,
success: function (result) {
var planList = this.plans;
// clear the plan list
planList.removeAll();
$.each(result.plans, function () {
var planDetail = new PlanDetail(this, self);
if (this.IsSelected) {
self.selectedItem(planDetail); // how do I set this without the subscriptions firing?
}
planList.push(planDetail);
});
},
error: function (result) {
alert("An error occured getting plans.");
}
});
}
self.selectedItem.subscribe(function (newItem) {
newItem.repositoryUpdateSelectedPlan();
} .bind(self));
}
You could restructure your code like this:
function PlanViewModel() {
var self = this;
self.plans = ko.observableArray();
self.getAllPlans = function () {
$.ajax({
// …
success: function (result) {
// …
$.each(result.plans, function () {
var planDetail = new PlanDetail(this, self);
if (this.IsSelected) {
self.selectedItem = ko.observable(planDetail);
}
planList.push(planDetail);
});
if (self.selectedItem === undefined) {
self.selectedItem = ko.observable();
}
self.selectedItem.subscribe(function (newItem) {
newItem.repositoryUpdateSelectedPlan();
}.bind(self));
},
// …
});
}
}
That is, only start Knockout after your desired initial state is achieved.
Thanks, I went down that route and its working with some modifications. The selectedItem observable must be defined on the model from the get go because its used in bindings all over the place but I did move the subscription portion like you've suggested and that's working out great.
function PlanViewModel() {
var self = this;
var selectedItemSubscription = null;
self.plans = ko.observableArray();
self.selectedItem = ko.observable();
self.getAllPlans = function () {
$.ajax({
url: "/Backoffice/Home/GetAllPlans",
type: "POST",
data: {},
context: this,
success: function (result) {
var planList = this.plans;
// clear the plan list
planList.removeAll();
$.each(result.plans, function () {
var planDetail = new PlanDetail(this, self);
if (this.IsSelected) {
if (selectedItemSubscription != null)
selectedItemSubscription.dispose();
self.selectedItem(planDetail);
}
planList.push(planDetail);
});
selectedItemSubscription = self.selectedItem.subscribe(function (newItem) {
newItem.repositoryUpdateSelectedPlan();
}.bind(self));
},
error: function (result) {
alert("An error occured getting plans.");
}
});
}
}

Categories