Angular ng-repeat with ng-form, accessing validation in controller - angularjs

I am trying to generate an editable list using ng-repeat. I want to remind the user to update any edits before moving on, so I am using ng-form to create "nested" forms on the fly because the documentation says I can then use validation on these dynamically created inputs.
While that seems to work within the HTML, I don't see how to access those dynamically created forms and related validation fields in the controller. Specifically, when the user changes the input I use the form $dirty property to bring up a button to tell the user to commit the changes. So far, so good. However, once the changes are committed I want to $setPristine() on the field to indicate that the changes have been set. There may be other ways of ensuring that changes are committed on each input before I allow the main form committed, but this was the best I could come up with.
Unfortunately, even though the documentation says that if I name the ng-form it will be propagated to the $scope object, I can't find a way to access it. $scope.dynamic_form is undefined.
Here is a plunker showing what I mean:
plnk
Thanks!
[EDIT] Just to add to the issue, what does work for this specific example is to add to the ng-click on the dynamically created input:
ng-click="namesForm.name.$setPristine();clean()"
But I still don't have access to the dynamically created form in the controller. I would like, for example, to add a watcher to the namesForm.name.$pristine so that I can set the mainForm.$setValidity(false) whenever the sub-form is $dirty to prevent the user from submitting the main form until all sub-form changes have been committed.
So in a nutshell, the issue is how to access in a parent controller the validation values of a dynamically created nested ngForm?

Updated 2015-01-17:
As pointed out by Leblanc Meneses in the comments Angular 1.3 now supports interpolation with form, ngForm and input directives.
This means that using expressions to name your elements:
<div ng-form="namesForm_{{$index}}" ng-repeat="name in names">
<input type="text"
name="input_{{$index}}_0"></input>
<!-- ... -->
</div>
will work as expected:
$scope['namesForm_0']
$scope.namesForm_1
// Access nested form elements:
$scope.namesForm_1.input_1_0
...
Original answer for Angular <= 1.2:
Working with forms and the ngFormController can get tricky pretty quickly.
You need to be aware that you can dynamically add form elements and inputs but they can't be dynamically named - interpolation does not work in the ngForm or name directives.
For example, if you tried to name your nested forms dynamically like this:
<div ng-form="namesForm_{{$index}}" ng-repeat="name in names">
<!-- ... -->
</div>
Instead of making all the nested forms available on the scope like this: scope['namesForm_0'] you would only have access to the single (last) form with the literal name scope['namesForm_{{$index}}'].
In your situation you need to create a custom directive that will be added along with ngFormto handle setting $pristine$ and $invalid for that form instance.
JavaScript:
This directive will watch the $dirty state of its form to set the $validity to prevent submission when dirty and handle setting the $pristine state when the 'clean' button is pressed.
app.directive('formCleaner', function(){
return {
scope: true,
require: '^form',
link: function(scope, element, attr){
scope.clean = function () {
scope.namesForm.$setPristine();
};
scope.$watch('namesForm.$dirty', function(isDirty){
scope.namesForm.$setValidity('name', !isDirty);
});
}
};
});
HTML:
Then the only change to your HTML is to add the formCleaner directive.
So change your original HTML from this:
<body ng-controller="MainCtrl">
<form name="mainForm" submit="submit()">
<h3>My Editable List</h3>
<div ng-form="namesForm"
ng-repeat="name in names">
<!-- ... -->
</div>
<button class="btn btn-default" type="submit">Submit</button>
</form>
</body>
to this, by adding form-cleaner next to ng-form:
<body ng-controller="MainCtrl">
<form name="mainForm" submit="submit()">
<h3>My Editable List</h3>
<!-- Add the `form-cleaner` directive to the element with `ng-form` -->
<div form-cleaner
ng-form="namesForm"
ng-repeat="name in names">
<!-- ... -->
</div>
<button class="btn btn-default" type="submit">Submit</button>
</form>
</body>
Here is an updated Plunker showing the new behaviour: http://plnkr.co/edit/Lxem5HJXe0UCvslqbJr3?p=preview

Related

ng-keyup firing but not reading properly when used with an ng-if inside a directive template

I have a directive and it works fine in a way such that when I type something the search() scope function inside my directive fires and sets $scope.query with the input text.
here is the directive template
<div class="container">
<div class="system-filter-header">
<div class="no-gutter">
<div class="system-search-wrapper search-wrapper-width">
<i ng-click="search($evt)" class="fa fa-search"></i>
<input type="text" ng-keyup=search($evt) class="search pull-left suggesstions-styles"
ng-model="query" ng-attr-placeholder="Search...">
</div>
</div>
</div>
</div>
here is the scope function which gets triggered
$scope.search = function() {
console.log($scope.query.length)
}
But when I used an ng-if="true" in first line of template (true used for generalizing only, I want to do a different conditional check inside ng-if) such that,
<div class="container" ng-if="true">
still the search gets triggered but the console.log gives always 0 and it doesn't seem to update the $scope.query value as it stays as $scope.query = ''
throughout the typing.
EDIT
Here is a an example codepen with almost similar behaviour. The problem is with the searchBox directive and I have added ng-if=true to the template but searching doesn't work. When I remove the ng-if searching works fine.
Any reason for this?
Rule of thumb in AngularJS: your ng-model should always include a dot. Otherwise AngularJS directives that create child scopes (like ng-if or ng-repeat) will create a duplicate property on that child scope instead of the parent scope. Following the controllerAs convention completely mitigates this behavior.

Binding raw object output to a form representation

I'm trying to update models from a JSON representation of an object to a form. Here's a link to an example
To recreate my issue,
Change the data in the form (see that the JSON changes).
Change the JSON (See that the form doesn't change).
Here's my code:
JS
var ppl = {
createdby: "foo",
dateCreated: "bar",
}
angular.module('myApp', [])
.controller("Ctrl_List", function($scope) {
$scope.people = ppl
$scope.print = JSON.stringify($scope.ppl)
})
HTML
<div ng-app="myApp">
<div class="container" ng-controller="Ctrl_List">
<!-- FORM -->
<div class="row" ng-repeat="(key,val) in people track by $index">
<div class="col-md-12">
<label for="{{key}}">{{key}}</label>
<input class=form-control" id="{{key}}" ng-model="people[key]">
</div>
</div>
<!-- JSON -->
<div class="editable" contenteditable="true" ng-model="people">{{people}}</div>
</div>
</div>
When a user changes the JSON, the form should update in real-time.
Here's some things I have tried:
Change the JSON display element from div to input but it prints [Object][Object]
Also <input ng-model="JSON.stringify(people)"> but I get an "unbindable element" error.
Also tried adding a new model: $scope.print = JSON.stringify(people) but it shows nothing in the raw output.
Is it even possible to update a live object or am I gonna have to do some sort of event that triggers the form to change?
PS: Angular 1.5.8
There are several reasons why this doesn't work:
ng-model on a div doesn't do anything
even if it did, it would save a string to people, and your form would thus not work anymore.
You should use a textarea to make it work, and bind it to another variable, of type string. Using ng-change on the textarea, and on the inputs of the form, allows populating the people object by parsing the JSON string, and vice-verse, populating the JSON string from the people object.
See https://codepen.io/anon/pen/peexPG for a demo.
Refering to Contenteditable with ng-model doesn't work,
contenteditable tag will not work directly with angular's ng-model because the way contenteditable rerender the dom element on every change.

Struts2 <s:checkbox> with 'value="true" not rendered as preselect if has Angular `ng-model`

I have observed a very peculiar behavior of <s:checkbox> rendering along with Bootstrap 3 and AngularJS.
I have these two <s:checkbox> in my page, wrapped by some elements of Bootstrap 3 styles:
<div class="col-md-1">
<div class="form-group">
<div class="form-other">
<label for="activaCheck"><s:text name="actividad.busqueda.activa"/></label>
<s:checkbox class="form-control" id="activaCheck" name="activaCheck" ng-model="formData.activaCheck" value="true"></s:checkbox>
<s:checkbox class="form-control" id="activaCheck2" name="activaCheck2" value="true"></s:checkbox>
</div>
</div>
</div>
As you can see, the only difference between them, is that the first has attribute ng-model = "xxx", while the second doesn't.
And, in my page, they are rendered differently, although they both are supposed to be pre-selected, because I set value="true". And when we inspect in FF, we can see the first <s:checkbox> has checked="checked", but is not rendered. I have tested in Chrome and FF, same.
I have also tested with <input type="checkbox" /> with ng-model set and checked="checked", the same: not checked when rendered in page.
So I am thinking about AngularJS is taking over part of rendering job which Struts 2 is responsible of, at least in this case. I want some explanation from developers of AngularJS, or this is the expected result?
I got the problem with unchecked checkbox. Because it has ng-model attribute the input control is bound to Angular's $scope. And if the scope doesn't define the property value for the above named checkbox it's not checked. Assumed that AngularJS modifies DOM as soon as it initializes.
I have created plnkr to demonstrate it.
You are right AngularJS starts working after document is loaded. At this time Struts has already done its work and returned html document to the browser. Now Angular continues to prepare the page to work only on one page. Both complement each other, but if Struts use to render
<input type="checkbox" ng-model="checkboxModel.value1" checked="checked">
Angular removes the checked state, because the value is commented
angular.module('checkboxExample', [])
.controller('ExampleController', ['$scope', function($scope) {
$scope.checkboxModel = {
//value1 : true,
value2 : 'YES'
};
}]);

AngularJS: create element dynamically

How do I go about create an element in my controller? e.g. on a click event?
example controller:
function AddCtrl($scope){
$scope.add = function(){
// do stuff to create a new element?
}
}
example view:
<div ng-controller="AddCtrl">
<button ng-click="add()">Add</button>
// create <input type="text" ng-model="form.anotherField">
</div>
Any suggestions much appreciated.
AngularJS is intended to follow MVC - so the controller creating an element in the view doesn't agree with the MVC behavior. The controller should not know about the view.
It sounds as if you want to have a control appear based on some conditional logic. One approach would be to bind to the visibility of the element.
In Angular, your controllers should not be manipulating the DOM directly. Instead, you should describe the elements you need in your templates, and then control their display with directives, like ng-switch, ng-hide / ng-show, or ng-if, based on your model, ie, your data.
For example in your controller you might do something like:
$scope.showForm = false;
And then in your partial:
<div id="myForm" ng-show="showForm">
<!-- Form goes here -->
</div>
By switching $scope.showForm between true and false, you will see your myForm div appear and disappear.
This is a classical mistake coming from jQuery moving to Angular or any other MVC library. The way you should think is to let the view react to changes in the scope.
$scope.items = []
$scope.add = function(){
$scope.items.push({});
}
In the view:
<input type="text" ng-repeat="item in items" ng-model="item.property">
If you want to display an element based on some condition or after the click, use ng-switch: http://docs.angularjs.org/api/ng/directive/ngSwitch
If you want to add multiple elements, create a repeated list of items and add an item to your view-model on clicking the button:
$scope.yourlistofitems = [];
$scope.add = function() {
$scope.yourlistofitems.push("newitemid");
}
And in the HTML:
<input type="text" ng-repeat="item in yourlistofitems" ng-model="item.property">

Pass a reference to DOM object with ng-click

I have multiple elements with the same callback on ng-click:
<button ng-click="doSomething()"></button>
<button ng-click="doSomething()"></button>
<button ng-click="doSomething()"></button>
<button ng-click="doSomething()"></button>
// In controller:
$scope.doSomething = function() {
// How do I get a reference to the button that triggered the function?
};
How can I get the reference to the object which made the call to doSomething? (I need to remove an attr from it)
While you do the following, technically speaking:
<button ng-click="doSomething($event)"></button>
// In controller:
$scope.doSomething = function($event) {
//reference to the button that triggered the function:
$event.target
};
This is probably something you don't want to do as AngularJS philosophy is to focus on model manipulation and let AngularJS do the rendering (based on hints from the declarative UI). Manipulating DOM elements and attributes from a controller is a big no-no in AngularJS world.
You might check this answer for more info: https://stackoverflow.com/a/12431211/1418796
The angular way is shown in the angular docs :)
https://docs.angularjs.org/api/ng/directive/ngReadonly
Here is the example they use:
<body>
Check me to make text readonly: <input type="checkbox" ng-model="checked"><br/>
<input type="text" ng-readonly="checked" value="I'm Angular"/>
</body>
Basically the angular way is to create a model object that will hold whether or not the input should be readonly and then set that model object accordingly. The beauty of angular is that most of the time you don't need to do any dom manipulation. You just have angular render the view they way your model is set (let angular do the dom manipulation for you and keep your code clean).
So basically in your case you would want to do something like below or check out this working example.
<button ng-click="isInput1ReadOnly = !isInput1ReadOnly">Click Me</button>
<input type="text" ng-readonly="isInput1ReadOnly" value="Angular Rules!"/>

Resources