AngularJS: multiple directives with transclusion on same element - javascript

I'm trying to inject 2 templates into an element and operate on them:
<div
ic-first="foo"
ic-second="bar"
ic-third="baz"
ic-fourth="qux"
>
</div>
icFirst should inject via a template an empty div as a child of its element. icSecond should inject a second div (with a bunch of content) as the second child of its element, so the resulting html would look like:
<div
ic-first="foo" // priority: 100
ic-second="bar" // priority: 50
ic-third="baz" // priority: 0
ic-fourth="qux" // priority: 0
>
<div id="foo"></div>
<div> <!-- a bunch of stuff from the templateUrl --> </div>
</div>
Both icFirst and icSecond will inject other elements into the newly created containers.
When I specify a directive template property on both directives, I get an error:
Error: Multiple directives [icFirst, icSecond] asking for template on: <div ic-first…
When I add transclude: true to both directives, icFirst executes just fine…but then the other directives on the same element are not executed. When I set transclude: 'element', the other directives execute but I get an error that the first child ($scope.firstObj) is undefined.
All four directives need access to each other's scope, so I'm doing most of my work in their controllers:
app.directive('icFirst', ['ic.config', function (icConfig) {
return {
restrict: 'A',
priority: 100,
template: '<div id="{{firstId}}"></div>',
replace: false,
transclude: 'element',
controller: function icFirst($scope, $element, $attrs) {
// …
$scope.firstId = $scope.opts.fooId;
$scope.firstElm = $element.children()[0];
$scope.firstObj = {}; // this is used by the other 3 directives
},
link: function(scope, elm, attrs) { … } // <- event binding
}
);
app.directive('icSecond', ['ic.config', function (icConfig) {
return {
restrict: 'A',
priority: 0,
templateUrl: 'views/foo.html',
replace: false,
transclude: 'element',
controller: function icSecond($scope, $element, $attrs) {
// …
$scope.secondElm = $element.children()[1];
$scope.secondObj = new Bar( $scope.firstObj );
// ^ is used by the remaining 2 directives & requires obj from icFirst
},
link: function(scope, elm, attrs) { … } // <- event binding
}
);
Note I have corrected the behaviour of replace: false to match the documented behaviour, as described in pull request #2433.
I tried instantiating $scope.firstObj in the controller, and setting it in the linkFn (hoping the transclusion would have completed by the time the linkFn executes), but I get the same problem. It appears first-child is actually a comment.

The only reason I can come up with that explains throwing this error is that the AngularJS team was trying to avoid needless overwrites/DOM manipulation:
Considering the actual behaviour of replace: false vs the documented behaviour, I think the actual is in fact the intended behaviour. If this is true, then allowing multiple templates/templateUrls to be used on the same element will cause subsequent templates to overwrite previous ones.
Since I already modified the source to match the documented behaviour, as a quick fix†, I modified the source again (/src/ng/compile.js:700) to remove the assertNoDuplicate check (which corresponds to angular.js:4624). Now I return the following 2 objects, and it works, and I can't find any negative repercussions:
// directive icFirst
return {
restrict: 'A',
priority: 100,
replace: false,
template: '<div id="{{firstId}}"></div>',
require: ["icFirst"],
controller: Controller,
link: postLink
};
// directive icSecond
return {
restrict: 'A',
require: ['icFirst'],
replace: false,
templateUrl: 'views/bar.html',
priority: 50,
controller: Controller,
link: postLink
};
† If made permanent, the check should probably be
if (directive.templateUrl && directive.replace)
(and similar for directive.template)

Related

AngularJs: Iterating over array inside directive in order to create nested set of directives

Inside angularJS directive I'm trying to iterate over array and based on values I would like to create nested list of directives.
Current version of directive
Directive type
.directive("type", function($compile, $log){
return{
restrict: "E",
replace: true,
transclude: true,
scope: {
type: '='
},
template: "<div></div>",
link: function(scope, element, attrs){
if (angular.isArray(scope.type)){
angular.forEach(scope.type, function(value, index){
$log.error(value);
element.append("<type type='scope.type['"+index+"]'></type>");
});
} else if (angular.isObject(scope.type)){
element.append("OBJECT")
} else {
element.append("<div>{{scope.type}}</div>")
}
$compile(element.contents())(scope)
}
};
})
I also tried to use above directive with next version of link function:
if (angular.isArray(scope.type)) {
element.append("<div ng-repeat='element in scope.type'><type type='element'></type></div>");
} else if (angular.isObject(scope.type)) {
element.append("OBJECT")
} else {
element.append("<div>{{scope.type}}</div>")
}
$compile(element.contents())(scope)
}
None of provided codes solve my issue.
Below you will find example explaining on specific case:
Let's say that I had next object in the scope.type = [null,"int"]. Now I would like to use <type type='type'><type> and as a result of first evaluation I want to have sth like:
<type type='type[0]'></type><type type='type[1]'></type>
Further evaluation of those values should lead to some simpler form but right now it is not important.
How I can achieve sth like this?
Edit
I tried even to exctract part of the code responsible for iteration to the seperate directive but it still does not work. Code:
Update link function in type directive:
link: function(scope, element, attrs) {
if (angular.isArray(scope.type)) {
element.append("<typeb type='scope.type'></typeb>")
} else if (angular.isObject(scope.type)) {
element.append("OBJECT")
} else {
element.append("<div>{{scope.type}}</div>")
}
$compile(element.contents())(scope)
}
New directive:
.directive("typeb", function($compile, $log){
return{
restrict: "E",
replace: true,
transclude: true,
scope: {
type: '='
},
template: "<div ng-repeat='t in type'>{{t}}</div>",
};
})
Problem still occurs but generated html contains only next pieces as a result of typeb directive:
<!-- ngRepeat: t in type -->
The problem you are getting is <!-- ngRepeat: t in type --> this is because your type didn't contains any value, when it is inside typeb directive
Your directive shouldn't be use scope.variable on view.
Scope variable will be directly accessible by their name like
{{type}} or <typeb type='type'></typeb>
Change your link code to below.
Directive link
link: function(scope, element, attrs) {
if (angular.isArray(scope.type)) {
element.append("<typeb type='type'></typeb>")
} else if (angular.isObject(scope.type)) {
element.append("OBJECT")
} else {
element.append("<div>{{type}}</div>")
}
$compile(element.contents())(scope)
}
Thanks.
Use an ng-repeat in the template
<your-directive attr="item.someattr" ng-repeat="item in items"></your-directive>
.directive("type", function($compile, $log){
return{
restrict: "E",
replace: true,
transclude: true,
scope: {
type: '='
},
template: "NG REPEAT HERE",
...
})

How to apply a directive after a transclusion in AngularJS

I'm trying to access a transcluded element in an element directive from a different inner directive applied within the first directive's template.
My (trivial example) setup:
Element directive
.directive('elementDirective', function() {
return {
restrict: 'E',
replace: true,
transclude: true,
templateUrl: 'template.html',
link: function (scope, elem, attr, ctrl, transclude) {
elem.find('transclusion').replaceWith(transclude());
}
};
})
Template
<div inner-directive>
<div id="transclusion"></div>
</div>
Inner Directive
.directive('innerDirective', function() {
return {
restrict: 'A',
link: function (scope, elem, attr, ctrl) {
//Do something here with child elements (which will be transcluded).
var some = elem[0].querySelector('[name]');
//...
}
};
})
Html
<element-directive>
<input name="input_name" />
</element-directive>
My Error:
I get an error of the kind.
element-directive element has no child input elements with a 'name' attribute
I'm assuming this means that the inner directive is compiled, linked, whatnot, before transclusion happens. So, how can I achieve what I'm trying to do?

AngularJS directive to directive communication throwing an error about controller not found

I have 2 directives, one for searching and one for pagination. The pagination directive needs to access the search directive to find out what property we're currently searching by. When I load the page though, it throws an error saying Error: [$compile:ctreq] Controller 'search', required by directive 'pagination', can't be found!. However I have a controller setup in my search directive.
Here is my search directive:
angular.module('webappApp')
.directive('search', function ($route) {
return {
templateUrl: 'views/search.html',
restrict: 'E',
scope: {
searchOptions: '=',
action: '=',
currentProperty: '=',
currentValue: '='
},
controller: function($scope) {
$scope.searchBy = $scope.searchOptions[0].text;
$scope.searchByProperty = $scope.searchOptions[0].property;
$scope.setSearchBy = function(event, property, text) {
event.preventDefault();
$scope.searchBy = text;
$scope.searchByProperty = property;
};
$scope.search = function() {
$scope.searching = true;
$scope.currentProperty = $scope.searchByProperty;
$scope.currentValue = angular.element('#searchCriteria').val();
$scope.action($scope.searchByProperty, $scope.currentValue, function() {
$scope.searching = false;
});
};
$scope.reload = function() {
$route.reload();
};
}
};
});
Here is my pagination directive:
angular.module('webappApp')
.directive('pagination', function () {
return {
templateUrl: 'views/pagination.html',
restrict: 'E',
require: '^search',
scope: {
basePath: '#',
page: '=',
sort: '='
},
link: function(scope, element, attrs, searchCtrl) {
console.debug(searchCtrl);
scope.searchByProperty = searchCtrl.searchByProperty;
}
};
});
In order for one directive to use another's controller by use of require, it needs to either share the same element as the controller containing directive, or it has to be a child of it.
You can't use require in the way you have, where the elements are siblings.
Angular docs about directives, including require
If it doesn't make sense to rearrange the DOM in the way I've described, you should inject a service into both directives which contains the data/methods you wish to share between the two.
Note: you could also experiment with the $$nextSibling / $$prevSibling properties of the directives' scopes, but this would present only a very fragile solution
You cannot use require in directive like that, however , since the only thing you need to pass between directives is a string , just bind them to the same property in parent controller (it can be parent directive controller):
...
<div ng-app='app' ng-controller='MyCtrl as ctrl'>
<my-dir-one s1='ctrl.message'></my-dir-one>
<my-dir-two s2='ctrl.message'></my-dir-two>
and first directives:
app.directive('myDirOne', function ($route) {
return {
templateUrl: 'views/my-dir-one.html',
restrict: 'E',
scope: {
s1: '=',
second directive
app.directive('myDirTwo', function ($route) {
return {
templateUrl: 'views/my-dir-one.html',
restrict: 'E',
scope: {
s2: '=',

Beginner angularjs - including directive within directive

This is probably as basic as it gets. I have some simple markup that's suppose to represent a list:
<unordered-list>
<list-item></list-item>
<list-item></list-item>
</unordered-list>
I also have two directives defined for unordered-list and list-item:
.directive('unorderedList', function() {
return {
restrict: 'E',
replace: true,
template: '<ul></ul>'
};
})
.directive('listItem', function() {
return {
restrict: 'E',
replace: true,
template: '<li>Test</li>'
}
});
Yet when I run this (http://jsfiddle.net/esWUD/), the only thing rendered is the <ul>. No <li>Test</li> elements render within the <ul>.
Why not don't they render?
You'll need to use ngTransclude to allow directives to have sub-elements. Set transclude: true in unorderedList.
transclude: true,
template: '<ul ng-transclude></ul>'
Working fiddle.

Wrapping Foundation 4 reveal in Angular

New to Angular, just trying to get some harmony with Zurb Foundation 4. A case in point; I am trying to make use of the http://foundation.zurb.com/docs/components/reveal.html component.
Straight-forward approach seemed to be to wrap as directives:
directive('modal', function() {
return {
template: '<div ng-transclude id="notice" class="reveal-modal">' +
'<a close-modal></a>' +
'</div>',
restrict: 'E',
transclude: true,
replace: true,
scope: {
'done': '#',
},
transclude: true,
link: function(SCOPE, element, attrs, ctrl) {
SCOPE.$watch('done', function (a) {
// close-modal
});
}
}
}).
directive('closeModal', function() {
return {
template: '<a ng-transclude href="#" class="close-reveal-modal">x</a>',
restrict: 'A',
transclude: true,
replace: true
}
}).
directive('showModal', function() {
return {
template: '<a ng-transclude class="reveal-link" data-reveal-id="notice" href="#"></a>',
restrict: 'A',
transclude: true,
replace: true,
}
});
This works fine up to a point, for example, I can use the modal to show different notices from a template:
<modal done="">
<div ng-include src="'partials/notices/' + notice + '.html'"></div>
</modal>
<select ng-model="notice" ng-options="n for n in ['notice-1', 'notice-2']">
<option value="">(blank)</option>
</select>
<a show-modal>show modal</a>
However, where it gets sticky is if I want to trigger close-modal/ show-modal from a controller/ on a certain event (e.g. within $watch). I'm assuming my directive needs a controller to trigger click, but would be good Angular practise?
This question is very old and I don't know if it works with Reveal. But I've wrapped the dropbox library into Angular only by calling the .foundation() method on the .run() method of angular:
app.run(function ($rootScope) {
$rootScope.$on('$viewContentLoaded', function () {
$(document).foundation();
});
});
That works for me. I guess that you can also create a directive to handle user interaction.
Controllers should not be triggering UI events directly neither manipulate UI elements directly. All that code is supposed to go in Directives.
What you can do is:
Bind a bool in Directive Scope to the Parent scope and add a watch on that. I think you already did that. OR
Do a scope.$broadcast on the controller and add scope.$watch on the directive to close then.

Categories