I cannot seem to avoid the need to generate dynamic sub forms in the application I am working on. The sub form is working as expected and the sub form shows $invalid=true when one or more of it's inputs are invalid. The parent form however has $invalid=false.
I have seen people achieve nested forms where invalid sub forms invalidate the parent form, but I can't seem to do it dynamically without wrapping the dynamic compiling of the sub form in a $timeout.
See Plunker HERE
In the above link I have recreated the scenario. I have three forms. The parent form, a sub form created at the same time as the parent form, and a dynamically created sub form.
If you clear the bottom existing sub form's input, it will invalidate the parent form (parent form turns red).
If you clear the top dynamic form's input, it will not invalidate the parent form (parent form remains green).
It will begin to work if you stick the addForm method in a $timeout:
// WORKS! : When you delete the dynamic added sub form input
// the parent form also becomes invalid
//timeout(addForm,0);
// FAILS! : When you delete the dynamic added sub form input text
// the parent form does NOT become invalid
addForm();
It's great that I have a workaround, but I would like to understand why I need the $timeout and if there is a solution that avoids the use of a $timeout.
As Michael said, the correct place to do any DOM manipulations is the link
function and not the controller
function of the directive. Some extra information on why what you already have does / does not work depending on the use of $timeout
:
According to the Angular documentation of the $compile
service for directive definitions the controller
is instantiated before the pre-linking phase
while the link
function
is executed after the template has been cloned
You can observe this yourself if you include a link
function in your directive and write two console.log
statements, one in the controller
function and one in the link
function. The link
function is always executed after the controller
. Now, when you include addForm();
in your controller this will be executed at the time the controller
is instantiated, ie. before the linking phase, at which time, as it is mentioned in the documentation it is
not safe to do DOM transformation since the compiler linking function
will fail to locate the correct elements for linking.
On the other hand, if you call the addForm()
function in the $timeout, this will actually be executed after the linking phase, since a $timeout
call with a zero timeout value causes the code in the timeout to be called in the next digest cycle, at which point the linking has been performed and the DOM transformation is performed correctly (once again you can see the timing of all these calls by adding console.logs
in appropriate places).
DOM manipulations should be done in the link
phase and not in the controller
.
See $compile
The link function is responsible for registering DOM listeners as well
as updating the DOM. It is executed after the template has been
cloned. This is where most of the directive logic will be put.
Detailed explanation:
The problem lies in the the angular FormController
. At intialization it will look for a parent form
controller instance. As the sub form was created in the controller phase - the parent form initialization has not been finished. The sub form won't find it's parent controller and can't register itself as a sub control element.
FromController.js
//asks for $scope to fool the BC controller module
FormController.$inject = ['$element', '$attrs', '$scope', '$animate', '$interpolate'];
function FormController(element, attrs, $scope, $animate, $interpolate) {
var form = this,
controls = [];
var parentForm = form.$$parentForm = element.parent().controller('form') || nullFormCtrl;
Plunker
Usually altering dom elements inside of a controller is typically not ideal. You should be able achieve what you are looking for without the need for '$compile' and make things a bit easier to handle if you introduce a second directive an array of items to use 'ng-repeat'. My guess is that $timeout()
is working to signal angular about the new elements and causes a digest cycle to handle the correct validation.
var app = angular.module('app',[]);
app.directive('childForm', function(){
return{
restrict: 'E"',
scope:{
name:"="
},
template:[
'<div ng-form name="form2">',
'<div>Dynamically added sub form</div>',
'<input type="text" name="input1" required ng-model="name"/>',
'<button ng-click="name=\'\'">CLEAR</button>',
'</div>'
].join('')
}
});
app.directive('myTest', function() {
return {
restrict: 'E',
scope: {},
controller: function ($scope, $element, $compile, $timeout) {
$scope.items = [];
$scope.items.push({
name:'myname'
});
$scope.name = 'test';
$scope.onClick = function () {
console.log("SCOPE:", $scope, $childScope);
};
$scope.addItem = function(){
$scope.items.push({name:''});
}
},
template: [
'<div>',
'<div>Parent Form</div>',
'<div ng-form name="form1">',
'<div class="form-container">',
'<div ng-repeat="item in items">',
'<child-form/ name="item.name">',
'</div>',
'</div>',
'<div>Existing sub form on parent scope</div>',
'<div ng-form name="form3">',
'<input type="text" name="input2" required ng-model="name"/>',
'<button ng-click="name=\'\'">CLEAR</button>',
'</div>',
'</div>',
'<button ng-click="addItem()">Add Form</button>',
'<button ng-click="onClick()">Console Out Scopes</button>',
'</div>'
].join('')
};
});
Updated plunkr