I understand how to pass directives through my custom directive, like this:
Page.html
<my-directive read-only-attr="myVariable" label-style-attr="anotherVariable"></my-directive>
Directive
myApp.directive("myDirective", function () {
return {
restrict: "E",
templateUrl: "myTemplate.html",
scope: {
readOnlyScopeVar: "=readOnlyAttr",
styleScopeVar: "=labelStyleAttr"
},
link: function (scope, element, attrs) {
}
};
});
Template
<div>
<label ng-style="styleScopeVar" />
<input type="text" ng-readonly="readOnlyScopeVar" />
</div>
My template is much more complex than this but I simplified it for the question.
My question is: How do I prevent ngReadonly and ngStyle from having to run if the user hasn't specified a "read-only-attr" or "label-style-attr" on my directive? There are tons of common angular directives that I want to allow people to apply to the input and other elements inside my template (ngClass, ngDisabled, ngChange, ngPattern, ngIf, etc), but I don't want to run them all if the person hasn't specified them on my directive. It's as if I need a template to build the template.
Also, note that I've read about transclusion but I don't like the idea of allowing the user to edit the input element directly, and there are multiple elements I may want to apply things to like in this example I could change the label color if the read-only-attr reference is true.
One way to do it would be to use $compile. Here's a working plnkr:
https://plnkr.co/edit/S8pUSH?p=preview
Note there are many ways to do this, and this one is just one simple example for demonstration:
var app = angular.module('app', []); //define the module
//setup the template
app.run(['$templateCache', function($templateCache){
$templateCache.put('someDirectiveTmpl','<div>\
<label $$%%ngStylePlaceholder$$%% />My Label:</label>\
<input type="text" $$%%ngReadonlyPlaceholder$$%% />\
</div>');
}])
/**
* #description someDirective gets a config object that holds dynamic directives' names and values. e.g.:
* {
* 'ngStyle': '{background: red;}',
* 'ngReadonly': true
* }
*
*/
app.directive('someDirective', ['$log', '$compile', '$templateCache', function($log, $compile, $templateCache){
return {
restrict: 'AE',
controllerAs: 'someDirectiveCtrl',
scope: {},
bindToController: {
directiveConfig: '='
},
controller: function($scope, $element){
// a method to convert camelcase to dash
function camelCaseToDash( myStr ) {
return myStr.replace( /([a-z])([A-Z])/g, '$1-$2' ).toLowerCase();
}
// get the template
var template = $templateCache.get('someDirectiveTmpl');
var placeHolderRegExp = /\$\$%%(.*)\$\$%%/g;
// place the binding per existing property
angular.forEach(this.directiveConfig, function(varName, key){
template = template.replace('$$%%' + key + 'Placeholder$$%%', camelCaseToDash(key) + '="someDirectiveCtrl.directiveConfig.' + key + '"');
});
// remove unneeded properties placeholders
template.replace(placeHolderRegExp, '');
//compile the directive
var templateElement = angular.element(template);
$compile(templateElement)($scope);
// append to element
$element.append(templateElement);
}
}
}]);
Note the $$%%ngAnythingPlaceholder$$%%in the template. I get the config from parent directive (in plnkr I've used a controller for simplicity). I use a config object in the example (you could do this with separate variables, but I like setting a config object API).
I then replace the placeholders according to what I've got in the config and remove what I don't need. I then compile the template.
In the parent directive's controller you could do something like what I did in the controller:
$scope.config = {
ngReadonly: true
}
Again, I note you should not use a controller and of course, not to
use the $scope itself but the directive's controller's this. I use
$scope and controller here only for ease of demo.
You can add anything you want to this config (and of course, add placeholders to the various parameters in the template).
Now just add the directive to your template:
<some-directive directive-config="config"></some-directive>
Related
I have an Angular JS directive that looks something like this:
function ($sce) {
'use strict';
return {
restrict: 'E',
templateUrl: $sce.trustAsResourceUrl('...'),
scope: {
serviceName: '#?',
},
controller: 'MyController',
link: function () {}
};
}
The directive is instantiated like so:
<my-directive service="My Cool Service"></my-directive>
Recently, some consumers of this directive would like the ability to modify the service attribute after the directive has been instantiated and have the directive reflect the change. Here is an example of what a specific consumer is doing:
const directive = document.querySelector('my-directive');
directive.setAttribute('service', 'Another Service Name');
This makes sense; however, the directive does not reflect the change once they set the attribute. I am figuring out a way to accomplish this. I have tried using scope.$watch and $observe to no avail; example:
link: function (_, _, attrs) {
attrs.$observe("serviceName", newValue => updateServiceName(newValue));
}
Any insights on how to accomplish this? Thanks!
The serviceName property inside your directive's scope definition should be bound with = instead of #. Then, from the outer code, just pass in the service name with a variable.
$scope.myServiceName = "My Cool Service";
<my-directive service="{{myServiceName}}"></my-directive>
Now if you change the myServiceName value, the change will be reflected in the child directive.
I have a directive that I use for date-time input and I'd like to use it in the template for another directive.
The problem is that if I use the parent directive anywhere and I forget to include the child directive in the page, it fails silently.
Is there a way to make a directive require another directive?
I tried listing it in the require attribute, but then it fails even when DateTimeInput is included.
angular
.module('main')
.directive("newTransferPane", ["TransferService", function(TransferService){
'use strict';
return {
restrict: "A",
templateUrl: "Templates/NewTransferPane.html",
scope: true,
require: "DateTimeInput",
link: function ($scope){
$scope.secondTransfer = false;
$scope.transfer_date = new Date();
$scope.$watch("secondTransfer", function(){
TransferService.secondTransfer = $scope.secondTransfer;
});
}
};
}]);
I'm having a go at a directive which will dynamically load a template based on a scope value passed into it.
I am using ng-repeat on my directive, and am using the iterated item as an attribute property:
<my-form-field ng-repeat="field in customFields" field="field">
In my directive I have the following code to set the template being used.
(function() {
'use strict';
angular
.module('app')
.directive('myFormField', myFormField);
function myFormField() {
var directive = {
restrict: 'E',
scope: {
field: '='
},
link: function(scope){
scope.getContentUrl = function() {
return 'app/modules/form_components/form_field/' + scope.field.type + '-template.html';
}
},
template: '<div ng-include="getContentUrl()"></div>'
};
return directive;
}
})();
Whilst the above works (which I found from other posts), I wonder if there is a better way.
For example I have seen examples of calling a function on the templateUrl config option instead, and then in that function access the scope attributes being passed in. When I tried this way, my field attribute was a literal 'field' string value (they are objects in my customFields array), so I think at that point the scope variables had not yet been evaluated.
With this current solution I am using, all of my templates get wrapped in an extra div since I am using ng-include, so I am just trying to make the rendered markup more succinct.
Any suggestions\advice is appreciated.
Thanks
I have something like this:
.controller('contr',['$scope', '$http',function($scope, $http){
$http.post(...).success(function(){
$scope.myTextVar = "some text here";
$scope.completed == true;
});
}]);
an HTML snippet like so:
<div class="myClass" ng-if="completed == true" manipulate-header>
<p>{{myTextVar}}</p>
</div>
and the directive, for simplicity sake let's say it looks like this:
.directive('manipulateHeader',function(){
return{
restrict: 'A',
link: function(scope, elem){
console.log(angular.element(elem).find('p'));
}
}
});
The manipulate-header directive is supposed to do some manipulation of the text inside the <p></p> tag, however, it runs before {{myTextVar}} gets replaced and hence it outputs {{myTextVar}} instead of some text here.
How may i get around this problem? (i can pass the variable inside the directive scope, but i'm thinking there must be another way).
EDIT: the controller is there and working as intended. Issue is not related to it. I didn't include it to shorten the post.
If it MUST be a directive
If you're trying to do string manipulation in your link function, you're going to have a bad time. The link function is executed before the directive is compiled (that's the idea of the link function), so any bindings (ng-bind or otherwise) will not have been compiled inside of link functions.
To execute code after the compilation stage, you should use a controller. However, you cannot access the DOM in controllers (or rather, you shouldn't). So the logical solution is to instead modify the scope argument instead. I propose something like this:
angular.directive('manipulateHeader', function() {
return {
scope: {
myTextVar: '='
},
controller: function($scope, myFilter) {
// you can't use bindToController here because bindToController executes *after*
// this function
this.modifiedText = myFilter($scope.myTextVar);
},
controllerAs: 'ctrl',
// display the modified text in a template
template: '<span ng-bind="ctrl.modifiedText"></span>'
};
})
.filter('myFilter', function() {
return function(inputText) {
// do some text manipulation here
};
});
Usage:
<manipulate-header myTextVar='myTextVar'></manipulate-header>
Or:
<p>{{ myTextVar | myFilter }}</p>
You could, of course, make this an attribute instead, but best practice indicates that directives that have a template should be an element instead.
The above is only if you need this to be a directive. Otherwise, it should almost definitely be a filter.
If you need to change the $scope variable from your controller you need to isolate scope ,
scope:{
myattr='#', // this will provide one way communication , you can define in your template as <p myattr="hello"><p>
message:'&', //This allows you to invoke or evaluate an expression on the parent scope of whatever the directive is inside
message:'=' // sets up a two-way binding expression between the directive's isolate scope and the parent scope.
}
refer https://docs.angularjs.org/guide/directive
As suggested by #DanPantry - you most likely want a filter not a directive
Read this guide about using filters
https://docs.angularjs.org/guide/filter
Here is an example of such a filter (from documentation)
angular.module('myStatefulFilterApp', [])
.filter('decorate', ['decoration', function(decoration) {
function decorateFilter(input) {
//This is the actual modification of text
//That's what you are looking for
return decoration.symbol + input + decoration.symbol;
}
decorateFilter.$stateful = true;
return decorateFilter;
}])
.controller('MyController', ['$scope', 'decoration', function($scope, decoration) {
$scope.greeting = 'hello';
$scope.decoration = decoration;
}])
.value('decoration', {symbol: '*'});
I am not sure whether you defined $scope.myTextVar
in correct scope. Like, if you defined it in any controller, then directive should be under the controller scope.
Here is the updated HTML
<div ng-controller ="MainController">
<div class="myClass" manipulate-header>
<p>{{myTextVar}}</p>
</div>
</div>
JS :
app.controller('MainController', ['$scope', function($scope) {
$scope.myTextVar = "some text here";
}]);
app.directive('manipulateHerader',function(){
return{
restrict: 'A',
link: function(scope, elem){
console.log(angular.element(elem).find('p'));
}
}
});
Here is the plunker
Im wondering if there is a way to pass an argument to a directive?
What I want to do is append a directive from the controller like this:
$scope.title = "title";
$scope.title2 = "title2";
angular.element(document.getElementById('wrapper')).append('<directive_name></directive_name>');
Is it possible to pass an argument at the same time so the content of my directive template could be linked to one scope or another?
here is the directive:
app.directive("directive_name", function(){
return {
restrict:'E',
transclude:true,
template:'<div class="title"><h2>{{title}}</h3></div>',
replace:true
};
})
What if I want to use the same directive but with $scope.title2?
You can pass arguments to your custom directive as you do with the builtin Angular-directives - by specifying an attribute on the directive-element:
angular.element(document.getElementById('wrapper'))
.append('<directive-name title="title2"></directive-name>');
What you need to do is define the scope (including the argument(s)/parameter(s)) in the factory function of your directive. In below example the directive takes a title-parameter. You can then use it, for example in the template, using the regular Angular-way: {{title}}
app.directive('directiveName', function(){
return {
restrict:'E',
scope: {
title: '#'
},
template:'<div class="title"><h2>{{title}}</h2></div>'
};
});
Depending on how/what you want to bind, you have different options:
= is two-way binding
# simply reads the value (one-way binding)
& is used to bind functions
In some cases you may want use an "external" name which differs from the "internal" name. With external I mean the attribute name on the directive-element and with internal I mean the name of the variable which is used within the directive's scope.
For example if we look at above directive, you might not want to specify another, additional attribute for the title, even though you internally want to work with a title-property. Instead you want to use your directive as follows:
<directive-name="title2"></directive-name>
This can be achieved by specifying a name behind the above mentioned option in the scope definition:
scope: {
title: '#directiveName'
}
Please also note following things:
The HTML5-specification says that custom attributes (this is basically what is all over the place in Angular applications) should be prefixed with data-. Angular supports this by stripping the data--prefix from any attributes. So in above example you could specify the attribute on the element (data-title="title2") and internally everything would be the same.
Attributes on elements are always in the form of <div data-my-attribute="..." /> while in code (e.g. properties on scope object) they are in the form of myAttribute. I lost lots of time before I realized this.
For another approach to exchanging/sharing data between different Angular components (controllers, directives), you might want to have a look at services or directive controllers.
You can find more information on the Angular homepage (directives)
Here is how I solved my problem:
Directive
app.directive("directive_name", function(){
return {
restrict: 'E',
transclude: true,
template: function(elem, attr){
return '<div><h2>{{'+attr.scope+'}}</h2></div>';
},
replace: true
};
})
Controller
$scope.building = function(data){
var chart = angular.element(document.createElement('directive_name'));
chart.attr('scope', data);
$compile(chart)($scope);
angular.element(document.getElementById('wrapper')).append(chart);
}
I now can use different scopes through the same directive and append them dynamically.
You can try like below:
app.directive("directive_name", function(){
return {
restrict:'E',
transclude:true,
template:'<div class="title"><h2>{{title}}</h3></div>',
scope:{
accept:"="
},
replace:true
};
})
it sets up a two-way binding between the value of the 'accept' attribute and the parent scope.
And also you can set two way data binding with property: '='
For example, if you want both key and value bound to the local scope you would do:
scope:{
key:'=',
value:'='
},
For more info,
https://docs.angularjs.org/guide/directive
So, if you want to pass an argument from controller to directive, then refer this below fiddle
http://jsfiddle.net/jaimem/y85Ft/7/
Hope it helps..
Controller code
myApp.controller('mainController', ['$scope', '$log', function($scope, $log) {
$scope.person = {
name:"sangeetha PH",
address:"first Block"
}
}]);
Directive Code
myApp.directive('searchResult',function(){
return{
restrict:'AECM',
templateUrl:'directives/search.html',
replace: true,
scope:{
personName:"#",
personAddress:"#"
}
}
});
USAGE
File :directives/search.html
content:
<h1>{{personName}} </h1>
<h2>{{personAddress}}</h2>
the File where we use directive
<search-result person-name="{{person.name}}" person-address="{{person.address}}"></search-result>
<button my-directive="push">Push to Go</button>
app.directive("myDirective", function() {
return {
restrict : "A",
link: function(scope, elm, attrs) {
elm.bind('click', function(event) {
alert("You pressed button: " + event.target.getAttribute('my-directive'));
});
}
};
});
here is what I did
I'm using directive as html attribute and I passed parameter as following in my HTML file. my-directive="push" And from the directive I retrieved it from the Mouse-click event object. event.target.getAttribute('my-directive').
Insert the var msg in the click event with scope.$apply to make the changes to the confirm, based on your controller changes to the variables shown in ng-confirm-click therein.
<button type="button" class="btn" ng-confirm-click="You are about to send {{quantity}} of {{thing}} selected? Confirm with OK" confirmed-click="youraction(id)" aria-describedby="passwordHelpBlock">Send</button>
app.directive('ngConfirmClick', [
function() {
return {
link: function(scope, element, attr) {
var clickAction = attr.confirmedClick;
element.on('click', function(event) {
var msg = attr.ngConfirmClick || "Are you sure? Click OK to confirm.";
if (window.confirm(msg)) {
scope.$apply(clickAction)
}
});
}
};
}
])