Angular 2++ | NgForm: Form.Dirty is Always Dirty - forms

Determine if NgForm Looks Exactly As It Did Before Any User-Input
It seems that form.dirty doesn't redact its value after it has been changed, and form.touched seems to always be false no matter what: dirty is touched, and touched is tetched.
template.html
<form #form="ngForm" (ngSubmit)="handleSubmission($event, {}, form)">
...
<input
#input
type="text"
[name]="item.title"
[(ngModel)]="item.estimate"
(ngModelChange)="handleEstimateChange(item, item.estimate, input, form)"
/>
...
</form>
component.ts
export class LeComponent {
#Input('data') public data: any;
public handleEstimateChange: Function;
constructor(private $: Sandbox) {
this.handleEstimateChange = $.debounce(this.handleEstimate.bind(this), (1000*0.2));
}
handleEstimate(item: any, estimate: number, input: HTMLInputElement, form: NgForm) {
if (!estimate) delete item.esitmate;
(this, item, estimate, input, form);
// Why does form.dirty never change back to pristine again???
}
}
In the TypeScript, I'm debouncing the ngModelChange handler to give Angular a chance to change the form.dirty value before I check it. This is because ngModelChange gets triggered before the NgForm object has been modified.
If !estimate, because estimate === "", then set it back to its original value of undefined. In this case, the form should look exactly like it did before any user-input had occurred.
However, when I put a breakpoint on the line right above the comment and I output form.dirty to the console, the NgForm never changes dirty back to false.
Is it possible to determine if the form looks exactly like it did before any user-input?
Obviously, I can write my own dirty logic, but wouldn't that mean that NgForm is kind of useless? There's got to be something I'm missing, right? How could dirty not mean dirty?
I've taken a look at some other SO questions -- the first one being similar but definitely not the question I am asking. They are asking if this is intentional -- I don't care; I'd like to know how to accomplish the goal above.
Close, but no cigar:
angular2 formcontrol stays dirty even if set to original value
Block routing if form is dirty [ Angular 2 ]
Angular 2 getting only the dirty values in a controlgroup
How do I programmatically set an Angular 2 form control to dirty?
Angular 2.x/4.x & bootstrap: patchValue does not alter dirty flag. Possible bug?

With template-driven forms and a very flat data model, I implemented it like this:
private currentProduct: IProduct;
private originalProduct: IProduct;
get isDirty(): boolean {
return JSON.stringify(this.originalProduct) !== JSON.stringify(this.currentProduct);
}
get product(): IProduct {
return this.currentProduct;
}
set product(value: IProduct) {
this.currentProduct = value;
// Clone the object to retain a copy
this.originalProduct = Object.assign({}, value);
}
But this only works for very simple cases.
As I mentioned in the comments, using reactive forms gives you more flexibility in managing your data model separate from your user entries.

What Was Most Useful
template.html
<form #form="ngForm" (ngSubmit)="handleSubmission($event, {}, form)">
...
<input
#input
type="text"
[name]="item.title"
[attr.name]="item.title"
[(ngModel)]="item.estimate"
(ngModelChange)="handleEstimateChange(item, item.estimate, input, form)"
/>
...
</form>
component.ts
export class LeComponent {
#Input('data') public section: any;
public handleEstimateChange: Function;
private resetFormControl = (input: HTMLInputElement, form: NgForm) => {
var name = input.name, control = form.controls[name];
control.reset();
// control.markAsPristine();
// control.setValue(undefined);
// control.updateValueAndValidity();
};
constructor(private $: Sandbox) {
this.handleEstimateChange = $.debounce(this.handleEstimate.bind(this), (1000*0.2));
}
handleEstimate(item: any, estimate: number, input: HTMLInputElement, form: NgForm) {
if (!estimate) this.resetFormControl(input, form);
(this, item, estimate, input, form);
// Why does form.dirty never change back to pristine again???
}
}
Note
[attr.name]="..." (template.html)
resetFormControl
Basically, simply deleteing the value was not enough because it was still present on the FormControl object (form.controls). To clear it properly, invoke control.reset() for the individual control -- this in-turn invokes .markAsPristine() which communicates to the parent NgForm. Also, input.name was empty as it was only represented by ng-reflect-name unless [attr.name] elucidated the same value -- [name] is really just there because its required by Angular.
Now, anytime an <input /> value changes -- and its falsey -- we reset the input ensuring that if all are falsey, Angular will automatically handle the NgForm's dirty-state correctly.

Related

Dynamic Vue Form from Input-Elements

I'm building a reusable vue-form-component. For more flexibility the basic idea is to NOT have to specify the form information in the vue-data-object beforehand, but to get the data-structure from the dom-input-elements itself.
On instance-creation the "v-model" attributes are read from the input-tags and applied to the instance via vue.set()
This works fairly well: https://jsfiddle.net/seltsam23/hrL3ec3z/9/
One detail is missing though: I need to only query the children-input-fields, and not the site-wide fields in case I'm using more than one form-component at the same time:
created() {
var inputs = document.querySelectorAll('input'); // works, but this returns ALL elements
var inputs = this.$el.querySelectorAll('input'); // Doesn't work because $el is only available after mounted().
...
}
mounted() {
var inputs = this.$el.querySelectorAll('input'); // works, but attribute "v-model" is removed from inputs at this point
...
}
I've tried to create data-path attribute in the created() phase to store the v-model value on the element itself, but after mount all those created attributes disappear.
Any ideas how to achieve this in an elegant way?
You should be aware that the only reason you see v-model attributes at all is that you're using inline-template. They are in the DOM during the created phase because the template has not yet been processed. What I'm saying is that you're trying to do something pretty hacky, and you probably shouldn't.
It's backward to the normal Vue approach of having the data model drive the DOM, but I know that in some cases it is useful to initialize things from the HTML.
How about this approach?
Vue.component('my-form', {
props: ['init'],
data() {
return {
form: {}
}
},
created() {
this.form = this.init;
}
})
new Vue({
el: '#vue',
})
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id="vue">
<my-form inline-template :init="{foo: {bar: 'one', baz: 'two'}}">
<form>
<input type="text" v-model="form.foo.bar">
<span v-text="form.foo.bar"></span>
<hr>
<input type="text" v-model="form.foo.baz">
<span v-text="form.foo.baz"></span>
</form>
</my-form>
</div>

Angular 2 date pipe inside a FormControl input

I have a dynamically generated Angular 2 FormGroup with multiple FormControl input fields. Some of the inputs are Dates, which are fetched from the server as unix timestamps.
What i would like to do is :
to be able to translate the unix timestamp to a human readable form,
when my FormGroup is populated, and also
translate the human
representation of the date to a unix timestamp when the form is
submitted.
Part 1 is somewhat simple, using Angular's date pipe like this :
<input class="form-control" [formControlName]="question.key"
[value]="this.form.controls[this.question.key].value | date:'dd/MM/yyyy'">
Where this.form is a reference to the FormGroup and this.question is a custom wrapper class based on the official tutorial about dynamic forms :
https://angular.io/docs/ts/latest/cookbook/dynamic-form.html
Trying to change the date input that way won't work because the pipe will constantly try to transform the input value, thus making the input unusable if not throwing an Invalid argument for pipe 'DatePipe' exception.
To clarify, i fill my form using the FormGroup.patchValue() api, and submit the form data using the FormGroup.getRawValue() api.
I have tried to use an Angular 2 date picker component, but they made my huge forms pretty slow, so i would like to do it without custom date pickers or any jQuery dependent widgets.
Thanks in advance.
One way to do such a thing would be to create a component for your input that implements ControlValueAccessor
A bridge between a control and a native element.
A ControlValueAccessor abstracts the operations of writing a new value
to a DOM element representing an input control.
Please see DefaultValueAccessor for more information.
Something like this should do the trick (not tested):
export const DATE_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MyDateInput),
multi: true
};
#Component({
template:`<input #input (input)="onChange($event)" (blur)="touchCallback()" type="date" [attr.disabled]="disabled?true:null">`
selector:"my-input",
styles:[],
providers:[DATE_VALUE_ACCESSOR]
})
export class MyDateInput implements ControlValueAccessor{
#ViewChild("input")
input:ElementRef;
disabled=false;
changeCallback=(data:any)=>{};
touchCallback=()=>{};
onChange(event){
let timestamp=this.convertToTimestamp(event.target.value);
this.changeCallback(timestamp);
}
convertToTimestamp(formatedDate){
//TODO:implement
}
convertFromTimestamp(timestamp){
//TODO:implement
}
writeValue(obj: any){
let formatedDate=this.convertFromTimestamp(obj);
this.input.nativeElement.value=formatedDate;
}
registerOnChange(fn: any){
this.changeCallback=fn;
}
registerOnTouched(fn: any){
this.touchCallback=fn;
}
setDisabledState(isDisabled: boolean){
this.disabled=isDisabled;
}
}
then you should be able to use it like this:
<my-input class="form-control" [formControlName]="question.key"></my-input>
or
<my-input [(ngModel)]="myModel"></my-input>

grails: custom validation of just one field/property

I have a "Thing" domain class, where each Thing has an record number (which is not the automatically generated id), that the user will use to access a Thing:
class Thing {
...
String recordNumber
...
}
There is a form to look for a Thing, knowing its recordNumber:
<g:form action="search">
<input name="recordNumber">
<g:submitButton name="btn" value="go to this Thing"/>
</g:form>
I would like to use a validation process in this form: if the recordNumber is not found (Thing.findByRecordNumber(recordNumber) == null), then the input field must turn in red, and a tooltip must show the error message "record number not found".
As far as I know/read (I'm a grails rookie), this has to be written as a constraint in the Thing class:
static constraints = {
recordNumber validator: { n -> Thing.findByRecordNumber(recordNumber) }
}
The problem is: I do not have in this form all the "Thing" properties to populate, just the recordNumber one, so I just can't call
new Thing(params).validate()
How to call validation on just one field, not on the whole object ?
If this is your main question, although I see others there:
"How to call validation on just one field, not on the whole object ?"
You can pass a list of values to validate and it will only validate those properties
new Thing(params).validate(["recordNumber"])
http://grails.org/doc/latest/ref/Domain%20Classes/validate.html
Validation is for constraints for domain class properties. You need an action in your controller:
def search = {
if(params.recordNumber && Thing.findByRecordNumber(params.recordNumber)){
redirect(action: "show", params:[id:Thing.findByRecordNumber(params.recordNumber).id])
}else{
flush.message = "No record found"
render(view:'VIEW_WITH_SEARCH_FORM')
}
}
If you want to validate without refreshing page, write a javascript code.

Invalidate form by custom component | AngularJS

I've searched my way, but can't figure this out. I made a directive manyToOneSelect (custom component) that loads items from the server, shows them to the user and lets the user pick one. That works well, but I cannot figure out how to prevent the form from being submitted if no item is picked by the user, i.e. how to invalidate the form.
Below is pretty much the directive:
angular.module('myApp.directives').
directive('manyToOneSelect', function(entityService) {
return {
restrict:'E',
templateUrl:'partials/control/n21select.html',
scope:{
entityName:'#',
entityField:'#',
bindVariable:'='
},
compile:function (tElement, tAttrs, transclude) {
return function (scope, element, attrs) {
var inner = element.children("#n21select");
scope.entities = [];
scope.$watch('entityName', function ($new, $old) {
entityService.getList(scope.entityName, function (data) {
scope.entities = data;
}, []);
}, true);
scope.lookup = function(uuid) {
for(var i in scope.entities) {
if(scope.entities[i].uuid == uuid) {
return scope.entities[i];
}}}}}}});
Here is the corresponding partial partials/control/n21select.html:
<select ng-hide="disable" ng-options="entity.uuid as entity[entityField] for entity in entities" ng-model="bindVariable" required></select>
<span ng-show="disable">{{lookup(bindVariable)[entityField]}}</span>
Here is how I use the directive:
<form ng-href="#/" ng-submit="save()">
<many-to-one-select entity-name="customer" entity-field="name"
bind-variable="entity.customerUuid"></many-to-one-select>
...
My problem seems lack of strategy, rather than "not entirely getting it to work", hence you don't see any attempt in the code I posted above. Let this be then a fairly open question: how to do it? :) Much appreciated already!
There's a few ways to do this.
Considering how you've already built out the directive, one way is to add a scope attribute for the form itself. something like:
scope: {
form: '='
}
Then you'd pass your form element in like so:
<form name="myForm" ng-submit="whatever()">
<my-directive-name form="myForm"></my-directive-name>
</form>
And in circumstance in your directive you wish to invalidate your form, you'd just call $setValidity on it:
link: function(scope, elem, attr) {
if(somethingIsWrong) scope.form.$setValidity('reason', false);
}
That's ONE way to do it, here's a BETTER way to do it if you can re-engineer your directive:
The other way, which is probably preferred, is to have your directive require ngModel. Then you'll have more grainular control over your validation, as ngModel's controller will be passed in and you can use that to invalidate both your form, and a singular field on your form:
app.directive('bettererWay', function() {
return {
require: 'ngModel',
restrict: 'E',
link: function(scope, elem, attr, ngModel) {
if(somethingIsBad()) ngModel.$setValidity('somethingIsBad', false);
}
};
});
And that's how you do it, in a nutshell. Hopefully that gets you started in the right direction.
EDIT: Weird issue with submission regardless of validity (in comments)
This is apparently an issue caused by Angular trying to adhere to the HTML specs.
From the comments in their code approx. line 214 here:
* To prevent double execution of the handler, use only one of ngSubmit or ngClick directives. This
* is because of the following form submission rules coming from the html spec:
*
* - If a form has only one input field then hitting enter in this field triggers form submit
* (`ngSubmit`)
* - if a form has has 2+ input fields and no buttons or input[type=submit] then hitting enter
* doesn't trigger submit
* - if a form has one or more input fields and one or more buttons or input[type=submit] then
* hitting enter in any of the input fields will trigger the click handler on the *first* button or
* input[type=submit] (`ngClick`) *and* a submit handler on the enclosing form (`ngSubmit`)
So, given the above, it might be a good idea to have your directive tied to an input element of type hidden on the page rather than being it's own element. If you have more than one element on the form, invalidity prevents submission just fine.

Bind a form field in Play 2.0 with a constant value?

I have a scala form with several fields.The fields in the form map to the member variables of a Java class. I want to bind one of the fields(say userId) with a value (I dont want the user to enter values for this field. Instead i want to pass this as a parameter to the scala template). However, i was unable to manually bind a form field. Any help is highly appreciated.
See the sample below for easier understanding :
`#(itemForm: Form[Item], user: User)
#import helper._
#main("Item list") {
#if(user != null) {
#form(routes.Application.newItem()) {
#itemForm("userId") = #user.id /**I want to bind the userId form field */
#inputText(itemForm("title"))
#inputText(itemForm("description"))
#inputText(itemForm("price"))
<input type="submit" value="Create">
}
}
}`
In this case it would be better to pass it as action's argument (remember to modify routes declaration)
#form(routes.Application.newItem(user.id)){
....
you can also just use common html
<input type="hidden" name="userId" value="#user.id" />
edit:
Validation in action.Note: it doesn't make sense to display errors on the page next to hidden field, so you do not need placeholders for error messages. It's up to you to pass VALID value into the hidden field. Displaying validation errors to user who can not change the value of hidden field is bad conception.
public static Result newItem(){
Form<ItemModel> itemForm = form(ItemModel.class).bindFromRequest();
if (itemForm.hasErrors(){
return badRequest(newItemView.render(itemForm));
}
itemForm.get().save();
return ok("Your new item is saved...");
}