Being unhappy with the way Angular does form validation, I decided to implement my own, and have run into an issue that has honestly left me stumped.
My setup is as follows:
A directive is used to instantiate a new form.
Its controller accesses the relevant form schema, which is then used to generate fields in the view via ng-repeat.
These input & textarea fields are then bound to the controller via ng-model.
On field change or form submit, the controller sends the form data to a validation service which returns an error if applicable, itself then bound to the DOM.
I've run into an issue trying to implement a sanitation step before the validation in part 4. This sanitation step should in theory update the controller data with the return value from a service method, updating the DOM binding and allowing the validation step to use the updated value. Although the controller value itself is being updated, this change is not being reflected in the DOM.
The relevant code is as follows:
View:
<div ng-repeat="(field, value) in form.schema">
<!-- ... -->
<textarea ng-model="form.data[field]" ng-model-options="{ updateOn: 'blur' }" ng-change="form.changed(field)"></textarea>
<div class="message">{{ form.errors[field] }}</div>
</div>
Controller:
// Controller submit method
ctrl.submit = function () {
var err;
for (var field in ctrl.schema) {
ctrl.data[field] = validationService.sanitizeField(ctrl.data[field], ctrl.schema[field]);
ctrl.errors[field] = validationService.validateField(ctrl.data[field], ctrl.schema[field]);
if (ctrl.errors[field] !== undefined) {
err = true;
}
}
if (err) {
return;
}
// Proceed ...
Service:
// Public field sanitation method
var sanitizeField = function (value, schema) {
try {
// Try sanitation
}
catch (e) {
// Error
}
return value;
}
Logging the new ctrl.data[field] value in the controller after sanitation yields the correct result. This result is also being correctly passed to the subsequent validateField method. However, the new data value isn't being updated in the DOM.
At first, I figured it might be an issue with the scope not being applied, or an issue with promises. Updating the service & controller accordingly didn't solve the issue. I've also tried wrapping the sanitation return value in an object, to no avail.
Strangely enough, changing the return value in the service from the value variable to a primitive, e.g. 'test', updates the DOM on return.
Likewise, errors returned from the service validation method (also strings rather than a variable) are updated in the DOM accordingly.
Despite a decent amount of searching, I haven't been able to find anything concrete on the topic. Any insights would be much appreciated!
Solved!
Unbeknownst to me, Angular features an ngTrim directive which is automatically bound to input fields and is by default set to true [Documentation].
With this directive, data is automatically trimmed before being picked up by the controller on form submission - the trimming being performed by my sanitation service therefore wasn't changing the data, which in turn wouldn't be reflected in the DOM as Angular wasn't picking up any changes.
This behaviour can be mitigated by setting ng-trim="false" on relevant fields in your view.
Try doing the follwoing;
for (var field in ctrl.schema) {
ctrl.data[field] = angular.copy(validationService.sanitizeField(ctrl.data[field], ctrl.schema[field]));
ctrl.errors[field] = angular.copy(validationService.validateField(ctrl.data[field], ctrl.schema[field]));
if (ctrl.errors[field] !== undefined) {
err = true;
}
}
Angular is tricky when it comes to updating objects/arrays with nested properties. You can either use $scope.$watchCollection to make sure that the object/array has updated, or you can use angular.copy(), which will make sure that DOM updates.
Related
I'm trying to detect when ever an object changes. The object is connected to a large form. Whenever a user changes the input I would like it to have save/cancel buttons popup at the bottom of the page.
My idea was to just make a copy of the object and do *ngIf="object !== object_copy" and if they hit cancel set the data equal to the copied object. I don't know if this the proper way to do it since I will be using it twice as many variables for a small task, but I've only used angular for a short time. I can't get this method to work however because when ever I make a type copy the object losses it's type.
Can someone help me with this or figure out a better way to do this?
If you are using a Form, then you could take advantage of Angular's form control, which will tell you anytime a form and any of its values have been altered in any way. Then, you could do something as simple as:
form.dirty
or even specific fields. There are tons of things you can do with reactive and template forms from Angular.
https://angular.io/guide/forms
You have to subscribe an event to handle the change event:
constructor(private formBuilder: FormBuilder) {
this.myForm = formBuilder.group({
name: 'Jose Anibal Rodriguez',
age: 23
})
this.myForm.valueChanges.subscribe(data => {
console.log('Form changes', data);
})
}
It should works.
ReactiveForm supports the dirty property. You can use 'myForm.dirty' to check the dirty status of the form.
Otherwise, you can set the initial value of the form to an object property using the getRawValue() method
this.initailFormValue = this.myForm.getRawValue();
Then just subscribe the form changes using
myForm.valueChanges.subscribe((value) => {
this.updatedFormValue = this.myForm.getRawValue();
},
(err) => {
//
}
);
Now you have the initial and current form values. You can compare and do the remaining.
I have just started with Angular 4 and I need to develop a CRUD grid, where the user can add, edit or delete rows.
During my research I found this article where it shows how to create the grid and also the actions: Angular 4 Grid with CRUD operations.
Looking at his code, what called my attention was the way he is using the ng-template to toggle between edit/view mode.
<tr *ngFor="let emp of EMPLOYEES;let i=idx">
<ng-template [ngTemplateOutlet]="loadTemplate(emp)" [ngOutletContext]="{ $implicit: emp, idx: i }"></ng-template>
</tr>
On the article he uses template driven forms to edit the row. However, I was trying to change to reactive forms.
In my attempt to do that, I tried to replace the [(ngModel)] to formControlName and I got some errors. My first attempt I tried to add the [formGroup] at the beginning of the template html inside form element. But when I tried to run and edit the row, I got the following error:
Error: formControlName must be used with a parent formGroup directive. You'll want to add a formGroup directive and pass it an existing FormGroup instance (you can create one in your class).
When I tried to move the [formGroup] inside the ng-template it works, however I was not able to bind the value to the fields and I had to set the values in the loadTemplate function:
loadTemplate(emp: Employee) {
if (this.selemp && this.selemp.id === emp.id) {
this.rForm.setValue({
id: emp.id,
name: emp.name
});
return this.editTemplate;
} else {
return this.readOnlyTemplate;
}
}
This works and show the values inside the fields in a read only mode :(
Here is the Plunker of what I have got so far.
How can I make a reactive form work with ng-template and how to set values to edit the entries?
Any help is appreciated! Thanks
Actually your form is not readonly, you are just constantly overwriting the input you are entering. Since you are having a method call in template (which is usually not a good idea), loadTemplate gets called whenever changes happen, which in it's turn means that
this.rForm.setValue({
id: emp.id,
name: emp.name
});
gets called over and over whenever you try and type anything. We can overcome this with instead setting the form values when you click to edit. Here we also store the index so that we can use it to set the modified values in the correct place in array, utilizing the index could perhaps be done in a smarter way, but this is a quick solution to achieve what we want.
editEmployee(emp: Employee) {
this.index = this.EMPLOYEES.indexOf(emp)
this.selemp = emp;
this.rForm.setValue({
id: emp.id,
name: emp.name
});
}
so when we click save, we use that index...
saveEmp(formValues) {
this.EMPLOYEES[this.index] = formValues;
this.selemp = null;
this.rForm.setValue({
id: '',
name: ''
});
}
Your plunker: https://plnkr.co/edit/6QyPmqsbUd6gzi2RhgPp?p=preview
BUT notice...
I would suggest you perhaps rethink this idea, having the method loadTemplate in template, will cause this method to fire way too much. You can see in the plunker, where we console log fired! whenever it is fired, so it is a lot! Depending on the case, this can cause serious performance issues, so keep that in mind :)
PS. Made some other changes to code for adding a new employee to work properly (not relevant to question)
You'll have to bear with me on this one, because it's a little involved and isn't answered by the following questions:
Two way binding Angularjs directives isn't working
AngularJS: Parent scope is not updated in directive (with isolated scope) two way binding
Both of those answers are more than 3 years old and no longer apply. Please do not mark this as a duplicate of those questions.
I wish that I could make a simple, reproducible version of this problem, but my Angular is honestly not good enough, and it is a fairly complex setup with lots of meddling from the outside. But the title is correct.
TL;DR version:
User presses escape from input box made by custom directive
Code in parent scope is called through ngKeyup in order to change the value, by updating the bound model
This update is not reflected in the data displayed by the directive
Putting code to do that update inside the directive itself, which directly updates its OWN bound model, to escape the problem of parent-scope bound-model changes not working, does not work.
In fact, even though it is changed inside its own directive controller, it sets the parent-scope model and its own model to the input value instead of the one I'm trying to overwrite with.
And it still doesn't work.
???
Long version with code:
I had a bunch of code I was repeating over and over again in my HTML, and decided to make a custom directive. The code uses many functions from the parent scope. You don't need to know what all these functions do, just note there's a ton of them that happen on click, on blur, etc. So, it looks like this:
template html file
<div ng-if="should_display" class="myclass" ng-class="cssSizeClass">
<div ng-if="allow_edits">
<span
class="text myapp-edit-hover"
ng-click="showEditField = true; captureValue(propertyString,binValue, $event)"
ng-hide="showEditField"
{{ binValue | customNumber: numberPlaceholder }}{{ numberType }}
</span>
<input
ng-show="showEditField"
class="myapp-editable-input"
type="text"
focus-on-show
ng-focus="selectText($event)"
ng-model="binValue"
ui-number-mask="numberPlaceholder"
ui-hide-group-sep
ng-blur="showEditField = false; inputUpdate(propertyString, binValue, 27, false )"
ng-keyup="inputUpdate(propertyString, binValue, $event, false)">
<span class="numberSuffix">{{ numberSuffix }}</span>
</div>
In my directive controller, in order to use those pesky parent-scope functions...
.directive('appDetailCardInput', function() {
return {
restrict: 'E',
templateUrl: '/static/js/directives/mytemplate.html',
scope: { // # = string, = = object, & = function
propertyName: '#',
cssSizeClass: '#',
numberPlaceholder: '#',
numberType: '#',
binValue: '=',
editable: '=',
numberSuffix: '#'
},
controller: function($scope) {
// define local properties
$scope.should_display = $scope.$parent.thing;
$scope.propertyString = 'thing.' + $scope.propertyName;
// HERE, outside functions are "captured"
$scope.captureValue = $scope.$parent.captureValue;
$scope.inputUpdate = $scope.$parent.inputUpdate;
$scope.selectText = $scope.$parent.selectText;
// check if field is editable
if (typeof $scope.editable === 'undefined') {
$scope.allow_edits = true;
}
else {
$scope.allow_edits = $scope.editable;
}
}}
};
});
This actually works 90%! When the bound model in the parent scope is updated outside the Angular digest cycle (the usual case is during a callback from an async messaging service), it updates the displayed value properly. Other things that occur "outside" the directive update it properly.
However. When escape is pressed or a blur happens, or any code is called that updates the value from "inside" the custom directive, no such luck.
I'd like to take a moment here to note that the $event target will blur properly when programmatically told to.
In the case of the escape key, the inputUpdate() function is supposed to set the value back as if someone hadn't typed something in the input. The problem comes here:
// (in inputUpdate function)
$event.target.blur(); // This works
$event.target.value = $scope.previouslyCapturedValue; // This works
setstr = "$scope.thing." + ser + " = " + $scope.previouslyCapturedValue + ";"; // This does not work
eval(setstr);
The eval() changes the parent scope model back, but that isn't reflected in the child scope.
So I tried both methods from the answers above. Adding a $watch in a link function only fires the SECOND time escape is pressed, not the first, so the first time someone hits escape on a field, it doesn't help! Fail.
The Angular docs suggest wrapping the changes to the value in a $timeout, so that the changes are applied outside of the digest cycle. Doing so doesn't help...
$timeout(function() {
console.log('evaluating outside digest supposedly');
$scope.nexttarget = $scope.nextval;
eval($scope.nextstr);
}, 0, false);
Nothing changes, the same behavior happens. The displayed value does not change (although the value that you change when clicking on it does).
So with some more digging, I find out that $event.target.value = $scope.previouslyCapturedValue; is just setting the value of the input, and not updating the actual bound model. That's fine. The bound model just needs to be updated too.
If I could somehow directly overwrite the text displayed on the , that would almost be a worthwhile solution compared to going back to how things were before.
But like I said before, when it is updated exactly the same as eval($scope.nextstr); from an external messaging service, the update happens perfectly correctly.
So at this point in the story I get it into my head: why not 'intercept' the ngKeyup directive and set the model right there inside the directive? I put this in my controller:
$scope.inputUpdate = function(ser, value, $event, dropdown) {
var keyCode = $event.keyCode;
console.log('got inputupdate!');
if ((parseInt(keyCode) == 27)
|| (typeof keyCode === 'undefined')) {
console.log('escape or deblur!');
if (ser !== $scope.dontBlur) {
console.log('going to set it!!!')
$scope.binValue = $scope.$parent.previouslyCapturedValue; // This is the correct value confirmed by console logs
}
}
$scope.$parent.inputUpdate(ser, value, $event, dropdown);
}
Nope! It gets there in code, but it still doesn't display the correct value! In fact, the bound model is updated to what had been typed in, even though it had been programmatically changed back right there!
Why does it update fine from outside its own code from another controller, but NOT from its own code, even if it's being done directly in the directive controller itself in the "intercepted" function?
Can't wrap my head around this. It's like the bound model is "locked" during this time period or something.
Thanks for any guidance.
Angular 2 data binding is great but i can't seem to find a angular 2 way of removing data binding on specific variables. My reason for this is i started hooking my application up to indexed DB and it works but i can't allow the temporary cache (just an array of all the indexed DB values) to be subject to data binding (if it was then the temporary cache would no longer mirror the database) my database is on an angular2 service. now i have found a way of removing the data binding but it isn't exactly pretty my code is this
app.copy=function(item){
return JSON.parse(JSON.stringify(item,app.replacer),app.reviver);
}
app.reviver=function(key,value){
if(value.fn){
value=new Function(value.parameters,value.body);
}else if(key==="time"){
value= new Date(value);
}
return value;
};
app.replacer=function(key,value){
if(typeof value ==="function"){
value=value.toString();
value={
fn:true,
parameters:value.match(/\(([\s\S]*?)\)/)[1].replace(/[\s\r\/\*]/g,""),
body:value.match(/\{([\s\S]*)\}/)[1].replace(/[\t\r\n]/g,"")
};
}
return value;
};
like i said it works but it isn't pretty. i can just run app.copy on the variables before they leave the cache so that they don't get data bound to anything. I was wondering if there was a cleaner way to tell angular 2 this variable isn't suppose to be data bound. and if not then at least i was able to get my solution up here for others.
If you establish "binding" imperatively you can stop the binding imperatively. There is currently no support in Angular2 to cancel a declarative binding imperatively.
Bind the view only to fields of the component.
Use observables in the service that fire an event when values change.
In the component subscribe to the observable and update the fields in the component when values in the service change.
Update values in the service when values change in the component.
I have a complex form in ExtJS 4, where various parts of the form are dynamically enabled or disabled based on what the user selects from some of the form fields. Whenever I disable a form field, I also clear any value it currently has in it.
I have a model class representing the form. To load the form, I use form.loadRecord(model). To update my model when the user "submits" the form, I use model.set(form.getValues()).
The problem is that ext's getValues() implementation skips form fields that are disabled. This causes problems in my case, because some of form fields that have changed values are disabled (ie. form fields whose values I cleared when I disabled them). As a result, these fields are not updated (cleared) in the model when I call model.set(...).
What would be the best way to work around this problem? I've considered the following ideas, but none seems very good. If you have a better one, I'd like to hear it.
Clear the model (set all fields to undefined) before calling model.setValues(). Unfortunately, there is no model.clear() method, so this gets ugly quickly - I have to get all fields and iterate over them, clearing each one individually.
Clear model fields also when I disable and clear the form fields. This seems to violate separation of concerns and also means the model gets changed, even when the user chooses to cancel and not submit the form.
Override ext's implementation of form.getValues() to not skip disabled fields. This is even more ugly because the actual code that needs to be changed is in the Ext.form.field.Field class, not Ext.form.Basic.
Disabled fields are commonly (not only extjs) always excluded from post data. Instead set fields readonly. The mean difference between readonly and disabled fields is just that.
This is the solution that you exposed in the thrid point:
The only way you have to change this behaviour is override this method.
Ext.override('Ext.form.field.Field', {
getSubmitData: function() {
var me = this,
data = null;
if (!me.isFileUpload()) {
data = {};
data[me.getName()] = '' + me.getValue();
}
return data;
}
});
About your first point, isnĀ“t .reject(false) useful?
The latest option could be override the getSubmitData for every single field in your form as follow:
{
xtype: 'textfield',
getSubmitData: this.getSubmitDataMyOwnVersion
}
I realize this is an old post but I have run into this same issue. IMHO this is a rather serious issue because it can cause data problems without you knowing about it. In my case I also set several check boxes to false when disabled but because of the way this works they were being left as true behind the scenes.
As a work around I now loop through all the fields in the form and manually update the record for each one. It's more work but I don't have to override any classes and the loop is generic enough that it will continue to work if/when the form definition is changed.
var fields = this.getForm().getForm().getFields();
var record = this.getForm().getRecord();
for (var i = 0; i < fields.length; i++) {
var name = fields.items[i].name;
var value = fields.items[i].value;
record.set(name, value);
}
Note that Mark Wagoner's answer breaks any advanced components / features of other components since it takes the value directly rather then getting the getSubmitValue(). I had to slightly modify Iontivero's answer as that was not the class I found Extjs calling at least in 4.2.0.
Ext.define('YourAppName.override.Field', {
override: 'Ext.form.field.Base',
getSubmitData: function() {
var me = this,
data = null,
val;
if (me.submitValue && !me.isFileUpload()) {
val = me.getSubmitValue();
if (val !== null) {
data = {};
data[me.getName()] = val;
}
}
return data;
}
});
then in Ext.application:
requires: ['YourAppName.override.Field'],
I haven't encountered that problem so far, but I update my model using the update() method rather than the setValue(). Maybe it handles disabled fields differently? Or maybe I'm headed down a path to need this answer as well since we're just starting major testing? -- This is the basic usage of the Form.update method though, assuming form is an Ext.form.Panel and this.record is a model instance:
//Save record
form.updateRecord(this.record);
this.record.save();
this.record.commit();
If that doesn't work for you, I would suggest writing a similarly named method and extending the form panel to include it that gets the array of values then goes through each one and updates it only if it's not null.