AngularJS: How create a new directive joining existents directives? - angularjs

I would like improve my current solution.
I have a big menu using <ul> and <li> tags. I need show to the user only the <li> tags that they have permission to access.
I have resolved this problem using two directives: ng-init and ng-show
...
<li ng-init="ok=hasPemission('item1')" ng-show="ok">
Item 1
</li>
...
I needed to use 'ng-init' to get a stable hasPermission result. I can't use ng-show="hasPemission('item1')" because 'hasPemission' returns a new 'promise' object and the ng-show has problem with not stable expressions ([$rootScope:infdig] infinit $digest loop).
Now, I wanted create a new directive that join this both directives. I created this one but I think that there is a way to reuse these two existents directives.
This is my new directive:
.directive('myPermissionShow',['$q','$animate','AccessControl',
function ($q, $animate, AccessControl) {
return {
restrict:'A',
scope: {
resourceName: '#myPermissionShow'
},
link: function ($scope, $element) {
var user = AccessControl.getLoggedUser();
AccessControl.hasPermission(user,$scope.resourceName).then(
function (value) {
// I copied the line below from ngShow directive:
$animate[value ? 'removeClass' : 'addClass']($element, 'ng-hide');
}
);
}
}
}])
So, my html changed to:
...
<li my-permission-show="item1">
Item 1
</li>
<li my-permission-show="item2">
Item 2
</li>
...
Is there a way to create this new directive reusing the directives 'ng-init' and 'ng-show'?
Something like this:
!!!NOT WORKING CODE!!!
.directive('myPermissionShow',['AccessControl',
function (AccessControl) {
return {
restrict:'A',
replaceAttribute: true, /* this property does not exists... */
template: 'ng-init="val=hasPermission(user,resourceName)" ng-show="val"',
scope: {
resourceName: '#myPermissionShow'
},
controller: function ($scope, $element) {
$scope.hasPemission = AccessControl.hasPermission;
$scope.user = AccessControl.getLoggedUser();
}
}
}])
!!!NOT WORKING CODE!!!

Related

How can I use interpolation to specify element directives?

I want to create a view in angular.js where I add a dynamic set of templates, each wrapped up in a directive. The directive names correspond to some string property from a set of objects. I need a way add the directives without knowing in advance which ones will be needed.
This project uses Angular 1.5 with webpack.
Here's a boiled down version of the code:
set of objects:
$scope.items = [
{ name: "a", id: 1 },
{ name: "b", id: 2 }
]
directives:
angular.module('myAmazingModule')
.directive('aDetails', () => ({
scope: false,
restrict: 'E',
controller: 'myRavishingController',
template: require("./a.html")
}))
.directive('bDetails',() => ({
scope: false,
restrict: 'E',
controller: 'myRavishingController',
template: require("./b.html")
}));
view:
<li ng-repeat="item in items">
<div>
<{{item.name}}-details/>
</div>
</li>
so that eventually the rendered view will look like this:
<li ng-repeat="item in items">
<div>
<a-details/>
</div>
<div>
<b-details/>
</div>
</li>
How do I do this?
I do not mind other approaches, as long as I can inline the details templates, rather then separately fetching them over http.
Use ng-include:
<li ng-repeat="item in items">
<div ng-controller="myRavishingController"
ng-include="'./'+item.name+'.html'">
</div>
</li>
I want to inline it to avoid the http call.
Avoid http calls by loading templates directly into the template cache with one of two ways:
in a script tag,
or by consuming the $templateCache service directly.
For more information, see
AngularJS $templateCache Service API Reference
You can add any html with directives like this:
const el = $compile(myHtmlWithDirectives)($scope);
$element.append(el);
But usually this is not the best way, I will just give a bit more detailed answer with use of ng-include (which actully calls $compile for you):
Add templates e.g. in module.run: [You can also add templates in html, but when they are required in multiple places, i prefer add them directly]
app.module('myModule').run($templateCache => {
$templateCache.put('tplA', '<a-details></a-details>'); // or webpack require
$templateCache.put('tplB', '<b-details></b-details>');
$templateCache.put('anotherTemplate', '<input ng-model="item.x">');
})
Your model now is:
$scope.items = [
{ name: "a", template: 'tplA' },
{ name: "b", template: 'tplB' },
{ name: "c", template: 'anotherTemplate', x: 'editableField' }
]
And html:
<li ng-repeat="item in items">
<div ng-include="item.template">
</div>
</li>
In order to use dynamic directives, you can create a custom directive like I did in this plunkr:
https://plnkr.co/edit/n9c0ws?p=preview
Here is the code of the desired directive:
app.directive('myProxy', function($compile) {
return {
template: '<div>Never Shown</div>',
scope: {
type: '=',
arg1: '=',
arg2: '='
},
replace: true,
controllerAs: '$ctrl',
link: function($scope, element, attrs, controller, transcludeFn) {
var childScope = null;
$scope.disable = () => {
// remove the inside
$scope.changeView('<div></div>');
};
$scope.changeView = function(html) {
// if we already had instanciated a directive
// then remove it, will trigger all $destroy of children
// directives and remove
// the $watch bindings
if(childScope)
childScope.$destroy();
console.log(html);
// create a new scope for the new directive
childScope = $scope.$new();
element.html(html);
$compile(element.contents())(childScope);
};
$scope.disable();
},
// controller is called first
controller: function($scope) {
var refreshData = () => {
this.arg1 = $scope.arg1;
this.arg2 = $scope.arg2;
};
// if the target-directive type is changed, then we have to
// change the template
$scope.$watch('type', function() {
this.type = $scope.type;
refreshData();
var html = "<div " + this.type + " ";
html += 'data-arg1="$ctrl.arg1" ';
html += 'data-arg2="$ctrl.arg2"';
html += "></div>";
$scope.changeView(html);
});
// if one of the argument of the target-directive is changed, just change
// the value of $ctrl.argX, they will be updated via $digest
$scope.$watchGroup(['arg1', 'arg2'], function() {
refreshData();
});
}
};
});
The general idea is:
we want data-type to be able to specify the name of the directive to display
the other declared arguments will be passed to the targeted directives.
firstly in the link, we declare a function able to create a subdirective via $compile . 'link' is called after controller, so in controller you have to call it in an async way (in the $watch)
secondly, in the controller:
if the type of the directive changes, we rewrite the html to invoke the target-directive
if the other arguments are updated, we just update $ctrl.argX and angularjs will trigger $watch in the children and update the views correctly.
This implementation is OK if your target directives all share the same arguments. I didn't go further.
If you want to make a more dynamic version of it, I think you could set scope: true and have to use the attrs to find the arguments to pass to the target-directive.
Plus, you should use templates like https://www.npmjs.com/package/gulp-angular-templatecache to transform your templates in code that you can concatenate into your javascript application. It will be way faster.

How to update Directive on State Changes

I have a root state that defines the overall structure of the Angular template. In the root state, I have the sidebar included that has dynamic menus via directive that changes based on the state. Like this:
.state(‘root', {
abstract: true,
url: ‘/root',
templateUrl: ‘views/root.html',
})
root.html includes the sidebar.html that has dynamic menu called through Directive like this:
sidebar.html
<ul class="nav" id="side-menu">
<li class="nav-header">
<img alt="avatar" ng-src="{{ avatar }}" />
</li>
<!—Dynamic Menus Directive -->
<li sidebar-menus></li>
</ul>
The directive shows the menu based on $state.includes(). But what happens is, the directive shows fine in the first load but it doesn’t update the directive during state changes. To resolve this, I tried the following methods but nothing worked:
Added the $state to scope in Main controller but it still doesn’t change the directive
once it is compiled first.
Tried adding $stateChangeSuccess watcher to trigger recompiling the directive, but it doesn’t
recompile again after the first time (or) maybe it is recompiling but the changes are not seen in the template (this is the code I have now
which I will give below).
Moving the sidebar inside separate child
states instead of having it in root state works, but it beats the
purpose since I am trying to load the overall structure in the root
state first and only refresh the menu sections in subsequent state
changes.
I am not really sure how to approach this. I have a feeling my approach can be out of whack and hoping someone can guide me here. This is my directive code at the moment:
.directive('sidebarMenus', ['$compile', '$state', '$rootScope',
function($compile, $state, $rootScope) {
return {
restrict: 'A',
replace: true,
link: function(scope, element, attrs) {
var state = scope.$state; // Scope from Main Controller
// HTML Template
function contructHtml(state) {
var htmlText = '';
// First Child State
if (state.includes('root.child1')) {
var htmlText = '<li>Child 1 Menu</li>';
}
// Second Child State
if (state.includes('root.child2')) {
var htmlText = '<li>Child 2 Menu</li>';
}
// Third Child State
if (state.includes('root.child3')) {
var htmlText = '<li>Child 3 Menu</li>';
}
$compile(htmlText)(scope, function( _element, _scope) {
element.replaceWith(_element);
});
}
$rootScope.$on('$stateChangeSuccess', function() {
var state = scope.$state; // scope.$state is added in main controller
contructHtml(state);
});
// Initial Load
contructHtml(state);
}
}
}])
You can get rid of the compile business by using template.
You template could look something like this:
<li ng-if="state.includes('root.child1')">Child 1 Menu</li>
<li ng-if="state.includes('root.child2')">Child 2 Menu</li>
<li ng-if="state.includes('root.child3')">Child 3 Menu</li>
So your directive code should look sth like this
return {
restrict: 'A',
replace: true,
template:'<div> <li ng-if="state.includes('root.child1')">Child 1 Menu</li>
<li ng-if="state.includes('root.child2')">Child 2 Menu</li>
<li ng-if="state.includes('root.child3')">Child 3 Menu</li>
</div>'
link: function(scope, element, attrs) {
$scope.state = scope.$state; // Scope from Main Controller
$rootScope.$on('$stateChangeSuccess', function() {
$scope.state = scope.$state; // scope.$state is added in main controller
});
}
}

Not able to require a directive from another directive in angularJS

Getting an error while trying to require directive msgpallete from directive itemCounter.
I have removed codes from some functions which i thought is irreelevant in this context.
Do ask for any code which you think is important.Kinda new to angular. So may have many misconceptions.
HTML snippet
<msgpallete msg="message"></msgpallete>
<li ng-repeat = "item in items">
<item-counter
startcounter = 1
resetter = 'reset'
name = {{item.name}} >
{{item.name}}
</item-counter><br><br>
</li>
JS snippet
angular.module('myApp',[])
.controller('MainCtrl', function($scope, $timeout) {
....code irreleveant in this context....
})
.directive('msgpallete',function(){
return{
restrict:"E",
scope:{},
template:"<h4>Added"+""+" "+"</h4>"
}
})
.directive('itemCounter',function(){
return {
restrict:'E',
require:'msgpallete',
scope:{
resetter:"="
},
transclude:true,
link:function(scope,elem,attr){
scope.qty = attr.startcounter
scope.add = function(){}
scope.remove = function(){}
scope.reset = function(){}
scope.$watch();
},
template:"<a ng-transclude></a> &nbsp"+
"<button ng-click='remove();' >less</button>"+
"{{qty}}" +
"<button ng-click='add();'>more</button>&nbsp"+
"<button ng-click='reset();'>submit</button>"
}
});
thanks in advance
require:'^msgpallete'
The require parameter has additional functionality if you look through the docs for it.
someDirective : Require someDirective on same element and pass it to
linking function
?someDirective : Pass someDirective controller if available on same element to linking function. If not, pass null.
^someDirective : Require someDirective on one of the parent elements and pass it to linking function.
?^someDirective : Pass someDirective controller if available on one of parent elements to linking function. If not, pass null.
It has been a little while since I have done this and the directive you are requiring may need to explicitly defined a controller. Just defining and return an empty controller is enough.
.directive('parent', function() {
return: {
controller: function() {return {}}
}
})

Menu and different submenu on click

I am trying to build a menu and a submenu in angular.
What I want to do is to have two arrays of objects
Menu
menu = [{name: 'Name1', link: '/link1'}, {name: 'Name2', link: '/link2'}]
submenu = [[{name: 'SubName1', link: '/Sublink1'}, {name: 'SubName1', link: '/sublink1'}],
[[{name: 'SubName2', link: '/Sublink2'}, {name: 'SubName2', link: '/sublink2'}]]
So when I click Name1 the first array of SubMenu will be selected and when clicking Name2 the second array will be selected.
How I can create two Directives one for the main menu and one for the second and be able to communicate between them on click. I have tried building this in a controller, I was able to select the submenu by using the $index, but the submenu can't be moved around as I like because it needs to be under the controller.
I finally managed to solve my problem here is the solution: http://jsfiddle.net/4kjjyL4s/4/
How can I improve my solution?
Don't reinvent the wheel :) UI router is a prepackaged solution that handles nested routing for you.
If you have a menu of items and you want to display another menu of items when one of the items is selected UI router does exactly that. https://github.com/angular-ui/ui-router
Can't give you the exact answer because information is lacking, but for example if you're using the directives with different menu items at other places in your app, I'd recommend to pass the menu array from controller (ng-controller, not directive's controller) through directive's scope.
Also, you're looking for kinda standard way for directives to communicate directly (in your case, communication between menu and submenu directive to notify the item selection change), use directive's controller. Here's a good tutorial.
https://thinkster.io/egghead/directive-to-directive-communication/
To communicate between controllers or directives, you should use services.
From the angular guide ( https://docs.angularjs.org/guide/services ):
Angular services are substitutable objects that are wired together using dependency injection (DI). You can use services to organize and share code across your app.
I checked the code you posted on jsfiddle ( http://jsfiddle.net/4kjjyL4s/4/ )and I tried to keep the most of it. Below are my changes in the JavaScript file ( please, read the comments in the code ).
var app = angular.module("app",[]);
app.controller('main', function(){});
// The service will be responsible for the shared objects and logic
app.service('MenuService', function () {
var list = [
{
name: "Menu1", link: "#menu1",
submenu: [
{ name: "Menu1Sub1", link: "#submenu1" },
{ name: "Menu1Sub2", link: "#submenu2" }
]
},
{
name: "Menu2", link: "#menu2",
submenu: [
{ name: "Menu2Sub1", link: "#submenu1" },
{ name: "Menu2Sub2", link: "#submenu2" }
]
}
];
var selected = [];
// methods and attributes published under the **this**
// keyword will be publicly available
this.getMenu = function () {
return list;
};
this.getSubmenu = function () {
return selected;
};
this.select = function ( menuItem ) {
// this does the *trick*!
// if we use the assignment operator here, we would replace the
// reference returned in the getSubmenu() method, so as the original
// reference did not change, angular's dirty checking would not detect it.
// using angular.copy() method, we are copying the contents of the
// selected submenu over the same reference returned by getSubmenu()
angular.copy( menuItem.submenu, selected );
};
});
// No $rootScope injection results in better re-usability. When you were
// relying in $rootScope sharing, both directives should live in the
// $rootScope, so if you add them inside a ng-controller created scope
// they would not work anymore
app.directive("menu", function() {
return {
restrict: "E",
// no need to isolate scope here, *scope:true* creates a new scope
// which inherits from the current scope
scope: true,
// with controllerAs (introduced in angular 1.2), you can skip
// polluting the scope injection.
controllerAs: "ctrl",
controller: function( MenuService ) {
this.list = MenuService.getMenu();
this.changeSub = function ( menuItem ) { MenuService.select( menuItem ); };
},
template: "<div ng-repeat='menu in ctrl.list'><button ng-click='ctrl.changeSub(menu)'>{{menu.name}}</button></div>"
};
});
app.directive("submenu", function() {
return {
restrict: "E",
scope: true,
controllerAs: "ctrl",
controller: function( MenuService ) {
this.sublist = MenuService.getSubmenu();
},
template: "<span ng-repeat='menu in ctrl.sublist'>{{menu.name}} | </span>aa"
};
});
And here is the updated HTML file, just to show both directives work now not directly inserted in the $rootScope
<div ng-app="app">
<div ng-controller="main">
<menu></menu>
<h1>Hello World!</h1>
<div class="main-content">
<submenu></submenu>
</div>
</div>
</div>
Hope it helps!
Try this Code:
function MyCtrl ($scope) {
$scope.subMenu = []; // default is false
$scope.toggleSubMenu = function (index) {
$scope.subMenu[index] = !$scope.subMenu[index];
};
}
HTML
<ul>
<li ng-class="{active: subMenu[0]}"> Name1
<ul>
<li>test</li>
<li>test</li>
<li>test</li>
</ul>
</li>
<li ng-class="{active: subMenu[1]}"> Name2
<ul>
<li>bar</li>
<li>bar</li>
<li>bar</li>
</ul>
</li>
</ul>
Also Check this

Triggering a function with ngClick within ngTransclude

I have an unordered list loaded with four items from an array while using ngRepeat. The anchor tag in the list item has a function in the ngClick attribute that fires up a message. The function call works well when used like this:
<ul>
<li ng-repeat="n in supsNames">
<a ng-click="myAlert(n.name)">{{n.name}}</a>
</li>
</ul>
I created a simple directive for inserting unordered lists with list items. The list is loaded just fine but the same functioned I previously mentioned does not fire up. The code is as follows:
<div list items="supsNames">
<a ng-click="myAlert({{item.name}})">{{item.name}}</a>
</div>
Here is my javascript and angularjs code:
var app = angular.module('myapp', []);
app.controller('myCtrl', function($scope) {
$scope.title = 'ngClick within ngTransclude';
$scope.supsNames = [
{"name" : "Superman"},
{"name" : "Batman"},
{"name" : "Aquaman"},
{"name" : "Flash"}
];
$scope.myAlert = function(name) {
alert('Hello ' + name + '!');
};
});
app.directive('list', function() {
return {
restrict: 'A',
scope: {
items: '='
},
templateUrl: 'list.html',
transclude: true,
link: function(scope, element, attrs, controller) {
console.log(scope);
}
};
});
I also have a plnkr in case you want to see what I tried to do:
http://plnkr.co/edit/ycaAUMggKZEsWaYjeSO9?p=preview
Thanks for any help.
I got the plunkr working. I'm not sure if its exactly what you're looking for. I copied the main code changes below.
Here's the plunkr:
http://plnkr.co/edit/GEiGBIMywkjWAaDMKFNq?p=preview
The modified directive looks like this now:
app.directive('list', function() {
return {
restrict: 'A',
scope: {
items: '=',
ctrlFn: '&' //this function is defined on controller
},
templateUrl: 'list.html',
transclude: true,
link: function(scope, element, attrs, controller) {
//directive fn that calls controller defined function
scope.dirFn = function(param) {
if(scope.ctrlFn && typeof scope.ctrlFn == 'function') { //make sure its a defined function
scope.ctrlFn( {'name': param} ); //not sure why param has to be passed this way
}
}
}
};
});
And here's how it's called in the html file that's bound to your controller:
<div list items="supsNames" ctrl-fn="myAlert(name)">
<a ng-click="dirFn(item.name)">{{item.name}}</a>
</div>
I think what was happening before is that you were trying to use a function defined in your controller within the isolated scope of the directive, so it wasn't working--that function was undefined in the directive. So what I did was added another parameter to the directive that accepts method binding (I think that's what its called) with the '&'.
So basically you pass your controller method to the directive, and that method gets invoked however you want by the directive defined method I creatively named "dirFn". I don't know if this is the best way per se, but I've used it in an existing project with good results ;)
you need to pass the function to the directive
scope: {
items: '=', 'myAlert': '='
},
The ng-repeat inside the template of the directive insert a new scope and it require to call transclude funcion manually to work. I suggest remove ng-repeat and make the transclusion manually passing a copy of the controller scope and setting the item on each copy:
for(var i=0,len=scope.items.length;i<len;i++){
var item=scope.items[i];
var itemScope=scope.$parent.$new();
$transcludeFn(itemScope, function (clone,scope) {
// be sure elements are inserted
// into html before linking
scope.item=item;
element.after(clone);
});
};
I edit the pluker and I hope that could be helpfull: http://plnkr.co/edit/97ueb8SFj3Ljyvx1a8U1?p=preview
For more info about transclusion see: Transclusion: $transcludeFn

Resources