I'm trying to build an AngularJS directive that, during the linking phase, moves it's children (which are also directives) into another location before the children get into their linking phase. The moving of the children is easily achieved using transclution, however, even though I don't immediately put the child elements back into the DOM they still get compiled and linked.
The use case that I'm building this for is an image gallery directive that manages the loading of its images so that a gallery with a large number of images only loads the ones that are actually needed for rendering. The 'images', that are the child directives of the gallery, set the image src during the linking phase. Unfortunately, the image directive can't be easily modified since it's used very prolifically throughout the rest of the site.
A generic prototype of the use case that I'm describing to can be seen here:
http://jsfiddle.net/vmJKK/1/
var app = angular.module('myApp', []);
app.directive('parent', function ($timeout) {
return {
restrict: 'E',
transclude: true,
template: '<div><ol><li ng-repeat="log in logs track by $index">{{log}}</li></ol><hr></div>',
link: function ($scope, $element, $attrs, ctrl, $transclude) {
var _children = [];
$scope.logs = [];
$transclude(function (clones) {
$scope.logs.push('Transcluding');
angular.forEach(clones, function (clone) {
if (clone.nodeName !== '#text') {
_children.push(clone);
}
});
});
$timeout(function () {
angular.forEach(_children, function (child) {
$scope.logs.push('Appending child');
$element.append(child);
});
}, 2000);
}
};
});
app.directive('child', function () {
return {
restrict: 'E',
template: 'I am a child!',
link: function ($scope, $element, $attrs) {
$scope.$parent.logs.push('Linking child');
}
};
});
The current output of the prototype execution is:
1. Transcluding
2. Linking child
3. Linking child
4. Appending child
5. Appending child
What I want it to be is:
1. Transcluding
2. Appending child
3. Linking child
4. Appending child
5. Linking child
...or something like that, where the linking of the child doesn't occur until it's being appended.
You should be able to change child directive without breaking functionality with the code similar to this:
link: function ($scope, $element, $attrs) {
function link() {
$scope.$parent.logs.push('Linking child');
}
if (angular.isDefined($attrs.defer)) {
$element[0].link = link;
} else {
link();
}
}
The idea is to wrap current link function inside function that is either called immediately or attached to DOM node as parameterless link() function and called from append function of parent directive (if defer attribute is provided). I hope this is acceptable in your case.
The code is here: http://jsfiddle.net/vmJKK/5/
By the way, I have never seen the question of this quality so far. Perfect.
Related
I'm currently coding a tab navigation example to gain practical experience with Angular. This example uses custom directives and controller inheritance.
Plunker can be found here.
The issue: once the directives have finished processing I'd like to select a default tab to display. But at the point of calling the selectTab method of the myTabs controller (line 41 in script.js $ctrl.selectTab(0)), Angular hasn't yet finished processing the myTab directive (which generates the tab links), so the tabs array is empty and the selection fails.
I tried using $timeout without a delay, but that fails. $timeout only works with a delay of say 500ms set, which is hacky.
Is there an event or command available that signals the end of Angular processing certain directives, particularly directives that inherit from one another?
What I suspect is happening:
The myTabs directive finishes processing then fires its link function, but it's fired before the myTab directive is done processing. I can't place the selectTab method call within the myTab link function because would get called multiple times (based on the number of tabs processed.) Hope I'm explaining this clearly...I need a massage
app.directive('myTabs', ['$timeout', function($timeout) {
return {
restrict: 'E',
transclude: true,
controllerAs: 'myTabsCtrl',
templateUrl: 'my-tabs.htm',
scope: {},
controller: function ($scope) {
vm = this;
this.tabs = [];
this.addTab = function (tab) {
this.tabs.push(tab);
};
this.selectTab = function selectTab (tabIndex) {
for (var i = 0; i < this.tabs.length; i++) {
this.tabs[i].selected = (i === tabIndex ? true : false);
}
};
},
link: function ($scope, $element, $attrs, $ctrl) {
$timeout(function () {
$ctrl.selectTab(0);
});
}
};
}]);
app.directive('myTab', function() {
return {
restrict: 'E',
require: '^^myTabs',
transclude: true,
templateUrl: 'my-tab.htm',
scope: {
title: '#'
},
link: function (scope, element, attrs, ctrl){
scope.tab = {
title: scope.title,
selected: false
};
ctrl.addTab(scope.tab);
}
};
});
Each individual tab is registering itself with the parent directive controller. Simply set the selected flag on the first tab when it registers itself:
this.tabs = [];
this.addTab = function (tab) {
console.log("add tab ", tab);
if (!this.tabs.length) {
//Set flag for first tab to register
tab.selected = true;
};
this.tabs.push(tab);
};
The DEMO on PLNKR
AngularJS 1.5.3 introduced the $postLink life-cycle hook:
Life-cycle hooks
Directive controllers can provide the following methods that are called by Angular at points in the life-cycle of the directive:
$postLink() - Called after this controller's element and its children have been linked. Similar to the post-link function this hook can be used to set up DOM event handlers and do direct DOM manipulation. Note that child elements that contain templateUrl directives will not have been compiled and linked since they are waiting for their template to load asynchronously and their own compilation and linking has been suspended until that occurs.
--AngularJS $compile Service API Reference -- Life-cycle hooks
The $postLink life-cycle hook won't work in this case because the directives use templateUrl. It also won't work with directives that build DOM after postlink; ng-repeat, ng-if, ng-include, etc.
I have this directive that is getting more and more complicated. So I decided to split it up into parts.
The directive itself loaded a garment SVG graphic, when the SVG loaded it then ran a configure method which would apply a design, applied picked colours (or database colours if editing) and other bits and pieces.
As I said, it was all in one directive, but I have now decided to separate the logic out.
So I created my first directive:
.directive('configurator', function () {
// Swap around the front or back of the garment
var changeView = function (element, orientation) {
// If we are viewing the front
if (orientation) {
// We are viewing the front
element.addClass('front').removeClass('back');
} else {
// Otherwise, we are viewing the back
element.addClass('back').removeClass('front');
}
};
return {
restrict: 'A',
scope: {
garment: '=',
onComplete: '&'
},
require: ['configuratorDesigns'],
transclude: true,
templateUrl: '/assets/tpl/directives/kit.html',
link: function (scope, element, attrs, controllers) {
// Configure our private properties
var readonly = attrs.hasOwnProperty('readonly') || false;
// Configure our scope properties
scope.viewFront = true;
scope.controls = attrs.hasOwnProperty('controls') || false;
scope.svgPath = 'assets/garments/' + scope.garment.slug + '.svg';
// Apply the front class to our element
element.addClass('front').removeClass('back');
// Swaps the design from front to back and visa versa
scope.rotate = function () {
// Change the orientation
scope.viewFront = !scope.viewFront;
// Change our view
changeView(element, scope.viewFront);
};
// Executes after the svg has loaded
scope.loaded = function () {
// Call the callback function
scope.onComplete();
};
}
};
})
This is pretty simple in design, it gets the garment and finds the right SVG file and loads it in using ng-transclude.
Once the file has loaded a callback function is invoked, this just tells the view that it is on that it has finished loading.
There are a few other bits and pieces that you should be able to work out (changing views, etc).
In this example I am only requiring one other directive, but in the project there are 3 required directives, but to avoid complications, one will suffice to demonstrate my problem.
My second directive is what is needed to apply the design. It looks like this:
.directive('configuratorDesigns', function () {
return {
restrict: 'A',
controller: 'ConfiguratorDesignsDirectiveController',
link: function (scope, element, attrs, controller) {
// Get our private properties
var garment = scope.$eval(attrs.garment),
designs = scope.$eval(attrs.configuratorDesigns);
// Set our controller designs array
controller.designs = designs;
// If our design has been set, watch it for changes
scope.$watch(function () {
// Return our design
return garment.design;
}, function (design) {
// If we have a design
if (design) {
// Change our design
controller.showDesign(element, garment);
}
});
}
}
})
The controller for this directive just loops through the SVG and finds the design that matches the garment design object. If it finds it, it just hides the others and shows that one.
The problem I have is that this directive is unaware of the SVG loading or not. In the "parent" directive I have the scope.loaded function which is executed when the SVG has finished loading.
The "parent" directive's template looks like this:
<div ng-transclude></div>
<div ng-include="svgPath" onload="loaded()"></div>
<span class="glyphicon glyphicon-refresh"></span>
So my question is this:
How can I get the required directives to be aware of the SVG loaded state?
If I understand your question correctly, $rootScope.broadcast should help you out. Just broadcast when the loading is complete. Publish a message from the directive you are loading the image. On the directive which needs to know when the loading is complete, listen for the message.
I have a directive which open up a bootstrap-tours on call of t.start():
app.directive('tourGuide', function ($parse, $state) {
var directiveDefinitionObject = {
restrict: 'E',
replace: false,
link: function (scope, element, attrs) {
var t = new Tour({container: $("#main"),
backdrop: false,
debug:true
});
t.addStep({
element: "#main",
title: "Title123",
content: "Content123"
});
t.init();
t.start();
}};
return directiveDefinitionObject;
});
I want to create a button which on click could call variable t.start(). Is it even possible? I want to achieve this so could be independent of functions inside controllers, because this directive will be on every single view of the application, so it would be nice if it could call a parameter inside itself. Ive tryed to create a template in directive with a button, and add a ng-clikc action with t.start() and ofcourse it failed because variable t is not known to controller where ever my directive is.
EXAMPLE:
Lets say i have 2 views ShowItems and CreateItem they have 2 dirfferent controllers. in those views i have 1 button/link, on click of it i want to show my TourGuide. Thats simple.
Now in my TourGuide i have 2 different Steps, and when i press on a button in CreateItem view i want to see the step in Tour Guide for CreateItem view, and vise versa.
Thats simple if i use functions inside my controller. But is it possible to use directive ONLY, because i could have 20 different controllers?
Based on a few assumptions - I assume what you want here is to dynamically call a routine in scope from a directive. Take the following code as an example
HTML/View Code
<div my-directive="callbackRoutine">Click Here</div>
Controller
function MyController($scope) {
$scope.callbackRoutine = function () {
alert("callback");
};
}
Directive
app.directive("myDirective", function () {
return {
restrict: 'A',
link: function (scope, element, attr){
element.bind('click', function (){
if (typeof scope[attr.myDirective] == "function"){
scope[attr.myDirective]();
}
});
}
};
});
In this, you specify the callback routine as part of the directive. The key to the equation is that the scope for the directive inherits from any parent scope(s) which means you can call the routine even from the scope passed to the directive. To see a working example of this, see the following plunkr: http://plnkr.co/edit/lQ1QlwwWdpNvoYHlWwK8?p=preview. Hope that helps some!
I have a directive that I'd like another directive to be able to call in to. I have been trying to use directive controllers to try to achieve this.
Directive one would be sitting on the same page as directive two, and directive one would call methods exposed by directive two's controller:
Directive 1:
'use strict';
angular.module('angularTestApp')
.directive('fileLibrary', function () {
return {
templateUrl: 'views/manage/file_library/file-library.html',
require: 'videoClipDetails',
restrict: 'AE',
link: function postLink(scope, element, attrs, videClipDetailsCtrl) {
scope.doSomethingInVideoClipDirective = function() {
videClipDetailsCtrl.doSomething();
}
}
};
});
Directive Two:
'use strict';
angular.module('angularTestApp')
.directive('videoClipDetails', function () {
return {
templateUrl: 'views/video_clip/video-clip-details.html',
restrict: 'AE',
controller: function($scope, $element) {
this.doSomething = function() {
console.log('I did something');
}
},
link: function postLink(scope, element, attrs) {
console.log('videoClipDetails directive');
//start the element out as hidden
}
};
});
File where the two are used and set up as siblings:
<div>
<div video-clip-details></div>
<!-- main component for the file library -->
<div file-library></div>
</div>
I know reading documentation I picked up that the controllers can be shared when the directives are on the same element, which makes me think I might be looking at this problem the wrong way. Can anyone put me on the right track?
From the angular.js documentation on directives
When a directive uses require, $compile will throw an error unless the specified controller is found. The ^ prefix means that this directive searches for the controller on its parents (without the ^ prefix, the directive would look for the controller on just its own element).
So basically what you are trying to do with having siblings directly communicate is not possible. I had run into this same issue but I did not want to use a service for communication. What I came up with was a method of using a parent directive to manage communication between its children, which are siblings. I posted the example on github.
What happens is that both children require the parent (require: '^parentDirective') and their own controller, both of which are passed into the link function. From there each child can get a reference to the parent controller and all of its public methods, as an API of sorts.
Below is one of the children itemEditor
function itemEditor() {
var directive = {
link: link,
scope: {},
controller: controller,
controllerAs: 'vm',
require: ['^itemManager', 'itemEditor'],
templateUrl: 'app/scripts/itemManager/itemManager.directives.itemEditor.html',
restrict: 'A'
};
return directive;
function link(scope, element, attrs, controllers) {
var itemManagerController = controllers[0];
var itemEditorController = controllers[1];
itemEditorController.itemManager = itemManagerController;
itemEditorController.initialize();
}
function controller() {
var vm = this;
// Properties
vm.itemManager = {};
vm.item = { id: -1, name: "", size: "" };
// Methods
vm.initialize = initialize;
vm.updateItem = updateItem;
vm.editItem = editItem;
// Functions
function initialize() {
vm.itemManager.respondToEditsWith(vm.editItem);
}
function updateItem() {
vm.itemManager.updateItem(vm.item);
vm.item = {};
}
function editItem(item) {
vm.item.id = item.id;
vm.item.name = item.name;
vm.item.size = item.size;
}
}
}
Note how the values passed into the require array are the parent directive's name and the current directive's name. These are then both accessible in the link function via the controllers parameter. Assign the parent directive's controller as a property of the current child's and then it can be accessed within the child's controller functions via that property.
Also notice how in the child directive's link function I call an initialize function from the child's controller. This is where part of the communication lines are established.
I'm basically saying, anytime you (parent directive) receive a request to edit an item, use this method of mine named editItem which takes an item as a parameter.
Here is the parent directive
function itemManager() {
var directive = {
link: link,
controller: controller,
controllerAs: 'vm',
templateUrl: 'app/scripts/itemManager/itemManager.directives.itemManager.html',
restrict: 'A'
};
return directive;
function link(scope, element, attrs, controller) {
}
function controller() {
var vm = this;
vm.updateMethod = null;
vm.editMethod = null;
vm.updateItem = updateItem;
vm.editItem = editItem;
vm.respondToUpdatesWith = respondToUpdatesWith;
vm.respondToEditsWith = respondToEditsWith;
function updateItem(item) {
vm.updateMethod(item);
}
function editItem(item) {
vm.editMethod(item);
}
function respondToUpdatesWith(method) {
vm.updateMethod = method;
}
function respondToEditsWith(method) {
vm.editMethod = method;
}
}
}
Here in the parent you can see that the respondToEditsWith takes a method as a parameter and assigns that value to its editMethod property. This property is called whenever the controller's editItem method is called and the item object is passed on to it, thus calling the child directive's editItem method. Likewise, saving data works the same way in reverse.
Update: By the way, here is a blog post on coderwall.com where I got the original idea with good examples of require and controller options in directives. That said, his recommended syntax for the last example in that post did not work for me, which is why I created the example I reference above.
There is no real way with require to communicate between sibling elements in the way you are trying to do here. The require works the way you have set up if the two directives are on the same element.
You can't do this however because both of your directives have an associated templateUrl that you want to use, and you can only have one per element.
You could structure your html slightly differently to allow this to work though. You basically need to put one directive inside the other (transcluded) and use require: '^videoClipDetails'. Meaning that it will look to the parent to find it.
I've set up a fiddle to demonstrate this: http://jsfiddle.net/WwCvQ/1/
This is the code that makes the parent thing work:
// In videoClipDetails
template: '<div>clip details<div ng-transclude></div></div>',
transclude: 'true',
...
// in markup
<div video-clip-details>
<div file-library></div>
</div>
// in fileLibrary
require: '^videoClipDetails',
let me know if you have any questions!
Hi I have a factory that GETs data from a backend. This data is then processed with a controller (as seen below) and injected in the web page with ng-repeat. Due to asynchronous nature of the system, I have troubles when I try to manipulate window. For example I need to use window.scrollTo function but only AFTER data was completely processed and displayed on screen with ng-repeat.
As you can see here, I tried to use a promise early in the controller. But it doesn't work: window.scrollTo is always processed before data has finished being processed on screen. I guess what I need is a way to actually force the data process to be completed and only then process the window.scrollTo function but I don't see how.
.controller('myCtrl',
function ($scope, prosFactory, fieldValues, $q ) {
$scope.listpros = function() {
prosFactory.getPros()
.success(function(data, status) {
var defer = $q.defer();
defer.promise
.then(function () {
$scope.prosItems = data; // pass data to ng repeat first
})
.then(function () {
$window.scrollTo(0, 66); // then scroll
});
defer.resolve();
}).error(function(data, status) {
alert("error");
}
);
};
I tried with a 2000 timeout on scrollTo function, but due to variation in internet speed, it sometimes delay the scroll to much, or sometime isn't enough.
A promise won't help much in this case, because it is not connected to the ng-repeat process by any means. However you could use a custom directive to $emit an event when the last item got processed. Your controller could listen to that specific event and react to it according to your needs.
This answer shows you how to achieve this, it even comes with a Plunker.
EDIT: General approach to control DOM creation
Within the AngularJS world, DOM elements are controlled and, in this case supervised, by directives. The answer I linked to above extends the behavior of a ng-repeat directive, but you could do the same for possibly any DOM element. A directive creation-emitter could look something like this:
myModule.directive('creationEmitter', function () {
return {
restrict: 'A',
compile: function compile(tElement, tAttrs, transclude) {
return {
pre: function (scope, iElement, iAttrs, controller) {
scope.$emit('preCompile', iElement)
},
post: function (scope, iElement, iAttrs, controller) {
scope.$emit('postCompile', iElement)
}
}
},
link: function (scope, iElement, iAttrs) {
scope.$emit('link', iElement)
}
};
});
You could now listen to all those events in a controller that's above the given element in the DOM (for child elements use $broadcast).
Its possible to chain promises and do intermediate processing.
The immideate callback must return what the next will receive.
I don't see any immediate problems with this approach:
prosFactory.getPros()
.then(processCb)
.then(doSomethingWithProcessedDataCb)
.catch(errorCb)
.finally(scrollWindowCb)
Check out the documentation, it's actually quite good.