Require controller in directive when terminal is true - angularjs

I can't load controller's parent in a directive. I have two directives: h-menu and h-menu-item. h-menu uses a controller, and h-menu-item requires that controller.
But h-menu directive has terminal = true, and with this I can't load controller. When I set terminal to false, I can load the controller.
JsFiddle: http://jsfiddle.net/gspVe/4/
html:
<div ng-app="test">
<h-menu>
<h-menu-item>
</h-menu-item>
</h-menu>
</div>
Here is the code js:
angular.module("test", [])
.controller("hMenu", function () {
this.msg = "controller was loaded";
return this;
})
.directive("hMenu", function($compile) {
return {
restrict: "E",
// comment this and controller will be loaded
terminal: true,
controller: "hMenu",
require: "hMenu",
link: function (scope, element, attrs) {
var ul = $("<ul/>");
ul.append(element.children());
$compile(ul)(scope);
element.replaceWith(ul);
}
};
})
.directive("hMenuItem", function($compile) {
return {
restrict: "E",
terminal: true,
require: "?^hMenu",
link: function (scope, element, attrs, controller) {
var li = $("<li/>");
if (controller)
li.html(controller.msg);
else
li.html("contoller not loaded!");
$compile(li)(scope);
element.replaceWith(li);
}
};
})

Just trying to understand what you're trying to do. If you want to replace HTML elements, why not use some of the facilities that a directive provides already?
For instance, it looks like you're trying to replace the directive elements with <ul> and <li>. Rather than define this as you've indicated, why couldn't you do the following:
angular.module("test", [])
.directive("hMenu", function() {
return {
restrict: "E",
transclude : true,
replace : true,
template : "<ul ng-transclude></ul>"
};
})
.directive("hMenuItem", function() {
return {
restrict: "E",
replace : true,
transclude : true,
template : '<li ng-transclude></li>'
};
})
Then, you can use the power of Angular to indicate menu items declaratively, for example:
<div ng-app="test">
<h-menu ng-init="menuitems=['Menu #1', 'Menu #2', 'Menu #3']">
<h-menu-item ng-repeat="m in menuitems">
{{m}}
</h-menu-item>
</h-menu>
</div>
This would show:
Menu #1
Menu #2
Menu #3
I've updated a jsfiddle at http://jsfiddle.net/gspVe/9/

Related

scope variable of the controller is not recognized in nested directives

I have created two directives and inserted the first directive into the second one. The content of template attribute works fine but the scope variable of the controller is not recognized. Please provide me solution on this
sample link: http://jsbin.com/zugeginihe/2/
You didn't provide the attribute for the second directive.
HTML
<div second-dir first-dir-scope="content">
<div first-dir first-dir-scope="content"></div>
</div>
Link demo: http://jsbin.com/jotagiwolu/2/edit
The best option would using parent directive, We could take use of require option of directive like require: '?secondDir' in firstDir
Code
var myApp = angular.module("myApp", []);
myApp.controller("myController", function($scope) {
$scope.content = "test1";
});
myApp.directive("firstDir", function() {
return {
restrict: "AE",
require: '?secondDir',
scope: {
firstDirScope: "="
},
template: "<div>first content</div>",
link: function(scope, element, attrs, secondDir) {
console.log(scope.firstDirScope, 'first');
}
};
});
myApp.directive("secondDir", function() {
return {
restrict: "AE",
scope: {
firstDirScope: "="
},
controller: function($scope) {
console.log($scope.firstDirScope, 'second');
}
};
});
Working JSBin

AngularJs How to pass the complex object in directives

iI have this directive :
amDirective.directive('directiveList', function() {
return {
restrict: 'A',
scope: {
'event': '&onEvent'
},
transclude: true,
templateUrl: ''
};
});
and in my page html
<div data-dy-directive-list data-elements="elements" on-event="listItemClicked(item)"></div>
and in my directive- template html
<table class="table" data-ng-show="elements!=null && elements.length>0">
<tr data-ng-repeat="element in elements">
<td><span data-ng-click="event(element)">{{element.title}}</span></td>
</tr>
<tr></tr>
</table>
How can I pass in my directive the complex object "item"?
angular directives use '&' to bind to parent scope expressions.
It means that your directive would evaluate it when an event occurs inside the directive.
Example
app.directive('directiveList', function() {
return {
restrict: 'A',
scope: {
'event': '&onEvent'
},
link: function(scope){
var myItem = {}
scope.event = function(item) {
element.bind('click', function(){
scope.event({item: myItem})
})
}
};
});
If you want to raise an event from the parent scope and let your directive know you should use "="
Example
app.directive('directiveList', function() {
return {
restrict: 'A',
scope: {
'event': '=onEvent'
},
link: function(scope){
scope.event = function(item) {
// manipulate item
return "something";
}
}
};
});
From your directive (either controller or link function) you would call: scope.event({item: item}), passing a named map from argument names to values to provide to the callback.
Example:
amDirective.directive('directiveList', function() {
return {
restrict: 'A',
scope: {
'event': '&onEvent'
},
transclude: true,
templateUrl: '',
link: function(scope, element, attrs) {
element.bind('click', function() {
scope.event({ item: { hello: 'world', x: 3 } });
});
}
};
});
Usage:
<a directive-list on-event="myHandler(item)">Click Me<a>
Here's an example plnkr.
See: Can an angular directive pass arguments to functions in expressions specified in the directive's attributes?

AngularJS - accessing parent directive properties from child directives

This should not be too hard a thing to do but I cannot figure out how best to do it.
I have a parent directive, like so:
directive('editableFieldset', function () {
return {
restrict: 'E',
scope: {
model: '='
},
replace: true,
transclude: true,
template: '
<div class="editable-fieldset" ng-click="edit()">
<div ng-transclude></div>
...
</div>',
controller: ['$scope', function ($scope) {
$scope.edit = ->
$scope.editing = true
// ...
]
};
});
And a child directive:
.directive('editableString', function () {
return {
restrict: 'E',
replace: true,
template: function (element, attrs) {
'<div>
<label>' + attrs.label + '</label>
<p>{{ model.' + attrs.field + ' }}</p>
...
</div>'
},
require: '^editableFieldset'
};
});
How can I easily access the model and editing properties of the parent directive from the child directive? In my link function I have access to the parent scope - should I use $watch to watch these properties?
Put together, what I'd like to have is:
<editable-fieldset model="myModel">
<editable-string label="Some Property" field="property"></editable-string>
<editable-string label="Some Property" field="property"></editable-string>
</editable-fieldset>
The idea is to have a set of fields displayed by default. If clicked on, they become inputs and can be edited.
Taking inspiration from this SO post, I've got a working solution here in this plunker.
I had to change quite a bit. I opted to have an isolated scope on the editableString as well because it was easier to bind in the correct values to the template. Otherwise, you are going to have to use compile or another method (like $transclude service).
Here is the result:
JS:
var myApp = angular.module('myApp', []);
myApp.controller('Ctrl', function($scope) {
$scope.myModel = { property1: 'hello1', property2: 'hello2' }
});
myApp.directive('editableFieldset', function () {
return {
restrict: 'E',
scope: {
model: '='
},
transclude: true,
replace: true,
template: '<div class="editable-fieldset" ng-click="edit()"><div ng-transclude></div></div>',
link: function(scope, element) {
scope.edit = function() {
scope.editing = true;
}
},
controller: ['$scope', function($scope) {
this.getModel = function() {
return $scope.model;
}
}]
};
});
myApp.directive('editableString', function () {
return {
restrict: 'E',
replace: true,
scope: {
label: '#',
field: '#'
},
template: '<div><label>{{ label }}</label><p>{{ model[field] }}</p></div>',
require: '^editableFieldset',
link: function(scope, element, attrs, ctrl) {
scope.model = ctrl.getModel();
}
};
});
HTML:
<body ng-controller="Ctrl">
<h1>Hello Plunker!</h1>
<editable-fieldset model="myModel">
<editable-string label="Some Property1:" field="property1"></editable-string>
<editable-string label="Some Property2:" field="property2"></editable-string>
</editable-fieldset>
</body>
You can get access to parent controller by passing attribute in child directive link function
link: function (scope, element, attrs, parentCtrl) {
parentCtrl.$scope.editing = true;
}

AngularJS issues with compiler attribute in directive

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">1
</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>{{ _item }}</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);
};
}
};
}
]);
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>{{ _item }}</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.
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.

AngularJS: How to target specific directive with multiple instances

I have multiple instances of a directive and I would like to target a specific instance only. For example in the code below, how can I make sure that the div with class="one" is the only one that gets triggered by the event $rootScope.$broadcast('myEvent').
JS:
myApp.directive('myDirective', function() {
return {
restrict: 'A',
controller: function(scope, el, attrs) {
scope.$on('myEvent', function() {
el.css({left: '+=100'});
});
},
};
});
HTML:
<div my-directive class="one"></div>
<div my-directive class="two"></div>
You should do this check in your event handler:
myApp.directive('myDirective', function() {
return {
restrict: 'A',
controller: function(scope, el, attrs) {
scope.$on('myEvent', function(ev,args) {
//do the check - you could provide a function, a value or something else
if(el.hasClass(args.class)){
el.css({left: '+=100'});
}
});
},
};
});
Then add the parameters in the $broadcast
$rootScope.$broadcast('myEvent',{class:'one'})
You could define a new attribute for this directive which references a callback that then is executed inside the callback of $on:
myApp.directive('myDirective', function() {
return {
restrict: 'A',
scope: {
callme: "="
},
controller: function(scope, el, attrs) {
scope.$on('myEvent', function() {
scope.callme(el)
});
},
};
});
HTML declaration:
<div my-directive class="one" callme="functionDefinedInScope"></div>
In your Controller scope:
$scope.functionDefinedInScope = function(el) {
el.css({left: '+=100'});
}
Read more about this here:
http://docs.angularjs.org/guide/directive (Section "Directive Definition Object")
can you use id instead of class
<div my-directive id="one"></div>
<div my-directive id="two"></div>
and in your controller
controller: function(scope, el, attrs) {
scope.moveElement = function() {
el.css({left: '+=100'});
}
}
and then instead of broadcast,
angular.element('#one').scope().moveElement()

Resources