Taking function from controller and placing in directive - angularjs

I've noticed my angular controller is growing and ideally should be used for passing data.
I have a function that is currently contained within my controller that is called from my HTML to calculate how many months worth of data has been displayed (within a 12 month period) and if less than 12, return the remaining as empty/no payment:
JS:
$scope.getEmptyCells = function(len){
var emptyCells = [];
for(var i = 0; i < 12 - len; i++){
emptyCells.push(i);
}
return emptyCells;
}
HTML:
<table>
<tr ng-repeat="payments in MyPayments">
<th>{{payments.name}}</th>
<td ng-repeat="paymentAmount in payments.Details.slice(0, 12)">
{{ paymentAmount }}
</td>
<td ng-repeat="emptyCell in getEmptyCells(payments.Details.length)" class="empty">
No Payment
</td>
</tr>
</table>
myNewDirective:
app.directive('ngGetEmptyCells', function () {
return {
restrict: 'EA',
template: '<td ng-repeat="emptyCell in getEmptyCells(payments.Details.length)" class="empty">No Payment</td>',
controller: [
function (len) {
var emptyCells = [];
console.log("ngGetEmptyCells - STARTED");
console.log("len = " + len);
for (var i = 0; i < 12 - len; i++) {
emptyCells.push(i);
}
return emptyCells;
}
]
};
});
MY new HTML:
<table>
<tr ng-repeat="payments in MyPayments">
<th>{{payments.name}}</th>
<td ng-repeat="paymentAmount in payments.Details.slice(0, 12)">
{{ paymentAmount }}
</td>
<ng-get-empty-cells></ng-get-empty-cells>
</tr>
</table>
My fiddle: http://jsfiddle.net/oampz/8hQ3R/9/

Your controller (in your directive) is incorrect. You can set the method getEmptyCells on your scope of your directive if you do it like this instead.
controller: function($scope) {
$scope.getEmptyCells = function(len){
var emptyCells = [];
for(var i = 0; i < 12 - len; i++){
emptyCells.push(i);
}
return emptyCells;
};
}
Although since you do not declare an isolated scope in your directive (nothing wrong with that), your directive should be able to access the parent scope where you could have left your getEmptyCells method. Actually not relying on the parent scope helps keeping your directives modular.
If this fails to work, provide a plunker (or equivalent) example.
EDIT: You really should NOT prefix your own directives with ng as those are considered native Angular directives
EDIT: I moved your fiddle to plunker as Angular seems to work better there. I posted a working example:
http://plnkr.co/edit/e11zA8LKvoPTgTqW2HEE
I changed the code to use attributes instead of elements. There seems to be some problems for angular to correctly insert the td's into the row if you are using E instead of A.
EDIT: I changed the syntax <td get-empty-cells payments="payment"> to <td get-empty-cells="payment"> for easier usage. You can view the old plunker version (through its interface) for comparison and perhaps help understanding.

You can pass data into a directive by reference or value. You have to pass at least getEmptyCells by reference in order to be able to call it. Here is how you do it:
http://jsfiddle.net/8hQ3R/12/
Using directive:
<my-empty-cells get-empty-cells="getEmptyCells" payments="payments"></my-empty-cells>
Declaring isolated scope with getEmptyCells passed by reference and payments by value:
scope: {
getEmptyCells: '=',
payments: '#'
}
BUT:
You're going to have problems with this directive template because it has to have single root element and you're having multiple table rows. I would recommend iterating via 1-12 or even months array with ngRepeat and using separate scope function to extract either actual data or empty cell placeholder from model.

Related

JSON Data not loading on ng-init on div, it's loading on ng-click on button

I have data attached to $scope object in controller, just like this
$scope.result = [];
$scope.jsonData = function () {
var result = [];
var keys = $scope.data[0];
for (var i = 1; i < $scope.data.length; i++) {
var item = {};
item[keys[0]] = $scope.data[i][0];
item[keys[1]] = $scope.data[i][1];
item[keys[2]] = $scope.data[i][2];
$scope.result.push(item);
}
console.log($scope.result);
};
I am able to access this data only while clicking button in HTML using ng-click directive
<button ng-click="jsonData()">
<table border="1">
<tr ng-repeat="x in result">
<td>{{x.Name}}</td>
<td>{{x.Age}}</td>
<td>{{x.Address}}</td>
</tr>
</table>
</button>
However, I am unable to access json data using ng-init directive. Am I doing anything wrong?
<div ng-init="jsonData()">
<table border="1">
<tr ng-repeat="x in result">
<td>{{x.Name}}</td>
<td>{{x.Age}}</td>
<td>{{x.Address}}</td>
</tr>
</table>
</div>
Why are you using ng-init this way? Take a look at the documentation:
ng-init documentation
This doesn't look like a valid use case for ng-init unless there is more code to your controller I cannot see.
I would get rid of the ng-init on your div and just run the function directly in your controller by calling:
$scope.jsonData();
right after you define the function. Like this:
$scope.result = [];
$scope.jsonData = function () {
var result = [];
var keys = $scope.data[0];
for (var i = 1; i < $scope.data.length; i++) {
var item = {};
item[keys[0]] = $scope.data[i][0];
item[keys[1]] = $scope.data[i][1];
item[keys[2]] = $scope.data[i][2];
$scope.result.push(item);
}
console.log($scope.result);
};
$scope.jsonData();
The likely cause of the problem is that the ng-init directive is executing before the data arrives from the server.
In the controller, chain from the promise that puts the data on scope:
$http.get(url).then(function(response) {
$scope.data = response.data;
}).then(function() {
$scope.jsonData();
});
In an MV* framework, the controller creates the model and then the framework maps the model to the DOM. The framework then collects events from the user and informs the controller which then updates the model.
Having the DOM initiate, by ng-init, a change to the model is a violation of the ZEN of Angular.
It is a very good idea to decouple DOM manipulation from app logic. This dramatically improves the testability of the code. By coupling the app sequencing and logic to the ng-init directive, the code becomes fragile, difficult to test, hard to maintain, and error prone.

Angularjs directive creation

I have a set of two functions that I user to perform a lookup to identify a user. In my app I need to do this for multiple roles (i.e: requester, processor, etc.). I think I want to create a directive so that I can reuse the code without having to copy and change it for each role - if I understand the concept of a directive.
I made an attempt but instead of an input field actually showing up for me to enter a name in, I see the name of the template html file. I am obviously doing something wrong, but since I am very new to directives (custom and creating them), I am sure I didn't do it right.
What I want is the ability to type a person's name in, and as I type, it should call a function that is doing the actual search. While a match is being looked for, I want a sliding bar to appear to indicate something is occurring. After I choose the appropriate user, I want the name and id retrieved to be stored in my model and the bar will close.
This is what I put in my HTML to call my directive:
<name-user idModel="request.requesterID" nameModel="request.requester"></name-user>
the requesterId and requester are where I want the results....
My directive:
app.directive('nameUser', function() {
return {
restrict: 'E',
require: {
idModel : '=',
nameModel : '='
},
template : 'app/views/nameUser.html',
link : function($scope){
$scope.searchName = function(searchQuery, bar){
var bar = "#loadingBar" + bar;
if(searchQuery && searchQuery.length >= 3){
$(bar).slideDown();
$http.get("/read/userinfo/" + searchQuery ).success(function(data){
$scope.nameSearchResults = data;
$(bar).slideUp();
});
}
}
$scope.selectName = function(pl){
nameModel.$setViewValue(pl.name);
idModel.$setViewValue(pl.user_id);
$scope.nameSearchResults = {};
}
}
};
});
My template has:
<input type="text" class="form-control input-md" ng-model="nameModel" ng-model-options='{ debounce: 800 }' ng-change="searchName(nameModel,'88')" value="nameModel"/>
<div class="row" style="padding:20px;display:none;" id="loadingBar88">
<div class="col-md-6 col-md-offset-5">
<img alt="Brand" src="files/img/loader.gif">
</div>
</div>
<table class="table table-bordered table-hover" ng-show="nameSearchResults.length">
<tr>
<th>User ID</th>
<th>User Name</th>
</tr>
<tr ng-repeat="entry in nameSearchResults" ng-click="selectName(entry)">
<td>{{entry.user_id}}</td>
<td>{{entry.name}}</td>
</tr>
Currently, each time I need a different user id/name for a role, I have to copy the functions and change the names and the bar number...I really think there has to be a way to make this so I can call it multiple times and just pass in the model values I want. While the code above does not give me any errors, it does not provide me the input field and functionality to perform the lookup....please help.
I had to change my directive a little based on the issues pointed out, plus one I figured out after those changes were made. I needed changes to my directive and a change to my HTML, so I will post the final versions that appear to be working.
app.directive('nameUser', function() {
return {
restrict: 'E',
scope: { //CHANGED TO 'SCOPE'
idModel : '=',
nameModel : '='
},
templateUrl : 'app/views/nameUser.html', // ADDED 'Url'
link : function($scope,nameModel,idModel){ // ADDED THE TWO SCOPE NAMES
$scope.searchName = function(searchQuery, bar){
var bar = "#loadingBar" + bar;
if(searchQuery && searchQuery.length >= 3){
$(bar).slideDown();
$http.get("/read/userinfo/" + searchQuery).success(function(data){
$scope.nameSearchResults = data;
$(bar).slideUp();
});
}
}
$scope.selectName = function(pl){
$scope.nameModel = pl.name; //CHANGED BY ADDING $SCOPE AND ASSIGNING THE VALUE
$scope.idModel = pl.user_id; //CHANGED BY ADDING $SCOPE AND ASSIGNING THE VALUE
$scope.nameSearchResults = {};
}
}
};
My HTML changed (inModel to id-model and nameModel to name-model):
<name-user id-model="request.requesterID" name-model="request.requester"></name-user>
Thanks again for the help.

Angular $watch just parent object instead of its multiple children

I have a div, listing properties of the object POI = {"address":"Martinsicuro (TE), Italy", "phone":"+39 333 45657", "website':'http://mysite.it"}. The object POI si owned by a Service. The directive's controller has the function getPoi() that gets the POI from the service, and returns it to the directive.
My current HTML is something like this:
<table ng-controller="Controller as ctrl">
<tr> <!-- address -->
<td>{{ctrl.getPoi().address}}</td>
</tr>
<tr> <!-- phone -->
<td>{{ctrl.getPoi().phone}}</td>
</tr>
<tr> <!-- website -->
<td>
<a ng-href="{{ctrl.getPoi().website}}">
{{ctrl.getPoi().website}}
</a>
</td>
</tr>
</table>
The controller
.controller('Controller', function(CurrentPoiService)
{
this.getPoi = function()
{ return CurrentPoiService.POI; }
}
The service
.service('CurrentPoiService', function()
{
this.POI = {"address":"Martinsicuro (TE), Italy", "phone":"+39 333 45657", "website':'http://mysite.it"}
}
In this way I am adding 3 watchers. Is there a way to add just 1 watcher, since it's the same parent object? Here it is a JSFiddle
Thank you
[UPDATE 1]
This is the (still not working) JSFiddle using the solution proposed by #Charlie
[UPDATE 2]
This is the working JSFiddle
As Claies has mentioned in a comment, you should never call your data from
the view through a function this way.
In this scenario you can create a watch for the POI object with the objectEquality argument true to watch the properties of the object in a single $watch. Then find your elements inside the listener and change the value in the way you want.
$scope.$watch('POI', function(){
//Assume that $scope.propertyIndex is the current property to display
angular.element($document[0].querySelector("#myTd" + $scope.propertyIndex)).html(POI.address);
angular.element($document[0].querySelector("#myTd" + $scope.propertyIndex)).html(POI.phone);
//etc...
}, true);
You have a better control this way. But please keep in mind that this method is not suitable if POI is a complex object.
UPDATE:
Here is a working example of showing a random number every second using a watch and a factory. You should be able to learn from this and apply it to your project.
myApp.controller('myController', function($scope, dataSource) {
$scope.poi = {rand: 0};
$scope.$watch('poi', function() {
$('#rand').html($scope.poi.rand);
}, true);
dataSource.open($scope);
});
myApp.factory('dataSource', function($interval) {
return {
open: function(scope){
$interval(function() {
scope.poi.rand = Math.random();
}, 1000);
}
}
});
Try inside your controller :
$scope.POI = ctrl.getPoi();
HTML :
<tr> <!-- address -->
<td>{{POI.address}}</td>
</tr>
<tr> <!-- phone -->
<td>{{POI.phone}}</td>
</tr>

Preserving Scope in ng-repeat ( not wanting child scope )

I might be missing something conceptually but I understand that ng-repeat creates child scopes but for my scenario this is undesirable. Here is the scenario. I have a 3way bind to a firebase dataset. The object is an object with n sub objects. In my current code structure I use ng-repeat to iterate and render these objects with a custom directive. The issue is that these objects are meant to be "live" ( meaning that they are 3-way bound. The highest level object is bound with angularfire $bind ).
So the simple scenario in my case would be where the ng-repeat created scope was not isolated from the scope that it was created from.
I am looking for ideas on how to do this? Or suggestions on other approaches.
This won't be a complete answer, but I can help with the angularFire portion, and probably an angular guru can fill in the blanks for you (see //todo).
First of all, don't try to share scope. Simple pass the variables you want into the child scope. Since you'll want a 3-way binding, you can use & to call a method on the parent scope.
For example, to set up this pattern:
<div ng-repeat="(key,widget) in widgets">
<data-widget bound-widget="getBoundWidget(key)"/>
</div>
You could set up your directive like this:
.directive('dataWidget', function() {
return {
scope: {
boundWidget: '&boundWidget'
},
/* your directive here */
//todo presumably you want to use controller: ... here
}
});
Where &boundWidget invokes a method in the parent $scope like so:
.controller('ParentController', function($scope, $firebase) {
$scope.widgets = $firebase(/*...*/);
$scope.getBoundWidget = function(key) {
var ref = $scope.widgets.$child( key );
// todo, reference $scope.boundWidget in the directive??
ref.$bind(/*** ??? ***/);
};
});
Now you just need someone to fill in the //todo parts!
You still have access to the parent scope in the repeat. You just have to use $parent.
Controller
app.controller('MainCtrl', ['$scope', function ($scope) {
$scope.someParentScopeVariable = 'Blah'
$scope.data = [];
$scope.data.push({name:"fred"});
$scope.data.push({name:"frank"});
$scope.data.push({name:"flo"});
$scope.data.push({name:"francis"});
}]);
HTML
<body ng-controller="MainCtrl">
<table>
<tr ng-repeat="item in data | filter: search">
<td>
<input type="text" ng-model="$parent.someParentScopeVariable"/>
<input type="text" ng-model="item.name">
</td>
</tr>
</table>
</body>
Plunker

How to dynamically add in custom directive from other custom directive

I've created a directive that has similar functionality to datatables, but it's been customized for our app. One thing I have in my directive scope is columnDefinitions. Each object in that array has a property called data. I've got it set up so that if it is set to a string, it looks for that property on the entity, and it's a function, it will call that function with the entity. So basically this:
scope.getEntityData = function(entity, currColumnDefinitionData) {
var entityData = null;
if (angular.isString(currColumnDefinitionData))
{
entityData = entity[currColumnDefinitionData];
}
else if(angular.isFunction(currColumnDefinitionData))
{
entityData = currColumnDefinitionData(entity);
}
else
{
$log.error("Column defintion data property must be a string or a function. Cannot get entity data.");
}
return entityData;
};
And then in my directive template, something like this:
<tr ng-repeat="currEntity in entities">
<td ng-repeat="currColDef in columnDefinitions">
{{getEntityData(currEntity, currColDef.data)}}
</td>
</tr>
This works great when I just need to output a string. I now have a case where I want it to insert a directive for the data in that column. I first just had the data property equal the HTML string. For example:
data: function(entity) {
return '<div my-directive></div>';
},
However, that resulted in the string just being inserted into the table cell (Angular escaping the text for me)
What I'm wanting to know, is how I can set up my directive so that I can get compiled directives into my table cells. I thought about having some way of telling myself it was a directive, and then compiling it with the $compile service, but then I don't know what to return from my function for it all to work right. Any ideas would be much appreciated.
Here's how I would do it
The directive:
angular.module('ui.directives').directive('uiCompile',
[ '$compile', function(compile) {
return {
restrict : 'A',
link : function(scope, elem, attrs) {
var html = scope.$eval('[' + attrs['uiCompile'] + ']')[0];
elem.html(html);
compile(elem.contents())(scope);
}
}
} ]);
The template:
<tr ng-repeat="currEntity in entities">
<td ng-repeat="currColDef in columnDefinitions" ui-compile="currColDef"></td>
</tr>
Basically for each column definition compile the content as a template using the current scope.

Resources