OK this is a contrived example, but...
Say I have a controller like this:
app.controller('TestCtrl', function() {
this.testString;
this.otherString;
});
And I have a template like this:
<div ng-controller='TestCtrl as test'>
<input demo type='text' ng-model='test.testString'>
{{test.otherString}}
</div>
And then I have a directive like this:
app.directive('demo', function() {
return {
require:'ngModel',
link: function(scope, elem, attrs, ctrl) {
scope.$watch(attrs.ngModel, function(newVal) {
/* How do I get otherString without knowing the controller alias?
This works but is not good practice */
scope.test.otherString = newVal + ' is cool!';
/* This doesn't work, but would if the property was in scope
instead of the controller */
scope[attrs.demo] = newVal + ' is cool!';
});
}
}
});
How do I get otherString without knowing the controller alias? I could just break apart attrs.ngModel to get it, but is there an 'angular' way to get the property?
EDIT
While this example didn't exactly reflect the issues I was having with my real scenario, I did find out how to get the controller's property in the link function, allowing me to update the model:
link: function(scope, elem, attrs, ctrl) {
var otherString = scope.$eval(attrs.demo);
scope.$watch(attrs.ngModel, function(newVal) {
otherString = newVal + ' is cool!';
}
}
A directive should have zero knowledge of anything outside of itself. If the directive depends on an outside controller having defined some arbitrary property, things will break very easily.
Defining a "scope" property on the directive lets you expose an explicit API for binding data to the directive.
myModule.directive('demo', function() {
return {
scope: {
demoString: '=demo',
},
link: function(scope, element, attrs) {
// You can access demoString here, or in a directive controller.
console.log(scope.demoString);
}
};
});
And the template
<div ng-controller='TestCtrl as test'>
<input demo="test.otherString" ng-model='test.testString'>
{{test.otherString}}
</div>
This isn't the only way to facilitate passing data or setting up bindings to a directive, but it is the most common way and should cover the majority of use-cases.
If you are trying to be more angular-like I would just use the $scope in the controller and pass that to the directive like so:
app.directive('demo', function() {
return {
scope: {strings: '='},
link: function(scope, elem, attrs, ctrl) {
scope.$watch('strings.test', function(newVal) {
/* How do I get otherString without knowing the controller alias? */
scope.strings.other = newVal + ' is cool!';
});
}
}
});
then in the html:
<div ng-controller='TestCtrl as test'>
<input demo type='text' strings="strings" ng-model="strings.test" />
{{strings.other}}
</div>
In the controller you would assign:
$scope.strings = {
test: '',
other: ''
}
Related
I've used a directive to utilize jqueryUI dialogs.
app.directive('popUp', function() {
return {
restrict: 'E',
scope: {
myId: '#',
onCancel: '&'
},
template:
'<div id="{{myId}}">
<button ng-click="onCancel()">...</button>
...
</div>'
link: function(scope, element, attrs) {
scope.closeDialog = function() {
$("#" + id).dialog('close');
}
// question 1: how to reference id of this directive (self)?
// question 2: should it be here, in compile, or in directive controller?
// question 3: 'ng-click=closeDialog()' missing when popup element inspected in firebug/dev tool
// question 4: is there a way to avoid jquery like selector $("#" + id) to reference this element?
}
};
});
And this is the html:
<pop-up my-id="success" on-cancel="closeDialog()"> ... </pop-up>
If I declare an external controller and closeDialog function attached to its $scope, this works fine, like this:
app.controller('DialogCtrl', function($scope) {
$scope.closeDialog = function(id) {
$("#" + id).dialog('close');
};
});
html
<div ng-controller="DialogCtrl">
<pop-up my-id="success" on-cancel="closeDialog('success')"> ... </pop-up>
</div>
But what I want to avoid is redundancy of the id. So I want the directive to have its own close function. If you also have answers on the other questions above, it is very much appreciated. Thanks.
This is essentially what you want. There's no need to use $ selectors and ids to find your dialog since element in the link function will give you a reference to the element that the directive is applied to.
Define the closeDialog function on the directive's scope and you can reference it from the directive's template. Each instance of the directive will have its own close function.
app.directive('popUp', function() {
return {
restrict: 'E',
scope: {
myId: '#'
},
template:
'<div id="{{myId}}">
<button ng-click="closeDialog()">...</button>
...
</div>'
link: function(scope, element, attrs) {
// initialise dialog
element.dialog();
// the template's ng-click will call this
scope.closeDialog = function() {
element.dialog('close');
};
}
};
});
No need for an on-cancel attribute now.
<pop-up my-id="success"></pop-up>
Say I have a directive like such:
<my-directive>This is my entry!</my-directive>
How can I bind the content of the element into my directive's scope?
myApp.directive('myDirective', function () {
return {
scope : {
entry : "" //what goes here to bind "This is my entry" to scope.entry?
},
restrict: "E",
template: "<textarea>{{entry}}</textarea>"
link: function (scope, elm, attr) {
}
};
});
I think there's much simpler solution to the ones already given. As far as I understand, you want to bind contents of an element to scope during initialization of directive.
Given this html:
<textarea bind-content ng-model="entry">This is my entry!</textarea>
Define bind-content as follows:
directive('bindContent', function() {
return {
require: 'ngModel',
link: function ($scope, $element, $attrs, ngModelCtrl) {
ngModelCtrl.$setViewValue($element.text());
}
}
})
Here's a demo.
I may have found a solution. It relies on the transclude function of directives. It works, but I need to better understand transclusion before being sure this is the right way.
myApp.directive('myDirective', function() {
return {
scope: {
},
restrict: 'E',
replace: false,
template: '<form>' +
'<textarea ng-model="entry"></textarea>' +
'<button ng-click="submit()">Submit</button>' +
'</form>',
transclude : true,
compile : function(element,attr,transclude){
return function (scope, iElement, iAttrs) {
transclude(scope, function(originalElement){
scope.entry = originalElement.text(); // this is where you have reference to the original element.
});
scope.submit = function(){
console.log('update entry');
}
}
}
};
});
You will want to add a template config to your directive.
myApp.directive('myDirective', function () {
return {
scope : {
entry : "=" //what goes here to bind "This is my entry" to scope.entry?
},
template: "<div>{{ entry }}</div>", //**YOU DIDN'T HAVE A TEMPLATE**
restrict: "E",
link: function (scope, elm, attr) {
//You don't need to do anything here yet
}
};
});
myApp.controller('fooController', function($scope){
$scope.foo = "BLAH BLAH BLAH!";
});
And then use your directive like this:
<div ng-controller="fooController">
<!-- sets directive "entry" to "foo" from parent scope-->
<my-directive entry="foo"></my-directive>
</div>
And angular will turn that into:
<div>THIS IS MY ENTRY</div>
Assuming that you have angular setup correctly and are including this JS file onto your page.
EDIT
It sounds like you want to do something like the following:
<my-directive="foo"></my-directive>
This isn't possible with ELEMENT directives. It is, however, with attribute directives. Check the following.
myApp.directive('myDirective', function () {
return {
template: "<div>{{ entry }}</div>", //**YOU DIDN'T HAVE A TEMPLATE**
restrict: "A",
scope : {
entry : "=myDirective" //what goes here to bind "This is my entry" to scope.entry?
},
link: function (scope, elm, attr) {
//You don't need to do anything here yet
}
};
});
Then use it like this:
<div my-directive="foo"></div>
This will alias the value passed to my-directive onto a scope variable called entry. Unfortunately, there is no way to do this with an element-restricted directive. What is preventing it from happening isn't Angular, it is the html5 guidelines that make what you are wanting to do impossible. You will have to use an attribute directive instead of an element directive.
I have the following markup:
<div class="controller" ng-controller="mainController">
<input type="text" ng-model="value">
<div class="matches"
positions="{{client.positions | filter:value}}"
select="selectPosition(pos)">
<div class="match"
ng-repeat="match in matches"
ng-click="select({pos: match})"
ng-bind="match.name">
Then, inside my matches directive I have
app.directive('matches', function()
{
return {
scope: {
select: '&'
},
link: function(scope, element, attrs)
{
scope.matches = [];
attrs.$observe('positions', function(value)
{
scope.matches = angular.fromJson(value);
scope.$apply();
})
}
}
}
When I do this, I can console log scope.matches, and it does change with the value from my input. However, the last div .match doesn't render anything! If I remove scope: {...} and replace it with scope: true, then it does render the result, but I want to use the & evaluation to execute a function within my main controller.
What do i do?
Use scope.$watch instead, you can watch the attribute select whenever changes are made from that attribute.
app.directive('matches', function()
{
return {
scope: true,
link: function(scope, element, attrs)
{
scope.matches = [];
scope.$watch(attrs.select, function(value) {
scope.matches = angular.fromJson(value);
});
}
}
}
UPDATE: Likewise, if you define select itself as a scope attribute, you must use the = notation(use the & notation only, if you intend to use it as a callback in a template defined in the directive), and use scope.$watch(), not attr.$observe(). Since attr.$observe() is only used for interpolation changes {{}}, while $watch is used for the changes of the scope property itself.
app.directive('matches', function()
{
return {
scope: {select: '='},
link: function(scope, element, attrs)
{
scope.matches = [];
scope.$watch('select', function(value) {
scope.matches = angular.fromJson(value);
});
}
}
}
The AngularJS Documentation states:
$observe(key, fn);
Observes an interpolated attribute.
The observer function will be invoked once during the next $digest
following compilation. The observer is then invoked whenever the
interpolated value changes.
Not scope properties defined as such in your problem which is defining scope in the directive definition.
If you don't need the isolated scope, you could use $parse instead of the & evaluatation like this:
var selectFn = $parse(attrs.select);
scope.select = function (obj) {
selectFn(scope, obj);
};
Example Plunker: http://plnkr.co/edit/QZy6TQChAw5fEXYtw8wt?p=preview
But if you prefer the isolated scope, you have to transclude children elements and correctly assign the scope of your directive like this:
app.directive('matches', function($parse) {
return {
restrict: 'C',
scope: {
select: '&',
},
transclude: true,
link: function(scope, element, attrs, ctrl, transcludeFn) {
transcludeFn(scope, function (clone) {
element.append(clone);
});
scope.matches = [];
attrs.$observe('positions', function(value) {
scope.matches = angular.fromJson(value);
});
}
}
});
Example Plunker: http://plnkr.co/edit/9SPhTG08uUd440nBxGju?p=preview
Suppose I have something like this:
<div my-custom-root="root">
<input type="text" my-custom-Path="path.to.somewhere" />
</div>
And now I want to translate this to something which essentially behaves equivilant to:
<input type="text" ng-model="root.path.to.somewhere" />
I get so far as to specify the two directives, get all the objects etc. These supply me with the root object and the path, but how do create a binding from those? I am missing the generation of the appropriate ng-model directive or the use of the NgModelController directly.
I created a plunkr here with the stuff I managed to put together so far.
For easier reference here is my code of the directives just like they can be found in my plunkr as well:
app.directive('myCustomRoot', function() {
var container;
return {
controller: function($scope) {
this.container = function() {
return container;
};
},
compile: function() {
return function ($scope, elm, attrs) {
$scope.$watch(attrs.myCustomRoot, function(newVal) {
container = newVal;
});
};
}
};
});
app.directive('myCustomPath', function($parse) {
return {
require: '^myCustomRoot',
link: function(scope, element, attrs, containerController) {
var getter = $parse(attrs.myCustomPath);
scope.$watch(function() {
return containerController.container();
},
function(newVal) {
if (newVal) {
alert('The value to be edited is: ' + getter(newVal) + '\nIt is the property "' + attrs.myCustomPath + '"\non Object: ' + JSON.stringify(newVal) + '\n\nNow how do I bind this value to the input box just like ng-model?');
}
}
);
}
};
});
You can see that I have all the things available in my alert-Box, but I don't know how to do the binding.
I hoped that there was some way to write someBindingService.bindInput(myInput, myGetter, mySetter), but I have done quite a lot of source code reading and unfortunately the binding is coupled closely to the compilation.
However thanks to the question "Add directives from directive in AngularJS" I managed to figure out a way which is less elegant but it is compact and effective:
app.directive('myCustomPath', function($compile, $parse) {
return {
priority: 1000,
terminal: true,
link: function(scope, element, attrs, containerController) {
var containerPath = element.closest('[my-custom-root]').attr('my-custom-root');
attrs.$set('ngModel', containerPath + '.' + attrs['myCustomPath']);
element.removeAttr('my-custom-path');
$compile(element)(scope);
}
}
});
This uses a little bit of jQuery, but it should not be to hard to do it with plain jQLite as well.
I'm trying to create a directive that contains an inputfield with a ng-model and knows if the inputcontrol is valid. (I want to change a class on a label within the directive based on this state.) I want to use the ngModelController.$valid to check this, but it always returns true.
formcontroller.$valid or formcontroller.inputfieldname.$valid do work as exprected, but since im trying to build a reusable component using a formcontroller is not very handy because then i have to determine what field of the form corresponds with the current directive.
I dont understand why one works and one doesnt, because in de angular source it seems to be the same code that should manage these states: The ngModelController.$setValidity function.
I created a test directive that contains a numeric field with required and a min value. As you can see in the fiddle below, the model controller is only triggered during page load and after that never changes.
jsfiddle with example directive
Directive code:
angular.module('ui.directives', []).directive('textboxValid',
function() {
return {
restrict: 'E',
require: ['ngModel', '^form'],
scope: {
ngModel: '='
},
template: '<input type="number" required name="somefield" min="3" ng-model="ngModel" /> '+
'<br>Form controller $valid: {{formfieldvalid}} <br> ' +
'Model controller $valid: {{modelvalid}} <br>'+
'Form controller $valid: {{formvalid}} <br>',
link: function (scope, element, attrs, controllers) {
var ngModelCtrl = controllers[0];
var formCtrl = controllers[1];
function modelvalid(){
return ngModelCtrl.$valid;
}
function formvalid(){
return formCtrl.$valid;
}
scope.$watch(formvalid, function(newVal,oldVal)
{
scope.modelvalid = ngModelCtrl.$valid;
scope.formvalid = formCtrl.$valid;
scope.formfieldvalid = formCtrl.somefield.$valid;
});
scope.$watch(modelvalid, function(newVal,oldVal)
{
scope.modelvalid = ngModelCtrl.$valid;
scope.formvalid = formCtrl.$valid;
scope.formfieldvalid = formCtrl.somefield.$valid;
//This one only gets triggered on pageload
alert('modelvalid ' + newVal );
});
}
};
}
);
Can someone help me understand this behaviour?
I think because you're watching a function and the $watch is only execute when this function is called !!
Watch the model instead like that :
scope.$watch('ngModel', function(newVal,oldVal)
{
scope.modelvalid = ngModelCtrl.$valid;
scope.formvalid = formCtrl.$valid;
scope.formfieldvalid = formCtrl.somefield.$valid;
//This one triggered each time the model changes
alert('modelvalid ' + ngModelCtrl.$valid );
});
I figured it out..
The textboxValid directive has a ng-model directive, and so does the input that gets created by the directive template. However, these are two different directives, both with their own seperate controller.
So, i changed my solution to use an attribute directive like below. This works as expected.
.directive('attributetest',
function() {
return {
restrict: 'A',
require: 'ngModel',
scope: {
ngModel: '='
},
link: function (scope, element, attrs, ngModelCtrl) {
function modelvalid(){
return ngModelCtrl.$valid;
}
scope.$watch(modelvalid, function(newVal,oldVal){
console.log('scope.modelvalid = ' + ngModelCtrl.$valid );
});
}
};
});