ng-repeat with ng-bind-html as pre and post-markup - angularjs

I have an array with multiple objects, similar to this:
[
{ title: 'abc', 'pre': '<div class="class1"><div class="class2">', 'post': '</div>' },
{ title: 'def', 'pre': <div class="class3">', 'post': '</div>' },
{ title: 'ghi', 'pre': '<div class="class3">', 'post': '</div></div>' }
]
<div ng-repeat="item in myVar">
<div ng-bind-html="item.pre" />{{ item.title }}<div ng-bind-html="item.post" />
</div>
The above does not work (I have to open two div's in one, and close in two other items in that array, as illustrated above). The problem is that ng-bind-html needs to be bound to an element, which I cannot use, neither does a filter work:
<div ng-repeat="item in myVar">
{{ item.pre | trust }}{{ item.title }}{{ item.post | trust }}
</div>
angular.module('myModule').filter('trust', ['$sce',function($sce) {
return function(value, type) { return $sce.trustAsHtml; }
}]);
Any ideas?

You'll have to perform the concatenation pre-view, trust that (or turn on ngSanitize, potentially better-yet), then inject it.
As far as I know, there's no way to inject a partial HTML element the way you're trying to.
In your controller:
$scope.items = [...];
for (var i = 0; i < $scope.items.length; i++) {
var e = $scope.items[i];
e.concatenated = $sce.trustAsHtml(e.pre + e.title + e.post);
}
Then in your view:
<div ng-repeat="item in items">
<div ng-bind-html="item.concatenated" />
</div>
Of course, you'll probably want ngSanitize turned on, just to avoid any issues with e.title. That is, if someone entered a title of <script>alert('ahh!')</script>, that would end up being trusted.
Your version did not work because of how ngBindHtml is written:
var ngBindHtmlDirective = ['$sce', '$parse', '$compile', function($sce, $parse, $compile) {
return {
restrict: 'A',
compile: function ngBindHtmlCompile(tElement, tAttrs) {
var ngBindHtmlGetter = $parse(tAttrs.ngBindHtml);
var ngBindHtmlWatch = $parse(tAttrs.ngBindHtml, function getStringValue(value) {
return (value || '').toString();
});
$compile.$$addBindingClass(tElement);
return function ngBindHtmlLink(scope, element, attr) {
$compile.$$addBindingInfo(element, attr.ngBindHtml);
scope.$watch(ngBindHtmlWatch, function ngBindHtmlWatchAction() {
// we re-evaluate the expr because we want a TrustedValueHolderType
// for $sce, not a string
element.html($sce.getTrustedHtml(ngBindHtmlGetter(scope)) || '');
});
};
}
};
}];
It injects using element.html(...), which needs a complete HTML element.

Related

Get height of div in angularjs

I have 3 div in which the data is filling from controller. and the div is dependent on dropdown select (particular div will be shown for particular dropdown value). the problem is that I am unable to get the height of that div when page is loaded and also when I changed the dropdown value. Everytime I am getting 0 height.
here is the code for html:
<div class="brand-categoryWrp">
<select ng-model="allbrandscategory" ng-change="catChange(allbrandscategory)">
<option value="all">AllBrands</option>
<option value="watches">Watches</option>
<option value="pens">Pens</option>
</select>
</div>
<div ng-show="firstdiv" allbrands-directive>
<ul>
<li ng-repeat="res in brands">
{{res.name}}
</li>
</ul>
</div>
<div ng-show="seconddiv" watches-directive>
<ul>
<li ng-repeat="res in brands1">
{{res.name}}
</li>
</ul>
</div>
<div ng-show="thirddiv" pens-directive>
<ul>
<li ng-repeat="res in brands2">
{{res.name}}
</li>
</ul>
</div>
and here is for controller:
// Code goes here
var myapp = angular.module('myModule', []);
myapp.controller('mycntrl',function($scope){
$scope.allbrandscategory = 'all';
$scope.firstdiv = true;
$scope.brands = [
{name: 'Adidas'},
{name: 'Armani'}
];
$scope.brands1 = [
{name: 'Adidas1'},
{name: 'Armani1'},
{name: 'Fossil'}
];
$scope.brands2 = [
{name: 'Adidas2'},
{name: 'Armani2'},
{name: 'Mont blanc'},
{name: 'calvin'}
];
$scope.catChange = function(val){
$scope.firstdiv = false;
$scope.seconddiv = false;
$scope.thirddiv = false;
if(val == 'all')
{
$scope.firstdiv = true;
}
else if(val == 'watches')
{
$scope.seconddiv = true;
}
else if(val == 'pens')
{
$scope.thirddiv = true;
}
};
});
myapp.directive('pensDirective', function($timeout) {
return {
restrict: 'A',
link: function(scope, element) {
//console.log(element);
console.log("pens: "+element[0].offsetHeight);
}
};
});
myapp.directive('watchesDirective', function($timeout) {
return {
restrict: 'A',
link: function(scope, element) {
// console.log(element);
console.log('watches: '+element[0].offsetHeight);
}
};
});
myapp.directive('allbrandsDirective', function($timeout) {
return {
restrict: 'A',
link: function(scope, element) {
//console.log(element);
console.log("brand: "+element[0].offsetHeight);
}
};
});
Here is the plunker
The link function is executed before the model data is bound to the view (the div does not contain any child elements when you're requesting the offsetHeight. Try wrapping it into a $timeout(function() { /* ... */ }), which will execute at the end of the current digest cycle.
Check out AngularJs event to call after content is loaded
This has a nice answer to your problem and seems to work. Second answer down.
I tested on yours by adding the below to your allbranddirective:
element.ready(function(){
console.log("brand: "+element[0].offsetHeight);
});
EDIT:
HTML:
<html>
<div class="brand-categoryWrp">
<select ng-model="divtoshow" ng-change="catChange(divtoshow)">
<option value="allbrands-directive">AllBrands</option>
<option value="watches-directive">Watches</option>
<option value="pens-directive">Pens</option>
</select>
</div>
<div allbrands-directive><ul><li ng-repeat='res in brands'>{{res.name}}</li></ul></div>
JS:
var myapp = angular.module('myModule', []);
myapp.controller('mycntrl',function($scope){
$scope.brands = [{name: 'Adidas'}, {name: 'Armani'}];
$scope.divtoshow = 'allbrands-directive'
$scope.catChange = function(val){
$scope.divtoshow = val;
if (val == 'allbrands-directive'){
$scope.brands = [{name: 'Adidas'}, {name: 'Armani'}];
}else if (val == 'watches-directive') {
$scope.brands = [{name: 'Adidas1'},{name: 'Armani1'},{name: 'Fossil'}];
}else if (val == 'pens-directive') {
$scope.brands = [{name: 'Adidas2'},{name: 'Armani2'},{name: 'Mont blanc'},{name: 'calvin'}];
}
}
});
myapp.directive('allbrandsDirective', function($timeout) {
return {
restrict: 'A',
link: function(scope, element) {
scope.$watch('brands', function(){
console.log("brand: "+element[0].offsetHeight);
});
}
};
});
So I have changed your code a bit, I have set up only one directive and in here I watch on a change of the brands variable, when this changes we then get the height of the element.
See plunker - http://plnkr.co/edit/vZhn2aMRI6wKn3yVharS?p=preview
If you need each section in separate directives see nulls answers

angularjs scope function of a repeated directive

I am trying to have a directive with a repeat on it and have it call a function on the parent control as well as child controls. however when I add a scope: { function:&function}
the repeat stops working properly.
fiddle
the main.html is something like
<div ng-app="my-app" ng-controller="MainController">
<div>
<ul>
<name-row ng-repeat="media in mediaArray" on-delete="delete(index)" >
</name-row>
</ul>
</div>
</div>
main.js
var module = angular.module('my-app', []);
function MainController($scope)
{
$scope.mediaArray = [
{title: "predator"},
{title: "alien"}
];
$scope.setSelected = function (index){
alert("called from outside directive");
};
$scope.delete = function (index) {
alert("calling delete with index " + index);
}
}
module.directive('nameRow', function() {
return {
restrict: 'E',
replace: true,
priority: 1001, // since ng-repeat has priority of 1000
controller: function($scope) {
$scope.setSelected = function (index){
alert("called from inside directive");
}
},
/*uncommenting this breaks the ng-repeat*/
/*
scope: {
'delete': '&onDelete'
},
*/
template:
' <li>' +
' <button ng-click="delete($index);">' +
' {{$index}} - {{media.title}}' +
' </button>' +
' </li>'
};
});
As klauskpm said is better to move common logic to an independent service or factory. But the problem that i see is that the ng-repeat is in the same element of your directive. Try embed your directive in an element inside the loop and pass the function in the attribute of that element or create a template in your directive that use the ng-repeat in the template
<li ng-repeat="media in mediaArray" >
<name-row on-delete="delete(media)" ></name-row>
</li>
As I've suggested you, the better approach to share methods is building a Factory or a Service, just like bellow:
app.factory('YourFactory', function(){
return {
setSelected: function (index){
alert("called from inside directive");
}
}
};
And you would call it like this:
function MainController($scope, YourFactory) {
$scope.setSelected = YourFactory.setSelected;
// Could even use $scope.yf = YourFactory;, and call yf.setSelected(index);
// at your view.
(...)
module.directive('nameRow', function(YourFactory) {
(...)
$scope.setSelected = YourFactory.setSelected;
(...)
Hope it will help you.

AngulajrJS filter output html

Hi I am writing a filter which takes and array and returns a comma delimited string.
When i append some HTML it is not showing in the output.
Below is my code
$scope.authors = [{ "authorName": 'Robin', "price": 40, "link": 'http://www.google.com' },
{ "authorName": 'Chetan', "price": 400, "link": 'http://www.bing.com' },
{ "authorName": 'Jack Wilson', "price": 450, "link": 'http://www.facebook.com'}
]
My Filter
myApp.filter('formatAuthorName', function () {
return function (input) {
var str = [];
var totalString = '';
if (input.length > 1) {
for (var i = 0; i < input.length; i++) {
var name = input[i];
**str.push('<a href='**name.link**'>'+name.authorName+'</a>');**
}
totalString = str.join(', ');
return totalString;
}
return totalString;
};
});
Also how to bind href to link from Scope ??.
Thanks
For outputting HTML that interacts with scope, a directive is more suitable than a filter.
This answer expands on the solution originally proposed by ajk.
Demo Plunker
Directive
app.directive('formatAuthorNames', function() {
return {
restrict: 'A',
scope: { authors: '=' },
template:
'<span ng-repeat="author in authors">
<a ng-href="{{ author.link }}">{{ author.authorName }}</a>
<span ng-if="$index < authors.length-1">, </span>
</span>',
link: function(scope, elem, attr) {
// access scope here
}
}
});
HTML
<body ng-app="app" ng-controller='MyController'>
<div authors="authors" format-author-names></div>
</body>
Use $sce in your filter if you want to render html:
<div ng-bind-html="authors | formatAuthorName"></div>
JS:
app.filter('formatAuthorName', ['$sce',
function($sce) {
return function(input) {
var str = [];
var totalString = '';
if (input.length > 1) {
for (var i = 0; i < input.length; i++) {
var name = input[i];
str.push('<a href=' + name.link + '>' + name.authorName + '</a>');
}
totalString = str.join(', ');
}
return $sce.trustAsHtml(totalString);
};
}
]);
DEMO PLUNKER
I'm not sure that a filter is what you're actually looking for here. You will probably be better served by using ng-repeat in combination with ng-href. Something like this:
<div ng-repeat="author in authors">
<a ng-href="{{ author.link }}">{{ author.authorName }}</a>
</div>
try using ng-href instead of a filter
https://docs.angularjs.org/api/ng/directive/ngHref
Hope this helps.

AngularJS Directive using Compile cannot access child elements

My intent was to create a directive that could rearrange (not reorder) its child elements into a Bootstrap CSS Grid, but I am having a lot of difficulty getting access to the child elements.
I've tried a lot of different things and have researched Compile vs Link vs Controller directive options. I think I might have to change the 'compile' to 'link' in my directive to get this to work, but I am unsure how to do that.
I have an AngularJS directive on GitHub that takes an array or object of parameters to render a simple or complex grid.
In the example below you can see the layoutOptions.data = [3, 4] which means the grid will have 3 cells in the top row and 4 in the second. This is working well.
The second step is that I would like to render some divs as child elements of the directive and the directive will place these in the cells of the grid as it is created. This is shown by the layoutOptions.content = ['apple', 'orange', 'pear', 'banana', 'lime', 'lemon', 'grape'] but this needs to be de-coupled so that it could be literally anything.
HTML Input
<div ng-app="blerg">
<div ng-controller="DemoCtrl">
<div class="container" hr-layout="layoutOptions">
<div ng-transclude ng-repeat="fruit in layoutOptions.content">{{fruit}}</div>
</div>
</div>
</div>
Desired (not actual) Output
Actual output is as below, but does not include the inner DIVs with fruit names
<div class="container hr-layout" hr-layout="layoutOptions">
<div class="row">
<div class="col-md-4"><!-- from ng-repeat --><div>apple</div></div>
<div class="col-md-4"><!-- from ng-repeat --><div>orange</div></div>
<div class="col-md-4"><!-- from ng-repeat --><div>pear</div></div>
</div>
<div class="row">
<div class="col-md-3"><!-- from ng-repeat --><div>banana</div></div>
<div class="col-md-3"><!-- from ng-repeat --><div>lime</div></div>
<div class="col-md-3"><!-- from ng-repeat --><div>lemon</div></div>
<div class="col-md-3"><!-- from ng-repeat --><div>grape</div></div>
</div>
</div>
And a jsFiddle that uses it here: http://jsfiddle.net/harryhobbes/jJDZv/show/
Code
angular.module('blerg', [])
.controller('DemoCtrl', function($scope, $timeout) {
$scope.layoutOptions = {
data: [3, 4],
content: ['apple', 'orange', 'pear', 'banana', 'lime', 'lemon', 'grape']
};
})
.directive("hrLayout", [
"$compile", "$q", "$parse", "$http", function ($compile, $q, $parse, $http) {
return {
restrict: "A",
transclude: true,
compile: function(scope, element, attrs) {
//var content = element.children();
return function(scope, element, attrs) {
var contentCount = 0;
var renderTemplate = function(value, content) {
if (typeof content === 'undefined' || content.length <= contentCount)
var cellContent = 'Test content(col-'+value+')';
else if (Object.prototype.toString.call(content) === '[object Array]')
var cellContent = content[contentCount];
else
var cellContent = content;
contentCount++;
return '<div class="col-md-'+value+'">'+cellContent+'</div>';
};
var renderLayout = function(values, content) {
var renderedHTML = '';
var rowCnt = 0;
var subWidth = 0;
angular.forEach(values, function(value) {
renderedHTML += '<div class="row">';
if(Object.prototype.toString.call(value) === '[object Array]') {
angular.forEach(value, function(subvalue) {
if(typeof subvalue === 'object') {
renderedHTML += renderTemplate(
subvalue.w.substring(4), renderLayout(subvalue.d)
);
} else {
renderedHTML += renderTemplate(subvalue.substring(4));
}
});
} else {
if(value > 12) {
value = 12;
} else if (value <= 0) {
value = 1;
}
subWidth = Math.floor(12 / value);
for (var i=0; i< value-1; i++) {
renderedHTML += renderTemplate(subWidth);
}
renderedHTML += renderTemplate((12-subWidth*(value-1)));
}
renderedHTML += '</div>';
rowCnt++;
});
return renderedHTML;
};
scope.$watch(attrs.hrLayout, function(value) {
element.html(renderLayout(value.data));
});
element.addClass("hr-layout");
};
}
};
}]);
This may help - http://jsfiddle.net/PwNZ5/1/
App.directive('hrLayout', function($compile) {
return {
restrict: 'A',
// allows transclusion
transclude: true,
// transcludes the content of an element on which hr-layout was placed
template: '<div ng-transclude></div>',
compile: function(tElement, tAttrs, transcludeFn) {
return function (scope, el, tAttrs) {
var data = scope.$eval(tAttrs.hrLayout),
dom = '';
transcludeFn(scope, function cloneConnectFn(cElement) {
// hide the transcluded content
tElement.children('div[ng-transclude]').hide();
// http://ejohn.org/blog/how-javascript-timers-work/‎
window.setTimeout(function() {
for(var row = 0; row < data.data.length; row++) {
dom+= '<div class="row">';
for(var col = 0; col < data.data[row]; col++) {
dom+= '<div class="col-md-' + data.data[row] + '">' + tElement.children('div[ng-transclude]').children(':eq(' + ( row + col ) + ')').html() + '</div>';
}
dom+= '</div>';
}
tElement.after(dom);
}, 0);
});
};
}
};
});
Your approach looks like too complex. Maybe you should use ngRepeat directive http://docs.angularjs.org/api/ng.directive:ngRepeat and orderBy filter http://docs.angularjs.org/api/ng.filter:orderBy insted of updating html of element every time when hrLayout was updated?

Angular : how to re-render compiled template after model update?

I am working on an angular form builder which generate a json.
Everything works fine except one thing.
You can find an example here : http://jsfiddle.net/dJRS5/8/
HTML :
<div ng-app='app'>
<div class='formBuilderWrapper' id='builderDiv' ng-controller="FormBuilderCtrl" >
<div class='configArea' data-ng-controller="elementDrag">
<h2>drag/drop</h2>
<form name="form" novalidate class='editBloc'>
<div data-ng-repeat="field in fields" class='inputEdit'>
<data-ng-switch on="field.type">
<div class='labelOrder' ng-class='{column : !$last}' drag="$index" dragStyle="columnDrag" drop="$index" dropStyle="columnDrop">{{field.type}}
</div>
<label for="{{field.name}}" data-ng-bind-html-unsafe="field.caption"></label>
<input data-ng-switch-when="Text" type="text" placeholder="{{field.placeholder}}" data-ng-model="field.value" />
<p data-ng-switch-when="Text/paragraph" data-ng-model="field.value" data-ng-bind-html-unsafe="field.paragraph"></p>
<span data-ng-switch-when="Yes/no question">
<p data-ng-bind-html-unsafe="field.yesNoQuestion"></p>
<input type='radio' name="yesNoQuestion" id="yesNoQuestion_yes" value="yesNoQuestion_yes" />
<label for="yesNoQuestion_yes">Oui</label>
<input type='radio' name="yesNoQuestion" id="yesNoQuestion_no" value="yesNoQuestion_no"/>
<label for="yesNoQuestion_no">Non</label>
</span>
<p data-ng-switch-when="Submit button" class='submit' data-ng-model="field.value">
<input value="{{field.name}}" type="submit">
</p>
</data-ng-switch>
</div>
</form>
</div>
<div id='previewArea' data-ng-controller="formWriterCtrl">
<h2>preview</h2>
<div data-ng-repeat="item in fields" content="item" class='templating-html'></div>
</div>
</div>
</div>
The JS :
var app = angular.module('app', []);
app.controller('FormBuilderCtrl', ['$scope', function ($scope){
$scope.fields = [{"type":"Text/paragraph","paragraph":"hello1"},{"type":"Yes/no question","yesNoQuestion":"following items must be hidden","yes":"yes","no":"no"},{"type":"Text/paragraph","paragraph":"hello2"},{"type":"Submit button","name":"last item"}] ;
}]);
app.controller('elementDrag', ["$scope", "$rootScope", function($scope, $rootScope, $compile) {
$rootScope.$on('dropEvent', function(evt, dragged, dropped) {
if($scope.fields[dropped].type == 'submitButton' || $scope.fields[dragged].type == 'submitButton'){
return;
}
var tempElement = $scope.fields[dragged];
$scope.fields[dragged] = $scope.fields[dropped];
$scope.fields[dropped] = tempElement;
$scope.$apply();
});
}]);
app.directive("drag", ["$rootScope", function($rootScope) {
function dragStart(evt, element, dragStyle) {
if(element.hasClass('column')){
element.addClass(dragStyle);
evt.dataTransfer.setData("id", evt.target.id);
evt.dataTransfer.effectAllowed = 'move';
}
};
function dragEnd(evt, element, dragStyle) {
element.removeClass(dragStyle);
};
return {
restrict: 'A',
link: function(scope, element, attrs) {
if(scope.$last === false){
attrs.$set('draggable', 'true');
scope.dragStyle = attrs["dragstyle"];
element.bind('dragstart', function(evt) {
$rootScope.draggedElement = scope[attrs["drag"]];
dragStart(evt, element, scope.dragStyle);
});
element.bind('dragend', function(evt) {
dragEnd(evt, element, scope.dragStyle);
});
}
}
}
}]);
app.directive("drop", ['$rootScope', function($rootScope) {
function dragEnter(evt, element, dropStyle) {
element.addClass(dropStyle);
evt.preventDefault();
};
function dragLeave(evt, element, dropStyle) {
element.removeClass(dropStyle);
};
function dragOver(evt) {
evt.preventDefault();
};
function drop(evt, element, dropStyle) {
evt.preventDefault();
element.removeClass(dropStyle);
};
return {
restrict: 'A',
link: function(scope, element, attrs) {
if(scope.$last === false){
scope.dropStyle = attrs["dropstyle"];
element.bind('dragenter', function(evt) {
dragEnter(evt, element, scope.dropStyle);
});
element.bind('dragleave', function(evt) {
dragLeave(evt, element, scope.dropStyle);
});
element.bind('dragover', dragOver);
element.bind('drop', function(evt) {
drop(evt, element, scope.dropStyle);
var dropData = scope[attrs["drop"]];
$rootScope.$broadcast('dropEvent', $rootScope.draggedElement, dropData);
});
}
}
}
}]);
app.controller('formWriterCtrl', ['$scope', function ($scope){
}]);
app.directive('templatingHtml', function ($compile) {
var previousElement;
var previousIndex;
var i=0;
var inputs = {};
var paragraphTemplate = '<p data-ng-bind-html-unsafe="content.paragraph"></p>';
var noYesQuestionTemplate = '<p data-ng-bind-html-unsafe="content.yesNoQuestion"></p><input id="a__index__yes" type="radio" name="a__index__"><label for="a__index__yes" />{{content.yes}}</label><input id="a__index__no" class="no" type="radio" name="a__index__" /><label for="a__index__no">{{content.no}}</label>';
var submitTemplate = '<p class="submit"><input value="{{content.name}}" type="submit" /></p>';
var getTemplate = function(contentType, contentReplace, contentRequired) {
var template = '';
switch(contentType) {
case 'Text/paragraph':
template = paragraphTemplate;
break;
case 'Yes/no question':
template = noYesQuestionTemplate;
break;
case 'Submit button':
template = submitTemplate;
break;
}
template = template.replace(/__index__/g, i);
return template;
}
var linker = function(scope, element, attrs) {
i++;
elementTemplate = getTemplate(scope.content.type);
element.html(elementTemplate);
if(previousElement == 'Yes/no question'){
element.children().addClass('hidden');
element.children().addClass('noYes'+previousIndex);
}
if(scope.content.type == 'Yes/no question'){
previousElement = scope.content.type;
previousIndex = i;
}
$compile(element.contents())(scope);
}
return {
restrict: "C",
link: linker,
scope:{
content:'='
}
};
});
On the example there are 2 areas :
- the first one does a ngRepeat on Json and allow to reorder items with drag and drop
- the second area also does a ngRepeat, it is a preview templated by a directive using compile function. Some elements are hidden if they are after what I called "Yes/no question"
Here is an example of Json generated by the form builder :
$scope.fields =
[{"type":"Text/paragraph","paragraph":"hello1"},{"type":"Yes/no question","yesNoQuestion":"following items must be hidden","yes":"yes","no":"no"},
{"type":"Text/paragraph","paragraph":"hello2"},{"type":"Submit button","name":"last item"}] ;
When the page load everything is ok, Hello1 is visible and Hello2 is hidden.
But when I drop Hello1 after "Yes/no question", dom elements are reorganised but Hello1 is not hidden.
I think it comes from $compile but I don't know how to resolve it.
Could you help me with this please?
Thank you
I only see you setting the 'hidden' class on the element based on that rule (after a yes/no) in the link function. That's only called once for the DOM element - when it's first created. Updating the data model doesn't re-create the element, it updates it in place. You would need a mechanism that does re-create it if you wanted to do it this way.
I see three ways you can do this:
In your linker function, listen for the same dropEvent that you listen for above. This is more efficient than you'd think (it's very fast) and you can re-evaluate whether to apply this hidden class or not.
Use something like ngIf or literally re-creating it in your collection to force the element to be recreated entirely. This is not as efficient, but sometimes is still desirable for various reasons.
If your use case is actually this simple (if this wasn't a redux of something more complicated you're trying to do) you could use CSS to do something like this. A simple rule like
.yes-no-question + .text-paragraph { display: none; }
using a sibling target could handle this directly without as much work. This is much more limited in what it can do, obviously, but it's the most efficient option if it covers what you need.

Resources