I am attempting to learn a little more about AngularJS' directives and have run into this situation. I would like to make a yes-no radio control that I can reuse. I have gotten most of the way - I think - but need a little push in the right direction.
I have this directive:
app
.directive('yesno', function () {
'use strict';
var config;
config = {
replace: true,
require: 'ngModel',
restrict: 'E',
scope: {
field: '=',
model: '='
},
templateUrl: 'views/yesno.html'
};
return config;
});
...and the template looks like this:
<fieldset class="yesno">
<input id="{{field}}-yes" name="{{field}}" ng-model="model" type="radio" value="yes" />
<label for="{{field}}-yes">Yes</label>
<input id="{{field}}-no" name="{{field}}" ng-model="model" type="radio" value="no" />
<label for="{{field}}-no">No</label>
</fieldset>
...and I am using it like this (simplified):
<form name="person">
<yesno field="'happy'" model="happy" />
</form>
Unfortunately what I am getting in the person object is a property {{field}} instead of happy like I would like. I keep telling myself that something like what I am attempting is possible and I just need to find it; but what.
Help please.
Update
Thank you, #HackedByChinese that helped a little but still not quite there. The problem is that I do want two way binding so that the value of the radios is populated into the parent scope; instead, when I inspect the person object it has a {{field}} property and not a happy property.
I am thinking that this is just something that AngularJS does not support in looking at:
AngularJS: Fields added dynamically are not registered on FormController
...and:
https://github.com/angular/angular.js/issues/1404
Well if you just want field to contain the string value that was entered, you can use the # prefix for the attribute to indicate it is a text binding (it will interpret the value of the attribute as literal text).
scope: {
field: '#',
model: '='
},
Click for demo.
On the other hand, if you need field to bind to the value an expression provided to the attribute (for example, you want to bind to a property on the parent scope), then you need to change the template HTML to evaluate field (simply {{field()}}) because they will be functions. The difference here is if people want to provide string values directly, they'll need to put it in quotes like your original example. I would also recommend a one-way binding, since it seems unlikely your directive would want to modify the parent scope value since it's just a name. Use the & prefix for that.
scope: {
field: '&',
model: '='
},
<fieldset class="yesno">
<input id="{{field()}}-yes" name="{{field()}}" ng-model="model" type="radio" value="yes" />
<label for="{{field()}}-yes">Yes</label>
<input id="{{field()}}-no" name="{{field()}}" ng-model="model" type="radio" value="no" />
<label for="{{field()}}-no">No</label>
</fieldset>
Click for second demo.
I ran into the same problem. The simplest solution is to inject the name value directly into the template string.
It works as long as you don't need the name value to be bound (ie. it doesn't need to change during the lifetime of the directive). Considering the way the name attribute is usually used, I think this constraint is not an problem.
app
.directive('yesno', function () {
return {
replace: true,
restrict: 'E',
scope: {
field: '#',
model: '='
},
template: function(element, attrs) {
return '<fieldset class="yesno"> \
<input id="{{field}}-yes" name="{{field}}" ng-model="model" type="radio" value="yes" /> \
<label for="{{field}}-yes">Yes</label> \
<input id="{{field}}-no" name="{{field}}" ng-model="model" type="radio" value="no" /> \
<label for="{{field}}-no">No</label> \
</fieldset>'.replace('{{field}}', attrs.field, 'g');
}
};
});
This solution is a bit messy, because of the inline html. If you want to load the template from a file as in the original question, you can do it like this:
app
.directive('yesno', ['$http', '$templateCache', '$compile',
function ($http, $templateCache, $compile) {
return {
restrict: 'E',
scope: {
field: '#',
model: '='
},
link: function(scope, element) {
$http.get('views/yesno.html', {cache:$templateCache})
.then(function(response) {
var content = angular.element(response.data.replace('{{field}}', scope.field, 'g'));
element.append(content);
$compile(content)(scope);
});
}
};
}]);
Related
First some background: My application allows users to control whether or not fields are required, disabled, etc. through an admin tool. I have a service that takes a field name and returns me the user defined rules in a format like this:
{
"disabled" : true,
"required" : true
}
I want a custom attribute directive that will control these properties on an input field using the service. I would expect the usage to look something like this:
<input type="text" my-rule="fieldName" ng-model="myfield" />
I'm able to accomplish this easily with a directive like the following:
angular.module('app').directive('myRule', ['$http',
function($http) {
return {
restrict: 'A',
scope: {
myRule: '#'
},
link: function(scope, element, attrs) {
$http.get('/api/rule/'+scope.myRule).success(function(rule) {
if (rule.disabled) {
element.attr('disabled','disabled');
}
if (rule.required) {
element.attr('required','required');
}
});
}
}
}
]);
My problem is that if a user does not have a field disabled I may still want to disable it until, for example, another field has been filled out. This should be easy to do with ng-disabled:
<input type="text" my-rule="fieldA" ng-model="fieldA" />
<input type="text" my-rule="fieldB" ng-model="fieldB" ng-disabled="!fieldA" />
However, this does not work because if the user chooses to disable fieldB then the field should always be disabled regardless of the ng-disabled attribute but instead the ng-disabled attribute overrides the user's rule. I tried something like this to remove the ng-disabled if the field is disabled by the user but that does not seem to have an effect:
angular.module('app').directive('myRule', ['$http',
function($http) {
return {
restrict: 'A',
scope: {
myRule: '#'
},
link: function(scope, element, attrs) {
$http.get('/api/rule/'+scope.myRule).success(function(rule) {
if (rule.disabled) {
element.attr('disabled','disabled');
element.removeAttr('ng-disabled');
}
if (rule.required) {
element.attr('required','required');
element.removeAttr('ng-required');
}
});
}
}
}
]);
This removes the attribute but it seems at that point it is too late and the field still becomes enabled as soon as fieldA is filled in.
How can I dynamically remove the ng-disabled attribute in my custom directive so that it no longer has an effect on the field?
Update:
I added a code snippet demonstrating my problem.
angular.module('app',[]).directive('myRule', ['$http',
function($http) {
return {
restrict: 'A',
scope: {
myRule: '#'
},
link: function(scope, element, attrs) {
// would normally be making an ajax call to get the rule
var rule = { disabled: scope.myRule != "fieldA" };
if (rule.disabled) {
element.attr('disabled','disabled');
element.removeAttr('ng-disabled');
}
}
}
}
]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app">
<p>Field B and Field C have been disabled by the user but since Field C includes an ng-disabled expression it will be incorrectly enabled when Field A is filled out.</p>
<input type="text" my-rule="fieldA" ng-model="fieldA" placeholder="Field A" />
<input type="text" my-rule="fieldB" ng-model="fieldB" placeholder="Field B" />
<input type="text" my-rule="fieldC" ng-model="fieldC" placeholder="Field C" ng-disabled="!fieldA" />
</div>
Tryprop('disabled', boolean) instead of attr(). This will change the element property which is not always the same as the attribute.
Since you are manipulating the DOM outside of angular you should probably tell angular to run a digest also by calling scope.$apply() or $timeout()
Not sure this will work and I think you will probably need a directive to wrap the whole input.
One suggestion is take a look at angular-formly which builds whole forms including conditional validation from object models
I have a bit of a strange scenario that is a little different to the other childscope and two way binding issues I have seen on Stackoverflow.
I have a field generation directive that receives a configuration object and some data and dynamically creates the correct type of field on screen and populates the data.
directive.js
.directive('myField', function () {
var stringTemplate = "scripts/directives/templates/my-string.tpl.html";
var textTemplate = "scripts/directives/templates/my-text.tpl.html";
var selectTemplate = "scripts/directives/templates/my-select.tpl.html";
var linker = function ($scope, elem, attrs) {
// Function to dynamically select the correct template
$scope.getTemplateUrl = function () {
var template = '';
if ($scope.options) {
if ($scope.options.optionList) {
template = selectTemplate;
} else {
switch ($scope.options.type) {
case 'String':
template = stringTemplate;
break;
case 'Text':
template = textTemplate;
break;
}
}
return template;
}
};
return {
restrict: 'E',
replace: true,
scope: {
options: '=',
data: '=',
fieldName: '#',
fieldWidth: '#',
labelWidth: '#',
},
link: linker,
template: '<ng-include src="getTemplateUrl()"/>'
}
});
I then have the corresponding template... I'm showing just the string template in this case.
my-string.tpl.html
<div class="form-group col-md-12">
<label for="{{fieldName}}" class="{{labelWidth}}">
{{options.label}}
</label>
<div class="{{fieldWidth}}">
<input type="text" class="form-control input-sm" id="{{fieldName}}" placeholder="{{options.watermark}}" ng-model="data" tooltip="{{options.tipText}}" ng-disabled="options.editable === false">
</div>
</div>
An example of how this might then be used would be
controller.js
$scope.person.firstName = "John";
$scope.person.lastName = "Doe";
$scope.options.person.firstName.type = "String";
index.html
<div class="row">
<my-field options="options.person.firstName" data="person.firstName" field-name="firstName" label-width="small" field-width="medium"></my-field>
The problem is the usual one, my-field directive has an isolated scope with a "data" property that is two-way bound to the controller. Because I am then using ng-include to dynamically load the correct template I am creating a further child scope that due to prototypical inheritance still populates correctly as it doesn't have its own data property so reaches to the parent. However when I modify the field, a shadow property is created on my child scope called data that doesn't propagate upwards the way that two way binding should.
I hope you are still with me
controller > my-field
ng-include causes the following scopes to exist
controller > my-field > ng-include
From reading around I understand that what I need to do to rectify this is pass an object rather than a primitive, however as there is effectively an intermediate layer between my controller and my final directive this is not straightforward.
I thought about changing the isolate scope in my-field to look like this
scope: {
....
data: {value: '=data'}
....
}
and then updating the template to refer to the object
my-string.tpl.html
<div class="form-group col-md-12">
<label for="{{fieldName}}" class="{{labelWidth}}">
{{options.label}}
</label>
<div class="{{fieldWidth}}">
<input type="text" class="form-control input-sm" id="{{fieldName}}" placeholder="{{options.watermark}}" **ng-model="data.value"** tooltip="{{options.tipText}}" ng-disabled="options.editable === false">
</div>
</div>
but this kills angular.
I have successfully got it to work by reaching back to the controller scope for binding by using
ng-model="$parent.$parent.data"
but I am not really happy with this as a solution as A it is ugly and B it involves knowing the depth of scope you are at which could vary.
Really stumped with this. Any help would be appreciated.
This is part of a much more complicated directive that needs to have its own scope as well as require ngModel and replace the existing input. How can I have the directive add the ng-pattern attribute? As you can see in this jsfiddel the validation doesn't change based on the input if the ng-pattern is added in the template. This is because this will be added to an existing application that has a ton of different attributes already on a ton of different input elements, and I'm trying to make the addition as easy to implement as possible by just adding functionality to the existing input fields without messing up other things.
http://jsfiddle.net/MCq8V/
HTML
<div ng-app="demo" ng-init="" ng-controller="Demo">
<form name="myForm" ng-submit="onSubmit()">
<input lowercase type="text" ng-model="data" name="number">
Valid? {{myForm.number.$valid}}
<input type="submit" value="submit"/>
</form>
</div>
JS
var module = angular.module("demo", []);
module.directive('lowercase', function() {
return {
require: 'ngModel',
restrict: 'A',
scope:{},
replace: true,
link: function(scope, element, attr, ngModelCntrl) {
},
template: '<input class="something" ng-pattern="/^\d*$/">',
};
});
module.controller('Demo', Demo);
function Demo($scope) {
$scope.data = 'Some Value';
}
Thanks so much for any help! Ideally I would be able to just change something small and keep the ng-pattern, but I think I may have to do the validation setting on my own.
Here's how the pattern attribute is added to input item in a directive I have in my application. Note the use of compile at the end of the link function. In your case, rather than replace the element contents with a template, you'd just work with the existing element input tag.
link: function (scope, element, attrs, formController) {
// assigned template according to form field type
template = (scope.schema["enum"] !== undefined) &&
(scope.schema["enum"] !== null) ?
$templateCache.get("enumField.html") :
$templateCache.get("" + scope.schema.type + "Field.html");
element.html(template);
// update attributes - type, ng-required, ng-pattern, name
if (scope.schema.type === "number" || scope.schema.type === "integer") {
element.find("input").attr("type", "number");
}
element.find("input").attr("ng-required", scope.required);
if (scope.schema.pattern) {
element.find("input").attr("ng-pattern", "/" + scope.schema.pattern + "/");
}
element.find("input").attr("name", scope.field);
// compile template against current scope
return $compile(element.contents())(scope);
}
I tried quite a few things and it seemed that using a directive to replace an input with an input was tricking Angular up somewhere - so this is what I came up with:
http://jsfiddle.net/MCq8V/1/
HTML
<div ng-app="demo" ng-init="" ng-controller="Demo">
<form name="myForm" ng-submit="onSubmit()">
<div lowercase model="data"></div>
Valid? {{myForm.number.$valid}}
<input type="submit" value="submit"/>
</form>
</div>
JS
var module = angular.module("demo", []);
module.directive('lowercase', function() {
return {
restrict: 'A',
scope:{
data:'=model'
},
replace: true,
template: '<input class="something" ng-pattern="/^\\d*$/" name="number" ng-model="data" type="text">',
};
});
module.controller('Demo', Demo);
function Demo($scope) {
$scope.data = 'Some Value';
}
Also, you needed to escape your backslash in your regex with another backslash.
I have the following directive:
directive('myInput', function() {
return {
restrict: 'AE',
scope: {
id: '#',
label: '#',
type: '#',
value: '='
},
templateUrl: 'directives/dc-input.html',
link: function(scope, element, attrs) {
scope.disabled = attrs.hasOwnProperty('disabled');
scope.required = attrs.hasOwnProperty('required');
scope.pattern = attrs.pattern || '.*';
}
};
});
with the following template:
<div class="form-group">
<label for="input-{{id}}" class="col-sm-2 control-label">{{label}}</label>
<div class="col-sm-10" ng-switch on="type">
<textarea ng-switch-when="textarea" ng-model="value" class="form-control" id="input-{{id}}" ng-disabled="disabled" ng-required="required"></textarea>
<input ng-switch-default type="{{type}}" ng-model="value" class="form-control" id="input-{{id}}" ng-disabled="disabled" ng-required="required" pattern="{{pattern}}"/>
</div>
</div>
It is used by this form:
<form ng-controller="UserDetailsCtrl" role="form" class="form-horizontal">
<div ng-show="saved" class="alert alert-success">
The user has been updated.
</div>
<my-input label="First name" value="user.firstName" id="firstName"></my-input>
<my-input label="Last name" value="user.lastName" id="lastName"></my-input>
<my-input label="Email" value="user.email" id="email" type="email" disabled></my-input>
<my-input label="Password" value="user.password" id="password" type="password"></my-input>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button ng-click="update()" class="btn btn-default">Save</button>
</div>
</div>
</form>
Which has this controller:
controller('UserDetailsCtrl', function($scope, $stateParams, User) {
$scope.user = User.get({userId: $stateParams.id});
/**
* Update the current user in this scope.
*/
$scope.update = function() {
console.log($scope.user);
$scope.user.$update({userId: $scope.user.id}).then(function(results) {
$scope.saved = true;
});
};
}).
The form is rendered fine, but when I click the Save button, the user values are never updated.
How can I use the updated values from within the myInput directive in the controller scope?
Here's the basic problem. Your ng-model is a primitive and is only being bound in one direction...it will update if parent object is changed, but since it is primitive it does not carry reference to parent object...just value. Thus updating the primitive does not update parent object that it's original value came from
Cardinal rule in angular...always have a dot in ng-model
Here's a solution that will pass the main user object to directive scope, as well as the property of that object to use for each input
<my-input id="firstName" model="user" field="firstName" label="First name"></my-input>
Now need to pass the object from controller into the directive scope:
app.directive('myInput', function() {
return {
scope: {
/* other props*/
field: '#',
model:'='/* now have reference to parent object in scope*/
},
......
};
});
Then in markup for an input will use [] notation in order to get our dot in:
<input ng-model="model[field]".../>
DEMO
In order to use angular validation you will likely have to require the ngModel controller in your directive or use nested form
Your problem is the ng-switch.
ng-switch like ng-repeat creates a new scope that inherits from the parent.
That means that if you have let's say:
$scope.foo = "hello";
And then you have something like:
<input type="text" ng-model="foo">
Inside a ng-switch. When you update foo it is going to create its own foo that hides/shadows the parent foo.
In other words, the input will show hello but when you modify it, a new foo is created hiding the parent one. That means that your parent one won't get updated (your problem).
That is not Angular.js issue, that is how Javascript works.
Normally you want to do a:
<input type="text" ng-model="foo.bar">
That way, you can play with the inheritance and instead of creating a new foo it will just update the bar on the parent.
Since that is not something you can do every time and maybe in your concrete use case you can't, the easy way is just to use $parent:
<input type="text" ng-model="$parent.value">
That way inside your ng-switch you will use directly the parent value.
I highly recommend you to read this ASAP: https://github.com/angular/angular.js/wiki/Understanding-Scopes
Example: http://plnkr.co/edit/z4D6Gk5fK7qdoh1mndzo?p=preview
Cheers.
I am fighting with the validation in an angular directive without success.
The form.name.$error object seems to be undefined, when I submit the name property to the directive template. If i use a fixed name-attribute inside the template, the $error object is fine, but of course identical for all elements.
The html is:
<form name="form" novalidate>
<p>
<testvalidation2 name="field1" form="form" field="testfield4" required="true">
</testvalidation2>
</p>
</form>
The directive looks like this:
app.directive('testvalidation2', function(){
return {
restrict: 'E',
scope: {
ngModel: '=',
newfield: '=field',
required: '=required',
form: '='
},
templateUrl: 'template2.html',
link: function(scope, element, attr){
scope.pattern = /\b(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/;
scope.name = attr.name;
}
} // return
});`
and finally the template:
<div>
<input name="{{name}}" type="text" ng-model="newfield" ng-required="required" ng-pattern="pattern"> {{FIELD}}</input>
<span ng-show="form.name.$error.required">Required</span>
<span ng-show="form.name.$error.pattern"> Invalid </span>
<p>Output {{form.name.$error | json}}</p>
</div>
I have created a plunker for my Angular Validation Problem
and would be happy, if someone would help me to win the fight.
Michael
I don't have a fix for this but I can tell you what the problem is.
Firstly in your html form="form" should have name of the form form="form2".
Secondly Since you are creating a new scope in the directive, the scope created is a isolated scope which does not inherit from parent, which means that the the template input control that you add would not get added to the parent scope form2.
The only way out currently i can think of is to not use isolated scope.