Angular directive template unknown scope - angularjs

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;
}
});

Related

How can I use interpolation to specify element directives?

I want to create a view in angular.js where I add a dynamic set of templates, each wrapped up in a directive. The directive names correspond to some string property from a set of objects. I need a way add the directives without knowing in advance which ones will be needed.
This project uses Angular 1.5 with webpack.
Here's a boiled down version of the code:
set of objects:
$scope.items = [
{ name: "a", id: 1 },
{ name: "b", id: 2 }
]
directives:
angular.module('myAmazingModule')
.directive('aDetails', () => ({
scope: false,
restrict: 'E',
controller: 'myRavishingController',
template: require("./a.html")
}))
.directive('bDetails',() => ({
scope: false,
restrict: 'E',
controller: 'myRavishingController',
template: require("./b.html")
}));
view:
<li ng-repeat="item in items">
<div>
<{{item.name}}-details/>
</div>
</li>
so that eventually the rendered view will look like this:
<li ng-repeat="item in items">
<div>
<a-details/>
</div>
<div>
<b-details/>
</div>
</li>
How do I do this?
I do not mind other approaches, as long as I can inline the details templates, rather then separately fetching them over http.
Use ng-include:
<li ng-repeat="item in items">
<div ng-controller="myRavishingController"
ng-include="'./'+item.name+'.html'">
</div>
</li>
I want to inline it to avoid the http call.
Avoid http calls by loading templates directly into the template cache with one of two ways:
in a script tag,
or by consuming the $templateCache service directly.
For more information, see
AngularJS $templateCache Service API Reference
You can add any html with directives like this:
const el = $compile(myHtmlWithDirectives)($scope);
$element.append(el);
But usually this is not the best way, I will just give a bit more detailed answer with use of ng-include (which actully calls $compile for you):
Add templates e.g. in module.run: [You can also add templates in html, but when they are required in multiple places, i prefer add them directly]
app.module('myModule').run($templateCache => {
$templateCache.put('tplA', '<a-details></a-details>'); // or webpack require
$templateCache.put('tplB', '<b-details></b-details>');
$templateCache.put('anotherTemplate', '<input ng-model="item.x">');
})
Your model now is:
$scope.items = [
{ name: "a", template: 'tplA' },
{ name: "b", template: 'tplB' },
{ name: "c", template: 'anotherTemplate', x: 'editableField' }
]
And html:
<li ng-repeat="item in items">
<div ng-include="item.template">
</div>
</li>
In order to use dynamic directives, you can create a custom directive like I did in this plunkr:
https://plnkr.co/edit/n9c0ws?p=preview
Here is the code of the desired directive:
app.directive('myProxy', function($compile) {
return {
template: '<div>Never Shown</div>',
scope: {
type: '=',
arg1: '=',
arg2: '='
},
replace: true,
controllerAs: '$ctrl',
link: function($scope, element, attrs, controller, transcludeFn) {
var childScope = null;
$scope.disable = () => {
// remove the inside
$scope.changeView('<div></div>');
};
$scope.changeView = function(html) {
// if we already had instanciated a directive
// then remove it, will trigger all $destroy of children
// directives and remove
// the $watch bindings
if(childScope)
childScope.$destroy();
console.log(html);
// create a new scope for the new directive
childScope = $scope.$new();
element.html(html);
$compile(element.contents())(childScope);
};
$scope.disable();
},
// controller is called first
controller: function($scope) {
var refreshData = () => {
this.arg1 = $scope.arg1;
this.arg2 = $scope.arg2;
};
// if the target-directive type is changed, then we have to
// change the template
$scope.$watch('type', function() {
this.type = $scope.type;
refreshData();
var html = "<div " + this.type + " ";
html += 'data-arg1="$ctrl.arg1" ';
html += 'data-arg2="$ctrl.arg2"';
html += "></div>";
$scope.changeView(html);
});
// if one of the argument of the target-directive is changed, just change
// the value of $ctrl.argX, they will be updated via $digest
$scope.$watchGroup(['arg1', 'arg2'], function() {
refreshData();
});
}
};
});
The general idea is:
we want data-type to be able to specify the name of the directive to display
the other declared arguments will be passed to the targeted directives.
firstly in the link, we declare a function able to create a subdirective via $compile . 'link' is called after controller, so in controller you have to call it in an async way (in the $watch)
secondly, in the controller:
if the type of the directive changes, we rewrite the html to invoke the target-directive
if the other arguments are updated, we just update $ctrl.argX and angularjs will trigger $watch in the children and update the views correctly.
This implementation is OK if your target directives all share the same arguments. I didn't go further.
If you want to make a more dynamic version of it, I think you could set scope: true and have to use the attrs to find the arguments to pass to the target-directive.
Plus, you should use templates like https://www.npmjs.com/package/gulp-angular-templatecache to transform your templates in code that you can concatenate into your javascript application. It will be way faster.

Angular directive - how to pass array to ng-repeat and bool to ng-show?

I am following John Papa's style guide while developing in Angular 1.x. I want to write a directive that takes an array as an attribute (parameter) and then performs ng-repeat on it. How do I do that? This is my code but it does not work
My HTML snippet. This is how I wolud like to call the directive
...
<my-repeat-directive
messages="pageController.messages"
show="pageController.show">
</my-repeat-directive >
...
In the pageController file I have defined messages and show. The values are fetched from the server but here I will just hard code them.
...
var vm = this;
vm.messages = ["Message 1", "Message 2", "Longer message 3"];
vm.show = true;
...
here is my directive file:
angular
.module('app.directives',[])
.directive('myRepeatDirective',myRepeatDirective);
function myRepeatDirective() {
return {
restrict: 'E',
scope: {
messages: '=',
show: '=',
},
template:'<div ng-repeat="message in messages">' +
'<p>Message {{message}}</p> </div>' +
'<p ng-show=show> This is a footer </p>'
}
}
How should I pass the parameter to ng-repat so it realizes it is an array?
Thanks

AngularJS : after isolating scope and binding to a parentFunction, how to pass parameters to this parentFunction?

I'm trying very hard to understand scopes in AngularJS and am running into problems.
I've created a simple "comments" application that
has an input box for publishing a comment (text + 'Reply' button) [this is working fine]
clicking 'Reply' button unhides another input box for publishing a reply (with a 'PublishReply' button)
clicking 'PublishReply' button, publishes the reply below the original comment and also indents it.
I generate comments within 'commentsDirective' using ng-repeat and embed a 'replyDirective' within each ng-repeat. I'm able to bind the parent scope's functions from the child directive's isolated scope, but I'm just not able to pass the arguments to this function.
Again, I think, a scope related problem is preventing me to hide/unhide the 'replyDirective' from the on-click of 'Reply' button.
Grateful for your help.
Here is the code in plunker: http://plnkr.co/edit/5AmlbOh6iEPby9K2LJDE?p=preview
<body ng-app="comments">
<div ng-controller="mainController">
<div class="publishComment"><input type="text" ng-model="contentForPublishing"/><button ng-click="publishComment(null, 0, contentForPublishing)">Publish Comment</button></div>
<comments-directive></comments-directive>
</div>
</body>
<script>
angular.module('comments', [])
.controller('mainController', function($scope) {
$scope.comments = [
{ id: 1, parentId: 0, content:'first comment'},
{ id: 2, parentId: 0, content:'second comment'}
];
$scope.publishComment = function (commentId, commentParentId, contentForPublishing){
if (commentId === null) {commentId = $scope.comments.length + 1;} // this (commentId === null) is sent only from the publishComments and not from publishReply
$scope.comments.push( { id: commentId, parentId:commentParentId, content:contentForPublishing } );
$scope.contentForPublishing = "";
}
$scope.replyWidgetVisible = false;
$scope.showReplyWidget = function() {
$scope.replyWidgetVisible = true;
}
})
.directive('commentsDirective', function() {
return {
restrict: 'E',
// template: '<div id="{{comment.id}}" class="commentWrapper" ng-class="{{ {true: '', false: 'indentLeft'}[{{comment.parentId}} === 0] }}" ng-repeat="comment in comments">' +
template: '<div id="{{comment.id}}" class="commentWrapper" ng-repeat="comment in comments">' +
'id: {{comment.id}} parentId: {{comment.parentId}}<br>>> {{comment.content}}<br>' +
'<button class="reply" ng-click="showReplyWidget()">Reply</button>' +
// '<reply-directive publish-reply="publishComment()" ng-show="{{replyWidgetVisible}}" reply-widget-visible="replyWidgetVisible"></reply-directive>' +
'<reply-directive publish-reply="publishComment()" comments-array="comments"></reply-directive>' +
'</div>'
};
})
.directive('replyDirective', function() {
return {
restrict: 'E',
scope: {
publishReply: '&',
commentsArray: '=',
replyWidgetVisible: '='
},
template: '<div class="publishComment"><input type="text" ng-model="contentForPublishing"/><button ng-click="publishReply(5, 1, contentForPublishing)">Publish Reply</button></div>'
};
});
</script>
Basically you need to "fetch" the publishComment function, since with publish-reply="publishComment()" you are telling Angular to call publishComment without any arguments, regardless of the arguments you are passing on your isolated scope. So, to actually reach the publishComment function (and not only the predefined executing function), so you can pass in arguments, you need to:
.directive('commentsDirective', function() {
return {
restrict: 'E',
template: '<div id="{{comment.id}}" class="commentWrapper" ng-repeat="comment in comments">' +
'id: {{comment.id}} parentId: {{comment.parentId}}<br>>> {{comment.content}}<br>' +
'<button class="reply" ng-click="showReplyWidget()">Reply</button>' +
'<reply-directive publish-reply="publishReply()" comments-array="comments"></reply-directive>' +
'</div>',
link: function(scope){
scope.publishReply = function(){
return scope.publishComment;
}
}
};
})
.directive('replyDirective', function() {
return {
restrict: 'E',
scope: {
publishReply: '&',
commentsArray: '=',
replyWidgetVisible: '='
},
template: '<div class="publishComment"><input type="text" ng-model="contentForPublishing"/><button ng-click="publishReply(5, 1, contentForPublishing)">Publish Reply</button></div>',
link: function(scope) {
scope.publishReply = scope.publishReply();
}
};
});
Think it like if you were doing: (function(){ return scope.publishComment(); })(5, 1, contentForPublishing);
Doing a "get reference to function" parent scope binding is mainly useful when the passed function is mutable. for example, my-cool-function="doThis()" and on another part of your app my-cool-function="doThat()". they exist so you can reuse the same directive in many situations, which isn't the case here.
A much simpler way would to $emit a publish event from your isolated scope and catch it in your comments directive. Or create a scope with true so you can access, in your newly created child scope, the function directly from the parent.
See updated plnkr in here http://plnkr.co/edit/nOWwFJ35XRXaIoxNPlW4?p=preview
Here is the plnkr showing how to keep just one reply box opened (you can keep as many open if you wish) http://plnkr.co/edit/za16eHPzltGLjK5ra1Vb?p=preview (see the revision before it for a widget state for each comment)

The load sequence difference between template and templateUrl in angular directive

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')

Dygraphs not working with ng-repeat

I'm new to AngularJS and building a dashboard with dygraphs.
Tried to put the example code from the dygraphs website in an ng-repeat-list, just to test. Expected the same sample graph for every x in y. Unfortunately the graph doesn't get drawn, just the axes, console doesn't show any errors.
<li ng-repeat="x in y">
<div id="graph">
<script>
new Dygraph(document.getElementById("graph"),
[ [1,10,100], [2,20,80], [3,50,60], [4,70,80] ],
{ labels: [ "x", "A", "B" ] });
</script>
</div>
</li>
If I remove ng-repeat, it works though (single graph) – so the dygraphs-code is valid. Of course it doesn't make sense to draw the graphs directly in the view like I did here, still I wonder why it doesn't work. Am I missing some general point here?
Your problem is that Angular will repeat your <div id="graph"> n times. So you now have n times div with id of 'graph' which are siblings. Therefore, when you call document.getElementById('graph'), that won't work very well.
That said, I don't know how well script tags inside ng-repeat works either, seems like a very strange use case.
The proper way to do this (as with all DOM related operations), is to use a directive. Here's an example:
Javascript:
var myApp = angular.module('myApp',[]);
myApp.controller('MyCtrl', function($scope) {
$scope.graphs = [
{
data: [ [1,10,100], [2,20,80], [3,50,60], [4,70,80] ],
opts: { labels: [ "x", "A", "B" ] }
},
{
data: [ [1,10,200], [2,20,42], [3,50,10], [4,70,30] ],
opts: { labels: [ "label1", "C", "D" ] }
}
];
});
myApp.directive('graph', function() {
return {
restrict: 'E', // Use as element
scope: { // Isolate scope
data: '=', // Two-way bind data to local scope
opts: '=?' // '?' means optional
},
template: "<div></div>", // We need a div to attach graph to
link: function(scope, elem, attrs) {
var graph = new Dygraph(elem.children()[0], scope.data, scope.opts );
}
};
});
HTML:
<div ng-controller="MyCtrl">
<graph ng-repeat="graph in graphs" data="graph.data" opts="graph.opts"></graph>
</div>
JSFiddle
Hope this helps!

Resources