I'm trying to use AngularJS 1.3.0's new $rollbackViewValue() method in ngModelController to cancel changes to a form in a modal popup or persist them when I close the modal. I'm using BootstrapUI for the $modal service.
I think I'm on the right track, but there is something that isn't quite working properly:
In my controller, I've got:
$scope.updateCharge = function (charge) {
var scope = $scope.$new();
scope.charge = charge;
var modalInstance = $modal.open({
templateUrl: 'client/app/views/providers/charges/updateCharge.html',
scope: scope
});
modalInstance.result.then(function () {
scope.charge.$save({providerId: providerId, chargeId: charge.id});
});
};
In my template, I have the following:
<form name="form" novalidate ng-model-options="{updateOn: 'save', debounce: {'save': 0 }}" class="form-horizontal" role="form">
<div class="modal-body">
<div class="form-group">
<label class="col-sm-3 control-label" for="chargeName">Name</label>
<div class="col-sm-9">
<input type="text" class="form-control" required ng-model="charge.code" id="chargeName"/>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary" ng-disabled="form.$invalid" ng-click="form.$broadcast('save'); $close()">Update</button>
<button class="btn btn-warning" ng-click="form.$rollbackViewValue(); $dismiss('cancel')">Cancel</button>
</div>
</form>
Generally speaking, this seems to work. When I click on cancel, my changes are reverted. When I click on Update, the modal closes but I do not see my updates in the scope.charge object.
I would have expected that my scope.charge object would be updated prior to the modal closing.
Am I using the ng-model-options incorrectly?
If I add a separate 'Apply' button that only does a form.$broadcast('save'), I see my scope object properly updated. So I am presuming that my $close() is being called prior to the event being processed by the ng-model-options. How can I avoid this race condition?
You can to use the $rollbackViewValue() method to revert changes but I think that is not the intention.
$rollbackViewValue(); Cancel an update and reset the input element's
value to prevent an update to the $modelValue, which may be caused by
a pending debounced event or because the input is waiting for a some
future event.
If you have an input that uses ng-model-options to set up debounced
events or events such as blur you can have a situation where there is
a period when the $viewValue is out of synch with the ngModel's
$modelValue.
In this case, you can run into difficulties if you try to update the
ngModel's $modelValue programmatically before these debounced/future
events have resolved/occurred, because Angular's dirty checking
mechanism is not able to tell whether the model has actually changed
or not.
The $rollbackViewValue() method should be called before
programmatically changing the model of an input which may have such
events pending. This is important in order to make sure that the input
field will be updated with the new model value and any pending
operations are cancelled.
The normal use case is to copy the model, optionally to persist the model and, if all is ok, refresh the model.
_this = this;
this.edit = function() {
this.modelToEdit = angular.copy(this.originalModel);
}
this.save = function () {
service.save(modelToEdit).then(function(savedModel) {
_this.originalModel = savedModel;
});
}
Or you can backup the model and restore when cancel
_this = this;
this.edit = function() {
this.backupModel = angular.copy(originalModel);
}
this.cancel = function() {
this.originalModel = this.backupModel;
}
this.save = function() {
service.save(this.originalModel).then(function(data) {}, function(error) {
_this.originalModel = _this.backupModel;})
}
I can see a few problems with your code:
ngModelOptions.updatedOn is meant to be a DOM event, i.e. click, blur, mouseenter, mouseover, etc,
The form controller does NOT have a $broadcast method, so it's never emitting an event.
I think the fact that it sort of works is because there is not type="button" on the <button> so they are considered as "submit" inside the form. And the model is updated because of that.
I suggest you use a simplified version, and
remove the ng-model-options from the form.
add a type="submit" to the Update button, and remove the form.$broadcast
add a type="button" to the Cancel button.
Am not sure how it would work with the modal, but here's a plunkr with ng-if: http://plnkr.co/edit/m37Fd0NybpnfslNkvJnO
I did not end up using any of the options suggested as answers, because the reality is, angular 1x has no "proper" way of doing what I want. Angular uses 2way binding, and yes I can write fancy directives to make life easier but infact it just makes the html looks even more complicated.
I settled with the suggested way as per many threads on the forum and that is to use angular.copy and then use the cloned model in your html. When you submit changes, set the cloned model to the original model.
There was heaps of examples on here on how to use angular.copy. And it works well.
Related
I have a form with a submit button which works fine. However I need to clear the form data from a clear button which is sitting outside the form on top right of the page. The clear button exist in a parent controller and sits above the form in top right header. The form sent from the clear button always comes up as undefined, which is because the clear button is not part of the form.
How do I pass the same instance of the form to clear? How do I clear the data? If this is a design issue I still need a workaround.
Here is a fiddle I have created to mimic that. Any help will be appreciated.
https://jsfiddle.net/SobDan/vj67rtb2/
<div ng-app>
<div class="col-md-10">
<h2>Todo</h2></div>
<div class="col-md-2">
<button class="btn pull-right" ng-click="clear(TodoForm)"> Close</button>
</div>
<br>
<div ng-controller="TodoCtrl">
<form name="TodoForm" ng-submit="addTodo()" name="testForm">
<input type="text" ng-model="todoText" size="30" placeholder="add new todo here">
<input class="btn-primary" type="submit" value="add">
</form>
</div>
function MainCtrl($scope) {
$scope.clear = function(form) {
alert(form); // the form is undefined
if (form.$dirty)
form.setPristine(); // clean this form
else
alert("form not dirty");
};
};
function TodoCtrl($scope) {
$scope.todoText = "test";
$scope.addTodo = function() {
alert("Submitted");
$scope.todoText = "";
// submit logic works fine
};
}
You should use $broadcast to communicate between controllers rather than trying to access the form which is outside scope.
Here is the fiddle and the explanation below
$broadcast function is used to broadcast events to all child $scope. Any interested child $scope can register to listen for the event using $on function. This functionality is used to communicate between controllers.
In your case, we signal to clear the form by broadcasting an event called clearForm from $rootScope. The TodoCtrl $scope listening on the event clearForm will receive a signal to clear form fields.
app.controller("MainCtrl", function($scope, $rootScope) {
$scope.clear = function(form) {
$rootScope.$broadcast("clearForm");
};
});
app.controller("TodoCtrl", function($scope) {
$scope.$on("clearForm", function() {
if ($scope.testForm.$dirty) {
$scope.testForm.$setPristine();
$scope.todoText = "";
} else {
alert("form not dirty");
}
});
});
AngularJS 1.1.x +
$scope.form.$setPristine() is only available from AngularJS version 1.1.x.
$setPristine() will only set the form status to pristine and will not clear the form field. You need to manually clear it by nullifying the $scope variables which will be reflected on the screen.
if ($scope.testForm.$dirty) {
$scope.testForm.$setPristine();
$scope.todoText = "";
}
AngularJS 1.0.x +
$setPristine function is not available in 1.0.x version.
The example Fiddle in your question seem to be configured to 1.0.x
In 1.0.x you simply clear the $scope variables
$scope.$on("clearForm", function() {
$scope.todoText = "";
});
<form name="editForm">
Title<input ng-model="task.title" type="text" name=taskTitle ng-model-options="{ updateOn: 'default' ,debounce{'default':2000000} }">
<a ng-click="UpdateTask(task.title)">SAVE</a>
<a ng-click="editForm.$rollbackViewValue();">DISCARD</a>
</form>
Since the debounce value is very long, when I click "DISCARD", the ng-model will not reflect its changes.
However, what I want is that when I click "SAVE", either I can change the debounce value or make it expired so the changes will be reflected immediately.
I can't find cancelDebounce() in AngularJs document, anyone would like to provide a solution? Thanks
I'd suggest you to have one scope variable which will have value of debounce like $scope.myDebounce = 2000000, then do create one discard function inside controller that will first reset the be-bounce value to 0, then do rollback the form changes. Reset the myDebounce variable to its actual value in next digest cycle.
Markup
<form name="editForm">
Title
<input ng-model="task.title"
type="text"
name=taskTitle
ng-model-options="{ updateOn: 'default', debounce: {'default': myDebounce } }">
<a ng-click="UpdateTask(task.title)">SAVE</a>
<a ng-click="editForm.$rollbackViewValue();">DISCARD</a>
</form>
Code
$scope.myDebounce = 2000000; //somewhere in controller
$scope.discard = function (form) {
$scope.myDebounce = 0; //resetting debounce to get quick `ng-model` update
form.$rollbackViewValue();
$timeout(function(){
//setting the actual debounce value to old one in next digest
$scope.myDebounce = 2000000;
});
}
I think what you really need is NgModelController.$commitViewValue();
From the angular doc
$commitViewValue()
Commit a pending update to the $modelValue.
Updates may be pending by a debounced event or because the input is waiting for a some future event defined in ng-model-options. this method is rarely needed as NgModelController usually handles calling this in response to input events.
I cannot access angular's Form members. I don't have a clue why. I'm using angular 1.4. The full code is at: http://jsbin.com/lujojiduru/edit?html,js,console,output
angular.module('test',[]).controller('testController', function($scope){
$scope.sendInvitations = function(){
var error = myForm.NewInvitations.$error;
console.log('sent');
console.log('value: ' + error );
};
});
the value of $error is always undefined. Any idea?
var error = myForm.NewInvitations.$error;
should be:
var error = $scope.myForm.NewInvitations.$error;
Notice the $scope
This is assuming you have the name="myForm" on your <form> tag
So something like:
<div ng-controller="testController">
<form name="myForm" novalidate>
...
</form>
</div>
you can also, if you prefer, send in the validity of your form, to your method on the controller.
So:
<button class="btn btn-success" ng-click="sendInvitations(myForm.$valid)">Send Invitations</button>
And in your controller:
$scope.sendInvitations = function(isValid){
if(!isValid)
return;
};
Update
You also don't have any errors.
Add required to your input.
So your controller is now:
angular.module('test',[]).controller('testController', function($scope){
$scope.sendInvitations = function(){
debugger;//
var error = $scope.myForm.NewInvitations.$error;
console.log('sent');
console.log(error );
};
});
and your input
<input style="width: 95%" name="NewInvitations" type="email" ng-model="newInvitations" required />
This is an update to your bin
http://jsbin.com/hebikucanu/1/edit?html,js,console,output
myForm is not accessible in the global scope. You can pass it in as an argument to sendInvitations.
ng-click="sendInvitations(myForm)
$scope.sendInvitations = function(myForm){
It's unlikely that you'd need to do this, though. You can do use the myForm properties in the view.
In angular, you should never touch the DOM. Meaning, you should never invoke HTML elements directly as you would in traditional HTML/JS settings. (read more)
Angular encourages the use of ng-model to employ two-way binding as a means to communicate between the controller and the view. (angular two-way data binding tutorial)
Thus, the first change you should attempt is to replace
var error = myForm.NewInvitations.$error;
with:
var error = $scope.NewInvitations;
This will cause the code to run.
But it appears that you want to retrieve the email input validation error and display it through angular.
Here is an excellent explanation with a tutorial that I think achieves what you're trying to do. If you just want to see the code in action, try this link. (Be sure to hit the Run button.)
Hope this helps!
I don't use Angular regularly, but I understand that one of the key features is that when data is updated on a form element, it is automatically updated in the model.
If you are instead using a library like jQuery, you must manually attach an event to the form input that updates the model when it is changed, as in $('#myInput').on('change', updateModel);
Although the above handler will be fired when myInput is changed by the user, it will not be fired if myInput is changed by Javascript code such as $('#myInput').val('hello world');
My question is, how does Angular know when a form input is changed in Javascript code?
Angular applies a scope digest every time it's needed (by an Angular function) during which it checks the states of all the scope variables, including the models used, of course.
If you modify some of those variables manually, using JavaScript, jQuery, etc... Angular will not know that the changes have occured and you need to tell it so either by doing $scope.$apply() or by wrapping the code block in a $timeout callback (these are the most commonly used methods).
If you don't do it manually, you'd have to wait for some (if any) other Angular event to trigger the digest cycle, which is never good.
See this example, note how nothing happens when you just update the value, but you need to do it manually (ng-click does it) in order for DOM to update:
angular.module('app', [])
.controller('Ctrl', function($scope){
$scope.ourValue = 'Initial Value';
window.exposedFunc = function(v, digest) {
$scope.ourValue = v;
if (digest) {
$scope.$apply();
}
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="Ctrl">
<button onclick="exposedFunc('First Button Value')">Update Value - No Digest</button>
<button onclick="exposedFunc('Second Button Value', true)">Update Value - Force Digest</button>
<button ng-click="">Force Digest only</button>
<p>{{ourValue}}</p>
</div>
Here's a super simple example of binding using keyup event. It should be enough to get you started on your projects:
var res = document.getElementById('r');
function handleChange(v) {
res.textContent = v;
}
<input onkeyup="handleChange(this.value)" type="text" value="Initial value" />
<p id="r">No binding yet</p>
Question related to forms-angular project.
Preamble
The default formInput directive of forms-angular can be override by a custom directive by specifying the form.directive property in an extended mongoose model,
var CategorySchema = new Schema({
name: String,
});
var PlansSchema = new Schema({
categories: {
type: [CategorySchema],
form: {
directive: 'plan-categories'
}
}
});
The custom plan-categories directive has a template where fields of [CategorySchema] can be edited.
What is working
Let's start with a first simple template:
<div>
<div ng-repeat="category in record.categories">
<input ng-model="category.name" />
</div>
</div>
Forms-angular can successfully detect changes in these custom plan-categories directive input fields bound to data (injected scope.record). In particular when changing the user changes the value of the above input fields, the "Save" button of the page is enabled, allowing the Save operation.
The activation of the Save button thanks to the following comparison in parent formInput's BaseCtrl scope false === $scope[$scope.topLevelFormName].$pristine (see base.js).
Not working
Now, the Save button doesn't get enabled, when changing the category.name variable with an expression or a function called by ng-click, as below:
<div>
<div ng-repeat="category in record.categories">
<input ng-model="category.name" />
<button ng-click="category.name = 'Hello'">Edit</button>
</div>
</div>
On button click, the category.name variable seems to be correctly changed, since the value in the input is changed accordingly. Unfortunately, the Save button stays disabled.
Note: I also unsuccessfully tried to pass to ng-click a method (from the scope injected in the link method of the custom directive) and setting the category.name variable in a $timeout call.
I guess the ng-model directive of the input field calls parent's (multi-ancestor?) $setDirty() method.
Question
how do I magically get $setDirty() called by forms-angular in order to enable the "Save" button
If it is not possible:
how do I access BaseCtrl scope and call $setDirty() when changing the record.categories elements?
Offhand I cannot think of a magical solution, but the decidedly non-magical way is to depend on $data.baseScope (see https://github.com/forms-angular/forms-angular/blob/master/js/controllers/base.js#L12) which saves going through lots of $parents.