How to render dynamic directives, functions and params in AngularJS - angularjs

I have an object that I'm iterating over with ngRepeat. This object includes information on what kind of directives to render in the markup and also what values to give those directives. I haven't been able to do this type of dynamic rendering with Angular so far, it would really make the templates feel like reusable templates.
Controller
$scope.myFunk = function(obj) {
alert(obj.target);
};
$scope.fruits = [
{
label: 'Banana',
params: { target: 'ground' },
action: {
directive: 'ng-click',
value: $scope.myFunk
}
}
];
Template
<ul>
<li ng-repeat="fruit in fruits">
<!-- Expecting to see "ground" in an alert() when clicking -->
<a href {{fruit.action.directive}}="{{fruit.action.value}}{{(fruit.params)}}">
{{fruit.label}}
</a>
</li>
</ul>
Here's a plunker
I'm trying to get angular to evaluate something like this:
<li>
<a href ng-click="myFunk({ target: 'ground' })">Banana</a>
</li>
Usually I'd simply declare directives in the markup the general angular-way, but here I really need to have the flexibility of rendering arbitrary directives with individual values that may be functions with their own parameters.
How can I achieve this?

Does this fit the bill?
app.directive('arbitraryThing', function(){
return {
restrict: 'EA',
scope: {
params: '='
},
link: function(scope, element, attr)
{
element.on(scope.params.action.directive, function(event){
event.preventDefault();
scope.params.action.value({ target: scope.params.params.target})
})
}
}
})
http://plnkr.co/edit/slr9M0YK0JuzcMPWrMqr?p=preview

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.

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

Angularjs create directive dynamically with ng-repeat

I'd like to know is there any way to create dynamic directive with ng-repeat. Something, like this:
<div ng-repeat="document in documents">
<div document.name></div>
</div>
You could use attributes to do that. For example:
<div ng-repeat="document in documents">
<document-viewer name="document.name"></document-viewer>
</div>
and
var app = angular.module("documents", []);
app.controller("AppCtrl", function($scope) {
$scope.documents = [
{id:1, name:'document1'},
{id:2, name:'document2'}
];
});
app.directive("documentViewer", function() {
return {
scope: {
//attribute name will match the name passed from your view,
//and you will be able to use it on your template or logic
name: "="
},
//Add your template here:
template: '<div><button ng-click="openDocument(name);"></div>',
//Add your logic here:
link: function(scope, element, attrs){ ... }
}
});
I don't know if that answers your question, but you are not making clear what is it you are trying to achieve.
Have a look at the following videos as they might come handy for you when dealing with directives and isolate scope:
http://egghead.io/lessons/angularjs-directive-to-directive-communication
http://egghead.io/lessons/angularjs-isolate-scope-expression-binding
http://egghead.io/lessons/angularjs-isolate-scope-attribute-binding
http://egghead.io/lessons/angularjs-isolate-scope-two-way-binding

Adding a CSS class to element on ng-click

I have several arbitrary number of menu items on my page. Simplified they look like this.
...
I would like to add a particular class to an item that is being clicked so that its state changes compared to others.
I know I should do it sugin this kind of a pattern:
<a href=""
class="menu-item"
ng-class="{ active: clicked }"
ng-click="clicked = true">
...</a>
but the problem is that I can't use a single variable as I have an arbitrary number of items. I should either use X number of variables or use an array. But in any case how would I know which item goes with each variable/array index unless I manually enter those indices by hand?
What I'm missing
I'm missing element reference that I could use in ng-click so I could add a particular class on element itself. But that would somewhat bind $scope and UI even though I wouldn't be using any $scope function that would manipulate UI. I'd do it all in the view...
How am I supposed to do this?
A directive that solves this problem:
app.directive("markable", function() {
return {
scope: {}, // create isolated scope, so as not to touch the parent
template: "<a href='#' ng-click='mark()' ng-class='{ active: marked }'><span ng-transclude></span></a>",
replace: true,
transclude: true,
link: function(scope, elem, attrs) {
scope.marked = false;
scope.mark = function() {
scope.marked = true;
};
}
};
});
Use it as:
<a markable>Mark me</a>
Relevant fiddle: http://jsfiddle.net/9HP99/
The advantage of a directive is that it is very easy to use.
The same with a DOM-manupulating directive, through Angular's jQuery-ish interface:
app.directive("markable", function() {
return {
link: function(scope, elem, attrs) {
elem.on("click", function() {
elem.addClass("active");
});
}
};
});
Usage:
<a href="#" markable>Mark me</a>
Fiddle: http://jsfiddle.net/atE4A/1/
You could do something like this:
...
then you can use this.ClassName to get nth item.
But if you # of items is arbitrary, you may consider trying to format the list in a way where you could use ng-repeat, then it would be as easy as this:
...

AngularJS directive only when the condition is true

I am going to have a contextmenu directive in ng-repeat items.
Based on whether a condition is true, the directive should be applied.
How do I put a condition like only when item.hasMenu == true then apply the directive ?
<ul ng-controller="ListViewCtrl" >
<li contextmenu ng-repeat="item in items">{{item.name}} </li>
</ul>
EDIT
This seems to have worked for me. First the directive.
app.directive('menu',function(){
return {
restrict : 'A',
link : function(scope,element,attrs){
if(scope.hasMenu){
element.contextmenu({
menu:[
{title:"Remove" , "cmd" : "remove"},
{title:"Add" , "cmd" : "add"},
],
select:function(event,ui){
//alert("select " + ui.cmd + " on" + ui.target.text());
if (ui.cmd ==='remove'){
alert('Remove selected on ' + scope.item);
}
if (ui.cmd ==='add'){
alert("Add selected");
}
}
});
}
}
}
}
);
Then the html
<ul ng-controller="ListViewCtrl" >
<li menu ng-repeat="item in items">{{item.name}} </li>
</ul>
Can you do something like this, using ng-if?
<ul ng-controller="ListViewCtrl" >
<li ng-repeat="item in items">
<span>{{item.name}}</span>
<div contextmenu ng-if="item.hasMenu"></div>
</li>
</ul>
Here are the docs for ng-if.
EDIT:
If you are driving the context menu off of a class, you should be able to do this:
<ul ng-controller="ListViewCtrl" >
<li ng-class="{'hasmenu': item.hasMenu}" ng-repeat="item in items">{{item.name}} </li>
</ul>
I think this is pretty tricky if you don't want to change your DOM structure. If you could just place your contextmenu directive on a sub DOM node inside the <li> things would be a lot easier.
However, let's assume you can't do that and let's also assume that you don't own the contextmenu directive so that you can't change it to your needs.
Here is a possible solution to your problem that might be a bit hackish (actually I don't know!)
'use strict';
angular.module('myApp', [])
.controller('TestController', ['$scope', function($scope) {
$scope.items = [
{name:1, hasMenu: true},
{name:2, hasMenu: false },
{name:3, hasMenu: true}
];
}])
.directive('contextmenu', function(){
return {
restrict: 'A',
link: function(scope, element){
element.css('color', 'red');
}
}
})
.directive('applyMenu', ['$compile', function($compile){
return {
restrict: 'A',
link: function(scope, element){
if (scope.item.hasMenu){
//add the contextmenu directive to the element
element.attr('contextmenu', '');
//we need to remove this attr
//otherwise we would get into an infinite loop
element.removeAttr('apply-menu');
//we also need to remove the ng-repeat to not let the ng-repeat
//directive come between us.
//However as we don't know the side effects of
//completely removing it, we add it back after
//the compile process is done.
var ngRepeat = element.attr('ng-repeat');
element.removeAttr('ng-repeat');
var enhanced = $compile(element[0])(scope);
element.html(enhanced);
element.attr('ng-repeat', ngRepeat);
}
}
}
}]);
I faked the contextmenu directive to just change the color to red just so that we can see it's taking place.
Then I created an apply-menu attribute directive. This directive than checks if the hasMenu property is true and if so hooks in and adds the contextmenu directive and does a manual $compile process.
However, what worries me a bit about this solution is that I had to temporally remove the ng-repeat directive (and also the apply-menu directive) to get the $compile process to act the way we want it to act. We then add the ng-repeat directive back once the $compile has been made. That is because we don't know the side effects of removing it entirely from the resulting html. This might be perfectly valid to do, but it feels a bit arkward to me.
Here is the plunker: http://plnkr.co/edit/KrygjX
You can do this way
angularApp.directive('element', function($compile) {
return {
restrict: 'E',
replace: true,
transclude: true,
require: '?ngModel',
scope: 'isolate',
link: function($scope, elem, attr, ctrl) {
$scope.isTrue = function() {
return attr.hasMenu;
};
if($scope.isTrue())
//some html for control
elem.html('').show();
else
//some html for control
elem.html('').show();
$compile(elem.contents())($scope);
}
};
});

Resources