AngularJS - Reset the state of a bound object - angularjs

I've made a really simple repeat of some books, when the user clicks the book, it opens a new modal. Where they can edit the book. As i'm using the two way binding, the 'display page' automatically changes as i type on the modal - which is brilliant.
However what i want to do is allow the user to press a cancel button, and the state of the book goes back to what it was before it was altered. Is this possible in Angular without going back and resetting the entire $scope.books object?
In the real application this would be an API call, and i'd rather not make another server call unless entirely necessary of course. Is there a pattern that takes care of this already?
(function(){
var app = angular.module('ngModalDemo', ['ui.bootstrap'])
.controller('formController', function($scope, $modal, $log){
$scope.books = [
{ Id: 1, Name:'A Feast For Crows'},
{ Id: 2, Name:'Before they are Hanged'}
];
$scope.openModal = function (currentBook) {
var modalInstance = $modal.open({
templateUrl: 'SomeModal.html',
controller: [
'$scope', '$modalInstance', function($scope, $modalInstance){
$scope.editBook = currentBook;
$scope.saveModal = function (book) {
$modalInstance.close();
};
$scope.cancelModal = function () {
// Restore the previous state here!
$modalInstance.close();
};
}]
});
};
})
})();
<div ng-controller="formController">
<p ng-repeat="displayBook in books">
{{displayBook.Name}}
</p>
<script type="text/ng-template" id="SomeModal.html">
<form name="editForm" ng-submit="saveModal(editBook)" noValidate>
<div class="modal-header">
Name: <input ng-model="editBook.Name" required /><br />
</div>
<div class="modal-footer">
<button type="button" class="btn btn-warning" ng-click="cancelModal()">Cancel</button>
<button class="btn btn-info" ng-disabled="editForm.$dirty && editForm.$invalid">Save</button>
</div>
</form>
</script>
</div>

You can create a deep copy of your object to a temp, and then set it back if neccessary:
var temp = angular.copy(currentBook);
$scope.editBook = currentBook;
$scope.saveModal = function (book) {
$modalInstance.close();
};
$scope.cancelModal = function () {
// Restore the previous state here!
angular.copy(temp, $scope.editBook);
$modalInstance.close();
};

Angular has method copy that makes job for you by cloning object or array.
The idea is to pass copy of your data to modal and not instance itself. So when user press Cancel the main instance doesn't change.
In your case instead:
$scope.editBook = currentBook;
write:
$scope.editBook = angular.copy(currentBook);

Consider caching a copy of the original model before displaying the modal, and then resetting it if the user cancels. This can easily be done directly in JavaScript, or you can opt to use Angular's $cacheFactory for more complex scenarios.
For instance, you can add an index to your ng-repeat:
<p ng-repeat="displayBook in books track by $index">
<a href="#" ng-click="openModal(displayBook, $index)">
{{displayBook.Name}}
</a>
</p>
And then alter your controller method to reset the $scope.books collection if the user cancels the modal:
$scope.openModal = function (currentBook, idx) {
// Cache the book, so that we can reset it later.
var cachedBook = angular.copy(currentBook);
var $outerScope = $scope;
var modalInstance = $modal.open({
// ...
controller: [
'$scope', '$modalInstance', function($scope, $modalInstance){
$scope.editBook = currentBook;
$scope.saveModal = function (book) {
// ...
};
$scope.cancelModal = function () {
// Restore the previous state
$outerScope.books[idx] = cachedBook;
// ...
};
}]
});
};
If you expect your users to be hitting Cancel more often than they actually save the edits, then perhaps consider reversing the operations and passing in a copy of the book instead of the original, only modify the original after saveModal is called.

Related

ng-if inside ng-repeat not updating from controller

So I am new to Angular, like just started right now so bear with me please... I want to loop thru an array and then inside the loop evaluate a condition to display some HTML it looks like this:
<div ng-controller="BuilderController">
<div ng-repeat="row in formRows" class="fe-form-row">
<div class="fe-form-draggable-item">
<div class="fe-form-l-side-menu bg-primary col-md-1 pull-left">
<i class="fa fa-cog"></i>
<i class="fa fa-close"></i>
</div>
<div class="gray-lighter col-md-11 pull-left">
<div ng-if="row.columns.length == 0">
<div ng-click="open('lg','layout')" id="fe-form-container-base" class="fe-form-r-side-menu gray-lighter col-md-12">
<div class="fe-form-insert-menu">
<i class="fa fa-plus-square-o"></i>
<span>Add Column(s)</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
The first time arround column is an empty array so it evaluates to true and the HTML piece is displayed, all good. And I tried setting it up as not an empty array and evaluates to false and the HTML is not showed. So far so good. The problem is when I change the value of column inside the controller:
admin_app.controller('BuilderController', function ($scope, $modal, $log) {
//default value, empty row
//TODO: assign value to these and the form should be automatically created
$scope.formRows = [
{
'layout': 'empty',
columns: []
}
];
$scope.animationsEnabled = true;
//TODO: these should be loaded from server-side somehow...
$scope.columnsOptions = ['1', '2', '3', '4', '5', '6'];
$scope.open = function (size, content) {
var modalInstance = $modal.open({
animation: $scope.animationsEnabled,
templateUrl: 'fe-form-' + content + '-container.html',
controller: 'ModalInstanceCtrl',
windowClass: 'fe-form-builder',
size: size,
resolve: {
columnsOptions: function () {
return $scope.columnsOptions;
}
}
});
modalInstance.result.then(function (selectedItem) {
$scope.selected = selectedItem;
}, function () {
$log.info('Modal dismissed at: ' + new Date());
});
$scope.toggleAnimation = function () {
$scope.animationsEnabled = !$scope.animationsEnabled;
};
};
});
admin_app.controller('ModalInstanceCtrl', function ($scope, $modalInstance, columnsOptions) {
$scope.columnsOptions = columnsOptions;
$scope.selected = {
columnsOption: $scope.columnsOptions[0]
};
$scope.ok = function () {
var columns = [];
for (var i = 0; i < $scope.selected.columnsOption; i++) {
columns.push({
type: $scope.selected.columnsOption,
elements: []
});
}
//update formRows var with new columns
$scope.formRows = [
{
'layout': $scope.selected.columnsOption,
columns: columns
}
];
console.log(' $scope.formRows ', $scope.formRows[0].columns.length);
$modalInstance.close($scope.selected.item);
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
});
The console.log shows that the columns have been correctly updated based on the user input, but the HTML is not re-rendered to hide the HTML piece since columns is no longer an empty array. Am I missing something here, is there something wrong with my approach? As I mentioned I am very new to Angular.
If I understand correct, your data structure is something like this:
formRows -> Array
row -> Object which contains another object columns
columns -> Array
I think the problem is that Angular is tracking any changes to formRows, which are not happening when your columns object change because there is no direct change in formRows. Angular is not trying to check changes at the sub-sub object level, so to speak. You could either create a new object everytime you're changing rows and add it back to formRows. Or you can add a deepwatch to check changes to your columns array. Check this:
How to deep watch an array in angularjs?

passing ng-show between two different controllers

I have a button which falls into Controller B and two block of HTML code which kind of falls under controller A...............and button falls into one block of HTML code
Example:
<div ng-controller="A">
<div ng-show="now">
<div>
<Button ng-controller="B"></Button>
</div>
</div>
<div ng-show="later">
</div>
</div>
On one button click I show up now block and later on button click of B controller I kind of hide now block and display later block.
How do I achieve this functionality?? I am not able to pass ng-show varibales between two different controller files......what should I use???
Hope this helps...!
angular.module('app', [])
.controller('A', function($scope) {
console.log('A');
$scope.state = {
now: true
};
$scope.showLater = function() {
$scope.state.later = true;
};
})
.controller('B', function($scope) {
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-controller="A" ng-app="app">
<div ng-show="state.now">
<div>
<button ng-controller="B" ng-click="showLater()">Show Later</button>
</div>
</div>
<div ng-show="state.later">LATER
</div>
<p> <pre ng-bind="state | json"></pre>
</p>
</div>
You could use a simple service that stores the state.
Example:
angular.module('mymodule').service('ActiveService', function() {
var service = {};
var status = false;
service.getStatus = function() {
return status;
}
service.toggle = function() {
status = !status;
}
return service;
})
And in your controller:
angular.module('mymodule').controller('SomeController', function(ActiveService) {
$scope.status = ActiveService.getStatus;
})
The Angularjs service is a singelton, so it will hold your values for you across different controllers, directives or pages.
Could also be used directly:
// Controller
$scope.service = ActiveService;
// Html
<div ng-show="service.getStatus()">
...
</div>
You can also achieve this by declaring the variable in $rootScope and watching it in controller A,
app.controller('A', function($scope, $rootScope) {
$rootScope.now = true;
$rootScope.later = false;
$rootScope.$watch("now", function() {
$scope.now = $rootScope.now;
$scope.later = !$rootScope.now;
})
});
In Controller B, you just change the value of now based on previous value like this on ng-click,
app.controller('B', function($scope, $rootScope) {
$scope.testBtn = function() {
$rootScope.now = !$rootScope.now;
}
});
I have implemented a button within different divs(now and later) in a plunker,
http://embed.plnkr.co/xtegii1vCqTxHO7sUNBU/preview
Hope this helps!

How do I $setValidity on an angular element in a form in $modal from within a controller?

I have an angular-ui $modal which users use to rapidly create a new element. When they do, I want to check that an element is unique. It calls $save(item) in the controller, which tries the save. If it succeeds, it closes the $modal, if not and returns a 409, it should set a validity error.
My modal template looks like:
<div class="modal-body">
<form name="createItem">
<table>
<tr><td>Name:</td><td><input ng-model="item.name" name="name"></input></td><tr>
</table>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-primary" ng-click="$save(item)">Save</button>
<button class="btn btn-warning" ng-click="$dismiss()">Cancel</button>
</div>
The controller that launches the modal looks like this:
$scope.createItem = function () {
var modalInstance = $modal.open({
templateUrl: '/partials/createItem.html',
controller: function ($scope) {
$scope.item = {};
$scope.$save = function(item) {
var s = new Item(item).$save({},function () {
modalInstance.close();
},function (httpResponse) {
if (httpResponse.status === 409) {
// I WANT TO SET VALIDITY HERE
} else if (httpResponse.status === 400) {
// I WANT TO SET VALIDITY HERE
} else {
// I WANT TO SET VALIDITY HERE
}
});
};
}
});
};
How do I $setValidity on the particular element?
UPDATE:
It appears that the $scope inside the controller has 2 child scopes. The second has all of the usual $scope.createItem form elements, etc.
As per the documentation, $modal creates a child scope for the modal contents. Best to work around this is to pass the form to your save function: ng-click="$save(item, createItem)" and then you have access to the form in your function, so calling createItem.name.$setValidity('server-error', false); should then work as expected.

Using ui-router with Bootstrap-ui modal

I know this has been covered many times and most articles refer to this bit of code: Modal window with custom URL in AngularJS
But I just don't get it. I don't find that to be very clear at all. I also found this jsfiddle which was actually great, very helpful except this doesn't add the url and allow for me to use the back button to close the modal.
Edit: This is what I need help with.
So let me try explain what I am trying to achieve. I have a form to add a new item, and I have a link 'add new item'. I would like when I click 'add new item' a modal pops up with the form I have created 'add-item.html'. This is a new state so the url changes to /add-item.
I can fill out the form and then choose to save or close. Close, closes the modal :p (how odd) . But I can also click back to close the modal as well and return to the previous page(state).
I don't need help with Close at this point as I am still struggling with actually getting the modal working.
This is my code as it stands:
Navigation Controller: (is this even the correct place to put the modal functions?)
angular.module('cbuiRouterApp')
.controller('NavbarCtrl', function ($scope, $location, Auth, $modal) {
$scope.menu = [{
'title': 'Home',
'link': '/'
}];
$scope.open = function(){
// open modal whithout changing url
$modal.open({
templateUrl: 'components/new-item/new-item.html'
});
// I need to open popup via $state.go or something like this
$scope.close = function(result){
$modal.close(result);
};
};
$scope.isCollapsed = true;
$scope.isLoggedIn = Auth.isLoggedIn;
$scope.isAdmin = Auth.isAdmin;
$scope.getCurrentUser = Auth.getCurrentUser;
$scope.logout = function() {
Auth.logout();
$location.path('/login');
};
$scope.isActive = function(route) {
return route === $location.path();
};
});
This is how I am activating the modal:
<li ng-show='isLoggedIn()' ng-class='{active: isActive("/new-item")}'>
<a href='javascript: void 0;' ng-click='open()'>New Item</a>
</li>
new-item.html:
<div class="modal-header">
<h3 class="modal-title">I'm a modal!</h3>
</div>
<div class="modal-body">
<ul>
<li ng-repeat="item in items"><a ng-click="selected.item = item">{{ item }}</a></li>
</ul>Selected:<b>{{ selected.item }}</b>
</div>
<div class="modal-footer">
<button ng-click="ok()" class="btn btn-primary">OK</button>
<button ng-click="close()" class="btn btn-primary">OK</button>
</div>
Also whilst this does open a modal it doesn't close it as I couldn't work that out.
It's intuitive to think of a modal as the view component of a state. Take a state definition with a view template, a controller and maybe some resolves. Each of those features also applies to the definition of a modal. Go a step further and link state entry to opening the modal and state exit to closing the modal, and if you can encapsulate all of the plumbing then you have a mechanism that can be used just like a state with ui-sref or $state.go for entry and the back button or more modal-specific triggers for exit.
I've studied this fairly extensively, and my approach was to create a modal state provider that could be used analogously to $stateProvider when configuring a module to define states that were bound to modals. At the time, I was specifically interested in unifying control over modal dismissal through state and modal events which gets more complicated than what you're asking for, so here is a simplified example.
The key is making the modal the responsibility of the state and using hooks that modal provides to keep the state in sync with independent interactions that modal supports through the scope or its UI.
.provider('modalState', function($stateProvider) {
var provider = this;
this.$get = function() {
return provider;
}
this.state = function(stateName, options) {
var modalInstance;
$stateProvider.state(stateName, {
url: options.url,
onEnter: function($modal, $state) {
modalInstance = $modal.open(options);
modalInstance.result['finally'](function() {
modalInstance = null;
if ($state.$current.name === stateName) {
$state.go('^');
}
});
},
onExit: function() {
if (modalInstance) {
modalInstance.close();
}
}
});
};
})
State entry launches the modal. State exit closes it. The modal might close on its own (ex: via backdrop click), so you have to observe that and update the state.
The benefit of this approach is that your app continues to interact mainly with states and state-related concepts. If you later decide to turn the modal into a conventional view or vice-versa, then very little code needs to change.
Here is a provider that improves #nathan-williams solution by passing resolve section down to the controller:
.provider('modalState', ['$stateProvider', function($stateProvider) {
var provider = this;
this.$get = function() {
return provider;
}
this.state = function(stateName, options) {
var modalInstance;
options.onEnter = onEnter;
options.onExit = onExit;
if (!options.resolve) options.resolve = [];
var resolveKeys = angular.isArray(options.resolve) ? options.resolve : Object.keys(options.resolve);
$stateProvider.state(stateName, omit(options, ['template', 'templateUrl', 'controller', 'controllerAs']));
onEnter.$inject = ['$uibModal', '$state', '$timeout'].concat(resolveKeys);
function onEnter($modal, $state, $timeout) {
options.resolve = {};
for (var i = onEnter.$inject.length - resolveKeys.length; i < onEnter.$inject.length; i++) {
(function(key, val) {
options.resolve[key] = function() { return val }
})(onEnter.$inject[i], arguments[i]);
}
$timeout(function() { // to let populate $stateParams
modalInstance = $modal.open(options);
modalInstance.result.finally(function() {
$timeout(function() { // to let populate $state.$current
if ($state.$current.name === stateName)
$state.go(options.parent || '^');
});
});
});
}
function onExit() {
if (modalInstance)
modalInstance.close();
}
return provider;
}
}]);
function omit(object, forbidenKeys) {
var prunedObject = {};
for (var key in object)
if (forbidenKeys.indexOf(key) === -1)
prunedObject[key] = object[key];
return prunedObject;
}
then use it like that:
.config(['modalStateProvider', function(modalStateProvider) {
modalStateProvider
.state('...', {
url: '...',
templateUrl: '...',
controller: '...',
resolve: {
...
}
})
}]);
I answered a similar question, and provided an example here:
Modal window with custom URL in AngularJS
Has a complete working HTML and a link to plunker.
The $modal itself doesn't have a close() funcftion , I mean If you console.log($modal) , You can see that there is just an open() function.
Closing the modal relies on $modalInstance object , that you can use in your modalController.
So This : $modal.close(result) is not actually a function!
Notice :
console.log($modal);
==>> result :
Object { open: a.$get</k.open() }
// see ? just open ! , no close !
There is some way to solve this , one way is :
First you must define a controller in your modal like this :
$modal.open({
templateUrl: 'components/new-item/new-item.html',
controller:"MyModalController"
});
And then , Later on , :
app.controller('MyModalController',function($scope,$modalInstance){
$scope.closeMyModal = function(){
$modalInstance.close(result);
}
// Notice that, This $scope is a seperate scope from your NavbarCtrl,
// If you want to have that scope here you must resolve it
});

Write to chosen scope variable with a service method

If 'User' is a service and 'User.get' is a method that doing a request.
<button class="btn" ng-click="User.get(users,'id,uname,email,pw')">
<p>{{users}}</p>
How do I assign the request data to the scope variable 'users' with that code example?
I like that code style but I do not know how to implement it. Any ideas?
example on the service provider:
angular.module('User',[]).
provider('User', function() {
var path = 'user.php';
this.$get = function($http) {
var r = {};
r.get = function(variable,fields){
$http.get(path+'?fields='+fields).
success(function(data){
//variable = data;
});
}
return r;
};
this.setPath = function(p){path = p;};
});
Use a controller and write the handler for the ng-click inside it. Here's an example:
Controller:
function Ctrl($scope) {
$scope.getUsers = function (fields) {
$scope.users = User.get(fields);
}
}
HTML:
<div ng-controller="Ctrl">
<button class="btn" ng-click="getUsers('id,uname,email,pw')">
<p>{{users}}</p>
</div>
Another way to do it is to pass the this reference (scope) and the property name ('users') into the service:
<button class="btn" ng-click="User.get(this, 'users', 'id,uname,email,pw')">
And then on the User service, after getting the result, do something like this:
r.get = function(scope, prop, fields){
$http.get(path+'?fields='+fields).
success(function(data){
scope[prop] = data;
});
}
where scope = this and prop = 'users'.
IMHO, the first approach is the best one, the second one sounds a little hacky.

Resources