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
Related
The Issue
Using my directive within another directive causes certain bindings to be lost, specifically in the ng-repeat usage.
The directive is used in many areas of my application without issue. It renders a list of inputs which is passed to it's scope from a parent template, like so:
<filter-header filters="filters"></filter-header>
Working Scenario
I have used the following scenario throughout the application and not come across an issue to date.
$routeProvider resolves filters list with WebAPI call for a controller
controller assigns list to its own scope, like so: $scope.filters = filters
template uses filter-header element and passes filters from it's scope to the directive, like so: <filter-header filters="filters"></filter-header>
The filter-header directive then renders the filters using an ng-repeat without issue. $$hashKey is present in each item of the filters collection, indicating the binding's existence.
Failing Scenario
In the following scenario, the binding seems to be lost and the ng-repeat fails to render anything.
$routeProvider resolves filters list with WebAPI call for a controller
controller assigns list to its own scope, like so: $scope.filters = filters
template uses a new element directive, assigns filters from it's scope to the new directive's via an attribute.
directive's template uses filter-header element and passes filters from it's scope to the directive, like so: <filter-header filters="filters"></filter-header>
The filter-header directive then FAILS TO render the filters using an ng-repeat. $$hashKey is NOT present in any item of the filters collection.
Annoyingly, I cannot replicate this in Plunker...
Oddity
The directive has another collection of item's passed to it, columns="columns" (can be seen in the code below). Columns binds correctly and is rendered in it's own ng-repeat. I cannot see how Columns is different from Filters as both are used almost exactly the same way.
Looking Deeper...
I have debugged the process all the way. The filters object is getting all the way to the end scope successfully. If I output the contents of filters to the screen within the final directive, using {{ filters }} I can see all of the filters as expected. However, in the very next line where my ng-repeat begins, no filters are iterated through.
To be certain it is not my list causing issues, I used a list that already works using the working scenario mentioned above and the ng-repeat does not render here.
To be certain it is not my directive's code causing issues, I converted it to a controller and routed directly to it (skipping the nested directive) as in the working scenario mentioned above and the ng-repeat now works.
Using $log to inspect the list, I notice one difference. In the working scenario, all lists contain a $$hashKey property for each item in the list. In the failing scenario, the $$hashKey is missing on all items in the list. This seems to indicate that the binding is being lost for some reason.
Can someone tell me the error in my ways? The only real difference I can see in my usage is that I pass the object to an middle-man directive before before passing it on the the directive where it is used. Strangely, in the very same directive, another list is used in a very similar way and it renders without issue within it's ng-repeat, and it's item's all have the $$hashKey property appended.
Code
There's a lot of code involved, so I'll try and pick out the relevant parts.
RouteProvider
$routeProvider.when('/Pride/Admin/AuditForms/:id', {
templateUrl: '/Templates/Admin/editAuditForm.html',
controller: 'editAuditFormController',
resolve: {
sectionFilters: function (auditFormSectionRepository) {
return auditFormSectionRepository.getFilters().$promise;
},
sectionColumns: function (auditFormSectionRepository) {
return auditFormSectionRepository.getColumns().$promise;
}
}
});
EditAuditForm Controller
prideModule.controller("editAuditFormController", function ($scope, sectionFilters, sectionColumns) {
$scope.sectionFilters = sectionFilters;
$scope.sectionColumns = sectionColumns;
});
EditAuditForm Template
<audit-admin-sections audit-form="auditForm" section-filters="sectionFilters" section-columns="sectionColumns" show-deleted="false"></audit-admin-sections>
AuditAdminSections Directive
prideModule.directive('auditAdminSections', function ($log) {
return {
restrict: 'E',
templateUrl: 'templates/admin/auditFormSections.html',
scope: {
sectionFilters: '=',
sectionColumns: '='
},
controller: function ($scope, $route, $timeout, $location, filterLogic, auditFormSectionRepository) {
// do stuff
}
});
AuditFormSections Template
<filter-header filters="sectionFilters" columns="sectionColumns"></filter-header>
FilterHeader Directive
prideModule.directive('filterHeader', function($log) {
return {
restrict: 'E',
templateUrl: 'templates/common/filterHeader.html',
scope: {
filters: '=',
columns: '='
},
controller: function ($scope, filterItemsRepository) {
$log.info("$scope.filters");
$log.info($scope.filters);
// This will log the filters as expected, however the $$hashKey property is missing from the items
}
});
FilterHeader template
<!-- at this point, {{ filters }} produces the list of filters -->
<form class="form-horizontal" ng-repeat="filter in filters">
<!-- at this point, nothing renders -->
<label class="col-sm-2 control-label">{{ filter.friendlyName }}</label>
</form>
Update 1
I pulled the code out of the directive and into a new controller to mimic the Working Scenario mentioned above. The ng-repeater now functions as expected and the $$hashKey is present again. So something is definitely related to the difference between route->controller->directive->directive vs route->controller->directive.
Worth mentioning, on top of the code above, there are watches on the filters among other usages.
Update 2: Offender Uncovered
I've nailed it. But it makes no sense yet. It seems as though the form element is to blame. Changing to a div resolves the issue. I'm thinking this may be an angular bug as I'm struggling to see why this could work in one scenario and not the other.
Have discovered two fixes so far, but they are more hacks than fixes as I cannot understand the cause of the original problem. I'm still interested if anyone can point out the real issue, but until then this is the best I have:
Solution 1
After much researching, debugging, head scratching, rewriting, I by luck came across a solution. I'm not happy with it, as it doesn't make sense (unless someone can elaborate for me).
The problem seems to be with the form element and using the ng-repeat attribute on it while being nested in angular directives...!!! This has to be a bug, right?
Solution was as simple as changing this:
<form class="form-horizontal" ng-repeat="filter in filters">
<label class="col-sm-2 control-label">{{ filter.friendlyName }}</label>
</form>
to this:
<div class="form-horizontal" ng-repeat="filter in filters">
<label class="col-sm-2 control-label">{{ filter.friendlyName }}</label>
</div>
Solution 2
It seems that the $scope variables were also playing a part in the issue. If I rename each directive's $scope variable name for this particular variable (ie, filters) so that it is unique for each directive, the form ng-repeat works. This makes it seem like there is some sort of conflict within the directives' isolated scopes, however why this is only an issue for form ng-repeat baffles me. As such, it still doesn't explain to me what is the root cause of this behavior.
I am having troubles implementing a custom directive with transclude: true that can have a "transclusion" content that is using ngRepeat.
[My case]
I want to implement a set of directives, that are fetching the data that they are supposed to show from a $http service. For that I want to use preLink phase interceptor that Angular provides, so I can catch the data and set it to the scope. That way if I have some dynamic (since this term is well overloaded - I mean a data which structure is unknown until the request is done) data coming from the service, I rely on that, that I will be able to retrieve a list with that dynamic data and store it inside the scope, then loop through that data via ngRepeat inside the HTML. Here comes my problem...
[My Problem]
Angular is not using the list that I am assigning to the scope during preLink.
[A plunkr]
I maded a plunker that illustrated just the problem that I am having.
http://plnkr.co/edit/XQOm4KWgKxRhn3pOWqzy?p=preview
[My question]
I really believe that such functionality is covered by angular and I am just missing something in the puzzle.
Can anyone tell me how to implement such behaviour?
Thanks!
EDIT:
Thank you rchawdry for your answer. Here are some details on my intentions. To make it simple I will try to give you an example.
Let's assume that we have these directives:
1. "page" - This directive is a labeled container for all the page content. Visually it is represented as some div - for header, for content and for other fancy stuff if needed. The directive does not know what is its data before the page loads. As the page loads the directive must retrieve the information for itself and its children from a REST resourse! Then the directive is setting the information needed for itself (label and other stuff) and stores its children content in childrenList scope variable. It creates a scope.
2. "section" - This section can be child of "page". Since "page" is retrieving its data from a server, then the information about how many "section"s does our "page" have is dynamic and we don't know how many "section"s we need to show on the screen. This depends on sectionList that is coming from the back-end. The section itself is almost the same as "page" - it is a labeled container, with the differences that - a). "section" is container of elements; b). "section" does retrieve its data from its parrent instead of making $http request. This directive creates a scope.
3. "element" - For this example, in order not to define many different elements and complicate it, let's assume that I have one element, called "element". It can consist of some "input" with "span" and "button" if needed. It is similar to the "section" with that, that it retrieves the data to show from it's parrent (in the general case, this is "section" or "page"). On the other hand it is different than "section" by the fact that it has no transcluded content.
Now after we have some of the concept here is what I am trying to achieve:
<page>
<element id='element1' someOtherStuffHere...></element>
<section id='static_section1' someOtherStuffHere...>
<element id='element2' someOtherStuffHere...></element>
</section>
<div class="row" ng-repeat="section in sections">
<section dynamic_id='dynamic_section'>
<div class="row" ng-repeat="elem in elements">
<element dynamic_id='dynamic_element'></element>
</div>
</section>
</div>
</page>
well, I believe that what your trying to achieve will be able by adding a ng-repeat attribute to the transcluded template.
by letting angular know about the 'repeat', it is supposed to work.
since plunkr is currently unavaliable, I can't prodivde any preview and do not have your original code. Ill try to recall it:
template: "<div id='container'>" +
"<div class='content' ng-repeat='item in [1]' ng-transclude'></div>" +
"</div>"
edit: http://plnkr.co/edit/xba4pU666OGxBtKtcDwl?p=preview
You've got a scope problem. The controller is using a variable that isn't defined in the controller (arrayListItemsPre and arrayListItemsPost). While they are declared in the directives, accessing them in a transcluded scope is a little tricky.
The easy way is to do the following. This will present the scope variables up to the controller where they can be used.
app.directive('container', function($compile) {
return {
transclude: true,
scope: true,
replace: true,
restrict: 'E',
template: "<div class='container'>" +
"<div class='content' ng-transclude></div>" +
"</div>",
compile: function(cElem, cAttrs) {
return {
pre : function preLink(scope, iElement, iAttrs) {
console.log("Hello from Container preLinkPhase");
scope.$parent.arrayListItemsPre = [1, 2];
},
post : function postLink(scope, iElement, iAttrs) {
scope.$parent.arrayListItemsPost = [1, 2];
}
};
}
};
});
There are other ways to do this that are better but it requires understanding why you're trying to iterate on variables that are defined in the directive. If you're going to be using this directive in another page that has different array elements, you'd have to change the directive code.
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.
I have a directive that makes things repeatable. For example, <div repeat-this><p>hey</p></div> will repeat <p>hey</p> , say, 5 times.
In the $compile function of repeat-this I wrap all the contents in a
<div class="repeatable" ng-repeat="item in collection">contents go here<div>
so that AngularJS takes care of replicating elements, the scope, etc for me.
However, the name of this collection is declared with a directive myCollectionName and can change depending on where the directive is (there are some ng-includes that can include more myCollectionName but consumers of this name should only use the closest one in the hierarchy). I can get the name of the collection using a directive controller in a parent element but then it is only available in the postLink function of repeat-this and I can't modify the item in collection part to make it like item in students or item in tickets. What would be a good way of doing this?
When accessing properties from scope within a directive you should never access the property directly.
For example (within a directive):
scope.myProperty = 'myval';
You should instead pass in the name of the property as a parameter to the directive:
<div myDirective="myProperty">
Inside your directive you can then use the passed in parameter to access the property:
link: function postLink(scope, element, attrs) {
var propertyAccessor = $parse(attrs.myDirective);
propertyAccessor.assign(scope, 'myval');
}
Using $parse is important because it allows you to use nested properties for instance myDirective="item.myProperty"
Say I have an ng-repeat with a big array.
When ng-repeat runs, it adds every element of that array to an isolated scope, as well as having the array itself in a scope. That means that $digest checks the entire array for changes, and on top of that, it checks every individual element in that array for changes.
See this plunker as an example of what I'm talking about.
In my use case, I never change a single element of my array so I don't need to have them watched. I will only ever change the entire array, in which case ng-repeat would re-render the table in it's entirety. (If I'm wrong about this please let me know..)
In an array of (say) 1000 rows, that's 1000 more expressions that I don't need evaluated.
How can I deregister each element from the watcher while still watching the main array?
Perhaps instead of deregistering I could have more control of my $digest and somehow skip each individual row?
This specific case is actually an example of a more general issue. I know that $watch returns a 'deregisteration' function, but that doesn't help when a directive is registering the watches, which is most of the time.
To have a repeater with a large array that you don't watch to watch every item.
You'll need to create a custom directive that takes one argument, and expression to your array, then in the linking function you'd just watch that array, and you'd have the linking function programmatically refresh the HTML (rather than using an ng-repeat)
something like (psuedo-code):
app.directive('leanRepeat', function() {
return {
restrict: 'E',
scope: {
'data' : '='
},
link: function(scope, elem, attr) {
scope.$watch('data', function(value) {
elem.empty(); //assuming jquery here.
angular.forEach(scope.data, function(d) {
//write it however you're going to write it out here.
elem.append('<div>' + d + '</div>');
});
});
}
};
});
... which seems like a pain in the butt.
Alternate hackish method
You might be able to loop through $scope.$$watchers and examine $scope.$$watchers[0].exp.exp to see if it matches the expression you'd like to remove, then remove it with a simple splice() call. The PITA here, is that things like Blah {{whatever}} Blah between tags will be the expression, and will even include carriage returns.
On the upside, you might be able to just loop through the $scope of your ng-repeat and just remove everything, then explicitly add the watch you want... I don't know.
Either way, it seems like a hack.
To remove a watcher made by $scope.$watch
You can unregister a $watch with the function returned by the $watch call:
For example, to have a $watch only fire once:
var unregister = $scope.$watch('whatever', function(){
alert('once!');
unregister();
});
You can, of course call the unregister function any time you want... that was just an example.
Conclusion: There isn't really a great way to do exactly what you're asking
But one thing to consider: Is it even worth worrying about? Furthermore is it truly a good idea to have thousands of records loaded into dozens of DOMElements each? Food for thought.
EDIT 2 (removed bad idea)
$watch returns a function that unbinds the $watch when called. So this is all you need for "watchOnce":
var unwatchValue = scope.$watch('value', function(newValue, oldValue) {
// Do your thing
unwatchValue();
});
Edit: see the other answer I posted.
I've gone and implemented blesh's idea in a seperable way. My ngOnce directive just destroys the child scope that ngRepeat creates on each item. This means the scope doesn't get reached from its parents' scope.$digest and the watchers are never executed.
Source and example on JSFiddle
The directive itself:
angular.module('transclude', [])
.directive('ngOnce', ['$timeout', function($timeout){
return {
restrict: 'EA',
priority: 500,
transclude: true,
template: '<div ng-transclude></div>',
compile: function (tElement, tAttrs, transclude) {
return function postLink(scope, iElement, iAttrs, controller) {
$timeout(scope.$destroy.bind(scope), 0);
}
}
};
}]);
Using it:
<li ng-repeat="item in contents" ng-once>
{{item.title}}: {{item.text}}
</li>
Note ng-once doesn't create its own scope which means it can affect sibling elements. These all do the same thing:
<li ng-repeat="item in contents" ng-once>
{{item.title}}: {{item.text}}
</li>
<li ng-repeat="item in contents">
<ng-once>
{{item.title}}: {{item.text}}
</ng-once>
</li>
<li ng-repeat="item in contents">
{{item.title}}: {{item.text}} <ng-once></ng-once>
</li>
Note this may be a bad idea
You can add the bindonce directive to your ng-repeat. You'll need to download it from https://github.com/pasvaz/bindonce.
edit: a few caveats:
If you're using {{}} interpolation in your template, you need to replace it with <span bo-text>.
If you're using ng- directives, you need to replace them with the right bo- directives.
Also, if you're putting bindonce and ng-repeat on the same element, you should try either moving the bindonce to a parent element (see https://github.com/Pasvaz/bindonce/issues/25#issuecomment-25457970 ) or adding track by to your ng-repeat.
If you are using angularjs 1.3 or above, you can use the single bind syntax as
<li ng-repeat="item in ::contents">{{item}}</li>
This will bind the value and will remove the watchers once the first digest cycle is run and the value changes from undefined to defined for the first time.
A very helpful BLOG on this.