Reputation: 2330
I'm learning AngularJS and I'm training for how to build a reusable directive.
The problem is that it works with an array with one element, but not with two or more.
The HTML tag is just: <breadcrumb></breadcrumb>
which in case, render as expected. But, I need to do manually what "replace:true" would do.
The error is: parent is null.
I exhausted all Google searches looking for an example.
But my case is peculiar, because there is an <inner ng-repeat>
inside <breadcrumb>
which is an another inner-directive instead an normal tag, that's why replace doesn't work and I need to do manually.
There is a problem related to "dynamic template load..."
OBS: I tried to do in both "link:" and "compile:" the logic, same error...
I could make the code work, but, I'm unable to remove <inner>
tag as transclude
would do automatic.
Now the output is almost perfect, and I just need to remove the <inner>
, but no luck until now. I tried to replace the element tag, but still no luck.
<ul class="breadcrumb">
<!-- I want to remove this <inner> and leave <li> alone! -->
<inner data-ng-repeat="_item in items track by $index" class="ng-scope">
<li class="ng-scope"><a href="#" class="ng-binding">1</a>
</li>
</inner>
<!-- remove for clarity -->
</ul>
var myModule = angular.module("myModule", []);
myModule.directive('breadcrumb', function($timeout) {
"use strict";
var directiveDefinitionObject = {
template: '<ul class="breadcrumb"><inner data-ng-repeat="_item in items track by $index"></inner></ul>',
replace: true,
// transclude: true,
restrict: 'E',
scope: {},
controller: ["$scope", "$element", "$attrs", "$transclude", controller],
link: ["scope", "iElement", "iAttrs", link]
};
function link(scope, iElement, iAttrs) {
scope.addNewItem = function(new_item) {
scope._push(new_item);
}
}
function controller($scope, $element, $attrs, $transclude) {
$scope.items = [1, 2, 3, 4, 5];
$scope._push = function(item) {
$scope.items.push(item);
};
$scope._pop = function() {
$scope.items.pop();
};
$scope.is_last = function(item) {
return $scope.items.indexOf(item) == ($scope.items.length - 1);
}
}
return directiveDefinitionObject;
});
myModule.directive("inner", ["$compile",
function($compile) {
"use strict";
function getItemTemplate(index) {
return '<li><a href="#">{{ _item }}</a></li>';
}
return {
require: "^breadcrumb",
restrict: "E",
compile: function compile(tElement, tAttrs)
{
return function postLink(scope, iElement, iAttrs)
{
iElement.html(getItemTemplate(0));
$compile(iElement.contents())(scope);
};
}
};
}
]);
Upvotes: 2
Views: 1902
Reputation: 2330
I was able to rebuilt it.
I've learnt a lot since my first attempt.
The solution was simplified because the dynamic template is rubbish to handle, because ng-repeat does not redraw the entire array. So, I did it my own way and it was a clean solution.
Upvotes: 0
Reputation: 23394
You can just remove the compile function of yours in inner
directive and set replace: true
, because it's just mocking the default behavior of replace
, as you stated. So you inner
directive would become:
myModule.directive("inner", ["$compile",
function($compile) {
"use strict";
return {
replace: true
require: "^breadcrumb",
restrict: "E",
template: '<li><a href="#">{{ _item }}</a></li>'
};
}
]);
But you have a problem that your items
array is defined into your breadcrumb
directive, what is wrong and will not let you make it reusable. You could bind it at scope
definition with something like this:
<breadcrumb items="someItemsArrayFromParentScope"></breadcrumb>
...directive('breadcrumb', function() {
...
return {
...
scope: {
items: '='
}
}
});
This would create a two directional binding between the array from parent and internal widget scope. But going further, you might want to let the user define the inner elements of the breadcrumb
, it would be as following:
myModule.directive('breadcrumb', function($timeout) {
var directiveDefinitionObject = {
template: '<ul class="breadcrumb" ng-transclude></ul>',
replace: true,
transclude: true,
restrict: 'E',
scope: {},
controller: ["$scope", "$element", "$attrs", "$transclude", controller]
};
function controller($scope, $element, $attrs, $transclude) {
$scope.addNewItem = function(new_item) {
$scope._push(new_item);
}
$scope._push = function(item) {
$scope.items.push(item);
};
$scope._pop = function() {
$scope.items.pop();
};
$scope.is_last = function(item) {
return $scope.items.indexOf(item) == ($scope.items.length - 1);
}
}
return directiveDefinitionObject;
});
myModule.directive("inner", function($compile) {
return {
require: "^breadcrumb",
restrict: "E",
template: '<li><a href="#" ng-transclude></a></li>',
replace: true,
transclude: true
};
}
);
And in your html you would come with:
<breadcrumb>
<inner ng-repeat="item in items"><i>{{item}}</i></inner>
</breacrumb>
The trick is the ng-transclude
directive inside the templates. It just get the contents of the element and "move it" to inside the element marked with ng-transclude
and link it against the parent scope, what is awesome, as you can have dynamic naming for the items that would be based on parent scope. The items
of the ng-repeat
, for example, would be defined in the parent scope, as expected. This is the preferred way, and you would even get the possibility of use different inner templates (as I did with the <i>
tag). You would even be able to not use an ng-repeat
and hardcode the inner
elements, if it's the case:
<!-- language: lang-html -->
<breadcrumb>
<inner>First Path</inner>
<inner>Second Path: {{someParentScopeVariable}}</inner>
</breacrumb>
Here is a working Plnker.
Upvotes: 2