This is going to sound complicated but, i hope someone here is more experienced than me and can sort through what follows without much difficulty.
To keep this in perspective this is all related to a pagination section like so: There's a controller that pulls some initial data objects from a test page via an ajax request. Those data objects pertain to each "puppy block" - they fill the the "puppy-block" template, of the puppy block directive, with the correct data.
The blocks are displayed using an ng-repeat (repeating for the number of data objects pulled) - this represents a page.
The pagination is independent and just modifies the controller data via different ajax requests.
I have this here ng-repeat:
<div class="puppiesContainer" ng-repeat="p in puppies track by $index">
<puppy-block></puppy-block>
</div>
It's purpose is to repeat a directive a number of times
The <puppy-block></puppy-block> directive template is this:
<div class="puppyA" preview-pup>
<div class="clearfix puppyCover" dataPreview="{{p.dataPreview}}">
<a ng-href="{{p.link}}"><img ng-src="{{previewData[currentPosition]}}" /></a>
</div>
</div>
As you can see, there is another directive called preview-pup on every element.
The code for that directive is this:
obApp.directive('previewPup', ['$timeout',function($timeout){
return {
restrict: 'A',
link: function(scope, el, attr){
//some code that needs to run often
}
}
}]);
My pagination directive changes the data of the puppy-block directives when moving between different pages, however i also need to trigger the link function of the preview-pup directive, inside each puppy-block directive, when the change happens. Both the pagination directive and the ng-repeat directive have a common controller from which they draw data (modify data).
My problem is that i have no idea how to manually trigger a directive's link function on an ng-click, for instance. How may i do that?
Alright, found a solution:
Inject $routeScope into your controller and the directive you want to trigger a change in.
Use that to notify the child directive of a change in the parent controller. For more details - in case anyone stumbles upon this question looking for something similar - Angular.js trigger another directive
I had this same issue. Removing the "track by $index" from the ng-repeat fixed the it for me. Unfortunately I don't know why yet.
Related
Should I use a directive or a controller?
I have a service that returns if I use bluetooth in the application or not. and I want to be able to indicate them in an on-screen icon. I seemed to read that to manipulate the DOM, either to create elements to modify or eliminate them, directives must be used. someone could give me an example of how it would be to implement a directive that does this. or would it be nice to use a controller? Thank you
controller
$scope.showIcon = bluetooth.useBluetooth();
html
<div ng-show="showIcon">
<img ng-src="ruta"/>
</div>
service
function useBluetooth() {
return usaBluetooth;
}
Since the useBluetooth function is a simple getter function is can be used by the ng-show directive directly:
$scope.useBluetooth = bluetooth.useBluetooth;
Usage:
<div ng-show="useBluetooth()">
<img ng-src="ruta"/>
</div>
On each digest cycle, the ng-show directive will fetch the bluetooth state and show or hide the element accordingly.
When using functions in Angular Expressions it is important that they be as simple as possible because they will be called every digest cycle sometimes multiple times.
but is it okay that I'm always monitoring the change of that variable? that value is defined only at the beginning and does not change throughout the application
If the expression will not change once set, it is a candidate for one-time binding:
<div ng-show="::useBluetooth()">
<img ng-src="ruta"/>
</div>
The main purpose of one-time binding expression is to provide a way to create a binding that gets deregistered and frees up resources once the binding is stabilized. Reducing the number of expressions being watched makes the digest loop faster and allows more information to be displayed at the same time.
For more information, see AngularJS Developer Guide - One-time binding.
but in the html do I have direct access to the service?or do I have to keep using the controller?
A custom directive can be used to put the service on scope:
app.directive("useBluetooth", ["bluetooth", function(bluetooth) {
return {
link: postLink
};
function postLink(scope, elem, attrs) {
scope.$bluetooth = bluetooth;
}
}])
Usage:
<div use-bluetooth ng-show="::$bluetooth.useBluetooth()">
<img ng-src="ruta"/>
</div>
forgive me for asking you. But is that a good practice?
My personal preference would be to use a component:
app.component("showBluetoothIcon", {
controller: ['bluetooth', function(bluetooth) {
this.bluetooth = bluetooth;
}]),
template: `
<img ng-show="$ctrl.bluetooth.useBluetooth()" ng-src="ruta"/>
`
})
Usage:
<show-bluetooth-icon></show-bluetooth-icon>
By combining the controller and the template in a single component, it becomes easy to understand, debug, test, and maintain.
For more information, see AngularJS Developer Guide - Understanding Components
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.
Here's a plunker example you can see: http://plnkr.co/edit/NQT8oUv9iunz2hD2pf8H
I have a directive that I would like to turn into a web component. I've thought of several ways as to how I can achieve that with AngularJS but am having difficulty with a piece of it. I'm hoping someone can explain my misstep rather than tell me a different way to do it.
Imagine you have a directive component that sets up some shell with css classes maybe some sub components, etc.. but lets the user define the main content of the component. Something like the following:
<my-list items="ctrl.stuff">
<div>List Item: {{ item.name }}</div>
</my-list>
The HTML for the list directive could be something like the following (with OOCSS):
<ul class="mas pam bas border--color-2">
<li ng-repeat="items in item track by item.id" ng-transclude></li>
</ul>
Normally this can be solved in the link function by linking the directives scope to the new content. And it does work for other components. However introducing the ng-repeat seems to break that portion of the control. From what I can tell, the appropriate place might be the compile function but the documentation says the transcludeFn parameter will be deprecated so I'm not sure how to proceed.
I should also note that when using the beta AngularJS, there is either a bug or a new paradigm coming, because this is no longer a problem. It seems like the transcluded content always gets access to the directives scope as well as the outer controllers scope.
I really appreciate any enlightenment on this.
It's by design that content added via ng-transclude will bind with an outer controller scope, not a scope of the current element that ng-transclude is on.
You could solve the problem by copy the ng-transclude's code and modify it a bit to give a correct scope:
.directive('myTransclude', function () {
return {
restrict: 'EAC',
link: function(scope, element, attrs, controllers, transcludeFn) {
transcludeFn(scope, function(nodes) {
element.empty();
element.append(nodes);
});
}
};
});
And replace the ng-transclude with my-transclude in your directive template.
Example Plunker: http://plnkr.co/edit/i7ohGeRiO3m5kfxOehC1?p=preview
I'm pulling my hair about a particular issue with my AngularJS application.
Basically, I have a controller, "PageCtrl", which pulls in a list of data via a service. The data is saved inside the PageCtrl. All works fine - so far so good.
Now my issue: for each object of the data inside the PageCtrl, I use ng-repeat to display each data just fetched.
In each of the ng-repeats, I use ng-include to get the correct template, depending of which data is in each object (basically the data is a list of elements that is to be displayed on the page. Each element has it's own controller.
As ng-include creates a newscope, all the created child scopes/controllers have access to the data, as it is located in the $parent controller - PageCtrl.
All works fine, except that each created controller in ng-repeat/ng-include needs to know which object in list of data in the parent, it needs to use to display.
Basically, in my ng-repeat, I would like to use the $index and pass it to the controller included in the ng-include. But I cannot do it! If I use ng-init, that code inside ng-init will be executed on the parent scope, ie. my original PageCtrl.
How can I, without the use of directives, pass information to the controller so that it knows which of the data-objects in its parent it should use?
Here is the code that loops over the data in MainCtrl:
<div ng-repeat="element in elements" >
<ng-include src=" GetTemplateFromElement(element.type) " ng-init=" SetElementNo($index); "></ng-include>
</div>
In the snippet, I call GetTemplateFromElement on the MainCtrl which returns the view to include. In the included view I refer which controller should do the work. I try to use $index to tell the included controller which index in the data it should use. But ofcourse the code is executed in MainCtrl scope. Example of a view:
<div ng-controller="ElementDocumentCtrl">
<div ng-bind-html-unsafe="element.document_content"></div>
</div>
In the snippet, "element.document_content" is on the scope of ElementDocumentCtrl, which pulls the data from its parent, PageCtrl.
How can I tell the directives included which index in the data-list in PageCtrl (it's parent controller) it should pick up?
Please don't tell me I need to make them all directives :)
Thanks, guys.
Maybe Im not understanding your question completely, but from the docs, $index, $first, $middle and $last are all exposed on the local scope of the ng-repeat children.
Ive put in a plunker to demonstrate this. The ChildCtrl accesses the $index from the parent to get the index of which element in the parent it is accessing.
At the moment I have an app that has a sidebar, and the sidebar loads different html templates using ng-include based on what operation the user chooses to do. It's a map related app, so for example, if the user selects the 'Add Leg' button it will load the add_leg.html template into the sidebar using ng-include:
// The Javascript code:
$scope.addLeg = function() {
$scope.sidebar = true;
$scope.sidebar_template = '/partials/legs/add_leg.html';
}
// Simplified example of HTML:
<html>
<div ng-app="MyApp" ng-controller="MainCtrl">
<a ng-click="addLeg()">Add Leg</a>
<a ng-click="addRoute()">Add Route</a>
<a ng-click="editLeg()">Edit Leg</a>
<a ng-click="editRoute()">Edit Route</a>
...
<div id="sidebar" ng-class="{'sidebar-hidden': sidebar == false}">
<div ng-include src="sidebar_template"></div>
</div>
<google-map></google-map>
</div>
This is all well and good and works as desired, but at the moment my app is only using one controller (MainCtrl in js/controllers.js) and it's starting to get really cluttered. I've got a lot of methods now because the apps functionality is expanding. I'd like to split my controller up into multiple controllers whilst retaining this sidebar functionality.
I want to have MainCtrl as the main controller that controls the loading of the sidebar template (by changing the sidebar_template variable that points to the file destination), and I want it to control some of the global map related methods (like fetching suburb names from coordinates, etc).
I've tried to split it like so:
controllers/js/main.js - MainCtrl
controllers/js/legs.js - LegCtrl
controllers/js/routes.js - RouteCtrl
I want the LegCtrl and RouteCtrl to inherit the MainCtrl so I can access its scope and methods, that's all fine. But now the problem is how do I dynamically load the controller onto the sidebar div based on what functionality is required. Originally all of my methods were in MainCtrl, and that's on the wrapper div that surrounds the sidebar div (see above again for an example), so it wasn't a problem.
For example, say I press the 'Add Leg' button, it's going to need to call addLeg in LegCtrl, but LegCtrl isn't loaded on the app, so it doesn't have access to the method. I could keep addLeg() inside the MainCtrl, and have it change the sidebar_template variable to load the template, but nothing in the template will work because it is calling methods from inside the LegCtrl now.
I need to somehow dynamically load the controller on the sidebar's ng-include div, something like this perhaps:
// MainCtrl
$scope.addLeg = function() {
$scope.required_controller = LegCtrl;
$scope.sidebar = true;
$scope.sidebar_template = '/partials/legs/add_leg.html';
LegCtrl.addLeg();
}
<div id="sidebar" ng-class="{'sidebar-hidden': sidebar == false}">
<div ng-include src="sidebar_template" ng-controller="{{required_controller}}"></div>
</div>
In the non-working example above you can see a possible solution I've thought of, but I need LegCtrlto be the actual controller function, not an object (for ng-controller to work). I also need some way to call addLeg on the LegCtrl from the MainCtrl.addLeg (perhaps using broadcast?).
If anyone can point me in the right direction that'd be great. Sorry for the huge post, it needed a bit of explaining to make it coherent. Hopefully it makes sense. Thanks.
Update: I think I've found a solution using a service to act as the navigation control (it will load the relevant templates and broadcast an event to the new controller being dynamically loaded to tell it what function to execute):
http://plnkr.co/edit/Tjyn1OiVvToNntPBoH58?p=preview
Just trying to test this idea out now, but the broadcast .on doesn't work. I think it's because the broadcast fires before the template loads. How can I fix this? Is this a good solution for what I'm doing?
If i have understood you correctly what you can try would be to create a template view specifically to create a new leg.
From the main controller implement some logic to show the template
$scope.addLeg=function() {
$scope.showAddLeg=true;
}
The AddLeg template view would load the controller and hence provide a mechanism to actually add new leg. The template would look like
<script type="text/ng-template" class="template" id="addLegTemplate">
<div ng-controller='LegsController'>
<!--html for adding a new leg-->
</div>
</script>
You can include this template inside you main html using ng-if + ng-include.
<div ng-if='showAddLeg'><div ng-include='addLegTemplate'/></div>
Basically you can create multiple view and bind to same controller (but instance would differ). In this case the LegsController can be binded to multiple views to support the complete functionality.
I like this data driven scenario, just manipulate the rows in the templates:
$scope.templates = [
{ name: 'include1.html', url: 'partials/include1.html' }
];
$scope.addTemplate = function(name, url) {
$scope.templates.push({ name: name, url: url });
};
and the markup:
<div ng-repeat="template in templates" ng-include="template.url"></div>
To add or remove views, just modify the templates et voila! If you need more controller code, then you can include a link to the script in the include. I guess there may be complications with binding to data in the partial view itself, but $compile should resolve those.
I've forked your plunkr demo into one that I believe does what you're looking for: http://plnkr.co/edit/whsjBT?p=preview
This demonstrates an event being broadcast from one controller to another AFTER the 2nd controller (LegCtrl in our example here) is loaded via ng-include and passing it data.
I used $includeContentLoaded event from ng-include to delay broadcasting the event until angular reports that add_leg.html is loaded. Also I think there were some issues with how you were using $broadcast()... it's first parameter should be the same as the one used in $on() in LegCtrl
Another idea to consider is simply using your Navigation service itself to share state between the controllers if that's appropriate in your scenario.