Getting ng-repeat to work inside of AngularJS's $interpolate service - javascript

I am using the AngularJs-UI components for Bootstrap. I would like to insert a filled out template into one of the data elements for the popover feature. This works find for all elements not inside of a ng-repeat. How do I get the ng-repeat elements to work inside of a interpolated template?
I have a plunker at http://plnkr.co/edit/Cuku7qaUTL1lxRkafKzv Its not working because I don't know how to get Angular-UI-bootstrap to in plunker.
<div data-popover="{{createHTML()}}">some content</div>
My local scope has the function createHTML() that looks something like this.
angular.module('myApp', ['ngSanitize'])
.controller("myController", function(myService){
$scope.createHTML = function() {
var thingy = { blah: "foobar", collection: [ "1", "2", "3" ] };
return myService.html_for(thingy);
}
});
And the service is
angular.module('myApp')
.service('myService', function($templateCache, $interpolate, $sanitize, $log) {
"use strict";
function html_for(thingy) {
var template = $templateCache.get('thingyTemplate.html'),
link = $interpolate(template),
html = link(thingy),
unsafe = $sanitize(html);
return unsafe;
}
return {
html_for: html_for
}
});
Templates:
<script type="text/ng-template" id="thingyTemplate.html">
<div>
<div><strong>Blah:</strong> {{blah}}</div>
<div data-ng-repeat="foo in collection"><strong>Collection:</strong> {{foo}}</div>
<div><strong>Collection[0]:</strong> {{collection[0]}}</div>
<div><strong>Collection[1]:</strong> {{collection[1]}}</div>
<div><strong>Collection[2]:</strong> {{collection[2]}}</div>
</div>
</script>
<script type="text/ng-template" id="template/popover/popover.html">
<div class="popover {{placement}}" data-ng-class="{ in: isOpen(), fade: animation() }">
<div class="arrow"></div>
<div class="popover-inner">
<h3 class="popover-title" data-ng-bind="title" data-ng-show="title"></h3>
<div class="popover-content" data-ng-bind-html="content"></div>
</div>
</div>
</script>

$interpolate doesn't handle directives like ngRepeat (
Difference between parse, interpolate and compile ). $interpolate:
Compiles a string with markup into an interpolation function. This
service is used by the HTML $compile service for data binding.
To handle ngRepeat and other directives you want $compile. But for your use case $compile is going to result, unfortunately, in a number of changes because:
It needs a scope to compile against rather than just a context like $interpolate. Moreover it needs the scope thingy is on.
This means we'll need to reference your properties like so {{thingy.blah}} instead of {{blah}} inside your template.
The compile needs to happen when the popup is on the dom.
The popup is only on the dom when it's open.
So we can't just replace $interpolate with $compile inside your service.
One approach is to replace data-ng-bind-html with the following directive that acts like an ng-bind-html that has a built in $compile (clearly you should only use this with html that you know is safe).
.directive('compile', function($compile) {
return function(scope, element, attrs) {
scope.$watch(
function(scope) {
return scope.$eval(attrs.compile);
},
function(value) {
var result = element.html(value);
$compile(element.contents())(scope.$parent.$parent);
}
);
};
});
Used like so (with compile replacing ng-bind-html:
<div class="popover-content" compile="content"></div>
One issue is that we need thingy to be in scope. There's a few of ways of handling that- but for demonstration purposes I've manually gone back up to the scope the popover is called from - which is 2 scopes up thus the scope.$parent.$parent.
Using this compile directive you no longer $interpolate or $sanitizeso the function in your service can shrink down to just returning the appropriate template:
function html_for() {
var template = $templateCache.get('thingyTemplate.html');
return template;
}
demo fiddle

Related

angularjs - get ng-repeat element using element.html() inside a jsPanel directive

I am trying to create a directive which will take the html content and place it n a jsPanel element.
So far I am able to compile the scope and able get elements with the scope. Then i tried to get html tag with ng-repeat, where i got stuck.
Following is the code used to create the directive
mainApp.directive('jspanelcontent', function($compile) {
console.log("loading");
var directive = {};
directive.restrict = 'AE';
directive.compile = function(element, attributes) {
var linkFunction = function($scope, element, attributes) {
console.log(element.html());
var contenthtml = element.html();
contenthtml = angular.element($compile(contenthtml)($scope));
element.html("");
$scope.jspanleinstance = $.jsPanel({
position: {
my: "left-bottom",
at: "left-bottom",
offsetY: 15
},
theme: "rebeccapurple",
contentSize: {
width: 600,
height: 350
},
headerTitle: attributes.panelcaption,
content: contenthtml,
});
// });
}
return linkFunction;
}
return directive;
});
And using like following in html
<div jspanelcontent panelcaption="list">
<div ng-controller="ListController">
{{test}}
<div ng-repeat="user in users">
<span>{{user.id}}</span>
<span>{{user.name}}</span>
<span>{{user.email}}</span>
<br />
</div>
</div>
The console log returns as
Output i am getting (jsPanel)
As you see the "test" variable is properly bind and the ng-repeat element is display as commented, I am not aware why it's returning like that. If anyone can figure out the issue, then i might get it working.
I know i can access the users data as $scope.users inside the directive. The problem here is that user able to use the directive commonly, so i have to assume that i don't the variables in the $scope.
I stuck in this place, and couldn't find any solutions to try. Any suggestions or solutions will be more helpful. :)
NOTE: No syntax errors, outside the directive the data is displaying (Tested)

Why is the ng-class directive being compiled after my custom directives

Link to plnkr for example explained below: http://plnkr.co/edit/oYvwHnAIvFb4rUqqwsz3?p=preview
Hi, I dont understand why angular is compiling my code this way and need some help understanding why it is doing so. I have an outer directive which doing an ng-repeat on and array and creating a new directive for each item in the array. Wrapping around this inner directive is a div which has an ng-class attached to it.
The issue I'm running into is that I want the ng-class to be applied to the wrapping div before the directive's link function is called, but this isn't the case. The controller and link function of all of the nested directives are compiled before any of the wrapping ng-class functions are called. You can see an example of what time talking about in the plnkr I've linked above (check the console to see the order in which things are getting printed). The print order I want it to be is the following:
adding class to inner directive 1
Inner - Controller undefined
Inner - Link 1
adding class to inner directive 2
Inner - Controller undefined
Inner - Link 2
Inner - Controller undefined
...
Any help on understanding this / getting it to compile in the order i need would be great.
relevant code:
html
<div bn-outer>
<div ng-repeat="a in arr">
<div ng-class="classFunction(a)">
<span bn-inner ng-model="model" ng-init="model=a">
directive: {{a}}
</span>
</div>
</div>
</div>
angular
// Create an application module for our demo.
var app = angular.module( "Demo", [] );
app.directive(
"bnOuter",
function() {
function Controller( $scope ) {
console.log( "Outer - Controller" );
$scope.arr = [1,2,3,4,5,6];
$scope.classFunction = function(int){
console.log("adding class to inner directive ", int);
};
}
function link( $scope, element, attributes, controller ) {
console.log( "Outer - Link" );
}
// Return directive configuration.
return({
controller: Controller,
link: link
});
}
);
app.directive(
"bnInner",
function() {
function Controller( $scope ) {
console.log( "Inner - Controller", $scope.model );
}
function link( $scope, element, attributes, controller ) {
console.log( "Inner - Link", $scope.model );
}
// Return directive configuration.
return({
controller: Controller,
link: link
});
}
);
Thanks!
An excellent explanation on this topic can be found here
http://www.bennadel.com/blog/2810-directive-architecture-template-urls-and-linking-order-in-angularjs.htm
And this
http://odetocode.com/blogs/scott/archive/2014/05/28/compile-pre-and-post-linking-in-angularjs.aspx
Basically it sounds you need to play with the 'pre-link' function

ng-switch not reacting to dynamic value change

i am new and experimenting with AngularJS.
That being said, i am unsure how to bind value to an ng-switch and change between views dynamically by changing the 'activeView' variable value.
Side note: is it a good practice to switch between views like this? The views are different ways of displaying the same data so its pretty much the same context and i did not see a reason to create a different page for the two views. Oh and the views contents are actually directives components.
the html:
<body ng-controller="SomeController as vm">
<div ng-switch="{{vm.activeView}}">
<div ng-switch-when="someView">
some directive component
</div>
<div ng-switch-when="anotherView">
another directive component
</div>
<div ng-switch-default>
nothing to show
</div>
</div>
</body>
the controller:
(function () {
'use strict';
angular
.module('app')
.controller('SomeController', SomeController);
function SomeController() {
var vm = this;
vm.activeView = '';
function setViewType(viewType) {
vm.viewTypes = viewType;
}
setViewType('anotherView');
}
})();
So what happen is that the default switch is visible, event after calling setViewType('anotherView');
Thanks in advance!
You don't need the interpolation when using ng-switch ({{vm.activeView}}).
You aren't assigning vm.activeView with a value.
Your angular.module doesn't have a dependency array. If this is the place that you define your module then add it.
(function () {
'use strict';
angular.module('app', []) // add the dependency array
.controller('SomeController', SomeController);
function SomeController() {
var vm = this;
vm.activeView = 'anotherView'; // change this
function setViewType(viewType) {
vm.viewTypes = viewType;
}
setViewType('anotherView'); // this changes the viewTypes prop
}
})();
Check on this JSFIDDLE.

AngularJS ng-switch without extra DOM

I have a custom directive and an object myObj on the current $scope (inside an ng-repeat).
If the object has a type of html, I want to use one template:
<span ng-bind-html="myObj.html"></span>`
Otherwise I want to use a different template:
<span>{{myObj.value}}</span>`
My problem
This is invalid because a custom directive template must contain exactly one root node:
<span ng-if="myObj.type==='html'" ng-bind-html="myObj.html"></span>
<span ng-if="myObj.type!=='html'">{{myObj.value}}</span>
This is invalid because it destroys my page with extra DOM: (wrapping all my spans (there could be thousands) in unnecessary ng-switch nodes...)
<ng-switch on="myObj.type">
<span ng-switch-when="html" ng-bind-html="myObj.html"></span>
<span ng-switch-default>{{myObj.value}}</span>
</ng-switch>
My Question
Is it possible to have a directive pick it's template based on the result of a switch, without creating extra unnecessary DOM? For example, you can specify replace: true when creating a directive - is it possible to similarly have an ng-switch where the result replaces the switch tag itself?
Edit
My Directive:
return {
replace: true,
controller: 'ChunkController',
scope: {
chunk: '=deChunk'
},
templateUrl: de.partial.chunk,
link: function (scope, el, attr, ctrl) {
el.on('keydown', handleKeypress.bind(ctrl));
el.on('click', ctrl.showValue);
}
};
And its usage:
<div class="content" contenteditable="{{node.type!=='static'}}">
<div data-ng-repeat="chunk in node.chunks" data-de-chunk="chunk"></div>
</div>
With the intent that the child <div> will be replaced with the sequence of <span>s from above.
I wouldn't even bother if you are storing the html in a service just check to see if a value for myObj.html exists in the object and if it does compile and bind the html in the linker function instead of using ng-bind-html
something like this maybe:
myapp.directive('something',function($compile){
return {
link: function(scope,elem,attrs) {
var obj = scope.$eval(attrs.something);
if(obj.html) {
var html = angular.element($compile(obj.html)(scope));
elem.append(html);
} else {
//go get the data and set obj.html
}
}
}
});

ng-repeat finish event

I want to call some jQuery function targeting div with table. That table is populated with ng-repeat.
When I call it on
$(document).ready()
I have no result.
Also
$scope.$on('$viewContentLoaded', myFunc);
doesn't help.
Is there any way to execute function right after ng-repeat population completes? I've read an advice about using custom directive, but I have no clue how to use it with ng-repeat and my div...
Indeed, you should use directives, and there is no event tied to the end of a ng-Repeat loop (as each element is constructed individually, and has it's own event). But a) using directives might be all you need and b) there are a few ng-Repeat specific properties you can use to make your "on ngRepeat finished" event.
Specifically, if all you want is to style/add events to the whole of the table, you can do so using in a directive that encompasses all the ngRepeat elements. On the other hand, if you want to address each element specifically, you can use a directive within the ngRepeat, and it will act on each element, after it is created.
Then, there are the $index, $first, $middle and $last properties you can use to trigger events. So for this HTML:
<div ng-controller="Ctrl" my-main-directive>
<div ng-repeat="thing in things" my-repeat-directive>
thing {{thing}}
</div>
</div>
You can use directives like so:
angular.module('myApp', [])
.directive('myRepeatDirective', function() {
return function(scope, element, attrs) {
angular.element(element).css('color','blue');
if (scope.$last){
window.alert("im the last!");
}
};
})
.directive('myMainDirective', function() {
return function(scope, element, attrs) {
angular.element(element).css('border','5px solid red');
};
});
See it in action in this Plunker.
If you simply want to execute some code at the end of the loop, here's a slightly simpler variation that doesn't require extra event handling:
<div ng-controller="Ctrl">
<div class="thing" ng-repeat="thing in things" my-post-repeat-directive>
thing {{thing}}
</div>
</div>
function Ctrl($scope) {
$scope.things = [
'A', 'B', 'C'
];
}
angular.module('myApp', [])
.directive('myPostRepeatDirective', function() {
return function(scope, element, attrs) {
if (scope.$last){
// iteration is complete, do whatever post-processing
// is necessary
element.parent().css('border', '1px solid black');
}
};
});
See a live demo.
There is no need of creating a directive especially just to have a ng-repeat complete event.
ng-init does the magic for you.
<div ng-repeat="thing in things" ng-init="$last && finished()">
the $last makes sure, that finished only gets fired, when the last element has been rendered to the DOM.
Do not forget to create $scope.finished event.
Happy Coding!!
EDIT: 23 Oct 2016
In case you also want to call the finished function when there is no item in the array then you may use the following workaround
<div style="display:none" ng-init="things.length < 1 && finished()"></div>
//or
<div ng-if="things.length > 0" ng-init="finished()"></div>
Just add the above line on the top of the ng-repeat element. It will check if the array is not having any value and call the function accordingly.
E.g.
<div ng-if="things.length > 0" ng-init="finished()"></div>
<div ng-repeat="thing in things" ng-init="$last && finished()">
Here is a repeat-done directive that calls a specified function when true. I have found that the called function must use $timeout with interval=0 before doing DOM manipulation, such as initializing tooltips on the rendered elements. jsFiddle: http://jsfiddle.net/tQw6w/
In $scope.layoutDone, try commenting out the $timeout line and uncommenting the "NOT CORRECT!" line to see the difference in the tooltips.
<ul>
<li ng-repeat="feed in feedList" repeat-done="layoutDone()" ng-cloak>
{{feed | strip_http}}
</li>
</ul>
JS:
angular.module('Repeat_Demo', [])
.directive('repeatDone', function() {
return function(scope, element, attrs) {
if (scope.$last) { // all are rendered
scope.$eval(attrs.repeatDone);
}
}
})
.filter('strip_http', function() {
return function(str) {
var http = "http://";
return (str.indexOf(http) == 0) ? str.substr(http.length) : str;
}
})
.filter('hostName', function() {
return function(str) {
var urlParser = document.createElement('a');
urlParser.href = str;
return urlParser.hostname;
}
})
.controller('AppCtrl', function($scope, $timeout) {
$scope.feedList = [
'http://feeds.feedburner.com/TEDTalks_video',
'http://feeds.nationalgeographic.com/ng/photography/photo-of-the-day/',
'http://sfbay.craigslist.org/eng/index.rss',
'http://www.slate.com/blogs/trending.fulltext.all.10.rss',
'http://feeds.current.com/homepage/en_US.rss',
'http://feeds.current.com/items/popular.rss',
'http://www.nytimes.com/services/xml/rss/nyt/HomePage.xml'
];
$scope.layoutDone = function() {
//$('a[data-toggle="tooltip"]').tooltip(); // NOT CORRECT!
$timeout(function() { $('a[data-toggle="tooltip"]').tooltip(); }, 0); // wait...
}
})
Here's a simple approach using ng-init that doesn't even require a custom directive. It's worked well for me in certain scenarios e.g. needing to auto-scroll a div of ng-repeated items to a particular item on page load, so the scrolling function needs to wait until the ng-repeat has finished rendering to the DOM before it can fire.
<div ng-controller="MyCtrl">
<div ng-repeat="thing in things">
thing: {{ thing }}
</div>
<div ng-init="fireEvent()"></div>
</div>
myModule.controller('MyCtrl', function($scope, $timeout){
$scope.things = ['A', 'B', 'C'];
$scope.fireEvent = function(){
// This will only run after the ng-repeat has rendered its things to the DOM
$timeout(function(){
$scope.$broadcast('thingsRendered');
}, 0);
};
});
Note that this is only useful for functions you need to call one time after the ng-repeat renders initially. If you need to call a function whenever the ng-repeat contents are updated then you'll have to use one of the other answers on this thread with a custom directive.
Complementing Pavel's answer, something more readable and easily understandable would be:
<ul>
<li ng-repeat="item in items"
ng-init="$last ? doSomething() : angular.noop()">{{item}}</li>
</ul>
Why else do you think angular.noop is there in the first place...?
Advantages:
You don't have to write a directive for this...
Maybe a bit simpler approach with ngInit and Lodash's debounce method without the need of custom directive:
Controller:
$scope.items = [1, 2, 3, 4];
$scope.refresh = _.debounce(function() {
// Debounce has timeout and prevents multiple calls, so this will be called
// once the iteration finishes
console.log('we are done');
}, 0);
Template:
<ul>
<li ng-repeat="item in items" ng-init="refresh()">{{item}}</li>
</ul>
Update
There is even simpler pure AngularJS solution using ternary operator:
Template:
<ul>
<li ng-repeat="item in items" ng-init="$last ? doSomething() : null">{{item}}</li>
</ul>
Be aware that ngInit uses pre-link compilation phase - i.e. the expression is invoked before child directives are processed. This means that still an asynchronous processing might be required.
It may also be necessary when you check the scope.$last variable to wrap your trigger with a setTimeout(someFn, 0). A setTimeout 0 is an accepted technique in javascript and it was imperative for my directive to run correctly.
I did it this way.
Create the directive
function finRepeat() {
return function(scope, element, attrs) {
if (scope.$last){
// Here is where already executes the jquery
$(document).ready(function(){
$('.materialboxed').materialbox();
$('.tooltipped').tooltip({delay: 50});
});
}
}
}
angular
.module("app")
.directive("finRepeat", finRepeat);
After you add it on the label where this ng-repeat
<ul>
<li ng-repeat="(key, value) in data" fin-repeat> {{ value }} </li>
</ul>
And ready with that will be run at the end of the ng-repeat.
<div ng-repeat="i in items">
<label>{{i.Name}}</label>
<div ng-if="$last" ng-init="ngRepeatFinished()"></div>
</div>
My solution was to add a div to call a function if the item was the last in a repeat.
This is an improvement of the ideas expressed in other answers in order to show how to gain access to the ngRepeat properties ($index, $first, $middle, $last, $even, $odd) when using declarative syntax and isolate scope (Google recommended best practice) with an element-directive. Note the primary difference: scope.$parent.$last.
angular.module('myApp', [])
.directive('myRepeatDirective', function() {
return {
restrict: 'E',
scope: {
someAttr: '='
},
link: function(scope, element, attrs) {
angular.element(element).css('color','blue');
if (scope.$parent.$last){
window.alert("im the last!");
}
}
};
});
i would like to add another answer, since the preceding answers takes it that the code needed to run after the ngRepeat is done is an angular code, which in that case all answers above give a great and simple solution, some more generic than others, and in case its important the digest life cycle stage you can take a look at Ben Nadel's blog about it, with the exception of using $parse instead of $eval.
but in my experience, as the OP states, its usually running some JQuery plugins or methods on the finnaly compiled DOM, which in that case i found that the most simple solution is to create a directive with a setTimeout, since the setTimeout function gets pushed to the end of the queue of the browser, its always right after everything is done in angular, usually ngReapet which continues after its parents postLinking function
angular.module('myApp', [])
.directive('pluginNameOrWhatever', function() {
return function(scope, element, attrs) {
setTimeout(function doWork(){
//jquery code and plugins
}, 0);
};
});
for whoever wondering that in that case why not to use $timeout, its that it causes another digest cycle that is completely unnecessary
I had to render formulas using MathJax after ng-repeat ends, none of the above answers solved my problem, so I made like below. It's not a nice solution, but worked for me...
<div ng-repeat="formula in controller.formulas">
<div>{{formula.string}}</div>
{{$last ? controller.render_formulas() : ""}}
</div>
I found an answer here well practiced, but it was still necessary to add a delay
Create the following directive:
angular.module('MyApp').directive('emitLastRepeaterElement', function() {
return function(scope) {
if (scope.$last){
scope.$emit('LastRepeaterElement');
}
}; });
Add it to your repeater as an attribute, like this:
<div ng-repeat="item in items" emit-last-repeater-element></div>
According to Radu,:
$scope.eventoSelecionado.internamento_evolucoes.forEach(ie => {mycode});
For me it works, but I still need to add a setTimeout
$scope.eventoSelecionado.internamento_evolucoes.forEach(ie => {
setTimeout(function() {
mycode
}, 100); });
If you simply wants to change the class name so it will rendered differently, below code would do the trick.
<div>
<div ng-show="loginsuccess" ng-repeat="i in itemList">
<div id="{{i.status}}" class="{{i.status}}">
<div class="listitems">{{i.item}}</div>
<div class="listitems">{{i.qty}}</div>
<div class="listitems">{{i.date}}</div>
<div class="listbutton">
<button ng-click="UpdateStatus(i.$id)" class="btn"><span>Done</span></button>
<button ng-click="changeClass()" class="btn"><span>Remove</span></button>
</div>
<hr>
</div>
This code worked for me when I had a similar requirement to render the shopped item in my shopping list in Strick trough font.

Categories