Angularjs directive creation - angularjs

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.

Related

Change vm variable value after clicking anywhere apart from a specific element

When I click anywhere in the page apart from ul element (where countries are listed) and the suggestion-text input element (where I type country name), vm.suggested in controller should be set to null. As a result ul element will be closed automatically. How can I do this?
I've seen Click everywhere but here event and AngularJS dropdown directive hide when clicking outside where custom directive is discussed but I couldn't work out how to adapt it to my example.
Markup
<div>
<div id="suggestion-cover">
<input id="suggestion-text" type="text" ng-model="vm.countryName" ng-change="vm.countryNameChanged()">
<ul id="suggest" ng-if="vm.suggested">
<li ng-repeat="country in vm.suggested" ng-click="vm.select(country)">{{ country }}</li>
</ul>
</div>
<table class="table table-hover">
<tr>
<th>Teams</th>
</tr>
<tr ng-if="vm.teams">
<td><div ng-repeat="team in vm.teams">{{ team }}</div></td>
</tr>
</table>
<!-- There are many more elements here onwards -->
</div>
Controller
'use strict';
angular
.module('myApp')
.controller('readController', readController);
function readController() {
var vm = this;
vm.countryNameChanged = countryNameChanged;
vm.select = select;
vm.teams = {.....};
vm.countryName = null;
vm.suggested = null;
function countryNameChanged() {
// I have a logic here
}
function select(country) {
// I have a logic here
}
}
I solved the issue by calling controller function from within the directive so when user clicks outside (anywhere in the page) of the element, controller function gets triggered by directive.
View
<ul ng-if="vm.suggested" close-suggestion="vm.closeSuggestion()">
Controller
function closeSuggestion() {
vm.suggested = null;
}
Directive
angular.module('myApp').directive('closeSuggestion', [
'$document',
function (
$document
) {
return {
restrict: 'A',
scope: {
closeSuggestion: '&'
},
link: function (scope, element, attributes) {
$document.on('click', function (e) {
if (element !== e.target && !element[0].contains(e.target)) {
scope.$apply(function () {
scope.closeSuggestion();
});
}
});
}
}
}
]);
This is just an example but you can simply put ng-click on body that will reset your list to undefined.
Here's example:
http://plnkr.co/edit/iSw4Fqqg4VoUCSJ00tX4?p=preview
You will need on li elements:
$event.stopPropagation();
so your html:
<li ng-repeat="country in vm.suggested" ng-click="vm.select(country); $event.stopPropagation()">{{ country }}</li>
and your body tag:
<body ng-app="myWebApp" ng-controller="Controller01 as vm" ng-click="vm.suggested=undefined;">
UPDATE:
As I said it's only an example, you could potentially put it on body and then capture click there, and broadcast 'closeEvent' event throughout the app. You could then listen on your controller for that event - and close all. That would be one way to work around your problem, and I find it pretty decent solution.
Updated plunker showing communication between 2 controllers here:
http://plnkr.co/edit/iSw4Fqqg4VoUCSJ00tX4?p=preview
LAST UPDATE:
Ok, last try - create a directive or just a div doesn't really matter, and put it as an overlay when <li> elements are open, and on click close it down. Currently it's invisible - you can put some background color to visualize it.
Updated plunker:
http://plnkr.co/edit/iSw4Fqqg4VoUCSJ00tX4?p=preview
And finally totally different approach
After some giving it some thought I actually saw that we're looking at problem from the totally wrong perspective so final and in my opinion best solution for this problem would be to use ng-blur and put small timeout on function just enough so click is taken in case someone chose country:
on controller:
this.close = function () {
$timeout(()=>{
this.suggested = undefined;
}, 200);
}
on html:
<input id="suggestion-text" type="text" ng-model="vm.countryName" ng-change="vm.countryNameChanged()" ng-blur="vm.close()">
This way you won't have to do it jQuery way (your solution) which I was actually trying to avoid in all of my previous solutions.
Here is plnker: http://plnkr.co/edit/w5ETNCYsTHySyMW46WvO?p=preview

Directive, Link Function, Array, DataBinding Order of things is ambiguous

I have a problem which is very challenging, that i don't even know how to define, so I'm going to give an example and hopefully you will be able to explain what am i doing wrong.
My HTML is:
<tr ng-repeat="object in Objects">
<td>
<div table-checkbox row-index="{{$index}}" bind-model="selectedChecks[$index]" selected-Checks="selectedChecks">
</div>
</td>
</tr>
The selectedChecks array is define the controller like this: $scope.selectedChecks = {}
and the result of the markup is a column of checkboxes
Here is my directive's code:
return {
restrict: 'EA',
scope: {
rowIndex: '#',
selectedChecks: '=',
bindModel: '='
},
template:'<input type="checkbox" ng-model="bindModel" ng-change="checkit(rowIndex)">',
link: function(scope, elem, attrs) {
scope.checkit = function(i){
alert(scope.bindModel + " " + scope.selectedChecks[i]);
};
}
};
The problem is that when the first checkbox is checked the alert will output: "true undefined"
form the second check/uncheck of any checkbox from now and own the output will be: "true false" and "false true"
My expectation was that the two variables will be the same since they should have point to the same value since:
scope.bindModel is sent to the directive from selectedChecks[$index] and scope.selectedChecks[i] should be the same.
Please help me understand what am i doing wrong here.
It seems that checkIt functions runs before Angular does the data-binding between the checkboxes and the array.
It seems that checkIt functions runs before Angular does the
data-binding between the checkboxes and the array.
No, checkIt function is not running before Angular dose the data binding. The reason why you get true undefined at the first time you check the first one is that $scope.selectedChecks = {} is empty. That's mean there is no property like:
$scope.selectedChecks = {
0: false
};
So, the time you check first checkbox, ng-model="bindModel" will update $scope.selectedChecks = {} like so:
$scope.selectedChecks = {
0: true
}
And the same with other checkboxes.

Nested scopes in AngularJS without nested DOM elements? Creating a view/activity "stack"

I'm developing a simple CRUD application to view end edit generic objects/rows in a database. Each type of CRUD-related action (browse, view, create, edit, delete, search...) has a corresponding type of view. Instances of that view would be something like "browse table A" or "edit row 45 of table B".
Views have actions that can create a child view, for example the user may be browsing a table and click "edit" on a particular row. This makes the views a stack, with only the topmost view actually displayed to the user. A view is pushed onto the stack when a new action is triggered, and popped when that action completes or the user cancels.
Right now my code looks something like this:
app.js:
angular.module('MyApp', [])
.controller('AppController', function($scope) {
var viewStack = [];
$scope.currentView = function() {
return viewStack[viewStack.length-1];
};
$scope.pushView = function(view) {
viewStack.push(view);
};
$scope.popView= function() {
viewStack.pop();
};
$scope.pushBrowseView = function(table) {
var view = {
type: "browse",
table: table,
rows: [],
refresh: function() {
// Load data into view.rows via AJAX
},
// ...
};
view.refresh();
$scope.pushView(view);
};
$scope.pushCreateView = function(table) {
var view = {
type: "create",
table: table,
newRow: {},
// ...
};
$scope.pushView(view);
};
$scope.pushEditView = function(table, row) {
var view = {
type: "edit",
table: table,
row: row,
// ...
};
$scope.pushView(view);
};
// More view types...
})
.controller('BrowseController', function($scope) {
$scope.create = function() {
$scope.pushCreateView($scope.currentView().table);
};
$scope.edit = function(row) {
$scope.pushEditView($scope.currentView().table, row);
};
})
.controller('CreateController', function($scope) {
$scope.submit = function() {
if(newRow.isValid()) {
// POST to server
window.alert('Row submitted');
$scope.popView();
} else {
window.alert('Not valid');
};
})
.controller('EditController', function($scope) {
// Similar to CreateController...
})
// More controllers for other view types
page.html:
<body ng-app="MyApp" ng-controller="AppController">
<!-- Stuff -->
<button ng-click="popView()">Back</button>
<!-- More Stuff -->
<div id="theview" ng-switch="currentView().type">
<div ng-switch-when="browse" ng-controller="BrowseController">
<button ng-click="create()">New Row</button>
<table>
<!-- header goes here -->
<tr ng-repeat="row in currentView().rows">
<td><button ng-click="edit(row)">Edit</button></td>
<td ng-repeat="column in currentView().table.columns">
{{ row[column] }}
</td>
</tr>
</table>
</div>
<div ng-switch-when="create" ng-controller="CreateController">
<form>
<div ng-repeat="column in currentView().table.columns">
<label>{{ column }}</label>
<input
name="{{ column }}"
ng-model="currentView().newRow[column]">
</div>
</form>
<button ng-click="submit()">Submit</button>
</div>
<div ng-switch-when="edit" ng-controller="EditController">
<!-- Kinda like "create" -->
</div>
<!-- And the rest of the view types -->
</div>
</body>
It's a lot fancier that that obviously but that's the gist of it. The problem is that there is one root scope for the app, with one child scope for each type of view. Since there could be more than one instance of each type of view on the stack at once, the state of each view needs to be completely stored in an object in viewStack instead of in that view type's $scope.
This doesn't at all seem like the proper way of doing things. It would make sense for there to be one scope per view instance, each one being a child of the scope of the view beneath it on the stack. That way I could have events naturally broadcast from the top view back through the others. I also wouldn't have to prefix every state variable with currentView(). But the only way I can think of doing this is with a recursive directive which potentially creates deeply-nested DOM elements that I don't want.
This seems like a pretty common design pattern. Is there a better way of doing it?
Going by what you are describing, I think each of your views would be better suited to a directive with isolated scope instead of putting each view into a viewStack object on a common scope.
In your example code you are getting the table information from your AppController and storing them on the $scope for your views to use. I can't see how those views are getting to the scope, but assuming they are coming from a web service some where, you could move the storage and fetching of that data to the directive itself. Eg,
app.directive('exampleView', ['myViewService`, function(myViewService) {
return {
restrict: 'E',
scope: {
'myTable': '=',
'myRows' : '=',
'onPopView': '&'
},
templateUrl: 'url/to/specific/view/template.html',
link: function($scope,element, attributes) {
//Logic for processing values and saving back to server/whatever as required
$scope.submit = function() {
if(newRow.isValid()) {
// POST to server
window.alert('Row submitted');
} else {
window.alert('Not valid');
}
};
if($scope.onPopView) {
// Tell parent controller that view is discarded?
$scope.onPopView();
}
}
};
}]);
The onPopView might not be appropriate for what you are doing, but used it as an example for creating a event/callback for parent controller to hook into. This allows you separate responsibilities between the directive and parent controller(s).

Taking function from controller and placing in directive

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.

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