The load sequence difference between template and templateUrl in angular directive - angularjs

We were facing this issue several days before. At that time, we were introducing Angular into our HTML5 based mobile family photo social application Family Snap. It's maintained by www.uhella.com.
During the restructure, I moved the dojo code into directive inline, it works well. The calendar_month_datepicker (dojox.mobile.SpinWheelDatePicker) was successfully injected by dijit to be a huge Div then.
After that, I want to separate it into individual html file as template, because html editor will understand my html code better. So I modify the code as following:
Familysnap/directive/homepickdata.js
'use strict';
/* Directives */
FamilySnapModule.directive('homePickdata', function() {
return {
restrict: 'EAC',
replace: true,
transclude: true,
templateUrl: 'Familysnap/templates/homePickdata.html'
//template: '<div id="calendar_month_datepicker" data-dojo-type="dojox.mobile.SpinWheelDatePicker" data-dojo-props=\'slotOrder: [0,1,2], monthPattern: "MM", dayPattern: "dd", align: "center"\'></div>'
};
});
Familysnap/templates/homePickdata.html
<div id="calendar_month_datepicker" data-dojo-type="dojox.mobile.SpinWheelDatePicker" data-dojo-props='slotOrder: [0,1,2], monthPattern: "MM", dayPattern: "dd", align: "center"'></div>
Familysnap/modules/dataPicker.js
require([
…
], function(dom, domStyle, domAttr, on, ready, registry, JSON, string,
ListItem, array, request, domClass, query, domProp, domConstruct, tap, swipe, Uuid, generateRandomUuid,
Pane, SpinWheelDatePicker, win, Opener, Heading, ToolBarButton, SwapView) {
function FamilySnapMonthToday()
{
…
setTimeout(function(){
registry.byId("calendar_month_datepicker").set("values", [global_calendar_current_year, global_calendar_current_month + 1, global_calendar_current_date]);
}, 500);
…
}
function FamilySnapMonthDone()
{…
var values = registry.byId("calendar_month_datepicker").get("values");
…
}
ready(function(){
…
on(dom.byId("calendar_month_done_btn"), "click", FamilySnapMonthDone);
…
});
});
After this modification, the calendar_month_datepicker (dojox.mobile.SpinWheelDatePicker) was not injected by dijit. It just injected by angular compiler.
And the “registry.byId("calendar_month_datepicker")” will always return null.
I finally figured out the load sequence between template and templateUrl is different in angular compiler by chrome source code debug tools (Debugging-in-PhoneGap ).
First of all, I set up break point on my directive.
At the time code paused on my break point.
Familysnap/directive/homepickdata.js
'use strict';
/* Directives */
FamilySnapModule.directive('homePickdata', function() {
return {
restrict: 'EAC',
replace: true,
transclude: true,
templateUrl: 'angular/templates/homePickdata.html'
//template: '<div id="calendar_month_datepicker" data-dojo-type="dojox.mobile.SpinWheelDatePicker" data-dojo-props=\'slotOrder: [0,1,2], monthPattern: "MM", dayPattern: "dd", align: "center"\'></div>'
};
});
The directive “home-pickdata” in both version is not injected. It's what we expect.
<div home-pickdata></div>
The different is here:
function bootstrap(element, modules) {
var doBootstrap = function() {
element = jqLite(element);
if (element.injector()) {
var tag = (element[0] === document) ? 'document' : startingTag(element);
throw ngMinErr('btstrpd', "App Already Bootstrapped with this Element '{0}'", tag);
}
modules = modules || [];
modules.unshift(['$provide', function($provide) {
$provide.value('$rootElement', element);
}]);
modules.unshift('ng');
var injector = createInjector(modules);
injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', '$animate',
function(scope, element, compile, injector, animate) {
scope.$apply(function() {
element.data('$injector', injector);
compile(element)(scope);
});
}]
);
return injector;
};
After the compile(element)(scope), the template version of directive is injected as following:
<div id="calendar_month_datepicker" data-dojo-type="dojox.mobile.SpinWheelDatePicker" data-dojo-props='slotOrder: [0,1,2], monthPattern: "MM", dayPattern: "dd", align: "center"'></div>
But the templateUrl version is still:
<div home-pickdata></div>
Even after the angularInit:
//try to bind to jquery now so that one can write angular.element().read()
//but we will rebind on bootstrap again.
bindJQuery();
publishExternalAPI(angular);
jqLite(document).ready(function() {
angularInit(document, bootstrap);
});
})(window, document);
The templateUrl version is still:
<div home-pickdata></div>
After this hook, the dojo injector will take care of all dojo tags,
but at that time, the templateUrl version of directive is still not injected by Angular.
So dojo/dijit don’t know the ID “calendar_month_datepicker”.
This explain why registry.byId(“calendar_month_datepicker”) return NULL in our code.
I am not familiar with dojo injector. I believe there's way to make dojo work together with angular templateUrl directive by some magic.
By reading the answer from Dimitri M and tik27, I update the code.
We need call "dojo/parser" manually to inject our widget.
Change data-dojo-type to data-familysnap-type.
<div id="calendar_month_datepicker" data-family-type="dojox.mobile.SpinWheelDatePicker" data-dojo-props='slotOrder: [0,1,2], monthPattern: "MM", dayPattern: "dd", align: "center"'></div>
and call parser on ready()
require(["dojo/parser"],function(dom,registry,parser)){
ready() {
parser.parse({scope: "familysnap"});
...
}
}
Then the calendar_month_datepicker was injected by dijit.
We can successfully call
registy.byId('calendar_month_datepicker')

Related

Calling controller method from directive in template

I have a directive that is sitting in a template that is then included on a page.
If I place my directive directly onto my page, then on a button click I can call a method within my controller.
However, when I place the directive within a template, and then the template on the page, I can no longer call a method in my controller from the directive.
I've tried a number of things with the posted code below my latest attempt. However, this code produces the error
asking for new/isolated scope on:
So HTML first;
This is on my HTML page.
<session-list trackid='san'></session-list>
This is the template HTML;
<div class="container col-sm-12 col-xs-12">
<div>Session list template for {{trackid}}</div>
<session-calendar callback-fn="ctrlFn()"></session-calendar>
</div>
My primary controller looks like this with the "eventClick" method I want to call.
angular.module('GAP.viewsessions', ['ngRoute'])
.controller('viewsessionsCtrl', ['$scope', function($scope){
$scope.eventClick = function(eventData){
console.log(eventData);
}
}]);
Then the "SessionList" directive;
angular.module("GAP.sessionList", [])
.directive("sessionList", function(){
return {
restrict: 'E',
link: function(scope, element, attributes){
},
scope: {
trackid: '#'
},
templateUrl: '/templates/sessionlist.html', // or use a path to a html file like 'path_to/template.html'
replace: true,
};
})
The other directive is a FullCalendar and in the click event of the event I have this;
eventClick: function(calEvent, jsEvent, view) {
scope.someCtrlFn();
if (scope.eventClick){
scope.eventClick(calEvent.data);
}
},
And If I include this;
scope: { someCtrlFn: '&callbackFn' },
I get the previously quoted error. If I leave it out, then the page renders but the "eventClick" method is never run in my controller.
One possible workaround is use an angular event
Inject $rootScope in directive then something like:
$rootScope.$broadcast('cal-event-clicked', eventData)
In controller
$scope.$on('cal-event-clicked', function(evt, data){
$scope.eventClick(data)
})

Angular directive template unknown scope

I know there is a lot of questions and posts about AngularJS and how directives are supposed to be used. And I got mine working just fine until I got another problem which I don't know how to resolve.
I use a directive on a custom HTML element. Directive transforms this element into a regular html tree as defined in a template. The HTML element has some attributes which are used when building the template. Data for one of the elements is received with HTTP request and is successfully loaded. This is the part which I got working fine.
Now I want to do something more. I've created a plunker which is an example of what I want to achieve. It's a fake one, but illustrates my problem well.
index.html:
<body ng-controller="MainCtrl">
<div id="phones">
<phone brand="SmartBrand" model="xx" comment="blah"></phone>
<phone brand="SmarterBrand" model="abc" comment="other {{dynamic.c1}}"></phone>
</div>
</body>
Angular directive:
app.directive('phone', function() {
return {
restrict: 'E',
replace: true,
scope: {
'comment': '#',
'brand': '#'
},
templateUrl: 'customTpl.html',
controller: function($scope) {
fakeResponse = {
"data": {
"success": true,
"data": "X300",
"dynamic": {
"c1": "12",
"c2": "1"
}
}
}
$scope.model = fakeResponse.data.data;
$scope.dynamic = fakeResponse.data.dynamic;
}
}
});
Template:
<div class="phone">
<header>
<h2>{{brand}} <strong>{{model}}</strong></h2>
</header>
<p>Comment: <strong>{{comment}}</strong></p>
</div>
So I would like to be able to customize one of the tags in the element (phone comment in this example). The trick is that the number of additional info that is going to be in the tag may vary. The only thing I can be sure of is that the names will match the ones received from AJAX request. I can make the entire comment be received with AJAX and that will solve my problem. But I want to separate template from the variables it is built with. Is it possible?
Ok, I got it working. It may not be the state of the art solution (I think #xelilof suggestion to do it with another directive may be more correct), but I'm out of ideas on how to do it (so feel free to help me out).
I've turned the {{comment}} part into a microtemplate which is analysed by a service. I've made a plunk to show you a working sample.
The JS part looks like this now:
app.directive('phone', ['dynamic', function(dynamic) {
return {
restrict: 'E',
replace: true,
scope: {
'comment': '#',
'brand': '#',
'color': '#',
'photo': '#'
},
templateUrl: 'customTpl.html',
controller: function($scope) {
fakeResponse = {
"data": {
"success": true,
"data": "X300",
"dynamic": {
"c1": "12",
"c2": "2"
}
}
}
$scope.model = fakeResponse.data.data;
$scope.comment2 = dynamic($scope.comment, fakeResponse.data.dynamic);
console.log("Comment after 'dynamic' service is: " + $scope.comment);
}
}
}]);
app.factory('dynamic', function() {
return function(template, vars) {
for (var v in vars) {
console.log("Parsing variable " + v + " which value is " + vars[v]);
template = template.replace("::" + v + "::", vars[v]);
}
return template;
}
});

angular component directives and templateurl, different response to outside controller

I am getting some behavior that was unexpected with my first foray into directives.
I am using a directive with a templateurl and isolated controller. I was expecting the dom inside the templateurl to react based on 2 way binding between the dom ( in this case a select element ) and the directives controller. I put a '$watch' on the bound attribute, which fires off when the directive is first loaded. But then if the user selects a different option, it does not fire. This is not the normal functionality ive come to expect from my experience with controllers so far.
my directive is as follows:
(function () {
'use strict';
var projMgrApp = angular.module('projMgrApp')
.directive('elementStructure', function () {
var structureController = ['$location', '$scope', '$window', '_', '$http', 'isAdmin', 'pmElement',
'GetProject', 'Enums', 'Users',
function ($location, $scope, $window, _, $http, isAdmin, pmElement, GetProject, Enums, Users) {
/* Ive removed most code for simplification
*/
$scope.NewElementType = null;
$scope.$watch('NewElementType', function (val) { alert(val); })
}];
return {
restrict: 'EA',
scope: {
elementType: '#',
projectId: '#',
structureTemplate: '#'
},
controller: structureController,
templateUrl: '/partials/structure.html',
};
});
})();
Inside my template url I have this select which im expecting to call the watcher..
<select class="form-control"
ng-options="d for d in ProductionCategoryOptions" ng-model="NewElementType"></select>
Im loading this directive twice as separate components ( and not intending to share any values )
<fieldset class="tab-pane fade in" id="ProductionStructure">
<element-structure element-type="Production" project-id={{Project.Id}}" structure-template={{Project.ProjectSettings.SceneStructureTemplate}}"></element-structure>
</fieldset>
<fieldset class="tab-pane fade in" id="AssetStructure">
<element-structure element-type="Asset" project-id="{{Project.Id}}"></element-structure>
</fieldset>
So I've got the directive working as intended. It seems as though there was a few fundamental quirks in the angular docs that hadn't quite clicked yet, and more than likely most that im still not.
To get it to work properly i needed to pull in an ngmodel into the directive and 2 way bind it ( '=' ). Prior to doing this i was creating the 'model' inside the directives controller, which then wasnt forcing the view to update ( or even calling the watcher - which im guessing is a scope issue ). So instead ive created the model in the outside controller, and bound the directive to that which seems to work now as i intended.
In my outside controller i create the object that is to bound to my directive:
$scope.ProductionStructureModel = {
Elements: null,
NewElement: {
Type: null,
Path: null,
Name: null,
Length: null,
Audio: null,
Parent: null,
}
};
my directive then references this as follows:
return {
restrict: 'EA',
requires: '^^ngModel',
scope: {
elementType: '#',
projectId: '#',
structureTemplate: '#',
Model: '=ngModel',
},
controller: structureController,
templateUrl: '/partials/structure.html',
link: function (scope, element, attrs) {
scope.$watch('Model', function (val) {
alert(val);
console.log(val);
});
}
};
the html that calls my directive:
<element-structure element-type="Production" project-id="{{Project.Id}}"
structure-template="{{Project.ProjectSettings.SceneStructureTemplate}}"
ng-model="ProductionStructureModel"
></element-structure>
One thing i havent figured out yet however, is that even though its adjusting the model as expected and updating the view - it still isnt calling the watcher.. Would anyone be able to shed some light on this for me?

testing ng-transclude doesn't work

I'm writing two directives that wrap ui-bootstrap's tabset and tab directives.
In order for the content of my directives to be passed to the wrapped directives, I'm using transclusion in both of them.
This works quite well, the only problem is that I'm failing at writing a test that checks that. My test uses a replacement directive as a mock for the wrapped directive, which I replace using $compileProvider before each test.
The test code looks something like this:
beforeEach(module('myModule', function($compileProvider) {
// Mock the internally used 'tab' which is a third party and should not be tested here
$compileProvider.directive('tab', function() {
// Provide a directive with a high priority and 'terminal' set to true, makes sure that
// the mock directive will get executed, and that the real directive will not
var mock = {
priority: 100,
terminal: true,
restrict: 'EAC',
replace: true,
transclude: true,
template: '<div class="mock" ng-transclude></div>'
};
return mock;
});
}));
beforeEach(function() {
inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
});
});
beforeEach(function() {
$scope = $rootScope.$new();
});
afterEach(function() {
$scope.$destroy();
});
it('Places the enclosed html inside the tab body', function() {
element = $compile("<div><my-tab>test paragraph</my-tab></div>")($scope);
$scope.$digest();
console.log("element.html() = ", element.html());
expect(element.text().trim()).toEqual("test paragraph");
});
The template of my directive looks something like this:
<div><tab><div ng-transclude></div></tab></div>
The directive module looks something like this:
angular.module('myModule', ['ui.bootstrap'])
.directive('myTab', function() {
return {
restrict: 'E',
replace: true,
transclude: true,
templateUrl: 'templates/my-tab.tpl.html',
scope: {
}
};
});
The result of the print to the console is this:
LOG: 'element.html() = ', '<div class="ng-isolate-scope" id=""><div id="" heading="" class="mock"><ng-transclude></ng-transclude></div></div>'
Any ideas on why the transclusion doesn't take place (again, it works outside of the test just fine) ?
Update
I've since moved on to other things and directives, and ran into this issue again, but now it's more crucial, the reason being, that the directive I place inside the parent directive, requires the parent controller in its link function.
I've done more research into this, and it turns out that for some reason, compiling the mock directive doesn't create an instance of the transcluded content.
The reason I know that, is that I've placed a printout in every hook possible in both directives (both the mock and the transcluded one), i.e. compile, pre-link, post-link and controller constructor, and I see that the only printouts are from the mock directive.
Now, here's the really interesting part: I've tried using the transclude function in the mock directive's link function to "force" the compilation of the transcluded directive, which worked ! (another proof that it didn't take place implicitly).
Where's the catch you ask ? Well, it still doesn't work. This time, since the link function of the transcluded directive fails since it doesn't find the controller of the mock directive. What ?!
Here's the code:
Code
var mod = angular.module('MyModule', []);
mod.directive('parent', function() {
return {
restrict: 'E',
replace: true,
template: '<div class="parent">...</div>',
controller: function() {
this.foo = function() { ... };
}
};
});
mod.directive('child', function() {
return {
restrict: 'E',
require: '^parent',
link: function(scope, element, attrs, parentCtrl) {
parentCtrl.foo();
}
};
});
Test
describe('child directive', function() {
beforeEach(module('MyModule', function($compileProvider) {
$compileProvider.directive('parent', function() {
return {
priority: 100,
terminal: true,
restrict: 'E',
replace: true,
transclude: true,
template: '<div class="mock"><ng-transclude></ng-transclude></div>',
controller: function() {
this.foo = jasmine.createSpy();
},
link: function(scope, element, attrs, ctrls, transcludeFn) {
transcludeFn();
}
};
});
}));
});
This test fails with an error message such as:
Error: [$compile:ctreq] Controller 'parent', required by directive
'child', can't be found!
Any thoughts, ideas, suggestions would be highly appreciated.
Ok, probably the shortest bounty ever in the history of SO ...
The problem was with the terminal: true and priority: 100 properties of the mock directive. I was under the impression (from an article I read online about how to mock directives), that these properties cause the compiler to stop compiling directives with the same name and prioritize the mock directive to be evaluated first.
I was obviously wrong. Looking at this and this, it becomes clear that:
'terminal' stops any other directives that were not processed yet from being processed
'priority' is used to make sure that the mock directive is processed before the directive it is mocking
The problem, is that this causes all other processing to stop, including the ng-transclude directive, which has the default priority of 0.
However, removing these properties causes all hell to break loose, since both directives have been registered, and so forth (I won't burden you with all the gory details). In order to be able to remove these properties, the two directives should reside in different modules, and there should be no dependency between them. In short, when testing the child directive, the only directive named parent that's evaluated should be the mock directive.
In order to support real life usage, I've introduced three modules to the system:
A module for the child directive (no dependencies)
A module for the parent directive (no dependencies)
A module that has no content, but has a dependency on both child and parent modules, which is the only module you'll ever need to add as a dependency in your code
That's pretty much it. I hope it helps anyone else that runs into such problems.

BxSlider and Angular js

If you are working with Angular, probably you will have case where you want to call some JQUERY library to do something for you. The usual way would be to call JQUERY function on page ready:
$(function(){
$(element).someJqueryFunction();
});
If your element content is dynamically added using Angular, than this approach is not good as I understood, and you need to create an custom directive in order to fulfill this functionality.
In my case, I want to load bxSlider on <ul class="bxslider"> element, but content of the <ul> element should be loaded using angular ng-repeat directive.
Picture names are collected using REST service from the database.
I call my directive called startslider using the following code:
<div startslider></div>
My custom Angular directive is: (pictures is variable in the $scope passed from my controller, which called REST service)
application.directive('startslider', function () {
return {
restrict: 'A',
replace: false,
template: '<ul class="bxslider">' +
'<li ng-repeat="picture in pictures">' +
'<img ng-src="{{siteURL}}/slide/{{picture.gallery_source}}" alt="" />'
'</li>' +
'</ul>',
link: function (scope, elm, attrs) {//from angular documentation, the link: function should be called in order to change/update the dom elements
elm.ready(function () {
elm.bxSlider({
mode: 'fade',
autoControls: true,
slideWidth: 360,
slideHeight:600
});
});
}
};
});
As result, I get all the pictures from the database displayed on my screen, but without bxSlider loaded (all pictures displayed one bellow other).
Please note that bxSlider is working, because when I call $(element).bxSlider(); on manually written code, the slider loads.
Why is this happening?
EDIT:
I think your problem is caused by trying to call the slider on HTMLUListElement, which is an object.
To call the slider on the DOM element, you could use $("." + $(elm[0]).attr('class')) that will use the existing bxslider class, or better assign an id to your <div startslider id="slideshow"></div> and call like $("." + $(elm[0]).attr('id'))
elm.ready(function() {
$("." + $(elm[0]).attr('class')).bxSlider({
mode: 'fade',
autoControls: true,
slideWidth: 360,
slideHeight:600
});
});
Full code:
http://jsfiddle.net/z27fJ/
Not my solution, but I'd thought I would pass it along.
I like this solution the best (Utilizes directive controllers)
// slightly modified from jsfiddle
// bxSlider directive
controller: function() {},
link: function (scope, element, attrs, controller) {
controller.initialize = function() {
element.bxSlider(BX_SLIDER_OPTIONS);
};
}
// bxSliderItem directive
require: '^bxSlider',
link: function(scope, element, attrs, controller) {
if (scope.$last) {
controller.initialize();
}
}
http://jsfiddle.net/CaioToOn/Q5AcH/8/
Alternate, similar solution, using events (I do not like)
Jquery bxslider not working + Angular js ng-repeat issue
Root issue
You cannot call scope.$apply in the middle of a digest cycle.
You cannot fire bxSlider() until the template gets compiled.
Ultimately, you need to wait for the template to be compiled and available before calling bxSlider()
Calling a function when ng-repeat has finished
this is the directive
.directive('slideit', function () {
return function (scope, elm, attrs) {
var t = scope.$sliderData;
scope.$watch(attrs.slideit, function (t) {
var html = '';
for (var i = 0; i <= t.length-1; i++) {
html += '<li><ul class="BXslider"><li class="BXsliderHead"><img src="'+scope.$imageUrl+'flight/'+t[i].code+'.gif" />'+t[i].name+'</li><li class="BXsliderChild">'+t[i].noneStop+'</li><li class="BXsliderChild">'+t[i].oneStop+'</li><li class="BXsliderChild">'+t[i].twoStop+'</li></ul></li>';
}
angular.element(document).ready(function () {
$("#" + $(elm[0]).attr('id')).html(html).bxSlider({
pager:false,
minSlides: 3,
maxSlides: 7,
slideWidth: 110,
infiniteLoop: false,
hideControlOnEnd: true,
auto: false,
});
});
});
};
});
this is the html in view
<div ui-if="$sliderData">
<ul id="experimental" slideit="$sliderData"></ul>
</div>
and the important part is the dependency of js file
<script src="/yii-application/frontend/web/js/libraries/angular/angular.min.js"></script>
<script src="/yii-application/frontend/web/js/libraries/angular/angular-ui.js"></script>
<script src="/yii-application/frontend/web/js/libraries/jquery/jquery-1.11.3.min.js"></script>
<script src="/yii-application/frontend/web/js/libraries/flex/jquery.bxslider.min.js"></script>
<script src="/yii-application/frontend/web/assets/e3dc96ac/js/bootstrap.min.js"></script>
<script src="/yii-application/frontend/web/js/libraries/jquery-ui/jquery-ui.min.js"></script>
<script src="/yii-application/frontend/web/js/searchResult.js"></script>
<script src="/yii-application/frontend/web/js/Flight.js"></script>
and don't forget to put ui in module
var app = angular.module('myApp', ['ui']);
this is the bxslider that i use !!!! maybe solve your problem

Resources