AngularJS Custom Directive isolated scope model binding - angularjs

I'm trying to implement a double select list form component just to try out angular and try to understand it better.
The functionality of this directive is that the parent model contains the list of Available Items to choose from, and contains the destination list (selected) where chosen items are placed. When an item is chosen it will be removed from the available array and placed in the selected array.
As a form may require more than one of these double select list components I don't want to rely on hard coded property names but instead allow the lists to be passed in for two way data binding.
Here is a JSBin of the code: Double Select Lists
I haven't gone any further than just trying to bind the data to the lists via the passed in model property names.
On the directive, the following scope properties are defined. The availableItems and selectedItems should both be bound to the parent controllers properties. The highlightedAvailable and highlightedSelected are to store the user selections prior to the button clicks.
scope: {
availableItems: '=',
selectedItems: '=',
highlightedAvailable: [],
highlightedSelected: []
},
Can someone please tell me why the controller properties never bind to the directive properties?
Thanks!

First, you have an error being caused by your scope:
scope: {
availableItems: '=',
selectedItems: '=',
highlightedAvailable: [],
highlightedSelected: []
},
should be:
scope: {
availableItems: '=',
selectedItems: '='
},
Declare the arrays somewhere else, like in the link function:
link: function (scope, element, attrs) {
scope.highlightedAvailable = [];
scope.highlightedSelected = [];
The next problem was the way you specified the attributes to the directive, you had:
<div ng-Select-Lists availableItems='availableItems' selectedItems='selectedItems'>
Try this instead:
<div ng-Select-Lists available-items='availableItems' selected-items='selectedItems'>

To expand on aet's answer, the reason that the way you specified your directive attributes in your html did not work is because HTML is case-insensitive. So the 'availableItems' attribute was actually being passed to your directive scope as 'availableitems'. On the other hand, snake cased words like 'available-items' will be converted to camel case in your angular code.
That's the reason you write angular directives in the html as 'ng-repeat', 'ng-model', and so on, but in the angular source code you'll see these directive names camel cased: 'ngRepeat', 'ngModel'...
Be super careful to use snake-case in HTML, and camel case in your Javascript (Angular)! I've spent way too long on some bugs caused by that confusion.

Related

angular.js $scope not attaching to form in external directive

I am working on an angular app. right now we have these directives that are breadcrumbs that guide you through long wizard forms. One of the problems I have run into is if you have an error in one part of the form and click on a breadcrumb to go back, it doesnt let you progress throughout the form (the next button breaks). So the solution to this (I believe) is to not let you click on these breadcrumbs unless the form doesn't have any errors.
These breadcrumbs are in directives. How can I access the form from this external directive? I have tried:
$scope.competitionCreateForm
$scope.$competitionCreateForm
competitionCreateForm.$error
etc and nothing seems to work. However, when I console.log competitionCreateForm I do see the actual form object, so I know that part is working.
Here is what the directive looks like (coffeescript)
angular.module('App').directive 'breadcrumb', [
'WizardHandler'
(WizardHandler) ->
{
restrict: 'E'
scope:
breadcrumbs: '=breadcrumbs'
placeholder: '#'
templateUrl: 'directives/templates/breadcrumbs.html'
#require: ''
controller: ['$scope', ($scope) ->
# checking right here
$scope.goToStep = (step) ->
$scope.currentStepValid = true
WizardHandler.wizard().goTo(step)
]
}
]
I have looked through other answers and it hasn't seemed to work. maybe I need to add something when I'm first creating the form, but I'm not sure. Does anyone know how I can check the status of my form object from an external directive?
So by using the scope property on the directive you are creating an isolated scope for the directive, this means that the directive can't access the parent directive's scope.
You have two options if you wish to continue using an isolated scope:
You can use two way binding like you did with breadcrumbs.
Another option is to use one way bindings i.e. '<' to pass the form state into your directive. This will update the directive when that property changes just as two way binding will but will not allow the form to be modified from the directive.
I was able to get this working by adding
scope:
breadcrumb: '=breadcrumbs'
placeholder: '#'
form: '?=form'
which adds the ability to optionally add a form to the breadcrumb directive
breadcrumb[breadcrumb='breadcrumb' form='createForm']

Directive implementation advice using attribute string binding

I am new to AngularJS and directives and am seeking some advice or guidance on a directive I have implemented. The directive in question will be used to display pdf's to the user. The directive exposes two attributes, documentPath and documentType that are defined using isolated scope as follows:
var directiveDefinition = {
templateUrl: 'app/views/directives/document.html',
scope: {
documentPath: '#documentPath',
documentType: '#documentType'
},
restrict: 'E',
templateNamespace: 'html',
link: linkFunc
};
In the view that uses the directive, I bind the properties using a model property for the view controller and a string.
<my-document document-path="{{ application.documentpath }}" document-type="Application"></my-document>
When I initially ran this, I found that the directive would sometimes run before the data had been returned by the model. So an empty document would be displayed. Other times, the model would load before the directive ran, so the document path would be present when the link function ran, allowing the document to be displayed.
I determined that one way to resolve this was to use a $watch listener on the documentPath attribute of the directive. This seems to resolve the issue.
Being new to AngularJS and also directive implementation my question is...was this the best solution? Any advice would be greatly appreciated. Thanks!
The $watch solution should work fine but depending on the complexity of your app and the number of total watchers this could lead to a slow $digest cycle.
Another option would be to send the parameters as a reference using the "." dot notation so you it will get updated when the data gets loaded from model (from the API most of the time since this is the slow part) instead of sending a string primitive.
Your directive scope declaration would become:
scope: {
documentPath: '=',
documentType: '='
},
and you will use it from the parent view like this:
<my-document document-path="application.documentpath" document-type="application.documenttype"></my-document>
If the documentType is always the same you may leave it as a string scope parameter.

What is the correct way to return a value from a directive?

I have the following set up as a directive in my angular project:
{
restrict: "AE",
replace: true,
template: template,
require: "ngModel",
scope: {
chosen: "=ngModel",
choices: "=choices",
placeholder: "#placeholder"
}
}
I have everything working internally for my directive, the missing piece right now is that when I select a value inside of it, the parent scope containing my directive isn't receiving any kind of update. Despite me making assigments to chosen from anywhere inside of my directive.
As the title states, what's the simplest way for me to assign a value chosen inside of my directive, to it's parent's scope?
Ideally I'd like the solution to:
Not require me to use the link function - I feel like this can be done declaratively
Not require my directive to guess anything about it's parent scope
As a followup question, is there any reason to use ngModel in this circumstance? Is it or could it be beneficial? Or could I just as easily get away with recycling a name attribute which contains the parent scope's desired return value?

How can I pass an ng-repeated object to my AngularJS directive?

Take a look at my code here...
Controller ("tasks" is an array of JSON objects resolved in my Routes.js):
app.controller('testCtrl', function(tasks){
$scope.tasks = tasks.data;
};
HTML:
<div class="client-wrapper">
<div class="task-wrapper" ng-repeat="taskData in tasks">
<div task="taskData">
</div>
</div>
</div>
Directive:
app.directive('task', function(){
return {
restrict: 'A',
scope: {taskData: '=taskData'},
templateUrl: "/templates/directives/task.html",
link: function(scope, element, attribute) {
console.log(scope.taskData);
}
};
});
For some reason, I seem incapable of figuring out how to pass the current object being looped through in the tasks array to this directive so that I can manipulate it therein.
I've tried numerous solutions, as seen below:
how to pass a json as a string param to a directive <--- which tells me to output {{ object }} inside if an HTML attribute, and then $eval that back to JSON in the directive...
That's a very gross way of doing it, and I definitely don't want to do it that way (nor do I think this will allow it to two-way-bind back to the actual object in the tasks array in the controllers scope. This method just converts the JSON to string --> evals back to JSON + makes a copy of that string inside the directives scope).
https://groups.google.com/forum/#!msg/angular/RyywTP64sNs/Y_KQLbbwzzIJ <-- Same as above, they're saying to output the JSON as a string in an attribute, and then $eval it back to JSON... won't work for me for the same reasons as the first link.
http://jsfiddle.net/joshdmiller/FHVD9/ <-- Copying his exact syntax isn't possible because the data I want to pass to my directive is the current index of the tasks array while being ng-repeated... This is close, but doesn't work within the constraints of ng-repeat apparently?
Angularjs pass multiple arguments to directive ng-repeat <-- This syntax isn't working for me -- if I attempt to pass the taskData object (the current representation object in the array being looped through) in a parameter, it passes the literal string "taskData" and not the object itself? At this point I'm really scratching my head.
What I'm trying to accomplish (since I might be going about this in the wrong way entirely and feel I should explain the problem as a whole):
I have a bunch of data called tasks. They have a few properties, such as:
completed: true || false
description: a string
tags: an array of strings
Etc, etc.
I am outputting a big table of rows for all of these tasks. Each row will be a directive, with some buttons you can press on that row in order to change the data pertaining to the task on this row.
I want to have the functions to manipulate the data of each individual task inside the link function of the directive. So like markAsCompleted() would be a function within the directive, that would update the completed boolean of whichever task was being clicked on.
I am doing it this way, because as I see it I have two options:
A function in the controller where I pass in the task to modify as a parameter, and then perform the data manipulation
Or a function in the angular directive that just manipulates the data of the object attached to this particular directive (and my current issue is my apparent inability to two-way bind an object to this particular directive)
I want to be able to accomplish the second option in order to make my directive modular and stand-alone.
So yeah. I'm confused as to how to go about doing this and would greatly appreciate some insight as to where I'm going wrong :)
scope: {taskData: '=taskData'} means Angular expects an attribute called task-data with the value to be passed in. So give this a try...
<div class="client-wrapper">
<div class="task-wrapper" ng-repeat="taskData in tasks">
<div task task-data="taskData">
</div>
</div>
</div>
In your current attempt your directive is expecting an attribute called task-data.
This should fix your problem:
app.directive('task', function(){
return {
restrict: 'A',
scope: {task: '='},
templateUrl: "/templates/directives/task.html",
link: function(scope, element, attribute) {
console.log(scope.taskData);
}
};
});
Notice i changed the name of your property in the directive's scope from task-data to task

ng-select is not working along with a custom directive

I have created a plunker in order to emphasize the problem:
http://plnkr.co/edit/QHUpCv?p=preview
If I remove the custom attribute, or move the ng-select out of it, the companies are listed as the should, in case I use ng select with or within the custom attribute directive that I have created it breaks.
I suspect that some kind of $watch is required there inside of the scope for menuCtrl, but I have no idea whatsoever how to implement it.
As far as i can tell, the problem is that you are generating a new scope for your directive, so a quick fix would be to forbid that via:
// [...]
restrict: "A",
scope: false,
link: //..
I made a plunkr here to illustrate.
If you do want it this way and prefer an own scope for this directive, you can pass in the values for the select, i.e.
<div restrict companies="companies" access="admin">
and read it in in the scopeof the directive:
restrict: 'A',
prioriry: 100000,
scope: {
companies: '='
},
of course, you would then use the companies directly with the select:
<select ng-model="data.selectedCompany" ng-options="company for (id, company) in companies">

Resources