generating a unique id and class for variable number of directives - angularjs

Here's a jsbin illustrating the issue, but I will explain it also below with code. I am making a custom directive in AngularJs that will be inserted once for every entry in the database, which I iterate over using ng-repeat as you see here.
<div ng-controller="DemoCtrl as demo">
<h1> Hello {{ name }}
<div class="some-list" ng-repeat="customer in customers">
<div id="not-unique" class="not-unique" my-dumb-graphic datajson=customer></div>
I need to make a unique class or id
</div>
</div>
In case you didn't notice, the directive tucked into that code is my-dumb-graph, with the corresponding myDumbGraphic name in the code below. In order to insert the graphic, which I will do in the link function of the directive below, I need to be able to select a unique id or class in the html above, and I will need to be able to select it from within the link function in the directive, so somehow need to reference the id from the html in the js. You can see in the jsbin that inside the link function of the directive, the id is not yet unique (i.e. the dynamic part hasn't been computed yet), even though it's eventually unique by the time it's rendered to the dom.
<div id="not-unique" class="not-unique" my-dumb-graphic datajson=customer></div>
Rest of the code
var app = angular.module('jsbin', ['ngRoute'])
.config(function ($routeProvider) {
'use strict';
var routeConfig = {
controller: 'DemoCtrll'
};
$routeProvider
.when('/', routeConfig)
.otherwise({
redirectTo: '/'
});
});
app.controller('DemoCtrl', function($scope, $routeParams) {
$scope.name = "World";
$scope.customers = [
{
name: 'David',
street: '1234 Anywhere St.'
},
{
name: 'Tina',
street: '1800 Crest St.'
},
{
name: 'Michelle',
street: '890 Main St.'
}
];
});
app.directive('myDumbGraphic', function () {
return {
restrict: 'A',
scope: {
datajson: '='
},
link: function (scope, elem, attrs) {
...code to insert my dumb graphic...need to select unique id or class or both...need to select unique id from here
}
};
});
Update
As several people have suggested, there are multiple ways to create a dynamic id for a div, however, the dynamic part of the div id won't have computed by the time I need it in the link function. For example, If, following suggestion of other answer, I set id in the html to this <div id="id_{{::id}}" then the dynamic part of it won't have computed by the time I need it to inside the link function, although if I inspect the dom after it's rendered it has computed. In the link function, I can access the div through the elem like this "#"+elem[0].id; and log statements show that at that time it hasn't computed- this is what log statements show for "#"+elem[0].id; ----> #id_{{::id}}

The directive link function has the corresponding element pass in as a parameter, so you already have the selector:
link: function(scope, element, attrs) {
}
Alternatively, you could take leverage your $scope.customers array keys as unique ids and set view elements, pass the id into the directive, and use that as a selector (this assumes you jQuery loaded):
<h1> Hello {{ name }} </h1>
<!-- use customers array index -->
<div class="some-list" ng-repeat="(customerId, customer) in customers">
<!-- append customer id to this element id so it is unique -->
<div id="directive-element-selector-{{customerId}}" class="not-unique" my-dumb-graphic datajson=customer customer-array-id=customerId></div>
<div id="inner-selector-{{customerId}">
I need to make a unique class or id
</div>
</div>
</div>
directive:
app.directive('myDumbGraphic', function () {
return {
restrict: 'A',
scope: {
datajson: '=',
customerArrayId: '='
},
link: function (scope, elem, attrs) {
$('#directive-element-selector-' + customerArrayId) // jQuery selector, this should equal elem pass in as link function param
$('#inner-selector-' + customerArrayId) // inner selector
}
};
});

You can use the $scope $id property which will be unique for each directive and scope in your system, so you can do something like
<div class="myclass_{{::$id}}"></div>
or something like that
Note:
the :: is for one time binding

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.

Two way binding in custom directive with ng-repeat

I basically have a list of items that are stored as an array of objects. Some of these items are "children" of others. This is denoted by a "parentid" setting in each object. This only means that I want to display children as a nested list within the parent.
So, I start by iterating through the array and find one with no parent. When I find it, I call a directive that takes that ID and then again iterates through the main array and finds any item whose parentID is identical to the one in question and displays it. There can be multiple levels of children so that the directive is called recursively.
This works fine and displays all of the children, but I want to be able to select any one of these items and see that ID or object at the top level. This is not working. I pass a variable into each directive with two way binding but no parent is ever updated. I understand the primitive problem and I am passing an object but that is not working. I think that I need to do something with the repeats, but I am not sure what. I do not think transclusion is the issue.
In the demo below, I need to display the updated var3 variable whenever I click on an item.
Initial HTML
<div>{{var3}}</div>
<div>
<my-directive var1="item1"
var2="item2"
var3="item3">
</my-directive>
</div>
Directive
(function ()
{
'use strict';
angular
.module('app.core')
.directive('myDirective', myDirective);
function structureContent($templateRequest, $compile)
{ return {
restrict : 'E',
scope:{
var1:'=',
var2:'#',
var3:'='
},
controller : ["$scope", "$element", "$attrs",
function($scope, $element, $attrs) {
}
],
link : function(scope, element, attrs){
scope.$watch('worksheet',
function(){
$templateRequest("myTemplate.html").then(function(html){
var template = angular.element(html);
element.html(template);
$compile(template)(scope);
});
}, true
);
}
}
};
})();
Directive HTML
<div>
<div ng-click="var3=thisItem.itemid;">(Display item) </div>
// var3 is what I want to be able to pull out at the top level
</div>
<div ng-repeat="thisObj in myArray | orderBy:'order' track by thisObj.itemid"
ng-switch on="thisObj.type_id"
ng-if="thisObj.content.parentid==thisItem.itemid"
class="subSection" >
<div ng-switch-when="1">
<my-directive var1="{{var1}}"
var2="var2"
var3="var3">
</my-directive>
</div>
</div>

AngularJS: one or many directives?

My problem: I have product object like this: {id: 1, category: 'Television', price: '$2000',...}, then I create product directive. User can buy a product by using product scope function buy(quantity). But I have many products, create scope with this function for every product might be waste of memory? Should I create additional directive, productBuyer has method: buy(product, quantity), then product directive require: '^productBuyer' will be put within it? Which design is better when application scales? Or is there any else way better?
More: I don't put buy to factory because product has to display error message if buy fail (for many reasons: out-of-date product, product-warehouse is empty, don't deliver to user's location...), this process method is passed to product directive.
I would restrict the use of directives to self-contained and reusable bits of functionality. In your example, put common functionality in the directive, but keep functionality related to the broader app in the view's controller - not in the directive.
app.js
angular.module("myApp", []).
.controller("shoppingCtrl", function($scope, productSvc){
productSvc.getProducts()
.then(function(products){
$scope.products = products;
});
$scope.buyProduct = function(product){
productSvc.buy(product)
.then(function(){ /* do something */ });
}
})
.directive("product", function(){
return {
restrict: "EA",
scope: {
product: "=",
onBuy: "&"
},
templateUrl: "productDirective.html",
controller: function($scope){
$scope.clickOnBuy = function(){
$scope.onBuy();
}
}
};
});
productDirective.html
<div>
<div>{{product.title}}</div>
<div>{{product.price}}</div>
<button ng-click="clickOnBuy()">Buy</button>
</div>
index.html
Finally, in your HTML you can do:
<div ng-controller="shoppingCtrl">
<div ng-repeat="item in products" product="item" on-buy="buyProduct(item)"></div>
<hr/>
</div>

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

Angular, ng-repeat to build other directives

I'm building a complex layout, that takes a JSON document and then formats it into multiple rows, with each row then having more rows and/or combinations of rows/columns inside them.
I'm new to Angular and am just trying to get to grips with Directives. They are easy to use for very simple things, but quickly become very difficult once you need to anything more complicated.
I guess I'm doing this the wrong way around, but is there a way to simply add the name of a directive (in the example below, I've used ) and get that directive to be rendered on an ng-repeat?
Maybe the same way that you can use {{{html}}} instead of {{html}} inside of mustache to get a partial to render as HTML and not text.
As expected, the example below simply writes the name of the directive into the dom. I need Angluar to take the name of the directive, understand it, and then render before before it is written. Due to the complex layout of the page I need to design, I could be rendering many different directives, all inside each other, all from 1 JSON document (which has been structured into different rows and then row / column combinations).
Example code that renders the name of the directive to the page, but gives you an idea of how I'd like to write a solution the problem...
<div app-pages></div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js"></script>
<script>
var app = angular.module("app", ['main']);
angular.module('main', [])
.controller("appPageController", ['$scope', function( $scope ){
$scope.pages = [];
var page1 = {
title: 'Page 1',
directive: '<app-page-type-1>'
};
var page2 = {
title: 'Page 2',
directive: '<app-page-type-2>'
};
$scope.pages.push(page1);
$scope.pages.push(page2);
}])
.directive("appPageType2", function factory() {
console.log('into page type 2');
return {
replace: true,
template: 'This is the second page type'
};
})
.directive("appPageType1", function factory() {
console.log('into page type 1');
return {
replace: true,
template: 'This is the first page type'
};
})
.directive("appPages", function factory() {
console.log('into pages');
return {
replace: true,
template: '<ul><li ng-repeat="page in pages">{{page.directive}}</li></ul>'
};
});
</script>
This is one possible alternative to your idea. The idea is to append the directive you defined in page object for each html element inside the ng-repeat. Please take a look at the demo. Hope it helps.
<div ng-app="myApp" ng-controller="appPageController">
<ul>
<li ng-repeat="page in pages" app-pages></li>
</ul>
</div>
.directive("appPages", function ($compile) {
console.log('into pages');
return {
replace: true,
link: function (scope, elements, attrs) {
var html = '<div ' + scope.page.directive + '></div>';
var e = angular.element(html);
elements.append(e);
$compile(e)(scope);
}
};
});
Demo

Resources