I'm making a directive for a States Select in angular. It's working, but I spent a while trying to figure out a way to compile the template before it's in the DOM. It currently works like this:
app.register.directive('stateDropdown', ['StatesFactory', '$compile', function (StatesFactory, $compile) {
function getTemplate(model) {
var html = '<select ng-model="' + model + '" ng-options="state.abbreviation as state.name for state in states" class="form-control"></select>';
return html;
}
function link (scope, element, attrs) {
scope.states = StatesFactory.States;
element.html(getTemplate(attrs.stateModel));
$compile(element.contents())(scope);
}
return {
replace: true,
link: link
}
}]);
But as such it inserts the template into the element THEN compiles it against scope. Is there a better way to do this? Such as compiling the template before it's even inserted?
Scratch what I had before.
[Edit 2]
Using a dynamic model is a bit problematic trying to fit it into the normal Angular workflow.
Instead you will need to compile the template in the directive by hand but add the ng-model before doing so, You will also need to manage the replacement of the existing element with the built template.
module.directive('stateDropdown', function (StatesFactory, $compile) {
var template = '<select ng-options="state.abbreviation as state.name for state in states" class="form-control"></select>';
return {
scope: true,
controller: function($scope) {
$scope.states = StatesFactory.states;
},
compile: function($element, $attrs) {
var templateElem = angular.element(template).attr('ng-model', '$parent.' + $attrs.stateModel);
$element.after(templateElem);
$element.remove();
var subLink = $compile(templateElem);
return {
pre: function(scope, element, attrs) {
subLink(scope);
},
post: function(scope, element, attrs) {
}
}
}
};
});
A working example of this can be found here: http://jsfiddle.net/u5uz2po7/2/
The example uses an isolated scope so that applying the 'states' to the scope does not affect existing scopes. That is also the reason for the '$parent.' in the ng-model.
Related
Is there a way to use javascript to apply the ng-model directive to a created element? In the code below, I want the new select element to be bound using ng-model to a scoped variable inside the controller:
angular.module('myApp').directive('dynamicHtml', function ($q, $http, $compile) {
return {
restrict: 'EAC',
scope: '=',
compile: function(element, attr) {
return function(scope, element, attr) {
var select = document.createElement('select');
// Here I want to use javascript to apply ng-model='controllerVar'
// to the new select element
element.append(select);
}
}
};
});
Whoops - my apologies...I forgot to compile the thing...carry on.
Its a simple directive:
app.directive('ngFoo', function($parse){
var controller = ['$scope', function ngNestCtrl($scope) {
$scope.getCanShow = function() {
console.log('show');
return true;
};
}];
var fnPostLink = function(scope, element, attrs) {
console.log('postlink');
};
var fnPreLink = function(scope, element, attrs) {
console.log('prelink');
};
var api = {
template: '<div ng-if="getCanShow()">foo</div>',
link: {
post: fnPostLink,
pre: fnPreLink
},
restrict: 'E',
controller:controller
};
return api;
});
My goal was to find when "show" gets output to console. At this moment I figure it happens after linking (pre & post).
This makes sense. Since the template is rendered after those phases. (Correct me if I am wrong).
But then again, why would the template be rendered twice?
http://plnkr.co/edit/JNhON2lY9El00dzdL39J?p=preview
Angular has multiple digest cycles and you're seeing two of them. This is totally normal and perfectly ok.
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.
If I have two directives, and need to have one of them use the controller of the other one, what is the best way to accomplish something like that in angular? I'm trying to use the require: otherDirective stuff I see in the docs, but I'm getting an
Error: No controller: dirOne
from the following example. I was under the impression that by specifying dirOne in the require statement, dirTwo would have access to the controller of dirOne. What am I doing wrong here?
Here's a plunkr as well
var app = angular.module('myApp', []);
app.directive('dirOne', function() {
return {
controller: function($scope) {
$scope.options = {
"opt1" : true,
"opt2" : false,
"opt3" : false,
}
this.getOptions = function() {
return $scope.options;
};
}
};
});
app.directive('dirTwo',function() {
return {
require: 'dirOne',
link: function(scope, element, attrs, dirOneCtrl) {
$scope.options = dirOneCtrl.getOptions();
alert($scope.options);
}
};
});
http://plnkr.co/edit/vq7vvz
There were two problems with your plunkr:
In order for a directive to require the controller of another directive, the two directives have to be on the same element, or if you use the ^ notation, the required directive can be on a parent element.
<div dir-one dir-two></div>
Also, in the second directive you called your parameter "scope" but then tried to use it as "$scope".
link: function(scope, element, attrs, dirOneCtrl) {
scope.options = dirOneCtrl.getOptions();
alert(scope.options);
}
I have a very boiled down version of what I am doing that gets the problem across.
I have a simple directive. Whenever you click an element, it adds another one. However, it needs to be compiled first in order to render it correctly.
My research led me to $compile. But all the examples use a complicated structure that I don't really know how to apply here.
Fiddles are here: http://jsfiddle.net/paulocoelho/fBjbP/1/
And the JS is here:
var module = angular.module('testApp', [])
.directive('test', function () {
return {
restrict: 'E',
template: '<p>{{text}}</p>',
scope: {
text: '#text'
},
link:function(scope,element){
$( element ).click(function(){
// TODO: This does not do what it's supposed to :(
$(this).parent().append("<test text='n'></test>");
});
}
};
});
Solution by Josh David Miller:
http://jsfiddle.net/paulocoelho/fBjbP/2/
You have a lot of pointless jQuery in there, but the $compile service is actually super simple in this case:
.directive( 'test', function ( $compile ) {
return {
restrict: 'E',
scope: { text: '#' },
template: '<p ng-click="add()">{{text}}</p>',
controller: function ( $scope, $element ) {
$scope.add = function () {
var el = $compile( "<test text='n'></test>" )( $scope );
$element.parent().append( el );
};
}
};
});
You'll notice I refactored your directive too in order to follow some best practices. Let me know if you have questions about any of those.
In addition to perfect Riceball LEE's example of adding a new element-directive
newElement = $compile("<div my-directive='n'></div>")($scope)
$element.parent().append(newElement)
Adding a new attribute-directive to existed element could be done using this way:
Let's say you wish to add on-the-fly my-directive to the span element.
template: '<div>Hello <span>World</span></div>'
link: ($scope, $element, $attrs) ->
span = $element.find('span').clone()
span.attr('my-directive', 'my-directive')
span = $compile(span)($scope)
$element.find('span').replaceWith span
Hope that helps.
Dynamically adding directives on angularjs has two styles:
Add an angularjs directive into another directive
inserting a new element(directive)
inserting a new attribute(directive) to element
inserting a new element(directive)
it's simple. And u can use in "link" or "compile".
var newElement = $compile( "<div my-diretive='n'></div>" )( $scope );
$element.parent().append( newElement );
inserting a new attribute to element
It's hard, and make me headache within two days.
Using "$compile" will raise critical recursive error!! Maybe it should ignore the current directive when re-compiling element.
$element.$set("myDirective", "expression");
var newElement = $compile( $element )( $scope ); // critical recursive error.
var newElement = angular.copy(element); // the same error too.
$element.replaceWith( newElement );
So, I have to find a way to call the directive "link" function. It's very hard to get the useful methods which are hidden deeply inside closures.
compile: (tElement, tAttrs, transclude) ->
links = []
myDirectiveLink = $injector.get('myDirective'+'Directive')[0] #this is the way
links.push myDirectiveLink
myAnotherDirectiveLink = ($scope, $element, attrs) ->
#....
links.push myAnotherDirectiveLink
return (scope, elm, attrs, ctrl) ->
for link in links
link(scope, elm, attrs, ctrl)
Now, It's work well.
function addAttr(scope, el, attrName, attrValue) {
el.replaceWith($compile(el.clone().attr(attrName, attrValue))(scope));
}
The accepted answer by Josh David Miller works great if you are trying to dynamically add a directive that uses an inline template. However if your directive takes advantage of templateUrl his answer will not work. Here is what worked for me:
.directive('helperModal', [, "$compile", "$timeout", function ($compile, $timeout) {
return {
restrict: 'E',
replace: true,
scope: {},
templateUrl: "app/views/modal.html",
link: function (scope, element, attrs) {
scope.modalTitle = attrs.modaltitle;
scope.modalContentDirective = attrs.modalcontentdirective;
},
controller: function ($scope, $element, $attrs) {
if ($attrs.modalcontentdirective != undefined && $attrs.modalcontentdirective != '') {
var el = $compile($attrs.modalcontentdirective)($scope);
$timeout(function () {
$scope.$digest();
$element.find('.modal-body').append(el);
}, 0);
}
}
}
}]);
Josh David Miller is correct.
PCoelho, In case you're wondering what $compile does behind the scenes and how HTML output is generated from the directive, please take a look below
The $compile service compiles the fragment of HTML("< test text='n' >< / test >") that includes the directive("test" as an element) and produces a function. This function can then be executed with a scope to get the "HTML output from a directive".
var compileFunction = $compile("< test text='n' > < / test >");
var HtmlOutputFromDirective = compileFunction($scope);
More details with full code samples here:
http://www.learn-angularjs-apps-projects.com/AngularJs/dynamically-add-directives-in-angularjs
Inspired from many of the previous answers I have came up with the following "stroman" directive that will replace itself with any other directives.
app.directive('stroman', function($compile) {
return {
link: function(scope, el, attrName) {
var newElem = angular.element('<div></div>');
// Copying all of the attributes
for (let prop in attrName.$attr) {
newElem.attr(prop, attrName[prop]);
}
el.replaceWith($compile(newElem)(scope)); // Replacing
}
};
});
Important: Register the directives that you want to use with restrict: 'C'. Like this:
app.directive('my-directive', function() {
return {
restrict: 'C',
template: 'Hi there',
};
});
You can use like this:
<stroman class="my-directive other-class" randomProperty="8"></stroman>
To get this:
<div class="my-directive other-class" randomProperty="8">Hi there</div>
Protip. If you don't want to use directives based on classes then you can change '<div></div>' to something what you like. E.g. have a fixed attribute that contains the name of the desired directive instead of class.