Issue with modifying objects that are added by Angular modal controller - angularjs

I'm having issue with modifying objects that are adding through angular modal controller
I have
.controller("viewController", function($scope, $modal) {
$scope.allPosts = [
{
id: 1,
owner: "Owner 2",
profile: "images/profile.png",
title: "Book title 1",
image: null,
price: 25,
reply: 2,
fav: 1,
isFaved: false,
content: "test"
},
{
id: 2,
owner: "Owner",
profile: "images/profile2.png",
title: "Ken Follett",
image: "images/book1.jpg",
price: 20,
reply: 12,
fav: 3,
isFaved: true,
content: "The book is in nice"
}
];
$scope.addFav = function(id) {
_.each($scope.allPosts, function(post) {
if(post.id === id) {
post.isFaved = !post.isFaved;
if(post.isFaved) {
post.fav++;
$scope.myFavs.push(post);
} else {
post.fav--;
$scope.myFavs = _.reject($scope.myFavs, function(post) {
return post.id === id;
});
}
}
});
};
$scope.addPost = function() {
var modalInstance = $modal.open({
templateUrl: 'myModalContent.html',
controller: 'ModalInstanceCtrl',
resolve: {
allPosts: function(){
return $scope.allPosts;
}
}
});
};
)
.controller('ModalInstanceCtrl', function ($scope, $modalInstance, allPosts) {
$scope.postId = 50;
$scope.ok = function () {
var temp = {};
temp.id = $scope.postId;
temp.profile = "images/profile.png";
temp.title = $scope.title;
temp.type = $scope.type;
temp.price = $scope.price;
temp.reply = 0;
temp.fav = 0;
temp.isFaved = false;
temp.content = $scope.description;
$scope.allPosts.push(temp);
$scope.postId++;
$modalInstance.close();
};
});
$scope.addFav(id) function works fine with existing $scope.allPosts. However, when I add new object by using the ModalInstanceCtrl, the $scope.allPosts is updated but when it goes to $scope.addFav(id), I can not modified the new object that is pushed in to $scope.allPosts from ModalInstanceCtrl. for example I try to update the fav property in post by using
post.fav++; // console.log(post) shows the fav property is not updated. it remains at 0.

As you don't show the markup I suspect that the ModalInstanceController must be nested within the scope of the viewController. This would explain how the same allPosts is available in both controllers. However the postId will be different on each scope due to the way that javascript's prototypical inheritance works. To overcome this you could define an object on scope something like this:
$scope.posts = {
postId: 0,
allPosts: []
}
Alternatively, and even better imho, define a Posts service that encapsulates all the post behaviours and inject that into both controllers. You are then insulated from any changes to the markup that could muck up the controller inheritance.

Related

AngularJS - moving Material mdDialog to service

I'm trying to cleanup code of one of my controllers which became too big. First I have decided to move attendee registration, which uses AngularJS Material mdDialog, to the service.
Original (and working) controller code looks like:
myApp.controller('RegistrationController', ['$scope','$routeParams','$rootScope','$location','$filter','$mdDialog', function($scope, $routeParams, $rootScope, $location, $filter, $mdDialog){
var attendee = this;
attendees = [];
...
$scope.addAttendee = function(ev) {
$mdDialog.show({
controller: DialogController,
templateUrl: 'views/regForm.tmpl.html',
parent: angular.element(document.body),
targetEvent: ev,
clickOutsideToClose:true,
controllerAs: 'attendee',
fullscreen: $scope.customFullscreen, // Only for -xs, -sm breakpoints.
locals: {parent: $scope}
})
.then(function(response){
attendees.push(response);
console.log(attendees);
console.log(attendees.length);
})
};
function DialogController($scope, $mdDialog) {
var attendee = this;
$scope.hide = function() {
$mdDialog.hide();
};
$scope.cancel = function() {
$mdDialog.cancel();
};
$scope.save = function(response) {
$mdDialog.hide(response);
};
}
}]);
and the code for the controller after separation:
myApp.controller('RegistrationController', ['$scope','$routeParams','$rootScope','$location','$filter','$mdDialog','Attendees', function($scope, $routeParams, $rootScope, $location, $filter, $mdDialog, Attendees){
...
$scope.attendees= Attendees.list();
$scope.addAttendee = function (ev) {
Attendees.add(ev);
}
$scope.deleteAttendee = function (id) {
Attendees.delete(id);
}
}]);
New service code looks like:
myApp.service('Attendees', ['$mdDialog', function ($mdDialog) {
//to create unique attendee id
var uid = 1;
//attendees array to hold list of all attendees
var attendees = [{
id: 0,
firstName: "",
lastName: "",
email: "",
phone: ""
}];
//add method create a new attendee
this.add = function(ev) {
$mdDialog.show({
controller: DialogController,
templateUrl: 'views/regForm.tmpl.html',
parent: angular.element(document.body),
targetEvent: ev,
clickOutsideToClose:true,
controllerAs: 'attendee',
fullscreen: this.customFullscreen, // Only for -xs, -sm breakpoints.
//locals: {parent: $scope}
})
.then(function(response){
attendees.push(response);
console.log(attendees);
console.log(attendees.length);
})
};
//simply search attendees list for given id
//and returns the attendee object if found
this.get = function (id) {
for (i in attendees) {
if (attendees[i].id == id) {
return attendees[i];
}
}
}
//iterate through attendees list and delete
//attendee if found
this.delete = function (id) {
for (i in attendees) {
if (attendees[i].id == id) {
attendees.splice(i, 1);
}
}
}
//simply returns the attendees list
this.list = function () {
return attendees;
}
function DialogController($mdDialog) {
this.hide = function() {
$mdDialog.hide();
};
this.cancel = function() {
$mdDialog.cancel();
};
this.save = function(response) {
$mdDialog.hide(response);
};
}
}]);
but I'm not able to "save" from the spawned mdDialog box which uses ng-click=save(attendee) neither close the dialog box.
What I'm doing wrong?
I'm not able to "save" from the spawned mdDialog box which uses ng-click=save(attendee) neither close the dialog box.
When instantiating a controller with "controllerAs" syntax, use the name with which it is instantiated:
<button ng-click="ctrl.save(ctrl.attendee)">Save</button>
this.add = function(ev) {
$mdDialog.show({
controller: DialogController,
templateUrl: 'views/regForm.tmpl.html',
parent: angular.element(document.body),
targetEvent: ev,
clickOutsideToClose:true,
controllerAs: ̶'̶a̶t̶t̶e̶n̶d̶e̶e̶'̶ 'ctrl',
fullscreen: this.customFullscreen, // Only for -xs, -sm breakpoints.
//locals: {parent: $scope}
})
.then(function(response){
attendees.push(response);
console.log(attendees);
console.log(attendees.length);
return response;
});
To avoid confusion, choose a controller instance name that is different from the data names.
$scope is not available to be injected into a service when it's created. You need to refactor the service and its methods so that you don't inject $scope into it and instead pass the current scope to the service methods when you call them.
I actually have a notification module that I inject which uses $mdDialog. Below is the code for it. Perhaps it will help.
(() => {
"use strict";
class notification {
constructor($mdToast, $mdDialog, $state) {
/* #ngInject */
this.toast = $mdToast
this.dialog = $mdDialog
this.state = $state
/* properties */
this.transitioning = false
this.working = false
}
openHelp() {
this.showAlert({
"title": "Help",
"textContent": `Help is on the way for ${this.state.current.name}!`,
"ok": "OK"
})
}
showAlert(options) {
if (angular.isString(options)) {
var text = angular.copy(options)
options = {}
options.textContent = text
options.title = " "
}
if (!options.ok) {
options.ok = "OK"
}
if (!options.clickOutsideToClose) {
options.clickOutsideToClose = true
}
if (!options.ariaLabel) {
options.ariaLabel = 'Alert'
}
if (!options.title) {
options.title = "Alert"
}
return this.dialog.show(this.dialog.alert(options))
}
showConfirm(options) {
if (angular.isString(options)) {
var text = angular.copy(options)
options = {}
options.textContent = text
options.title = " "
}
if (!options.ok) {
options.ok = "OK"
}
if (!options.cancel) {
options.cancel = "Cancel"
}
if (!options.clickOutsideToClose) {
options.clickOutsideToClose = false
}
if (!options.ariaLabel) {
options.ariaLabel = 'Confirm'
}
if (!options.title) {
options.title = "Confirm"
}
return this.dialog.show(this.dialog.confirm(options))
}
showToast(toastMessage, position) {
if (!position) { position = 'top' }
return this.toast.show(this.toast.simple()
.content(toastMessage)
.position(position)
.action('OK'))
}
showYesNo(options) {
options.ok = "Yes"
options.cancel = "No"
return this.showConfirm(options)
}
uc() {
return this.showAlert({
htmlContent: "<img src='img\\underconstruction.jpg'>",
ok: "OK",
title: "Under Construction"
})
}
}
notification.$inject = ['$mdToast', '$mdDialog', '$state']
angular.module('NOTIFICATION', []).factory("notification", notification)
})()
Then inject notification (lower case) into my controllers in order to utilize it. I store notification in a property of the controller and then call it with something like this:
this.notification.showYesNo({
clickOutsideToClose: true,
title: 'Delete Customer Setup?',
htmlContent: `Are you sure you want to Permanently Delete the Customer Setup for ${custLabel}?`,
ariaLabel: 'Delete Dialog'
}).then(() => { ...
this.notification.working = true
this.ezo.EDI_CUSTOMER.remove(this.currentId).then(() => {
this.primaryTab = 0
this.removeCustomerFromList(this.currentId)
this.currentId = 0
this.notification.working = false
}, error => {
this.notification.working = false
this.notification.showAlert({
title: "Error",
htmlContent: error
})
})

Modal POP scope not retain value

I have a html page there is one button. on click this button a angular modal pop is open. we have sent some data from parent window to pop window. There are three kendo dropdown on pop window. data is sent from parent window to popup will be selected value in drop down. but data sent, becomes blank on load of pop window, even data successfully sent to pop window..
My parent page code is below :
$scope.openPOPWindow = function (condition) {
var objRegelDetails = {
//param1: $scope.modelObject[condition.ParameterFejl1Model],
//param2: $scope.modelObject[condition.ParameterFejl2Model],
//param3: $scope.modelObject[condition.ParameterFejl3Model],
param1: '13',
param2: '14',
param3: '15',
conditionKode: condition.ConditionKode
};
var modalInstance = $modal.open({
backdrop: 'static',
templateUrl: './RuleEngine/UI/RegelConditionPopUp',
controller: RegelContrl.RegelConditionController,
keyboard: false,
resolve: {
items: function () {
return objRegelDetails;
}
}
});
modalInstance.result.then(function (selectedItem) {
if (!IsNullorEmpty(selectedItem)) {
alert(selectedItem.Param1);
}
}, function () {
});
};
and POP window controller is below :
var adm = angular.module('AdminModule');
adm.controller('RegelConditionPopUpCtrl', ['$scope', 'RegelService', '$q', '$rootScope', '$modalInstance', 'items', 'DirtyFlagPopup', 'SagService', 'localize', 'toaster', 'ValidationService',
function ($scope, RegelService, q, $rootScope, $modalInstance, items, DirtyFlagPopup, SagService, localize, toaster, ValidationService) {
function loadhandlingType() {
var paramTypes = RegelService.GetParamTypes();
//angular.element('#dvLoadingUA').show();
q.all([paramTypes]).then(function (responses) {
if (responses[0].length > 0) {
$scope.drpParamType.data(responses[0]);
}
else {
$scope.drpParamType.data([]);
}
//angular.element('#dvLoadingUA').hide();
});
};
$scope.cancel = function () {
$modalInstance.close();
};
$scope.PushRegelConditionParamData = function () {
var selectedData = {
Param1: '',
Param2: '',
Param3: ''
};
selectedData.Param1 = angular.element('#ddlparameterOption1' + $scope.RegelConditionUID).val();
selectedData.Param2 = angular.element('#ddlparameterOption2' + $scope.RegelConditionUID).val();
selectedData.Param3 = angular.element('#ddlparameterOption3' + $scope.RegelConditionUID).val();
$modalInstance.close(selectedData);
};
$scope.initializedDropDown = function () {
$scope.drpHandlingType = new kendo.data.DataSource({
data: [],
type: "json",
});
$scope.drpHandlingTypeOption = {
dataSource: $scope.drpHandlingType,
dataTextField: FindRegelEnum.TextField,
dataValueField: FindRegelEnum.ValueField
};
$scope.drpParamType = new kendo.data.DataSource({
data: [],
type: "json",
});
$scope.drpParamTypeOption = {
dataSource: $scope.drpParamType,
optionLabel: localize.getLocalizedString("_RE_Home_DefaultSelect_"),
dataTextField: FindRegelEnum.TextField,
dataValueField: FindRegelEnum.ValueField
};
loadhandlingType();
};
$scope.InitializeRC = function () {
$scope.RegelConditionUID = GetUID();
$scope.ModelData = {
Parameter1: '',
Parameter2: '',
Parameter3: ''
};
$scope.initializedDropDown();
$scope.ModelData.Parameter1 = items.param1,
$scope.ModelData.Parameter2 = items.param2,
$scope.ModelData.Parameter3 = items.param3
};
}]);
but problem is that on html we can see value for ModelData.Parameter2 for a second and than it disappear...
please tell me why scope variable not retain their values

passing form data from one controller to another in angular

.controller('CyclesController', function ($scope, $state) {
$scope.age = 0;
$scope.name = "";
$scope.email = "";
$scope.calculateByAge = function (age, name, email) {
$scope.data = $scope.data || {};
if (age > 0) {
$scope.data.age = age;
$scope.data.name = name;
$scope.data.email = email;
$state.go('tab.cycles-detail');
}
}
})
.controller('CyclesDetailController', function ($scope, $stateParams, CyclesService) {
console.log('scope data', $scope.data); // <--- undefined.
})
This may be a dumb question, but can get to get the data from the form on the CyclesDetailController controller.
If it's simple property you could do it by routing. Just change your "tab.cycles-detai' to 'tab.cycles-detai/:age' in your ui-router configuration and pass it when you're redirecting: $state.go('tab.cycles-detail', {age: age});
in 'CyclesDetailController' access it by $stateParams.age;
e.g:
//app.config
//...
.state('tab.cycles-detail', {
url: "^detail/{age:int}",
controller: 'CyclesDetailController',
templateUrl: "url_for_detail_template"
})
//...
// CyclesController
.controller('CyclesController', function ($scope, $state) {
$scope.age = 0;
$scope.calculateByAge = function (age) {
$scope.data = $scope.data || {};
if (age > 0) {
$scope.data.age = age;
$state.go('tab.cycles-detail', {age: age);
}
}
})
//CyclesDetailController
.controller('CyclesDetailController', function ($scope, $stateParams, CyclesService) {
console.log('scope data', $stateParams.age);
})
//
If you want to pass data from one route to another but dont want to expose it in browser menu bar, you can use squash.
Example -
.state('app.enroll', {
url: '/enroll/',
params: {
classId: {
value: null,
squash: true
},
className: {
value: null,
squash: true
}
},
title: 'Students Enrollment',
templateUrl: helper.basepath('enroll.html')
})
2nd Technique -
You can use localStorage / cookies to save data and retrieve at later stage.
3rd Technique -
You can always share data via services/factory in between controllers.

how to test inner controller which loads data for select control in angular-formly

I have a ui-select field
{
key: 'data_id',
type: 'ui-select',
templateOptions: {
required: true,
label: 'Select label',
options: [],
valueProp: 'id',
labelProp: 'name'
},
controller: function($scope, DataService) {
DataService.getSelectData().then(function(response) {
$scope.to.options = response.data;
});
}
}
How can I access that inner controller in my unit tests and check that data loading for the select field actually works ?
UPDATE:
An example of a test could be as such:
var initializePageController = function() {
return $controller('PageCtrl', {
'$state': $state,
'$stateParams': $stateParams
});
};
var initializeSelectController = function(selectElement) {
return $controller(selectElement.controller, {
'$scope': $scope
});
};
Then test case looks like:
it('should be able to get list of data....', function() {
$scope.to = {};
var vm = initializePageController();
$httpBackend.expectGET(/\/api\/v1\/data...../).respond([
{id: 1, name: 'Data 1'},
{id: 2, name: 'Data 2'}
]);
initializeSelectController(vm.fields[1]);
$httpBackend.flush();
expect($scope.to.options.length).to.equal(2);
});
You could do it a few ways. One option would be to test the controller that contains this configuration. So, if you have the field configuration set to $scope.fields like so:
$scope.fields = [ { /* your field config you have above */ } ];
Then in your test you could do something like:
$controller($scope.fields[0].controller, { mockScope, mockDataService });
Then do your assertions.
I recently wrote some test for a type that uses ui-select. I actually create a formly-form and then run the tests there. I use the following helpers
function compileFormlyForm(){
var html = '<formly-form model="model" fields="fields"></formly-form>';
var element = compile(html)(scope, function (clonedElement) {
sandboxEl.html(clonedElement);
});
scope.$digest();
timeout.flush();
return element;
}
function getSelectController(fieldElement){
return fieldElement.find('.ui-select-container').controller('uiSelect');
}
function getSelectMultipleController(fieldElement){
return fieldElement.find('.ui-select-container').scope().$selectMultiple;
}
function triggerEntry(selectController, inputStr) {
selectController.search = inputStr;
scope.$digest();
try {
timeout.flush();
} catch(exception){
// there is no way to flush and not throw errors if there is nothing to flush.
}
}
// accepts either an element or a select controller
function triggerShowOptions(select){
var selectController = select;
if(angular.isElement(select)){
selectController = getSelectController(select);
}
selectController.activate();
scope.$digest();
}
An example of one of the tests
it('should call typeaheadMethod when the input value changes', function(){
scope.fields = [
{
key: 'selectOneThing',
type: 'singleSelect'
},
{
key: 'selectManyThings',
type: 'multipleSelect'
}
];
scope.model = {};
var formlyForm = compileFormlyForm();
var selects = formlyForm.find('.formly-field');
var singleSelectCtrl = getSelectController(selects.eq(0));
triggerEntry(singleSelectCtrl, 'woo');
expect(selectResourceManagerMock.searchAll.calls.count()).toEqual(1);
var multiSelectCtrl = getSelectController(selects.eq(1));
triggerEntry(multiSelectCtrl, 'woo');
expect(selectResourceManagerMock.searchAll.calls.count()).toEqual(2);
});

Scope property which depends on another scope property?

I'm just starting out with Angular and could use a hand re: binding to scopes.
I'm trying to load in data which populates a property.
I then want to update other properties based off this property, is there any way to trigger this easily?
At the moment I'm just doing:
Load JSON data
Set mythings
Trigger calculation of mythings_processed
I feel like I should be able to trigger (3) with (2)?
Example
Javascript
app.controller('MyCtrl', function($scope, $http) {
$scope.loadData = function () {
$http.get('/api/orgs/gocardless/issues.json').success(function(data) {
$scope.mythings = data.things;
$scope.generateCalculatedItems() // Surely this isn't necessary?
});
};
$scope.generateCalculatedItems = function () {
mythings_processed = {}; // Generated using mythings
$scope.mythings_processed = mythings_processed;
};
};
HTML
<div ng-controller="IssuesCtrl" ng-init="loadData()">
<ul>
<li ng-repeat="thing in things">{{thing.id}}</li>
</ul>
<ul>
<li ng-repeat="processed_thing in mythings_processed">{{processed_thing.id}}</li>
</ul>
</div>
I'm sure I'm doing something horrible to Angular here so I apologise!
I did feel like I should be able to do:
$scope.mythingsProcessed = function () {
mythings_processed = {}; // Generated using mythings
return mythings_processed;
};
<ul>
<li ng-repeat="processed_thing in mythingsProcessed()">{{processed_thing.id}}</li>
</ul>
But when I did, it gave me the error: 10 $digest() iterations reached. Aborting!
Any ideas?
UPDATE: Example data/use case
I have a list of items fetched via ajax:
{
items: [
{name: 'pete', value: 1},
{name: 'john', value: 2},
{name: 'pete', value: 3},
{name: 'steve', value: 2},
{name: 'john', value: 1}
]
}
I want to display all of these, but I also want to process (sum) them into:
{
processed_items: [
{ name: 'pete', value: 4 }
{ name: 'john', value: 3 }
{ name: 'steve', value: 2 }
}
and then display these in a list as well.
I am not 100% sure what you are trying to acheive. But to trigger (3) with (2) you could use the watch function of the $scope object:
app.controller('MyCtrl', function($scope, $http) {
$scope.loadData = function () {
$http.get('/api/orgs/gocardless/issues.json').success(function(data) {
$scope.mythings = data.things;
});
};
scope.$watch('mythings', function(newValue, oldValue) {
$scope.generateCalculatedItems();
}
$scope.generateCalculatedItems = function () {
mythings_processed = {}; // Generated using mythings
$scope.mythings_processed = mythings_processed;
};
};
To bind mythings to mythings_processed you have to do the binding manually via the $watch-function in AngularJS.
With your updated example I think the focus of your question shifted from the problem with the binding to how you could process the incoming data. Is that correct?
I made a plunker with a possible solution:
http://plnkr.co/edit/BMNmE5
I would process the data as soon as the success method is called. And not do a watch in this case.
$scope.loadData = function() {
$http.get('data.json').success(function(data) {
$scope.mythings = data;
$scope.mythingsProcessed = processThings(data);
});
};
Then the function processThings makes the sum as you want them (it uses the underscore.js library):
var processThings = function(things) {
var result = _.reduce(
things,
function(result, obj) {
var storedObject = _.findWhere(result, {'name': obj.name});
if(storedObject) {
storedObject.value += obj.value;
return result;
}
else {
return _.union(result, _.clone(obj));
}
},
[]);
return result;
};

Resources