AngularJS: one or many directives? - angularjs

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>

Related

generating a unique id and class for variable number of directives

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

What is the "right" way in Angularjs of doing "master-detail" inter directive communication

I have a directive that displays a list of "master" items and when the user clicks on one of these items I want any "details" directives on the page (there could be more than one) to be updated with the details of the currently selected "master" item.
Currently I'm using id and href attributes as a way for a "details" directive to find its corresponding master directive. But my impression is that this is not the angular way, so if it's not, what would be a better solution?
I appreciate that typically when the issue of inter-communication between directives is raised then the obvious solutions are either to use require: "^master-directive" or to use a service, but in this case the directives are not in the same hierarchy and I don't think using a service is appropriate, as it would make the solution more complicated.
This is some illustrative code showing what I'm doing currently.
<div>
<master-list id="master1"></master-list>
</div>
<div>
<details-item href="#master1" ></details-item>
</div>
In the master-list directive when an item is selected I set an attribute to indicate the currently selected master item:
attrs.$set('masterListItemId',item.id);
In the details-item directive's link function I do:
if (attrs.href) {
var id = attrs.href.split('#')[1];
var masterList = angular.element(document.getElementById(id));
if (masterList) {
var ctrl = masterList.controller('masterList');
ctrl.attrs().$observe('masterListItemId',function(value) {
attrs.$set('detailItemId',value);
});
}
}
attrs.$observe('detailItemId',function(id) {
// detail id changed so refresh
});
One aspect that put me off from using a service for inter-directive communication was that it is possible (in my situation) to have multiple 'masterList' elements on the same page and if these were logically related to the same service, the service would end up managing the selection state of multiple masterList elements. If you then consider each masterList element had an associated detailItem how are the right detailItem elements updated to reflect the state of its associated masterList?
<div>
<master-list id="master1"></master-list>
</div>
<div>
<master-list id="master2"></master-list>
</div>
<div>
<details-item href="#master1" ></details-item>
</div>
<div>
<details-item href="#master2" ></details-item>
</div>
Finally I was trying to use directives, rather than using controller code (as has been sensibly suggested) as I'd really like the relationship between a masterList and its associated detailItems to be 'declared' in the html, rather than javascript, so it is obvious how the elements relate to each other by looking at the html alone.
This is particularly important as I have users that have sufficient knowledge to create a html ui using directives, but understanding javascript is a step too far.
Is there a better way of achieving the same thing that is more aligned with the angular way of doing things?
I think I would use a service for this. The service would hold the details data you care about, so it would look something like this.
In your master-list template, you might have something like a list of items:
<ul>
<li ng-repeat"item in items"><a ng-click="select(item)">{{item.name}}</a></li>
</ul>
...or similar.
Then in your directives, you would have (partial code only)
.directive('masterList',function(DetailsService) {
return {
controller: function($scope) {
$scope.select = function(item) {
DetailsService.pick(item); // or however you get and retrieve data
};
}
};
})
.directive('detailsItem',function(DetailsService) {
return {
controller: function($scope) { // you could do this in the link as well
$scope.data = DetailsService.item;
}
};
})
And then use data in your details template:
<div>Details for {{data.name}}</div>
<ul>
<li ng-repeat="detail in data.details">{{detail.description}}</li>
</ul>
Or something like that.
I would not use id or href, instead use a service to retrieve, save and pass the info.
EDIT:
Here is a jsfiddle that does it between 2 controllers but a directive would be the same idea
http://jsfiddle.net/u3u5kte7/
EDIT:
If you want to have multiple masters and details, leave the templates unchanged, but change your directive controllers and services as follows:
.directive('masterList',function(DetailsService) {
return {
controller: function($scope) {
$scope.select = function(item) {
DetailsService.pick($scope.listId,item); // or however you get and retrieve data
};
}
};
})
.directive('detailsItem',function(DetailsService) {
return {
controller: function($scope) { // you could do this in the link as well
$scope.data = DetailsService.get($scope.listId).item;
}
};
})
.factory('DetailsService',function(){
var data = {};
return {
pick: function(id,item) {
data[id] = data[id] || {item:{}};
// set data[id].item to whatever you want here
},
get: function(id) {
data[id] = data[id] || {item:{}};
return data[id];
}
};
})
I would opt for a different approach altogether without directives. Directives are ideal for DOM manipulation. But in this case I would stick to using just the template and a controller that manages all the data and get rid of the directives. Use ng-repeat to repeat the items
Check out this fiddle for an example of this: http://jsfiddle.net/wbrand/2xrne4k3
template:
<div ng-controller="ItemController as ic">
Masterlist:
<ul><li ng-repeat="item in ic.items" ng-click="ic.selected($index)">{{item.prop1}}</li></ul>
Detaillist:
<ul><li ng-repeat="item in ic.items" >
{{item.prop1}}
<span ng-if="item.selected">SELECTED!</span>
</li></ul>
</div>
controller:
angular.module('app',[]).controller('ItemController',function(){
this.items = [{prop1:'some value'},{prop1:'some other value'}]
this.selectedItemIndex;
this.selected = function(index){
this.items.forEach(function(item){
item.selected = false;
})
this.items[index].selected = true
}
})

Is one of these the more "Angular" way of communicating with a directive?

I asked this question on the Programmer's stack exchange, but didn't get any replies, so I thought I'd try my luck here...
I am working on a project where I would like to encapsulate a directive library and distribute it to other developers to use. I would like to keep the changes to the model within this encapsulated code, so I don't really want the dev's changing the scope variables outside of the lib.
In my code, I have 2 different approaches to communicating with my lib from the parent controller.
The first theory is to create a lib that contains a directive and a service. The parent controller would call the service, which would handle all of the changes to the lib model and the directive would react depending on these changes.
The second theory is to put all the functions to change the model in the directive itself and call the directive scope on the parent to make the changes.
Here is a plunker that shows what I'm asking in better detail. It's a simple example, but illustrates the 2 different methods.
http://plnkr.co/edit/CR350Vx7NiHs5tkjNWZL?p=preview
I'm leaning towards the second method as it just seems cleaner to implement from a development scenario.
Any advice from the Angular experts out there?
Plunker Html:
<body ng-app="myApp">
This is Example 1 - Using a service to modify directive
<div ng-controller="Example1Ctrl">
<example1-directive ng-model='example1Model'></example1-directive>
<br>
<br>
<input type="button" value="Change Example 1" ng-click='changeExample1()' />
</div>
<br>
<br>
This is Example 2 - Modifying directive in the scope of the directive
<div ng-controller="Example2Ctrl">
<example2-directive ng-model='example2Model'></example2-directive>
<br>
<br>
<input type="button" value="Change Example 2" ng-click='changeExample2()' />
</div>
</body>
Plunker js
var app = angular.module("myApp", []);
//--------------------------------------------------
//-------- This is example 1
//--------------------------------------------------
app.controller("Example1Ctrl", function($scope, example1Svc) {
$scope.example1Model = {
value: "Example 1 - Original Value"
}
$scope.changeExample1 = function() {
example1Svc.change($scope.example1Model, "Example 1 - Changed Value");
}
});
/// This part would be encapsulated in a lib
app.directive("example1Directive", function() {
return {
restrict: "E",
scope: {
model: "=ngModel"
},
template: "{{model.value}}"
}
});
app.service("example1Svc", function() {
this.change = function(example1Model, newValue) {
example1Model.value = newValue;
}
})
// End lib
//--------------------------------------------------
//-------- This is example 2
//--------------------------------------------------
app.controller("Example2Ctrl", function($scope, example1Svc) {
$scope.example2Model = {
value: "Example 2 - Original Value"
}
$scope.changeExample2 = function() {
$scope.example2Model.change("Example 2 - Changed Value");
}
});
/// This part would be encapsulated in a lib
app.directive("example2Directive", function() {
return {
restrict: "E",
scope: {
model: "=ngModel"
},
template: "{{model.value}}",
controller: function ($scope) {
$scope.model.change = function(newValue) {
$scope.model.value = newValue;
}
}
}
});
// end lib
I'm somewhat confused by your example #1. What does exampleSvc.change do?
Example #2 definitely goes against MVVM best practice, as it couples the controller with the view. Controllers (of views) should be view-agnostic. They should only change the ViewModel to reflect the current state of the app. The View would then react (however the View chooses to) to changes in the ViewModel.
In particular, these lines "offend" the best practice in my mind:
$scope.model.change = function(newValue) {
$scope.model.value = newValue;
}
Now your controller relies on the view to define what the function does (or whether it is defined to begin with). Also, what if another directive decides to change the .change function?
EDIT:
Take a look at this SO question, and in particular an answer by Mark.
EDIT #2:
There is an interesting case for when some event needs to reach whichever directives or child controllers that could be interested. Use $scope.$broadcast (in controller) and $scope.$on (in directive) to handle. Here's a plunker
I'm going to agree with #New Dev and add a couple more thoughts. If you're building a directive library you don't want to also bundle controllers that the consumer of the library has to use. Your directives should be more or less self contained and provide enough of an api to be extensible and potentially used in other directives.
What does this mean? Your directives may want to define a controller so that they can be injected into other directives. E.g.
//your library
directiveModule.directive("example1Directive", function() {
return {
controller: function($scope, $element, $attrs) {
...
},
...
}
});
-
//application
app.directive("appDirective", function() {
return {
require: '?example1Directive',
link: function(scope, element, attrs, example1Directive) {
...
}
});
You may also want to specify various options that can be set on your directive with parameters e.g.
<div example1-directive="{opt1: 'val', opt2: scopeProp}"></div>
Your directive would then need to parse the the attribute and execute on the scope to generate the options. There's lots more you can do, I'd suggest taking a look at ngmodules.org and see what other people are doing.

Place element outside the controller and have them still work AngularJS

How can I control variables and elements using filters that are outside the controller?
I have set up an example to better help me explain my question http://embed.plnkr.co/7E5Ls3oH0q4HuEZsewJL/
You will see I have a div that is being toggled using ng-show and then inside a search input that filters a list of names. The problem arrises when I need to take the toggled div and the search filter and put it outside the 'MainCtrl'. Is there a way that I can have these sitting outside the controller but still interacting with the content of the 'MainCtrl'?
There are several ways to communicate between components which aren't prototypically linked in your app. You could use Angular events to broadcast your search and then handle it from your controller. Or, you could even use $rootScope in both components as a global space both can use to store variables and methods. These are bad ideas - they will make your life harder down the road.
Instead, whenever you need to share information across controllers and/or directives which aren't directly linked, your first thought should to create a service.
Such a service might look like this:
app.factory('Search', function() {
var search = {
results: [],
query: '',
showDetails: false
};
return search;
});
You would inject it into both the controller and the controller or directive (I chose to create a directive) for the search box and initialize the service to a scope variable in each:
app.controller('MainCtrl', function ($scope, friendsFactory, Search) {
$scope.friends = friendsFactory.query();
$scope.search = Search;
});
app.directive('search', function(Search){
return {
restrict: 'E',
scope: {},
templateUrl: 'directive-search.html',
link: {
pre: function(scope, elem, attrs) {
scope.search = Search;
}
}
}
});
You could use that variable in your views, as the model for the search box and the filter for your ng-repeat:
Directive Template
<div ng-class="'details'" ng-show="search.showDetails">
<label for="">Search Names</label>
<input type="text" ng-model="search.query" />
</div>
Filter
<div class="content" ng-controller="MainCtrl">
<a ng-click="search.showDetails = !search.showDetails"> Click Me</a>
<div ng-repeat="friend in friends | filter:search.query | limitTo: 5">
{{friend.name}}
</div>
</div>
Plunker Demo
Note: this simple implementation allows for one individual search/results pair on the page. If you need to have more than one, you will need to further develop the service to allow for more than one instance.

AngularJS: What is the best way to bind a directive value to a service value changed via a controller?

I want to create a "Header" service to handle the title, buttons, and color of it.
The main idea is to be able to customize this header with a single line in my controllers like this:
function HomeCtrl($scope, Header) {
Header.config('Header title', 'red', {'left': 'backBtn', 'right': 'menuBtn'});
}
So I created a service (for now I'm only focussing on the title):
app.service('Header', function() {
this.config = function(title, color, buttons) {
this.title = title;
}
});
...And a directive:
app.directive('header', ['Header', function(Header) {
return {
restrict: 'E',
replace: true,
template: '<div class="header">{{title}}</div>',
controller: function($scope, $element, $attrs) {
$scope.$watch(function() { return Header.title }, function() {
$scope.title = Header.title;
});
}
};
}]);
So, this actually works but I'm wondering if there are no better way to do it.
Especially the $watch on the Header.title property. Doesn't seem really clean to me.
Any idea on how to optimize this ?
Edit: My header is not in my view. So I can't directly change the $scope value from my controller.
Edit2: Here is some of my markup
<div class="app-container">
<header></header>
<div class="content" ng-view></div>
<footer></footer>
</div>
(Not sure this piece of html will help but I don't know which part would actually...)
Thanks.
If you are using title in your view, why use scope to hold the object, rather than the service? This way you would not need a directive to update scope.header, as the binding would update it if this object changes
function HomeCtrl($scope, Header) {
$scope.header = Header.config('Header title', 'red', {'left': 'backBtn', 'right': 'menuBtn'});
}
and refer to title as
<h1>{{header.title}}</h1>
Update
Put this in a controller that encapsulates the tags to bind to the header:
$scope.$on("$routeChangeSuccess", function($currentRoute, $previousRoute) {
//assume you can set this based on your $routeParams
$scope.header = Header.config($routeParams);
});
Simple solution may be to just add to rootScope. I always do this with a few truly global variables that every controller will need, mainly user login data etc.
app.run(function($rootScope){
$rootScope.appData={
"header" : {"title" : "foo"},
"user" :{}
};
});
.. then inject $rootScope into your controllers as warranted.

Resources