extending angularjs form controller - angularjs

I have multiple child ng-form which are part of parent ng-form, I want to set the $submitted status of all the child forms, when I set the parent ng-form to $submitted status.
As of now no such method is available on the form-controller, checked here
Lets say, if I want to extend the current form controller to do this, how should I do that? how do I add a new method $setChildFormsToSubmittedState() ? of course I want to do it without disturbing/touching the angular code.
Is it possible? I think it should be given all the child forms hook into parent form using $addControl();.
No idea from where to start.

You can create a directive that appends a functionality to the form controller. Simply create a method that iterates over all the controls by checking if an item has a $$parentForm property that is equal to the form object it belongs to.
DEMO
Javascript
.directive('myForm', function() {
return {
require: 'form',
link: function(scope, elem, attr, form) {
form.__setSubmitted = function() {
setSubmitted(form);
};
function setSubmitted(form) {
form.$submitted = true;
angular.forEach(form, function(item) {
if(item && item.$$parentForm === form) {
setSubmitted(item);
}
});
}
}
};
});
HTML
<form name="myForm" my-form ng-submit="myForm.__setSubmitted()">
<ng-form name="mySubForm1">
<input type="text" ng-model="data.something1" name="something">
{{mySubForm1.$submitted}}
</ng-form>
<br>
<ng-form name="mySubForm2">
<input type="text" ng-model="data.something2" name="something">
{{mySubForm2.$submitted}}
</ng-form>
<br>
<button type="submit">Button</button>
{{myForm.$submitted}}
</form>

Related

Trouble with executing Angular directive method on parent

I have a directive inside a directive, and need to call a parent method using the child directive. I'm having a bit of trouble passing the data around, and thought y'all might have an idea.
Here's the setup:
My parent directive is called screener-item. My child directive is called option-item. Inside of every screener-item, there might be n option-items, so they're dynamically added. (Essentially, think of this as dynamically building a dropdown: the user gives it a title, then a set of options available)
Here's how this is set up:
screener-item.directive.js
angular.module('recruitingApp')
.directive('screenerItem', function(Study, $compile) {
return {
templateUrl: 'app/new-study/screener-item/screener-item.html',
scope: {
study: '='
},
link: function(scope, el, attrs) {
var options = [];
scope.addOptionItem = function(item) {
options.push(item);
}
scope.saveScreenerItem = function() {
if (scope.item._id) {
var isEdit = true;
}
Study.addScreenerQuestion({id:scope.study._id},{
_id: scope.item._id,
text: scope.item.text,
type: scope.item.type
}, function(item){
scope.mode = 'show';
scope.item._id = item._id;
if (!isEdit) {
el.parent().append($compile('<screener-item study="newStudy.study"></screener-item')(scope.$parent));
}
});
}
}
}
});
screener-item.html
<div class="screener-item row" ng-hide="mode == 'show'">
<div class="col-md-8">
<input type="text" placeholder="Field (e.g., name, email)" ng-model="item.text">
</div>
<div class="col-md-3">
<select ng-model="item.type">
<option value="text">Text</option>
<option value="single_choice">Single Select</option>
<option value="multi_choice">Multi Select</option>
</select>
<div ng-show="item.type == 'single_choice' || fieldType == 'multi_choice'">
<h6>Possible answers:</h6>
<option-item item-options="options" add-option-item="addOptionItem(value)"><option-item>
</div>
</div>
<div class="col-md-1">
<button ng-click="saveScreenerItem()">Save</button>
</div>
</div>
<div class="screener-item-show row" ng-model="item" ng-show="mode == 'show'">
<div class="col-md-8">{{item.text}}</div>
<div class="col-md-3">({{item.type}})</div>
<div class="col-md-1">
<a ng-click="mode = 'add'">edit</a>
</div>
</div>
You'll notice option-item which is included there in them middle. That's the initial option offered to the user. This may be repeated, as the user needs it to be.
option.item.directive.js
angular.module('recruitingApp')
.directive('optionItem', function($compile) {
return {
templateUrl: 'app/new-study/screener-item/option-item.html',
scope: {
addOptionItem: '&'
},
link: function(scope, el, attrs) {
scope.mode = 'add';
scope.addItem = function(value) {
console.log("Value is ", value);
scope.addOptionItem({item:value});
scope.mode = 'show';
var newOptionItem = $compile('<option-item add-option-item="addOptionItem"></option-item')(scope);
el.parent().append(newOptionItem);
}
}
}
});
option-item.html
<div ng-show="mode == 'add'">
<input type="text" ng-model="value">
<button ng-click="addItem(value)">Save</button>
</div>
Here's what I want to happen: When the user enters a value in the option-item textbox and saves it, I want to call addItem(), a method on the option-item directive. That method, then, would call the parent method - addOptionItem(), passing along the value, which gets pushed into an array that's kept on the parent (this array keeps track of all the options added).
I can get it to execute the parent method, but for the life of me, I can't get it to pass the values - it comes up as undefined each time.
I'm trying to call the option-item method instead of going straight to the parent, so that I can do validation if needed, and so I can dynamically add another option-item underneath the current one, once an item is added.
I hope this makes sense, please let me know if this is horribly unclear.
Thanks a ton!
EDIT: Here's a jsFiddle of it: http://jsfiddle.net/y4uzbapz/1/
Note that when you add options, the logged out array of options on the parent is undefined.
Got this working. All the tutorials have this working by calling the parent method on ng-click, essentially bypassing the child controller. But, if you need to do validation before passing the value up to the parent, you need to call a method on the child directive, then invoke the parent directive's method within that call.
Turns out, you can access it just the same way that you can as if you were putting the expression inside of ng-click.
Here's a fiddle showing this working: http://jsfiddle.net/y4uzbapz/3/
Notice that the ng-click handler is actually on the child directive, which calls the parent directive's method. This lets me do some pre/post processing on that data, which I couldn't do if I'd invoked the parent directive directly from ng-click.
Anyway, case closed :)

What's the point of $validators and $setValidity?

I think I'm missing something with $validators and $setValidity (which I understand, do the exact same thing so you don't need both - please correct me if I'm wrong). Whether I have the $validators statement in there or not, I get the ng-invalid class added to the input form, which is adding a red border around the input. So why do I need the $validators? I am trying to set the parent form to be invalid if a user does not select a row from the dropdownnin the directive template. I don't want to show any error messages or anything, I just want to add the invalid class and red border based on if a row in the dropdown was selected.
Should I be using $validators or $setValidity? Do I need both $validator and $setValidity like I have below? Also, does $setValidity required ngModelCtrl or not? I get undefined for setValidity if it is not inside the $validators function. Any help is appreciated.
If I want to make the parent form invalid as a whole if no selection is made and I am getting ng-invalid class when I touch and then blur if no selection is made without $validators and $setValidity, then why do I need the $validators and $setValidity??
index.html
<form name="myForm">
<validator
rows="[{name:'tom', city:'san fran', state: 'mn', zip: 34212},
{name: 'joe', city:'san fran', state: 'mn', zip: 45675}]"
ng-required="true"
ng-model="hey">
</validator>
</form>
validate.js - DDO
return {
restrict: 'E',
require: {
ngModelCtrl: 'ngModel',
formCtrl: '?^form'
},
replace: true,
templateUrl: 'view.html',
scope: {},
controllerAs: 'ctrl',
bindToController: {
rows: '=',
onSelected: '&?', //passsed selected row outside component
typedText: '&?', //text typed into input passed outside so
//developer can create a custom filter,
//overriding the auto
textFiltered: '#?', //text return from the custom filter
ngRequired: "=?" //default false, when set to true the component
//needs to validate that something was selected on blur.
//The selection is not put into the input element all the
//time so it can't validate based on whether or not
//something is in the input element itself.
//I need to validate inside the controller where I can see
//if 'this.ngModel' (selectedRow - not passed through scope)
//is undefined or not.
},
controller: 'validatorController'
};
.
function validatorController () {
var ctrl = this;
var rowWasSelected = false;
var input = ctrl.formCtrl.inputField;
//called via ng-click on the dropdown row
//if this is called a row was selected
ctrl.rowSelected = function (row){
rowWasSelected = true;
}
//called via ng-blur of the input element
ctrl.ngModelCtrl.$validators.invalidInput = function (modelValue, viewValue) {
return rowWasSelected;
}
ctrl.$onInit = $onInit; //angular will execute this after
//all conrollers have been initialized, only safe to use
//bound values (through bindToController) in
//the $onInit function.
//i understand this need to be there with Angular 1.5
//using ngModel in the controller
//but I really only need to validate on ng-blur of the input
function $onInit() {
ctrl.validateInput();
}
}
};
}
view.html - template for directive
<div class="dropdown" ng-class="{'open' : ctrl.isOpen}">
<div class="form-group">
<input type="text" class="form-control" name="inputField"
placeholder="select" ng-click="ctrl.openDropdown()"
ng-blur="ctrl.validateInput()"
ng-model="ctrl.currentRow.name"
ng-required="ctrl.ngRequired">
</div>
<ul class="dropdown-menu list-group">
<li class="list-group-item" ng-repeat="row in ctrl.rows"
ng-click="ctrl.onSelectedLocal(row)">
{{row.name}}
</li>
</ul>
</div>
I must be getting the invalid class regardless of the $validators function I have because it is adding the ng-invalid class whether its there or not??
$setValidity(validationErrorKey, isValid);
Change the validity state, and notify the form.
This method can be called within $parsers/$formatters or a custom validation implementation. However, in most cases it should be sufficient to use the ngModel.$validators and ngModel.$asyncValidators collections which will call $setValidity automatically.1

AngularJS trigger field validation after form loads

I have set of data fields.
They look like this:
// Read-Only
<div class="field-group" ng-if="feature.edit == false">
<div class="label" ng-class="{'feature-required': field.Validations.Required === true}">{{field.Label}}</div>
<div class="value">{{field.Value}}</div>
</div>
// Editor
<div class="field-group" ng-show="feature.edit == true">
<label for="F{{field.FieldID}}">
<span ng-class="{'feature-required': field.Validations.Required === true, 'feature-notrequired': field.Validations.Required === false}">{{field.Label}}</span>
<input type="text"
id="F{{field.FieldID}}"
name="F{{field.FieldID}}"
ng-change="onFieldUpdate()"
ng-model="field.Value"
jd-field-attributes attr-field="field"
jd-validate on-error="onFieldError"
field="field">
</label>
</div>
feature.edit is controlled by button and you can have data read-olny or editable. Each field has some validation, usually, if required it must be different than null.
I want to trigger that validation after I click edit and input fields show up.
One way to do it is to loop through all input fields and use jQuery trigger("change"). I have to do it with some delay (it takes Angular to populate all fields).
Is there any way to trigger ng-change or run onFieldUpdate(), after that input becomes visible?
I have tried ng-init, but it didn't work.
You could move your validation logic to custom validators in the ngModel $validators pipeline. Functions you add to this pipeline will evaluate against the input model value every time it changes and automatically add the associated valid/invalid classes to the input.
Here's an example of how you can add custom validators:
app.directive('moreValidation', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attributes, ngModel) {
ngModel.$validators.first = function(input) {
return true; // add your custom logic here
}
ngModel.$validators.second = function(input) {
return true;
}
ngModel.$validators.third = function(input) {
return true;
}
}
};
});
// markup
<input type="text" ng-model="modelObject" more-validation />
Here's a small working plnkr example: http://plnkr.co/edit/mKKuhNqcnGXQ4sMePrym?p=preview

Retrieve all inputs values from a form AngularJS

I have a massive form with more or less 80 / 90 inputs.
My main problem is How can I pass all those inputs belonging to unique form in an ajax request without map the inputs manually into a object?
I know that with jquery you can use serialize() function selecting the form. Is there any helper function in angular to achieve that?
Thanks
Indeed, as Michal's comment suggested, it doesn't look like you are using ng-model.
Angular's jqLite doesn't support serialize() since with Angular one would typically build a ViewModel that would then be bound to a form.
But, if you are out of luck, you could add jQuery.js to get the serialize() support and create a simple directive - serializer - that would act on a form.
app.directive("serializer", function(){
return {
restrict: "A",
scope: {
onSubmit: "&serializer"
},
link: function(scope, element){
// assuming for brevity that directive is defined on <form>
var form = element;
form.submit(function(event){
event.preventDefault();
var serializedData = form.serialize();
scope.onSubmit({data: serializedData});
});
}
};
});
And use it as follows:
<form serializer="submit(data)">
<input name="foo">
<input name="bar">
<button type="submit">save</button>
</form>
And in the controller:
$scope.submit = function(data){
console.log(data);
}
plunker
EDIT:
If you are using ng-model and in fact have a proper ViewModel, then this is the "Angular way" and so, you should have some object that is bound to the form inputs - you should just submit that.
<form name="foo" ng-submit="onSubmit()">
<input ng-model="fooObject.a">
<input ng-model="fooObject.b">
...
</form>
$scope.fooObject = {};
$scope.onSubmit = function(){
$http.post("url/to/server", {data: $scope.fooObject})
.success(...);
}

Error: [ngModel:nonassign] Expression is non-assignable

Trying to display a columnvalue from a gridcollection based on another value in that same row.
The user can select/change values in a modal which contains a grid with values. When the modal closes the values are passed back. At that moment I would like to set a value for 'Also known as':
html:
Also known as: <input type="text" `ng-model="displayValue(displayNameData[0].show,displayNameData[0].value)">`
I created a function on scope to select the value only when the 'show' value is true:
$scope.displayValue = function (show, val) {
if (show) {
return val;
}
else {
return '';
}
}
However when I close the modal I get an error:
Error: [ngModel:nonassign] Expression 'displayValue(displayNameData[0].show,displayNameData[0].value)' is non-assignable.
plnkr reference:http://plnkr.co/edit/UoQHYwAxwdvX0qx7JFVW?p=preview
Using ng-value instead of ng-model worked for me.
As HackedByChinese mentioned, you can't bind ng-model to a function, so try like this:
<input type="text" ng-if="displayNameData[0].show"
ng-model="displayNameData[0].value">
Or if you want this control to be visible you can create directive, add function to $parsers that will set empty value according to show:
angular.module('yourModule').directive('bindIf', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, ngModel) {
function parser(value) {
var show = scope.$eval(attrs.bindIf);
return show ? value: '';
}
ngModel.$parsers.push(parser);
}
};
});
HTML:
<input type="text" bind-if="displayNameData[0].show"
ng-model="displayNameData[0].value">
You can bind ng-model to function
Binding to a getter/setter
Sometimes it's helpful to bind ngModel to a
getter/setter function. A getter/setter is a function that returns a
representation of the model when called with zero arguments, and sets
the internal state of a model when called with an argument. It's
sometimes useful to use this for models that have an internal
representation that's different from what the model exposes to the
view.
index.html
<div ng-controller="ExampleController">
<form name="userForm">
<label>Name:
<input type="text" name="userName"
ng-model="user.name"
ng-model-options="{ getterSetter: true }" />
</label>
</form>
<pre>user.name = <span ng-bind="user.name()"></span></pre>
</div>
app.js
angular.module('getterSetterExample', [])
.controller('ExampleController', ['$scope', function($scope) {
var _name = 'Brian';
$scope.user = {
name: function(newName) {
// Note that newName can be undefined for two reasons:
// 1. Because it is called as a getter and thus called with no arguments
// 2. Because the property should actually be set to undefined. This happens e.g. if the
// input is invalid
return arguments.length ? (_name = newName) : _name;
}
};
}]);

Resources