Undo changes to model state when angular form is canceled - javascript

In an Angular 4 application, I have a template driven form with controls bound to values in my component. When the form input changes the object bound to the input changes immediately (two-way binding). When the cancel button is clicked, I want to undo the change to the bound object. This allows the user to change values then change their mind and cancel their changes.
https://plnkr.co/edit/RnnPwtHZY0qTN1H6er0z?p=preview
The plunker above has such a form with a bound field to read the hero.name
<h2>{{hero.name}} details!</h2>
An input bound to a hero object.
<form #myForm="ngForm" (ngSubmit)="save(myForm)">
<div class="form-group">
<label>name: </label>
<input name="heroName" [(ngModel)]="hero.name" placeholder="name" />
</div>
<button (click)="cancel(myForm)">Cancel</button>
<button type="submit">Save</button>
</form>
The cancel button calls the ngForm's resetForm() method.
cancel(myForm){
myForm.resetForm();
}
Repro steps
Change the hero name; Observe the h2 changes immediately proving that the bound object changed as well
Click cancel; Observe the name is cleared and the h2 changes because the hero.name is now null
I expected the cancel button to change the hero.name back to the original value. Is this how resetForm() is supposed to work? Is there a different way?

If you make a reset() method, where you set the default values, then you can call it whenever it's needed, as in ngOninit and reset button click:
ngOnInit(){
this.reset();
}
reset(){
this.hero = new Hero(1,'Plunker');
}
cancel(myForm){
this.reset();
}
DEMO
You can reunite cancel() and reset() by refactoring, but you may want keep it as is in case you add something else in cancel.

From the angular documentation, I guess the solution is to use a reactive form instead of a template driven form.
https://angular.io/guide/reactive-forms reads (emphasis :
In keeping with the reactive paradigm, the component preserves the
immutability of the data model, treating it as a pure source of
original values. Rather than update the data model directly, the
component extracts user changes and forwards them to an external
component or service, which does something with them (such as saving
them) and returns a new data model to the component that reflects the
updated model state.

Related

Triggering AngularJS form validation with javascript chrome extension

I am struggling to figure out a way to trigger these AngularJS classes on a form I am trying to automatically fill with a chrome extension I am making. The form (specifically a textbox) has to be validated/modified before it will be validated and therefore submitted.
I originally tried using javascript to set the value of the textbox using the value property. This did not validate the form. I then tried using a dispatch event to send a key to the textbox, which resulted in nothing being input into the text box. How can I validate the form without requiring human input, or is this not possible?
Clarification, I am trying to replicate this action without user input by using a chrome extension.
Reference https://www.w3schools.com/angular/angular_validation.asp
Sounds like you need to create some events to simulate whatever angular is listening for, probably change or blur. Here's an example using click from mozilla:
https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events#Triggering_built-in_events
function simulateClick() {
var event = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true
});
var cb = document.getElementById('checkbox');
var cancelled = !cb.dispatchEvent(event);
if (cancelled) {
// A handler called preventDefault.
alert("cancelled");
} else {
// None of the handlers called preventDefault.
alert("not cancelled");
}
}
How can I validate the form without requiring human input
Get the forms controls:
var controls = $scope.tdForm.$getControls();
Trigger their validators:
controls.forEach( _ => _.$validate() );
From the Docs:
$validate();
Runs each of the registered validators (first synchronous validators and then asynchronous validators). If the validity changes to invalid, the model will be set to undefined, unless ngModelOptions.allowInvalid is true. If the validity changes to valid, it will set the model to the last available valid $modelValue, i.e. either the last parsed value or the last value set from the scope.
For more information, see
AngularJS Form Controller API Reference
AngularJS ngModelController API Reference
When you type into the form, it updates the state of its controls (touched, dirty, etc.). According to how you define your fields validators (required, minLength...) the form will be valid or not after the user input.
In your submit method you should not proceed if any form fields are not valid. See AngularJS Developer Guide — Forms or Scotch Tutorials — AngularJS Form Validation you can have more details about AngularJS validation.
As Mike mentioned, you can use ngClass conditionally (see below) to apply some style classes only if a boolean condition occurr, for example the form is not valid.
<div ng-controller="ExampleController">
<form name="form" novalidate class="css-form">
<input type="text" ng-model="user.name" name="username" ng-class="{ 'error': !isValid }"/>
<div ng-show="form.$submitted>
<span ng-show="form.username.$error">Wrong Name</span></span>
</div>
<button ng-click="submit(user)"> Submit </button>
</form>
</div>
angular.module('formExample', [])
.controller('ExampleController', ['$scope', function($scope) {
$scope.isValid = true;
$scope.submit= function(user) {
if (user.name != 'Carl') {
$scope.isValid = false;
}
};
}]);
You can always programmatically change the form states if needed. For example to set the field to pristine:
$scope.form.$setPristine();
$scope.form.$setUntouched();
$setPristine sets the form's $pristine state to true, the $dirty state to false, removes the ng-dirty class and adds the ng-pristine class.
Additionally, it sets the $submitted state to false. This method will also propagate to all the controls contained in this form.
$setUntouched sets the form to its untouched state. This method can be called to remove the 'ng-touched' class and set the form controls to their untouched state (ng-untouched class).
Setting a form controls back to their untouched state is often useful when setting the form back to its pristine state.
UPDATE
Now it is clear what you are attempting to achieve. The two methods above can be used to set the form state, but if you want to validate it from code (this can be done passing the form to a service or directly in the controller for instance) then $validate() method will allow you to achieve that as mentioned by George.

ng-show expression not updating after variable update

I have a controller which prints some data in a ul using a ng-repeat. Each li is divided in two parts:
<li>
<div>
<button type="button" class="some classes" ng-show="!field.edit.state" ng-click="field.edit.update(true)">Edit</button>
<button type="button" class="some classes" ng-show="field.edit.state" ng-click="field.edit.update(false)">Save</button>
{{field.title}}
</div>
<div ng-show="field.edit.state">
<input />
</div>
The idea is, when the "Edit" button is clicked the input field and the "Save" button are shown, while the "Edit" button itself is hidden. Clicking on the "Save" button goes on the other way around. Since I need to execute some functions on the start and the end of the editing the update also triggers some callbacks from my controller. Now, the callbacks are correcly triggered, the state is updated, but nothing changes on my page.
The update function code is pretty simple:
function registerEdit(callback){
var state = false;
var update = function(newState){
state = newState;
callback(newState)
$timeout(function(){
$scope.$apply()
})
}
return {
state : state,
update : update
}
}
Each field element is injected its own registerEdit property when the data are fetched from the server and injected in the $scope, so they are unique (I've checked that part, and it works). From what I know about Angular, after changing the $scope all I needed to do was to call the $apply function to have Angular change my view, but even as the state changes (I logged it) the display remains the same.

How to modify a scope variable with ng-click in a button and reflect the change immediately in one of the buttons attributes

Scenario
I have a button (created via a directive) that looks like below (after being rendered via the directive):
<button class="btn btn-sm btn-primary" create-element="row" ng-click="setSettings([{span: 6}, {span: 6}], create);" settings="{}" data-dismiss="modal">One half block<button>
Note: The settings attribute of the button is bound to $scope.settings object.
The code in my controller is as follows:
$scope.settings = {}; // the settings object that I want to bind to the button
// function to be called if I'd want to set some data to the settings variable from the button
$scope.setSettings = function (setting, callback){
$scope.settings = setting;
callback();
};
$scope.create = function () {
$log.log($attrs.settings); // always reflects data set from previous click event
var settings = $attrs.settings;
var type = $attrs.createElement;
var element = DOMService(type, settings);
getTarget().append($compile(element)($scope));
};
Problem
I'd want to produce a two way binding via the ng-click directive so that any change in the variable settings is reflected in one of the buttons own attributes.
The first click on the button does change the $scope.settings object but the change isn't there in the button's settings attribute. So, upon every second click, the data that was set in the previous click is reflected in the attribute.
When the button is clicked, it calls the setSettings() function via which I'm passing an arbitrary settings object, and soon after, the callback is called. The problem is that when the settings object is set to some value with the click on the button, I cannot reflect the change occurring in it immediately in the view.
I know that a two way binding for instance on an input[type=text] is possible via ng-model directive. So I'm able to produce a two way binding as such:
<input class="input-sm form-control" ng-model="settings" placeholder="Title" />
and to reflect the change (in settings object), I can echo the object out in, say, another button in this way:
<button ng-click="create()" settings="settings" data-dismiss="modal">Create</button>
So, how do I achieve the same two-way binding through the ng-click directive. And if, the way I'm wanting to achieve this is wrong, then what is the appropriate one?

Making Meteor reactive for html elements

So I have a modal box that allows the user to edit / save some data.
I just want to add that unlike other Meteor apps, I don't want to save the data straight away - I want the user to fill in all the fields before hitting save where it will save to the database and send to server etc. This is mainly because I want the user to be able to hit the "cancel" button to revert all changes.
I have a drop down box at the start of the form where depending on the value, fields will be shown or hidden
<select class="form-control" id="ddlNewInputType" placeholder="Enter your input type">
<option value="input">Input</option>
<option value="formula">Formula</option>
</select>
And I have a handlebar around a field like this to determine whether I want to show it
{{#if isFormula }}
<div class="row">
<input type="text"
id="txtNewInputFormula" placeholder="Enter formula">
</div>
{{/if}}
With a helper looking like this
isFormula: ->
$('#ddlNewInputType').val() == 'formula'
However, this doesn't work. Aside from when it first loads onto the page, it never hits isFormula, probably because Meteor doesn't consider any of the HTML elements as reactive so it never re-evaluates when the HTML element changes.
What is a suitable way to get around this? Is it possible to make something reactive explicitly in Meteor? I was also considering putting the dropdown list value into a session variable but that just seems messy because I'm going to need to manage this session variable (remember to clear it when the modal box closes etc.)
Your analysis is correct - a reactive variable needs to be involved in order for your helper to reevaluate after changing the select element. The basic strategy looks like:
Initialize a reactive variable when the template is created.
Whenever the select changes, update the reactive variable.
Read the reactive variable in your helper.
Rather than use a session variable, let's use a ReactiveVar scoped to your template. Here's an example set of modifications:
Template.myTemplate.helpers({
isFormula: function() {
return Template.instance().isFormula.get();
}
});
Template.myTemplate.events({
'change #ddlNewInputType': function (e, template) {
var isFormula = $(e.currentTarget).val() === 'formula';
template.isFormula.set(isFormula);
}
});
Template.myTemplate.created = function() {
// in your code, default this to the current value from
// your database rather than false
this.isFormula = new ReactiveVar(false);
};
Remember that you'll need to also do:
$ meteor add reactive-var
See my post on scoped reactivity for a full explanation of this technique.

The trouble with $scope

I have an input file element within an angular view/form. I'm using ng-upload like this:
<input id="img" type="file" name="image" onchange="angular.element(this).scope().setFile(this)">
<input id="imgname" type="hidden" value=""></div>
Since I can't tell angular to listen for changes on input[type="file"] element, I've created the method that updates the hidden input that just holds the current filename. That way I can run my validator on the second field.
Another field I have has some sort of validator, like this:
<input ng-model="other" ng-change="chg()"/>
Now, the trouble is, if I trigger the validator, $scope.chg(), from setFile() method, I think I don't get the same scope - chg() runs, but it's as if the validator is in another scope and doesn't set my actual submit button to enabled. I tried logging from the chg() - it shows different scope then what I actually see on the view.
And if I later trigger the ng-change by changing the regular input field ("other"), it picks up the changes, or actually, it sets the submit button state correctly.
Now, I suspect this has to do with me calling the angular.element(this).scope().setFile(this) from my form instead of direct, $scope-bound method. But I cannot call $scope-bound method because it does not trigger - if I understood correctly, that's due to Angular not (yet) working with input type=file fields.
What can I do here?
I simply want to detect if there is a file or not so I can enable/disable the submit button appropriately.
I used followed flow that works for me:
<input type="file"
ng-model="upFile"
onchange="angular.element(this).scope().setFileEventListener(this)"
/>
From controller:
$scope.setFileEventListener = function(element) {
$scope.uploadedFile = element.files[0];
if ($scope.uploadedFile) {
$scope.$apply(function() {
$scope.upload_button_state = true;
});
}
}
Hope it will help.

Categories