Custom directive: How evaluate bindings with dynamic HTML - javascript

Setup:
Very simplified HTML:
<td ng-repeat="col in cols">
<div ng-bind-html="col.safeHTML"></div>
</td>
JS controller:
$scope.cols = [
{
field : 'logo',
displayName : 'Logo',
cellTemplate: '<div style="color:red">{{col}}</div>'
},
{
field : 'color',
displayName : 'Color',
cellTemplate: '<div style="color:green">{{col}}</div>
}
];
JS link directive link function:
for (var i = 0, j = $scope.cols.length;
i < j;
i++) {
if ($scope.cols[i].hasOwnProperty('cellTemplate')) {
$scope.cols[i].safeHTML = $sce.trustAsHtml($scope.cols[i].cellTemplate);
}
}
And it is escaping correctly the HTML but the bindings ({{some_var}}) are not being interpolated.
How can make Angular compute the bindings in the safe HTML? I tried to use several variations of bind like ngBindTemplate but was for no use :(

You actually want to use the $compile service if you plan to dynamically compile angular components and add them to the DOM manually.
With a little bit of custom directive work, you can make this work pretty easily.
function compileDirective($compile) {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
//Watch for changes to expression
scope.$watch(attrs.compile, function(newVal) {
//Compile creates a linking function
// that can be used with any scope
var link = $compile(newVal);
//Executing the linking function
// creates a new element
var newElem = link(scope);
//Which we can then append to our DOM element
elem.append(newElem);
});
}
};
}
function colsController() {
this.cols = [{
name: "I'm using an H1",
template: "<h1>{{col.name}}</h1>"
}, {
name: "I'm using an RED SPAN",
template: "<span style=\"color:red\">{{col.name}}</span>"
}];
}
angular.module('sample', [])
.directive('compile', compileDirective)
.controller('colsCtrl', colsController);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.4/angular.min.js"></script>
<div ng-app="sample">
<ul ng-controller="colsCtrl as ctrl">
<li ng-repeat="col in ctrl.cols">
<!-- The "compile" attribute is our custom directive -->
<div compile="col.template"></div>
</li>
</ul>
</div>

Related

How to add dynamic directive names from JSON with the $interpolate function

I'm trying to dynamically add directive names to my directive from a json object. Angular however is only interpolating the directive name which is pulled from a JSON tree once, Angular is then not recognizing and compiling the dynamic children directives once the name is interpolated.
I have tried adding the interpolate service to my DDO so that I can manually interpolate the JSON values, and then have Angular compile.
I however get undefined for $interpolate(tAttrs.$attr.layout) I'm passing the json object to my isolated scope as layout, when I try to access the attr layout I get undefined. My question is how can I access layout object values in the pre link or before compile so that I can interpolate the values and inject them in.
Or do I need to have angular recompile as described here: How do I pass multiple attributes into an Angular.js attribute directive?
Any help would be great.
{
"containers": [
{
"fluid": true,
"rows": [
{
"columns": [
{
"class": "col-md-12",
"directive": "blog"
}
]
},
{
"columns": [
{
"class": "col-md-6 col-md-offset-3 col-xs-10 col-xs-offset-1",
"directive": "tire-finder"
}
]
}
]
}
]
}
...
<div layout="layout" ng-repeat="container in layout.containers" ng-class="container">
<div ng-repeat="row in container.rows">
<div ng-repeat="column in row.columns" ng-class="column.class">
<{{column.directive}}></{{column.directive}}>
</div>
</div>
</div>
...
angular.module('rpmsol').directive('wpMain', wpMainDirective);
function wpMainDirective($interpolate) {
var controller = function(brainService, $scope, $state) {
$scope.directive = {};
var currentState = $state.current.name;
brainService.getDirectiveScope('wpMain', {}).then(function(response) {
$scope.layout = response.states[currentState];
});
};
var compile = function(tElement, tAttrs, transclude) {
var directiveNames = $interpolate(tAttrs.$attr.layout);
}
return {
restrict: 'E',
// replace: true,
scope: {
layout: '=',
},
controller: controller,
templateUrl: 'directive/wpMain/wpMain.html',
compile: compile
};
};
If you're only dealing with a couple options for what a column might be, I would suggest going with #georgeawg's answer.
However, if you expect that number to grow, what you might opt for instead is something along the following lines:
<div layout="layout" ng-repeat="container in layout.containers" ng-class="container">
<div ng-repeat="row in container.rows">
<div ng-repeat="column in row.columns" ng-class="column.class">
<column-directive type="column.directive"></column-directive>
</div>
</div>
and then in your JS...
yourApp.directive('columnDirective', columnDirectiveFactory);
columnDirectiveFactory.$inject = ['$compile'];
function columnDirectiveFactory ($compile) {
return {
restrict: 'E',
scope: {
type: '='
},
link: function (scope, elem, attrs) {
var newContents = $compile('<' + scope.type + '></' + scope.type + '>')(scope);
elem.contents(newContents);
}
};
}
To the best of my knowledge, Angular doesn't have any built-in facility to choose directives in a truly dynamic fashion. The solution above allows you to pass information about which directive you want into a generic columnDirective, whose link function then goes about the business of constructing the correct element, compiling it against the current scope, and inserting into the DOM.
There was an issue with the promise in my original posted code which was preventing me from recompiling the template with the correct directive names. The issue was that I was trying to access the JSON object in the preLink function, but the promise hadn't been resolved yet. This meant that my scope property didn't yet have data.
To fix this I added my service promise to the directive scope $scope.layoutPromise = brainService.getDirectiveScope('wpMain', {}); to which I then called and resolved in my link function. I managed to have Angular compile all of my directive names from the JSON object, but I had to do it in a very hackish way. I will be taking your recommendations #cmw in order to make my code simpler and more 'Angulary'
This is currently my working code:
...
angular.module('rpmsol').directive('wpMain', wpMainDirective);
function wpMainDirective($interpolate, $compile) {
var controller = function(brainService, $scope, $state) {
$scope.currentState = $state.current.name;
$scope.layoutPromise = brainService.getDirectiveScope('wpMain', {});
};
var link = function(scope, element, attributes) {
scope.layoutPromise.then(function sucess(response) {
var template = [];
angular.forEach(response.states[scope.currentState].containers, function(container, containerKey) {
template.push('<div class="container' + (container.fluid?'-fluid':'') + '">');
//loop rows
angular.forEach(container.rows, function(row, rowkey) {
template.push('<div class="row">');
angular.forEach(row.columns, function(column, columnKey) {
template.push('<div class="' + column.class + '">');
template.push('<' + column.directive +'></' + column.directive + '>')
template.push('</div>');
});
template.push('</div>');
});
template.push('</div>');
});
template = template.join('');
element.append($compile(template)(scope));
})
};
return {
scope: true,
controller: controller,
link: link
};
};

click selectively ng-if directive in angular js

HTML :
<div ng-app="myApp" ng-controller="someController as Ctrl">
<div class="clickme" ng-repeat="elems in Ctrl.elem" ng-click="Ctrl.click(elems.title)">
{{elems.title}}
<span>click me</span>
<div id="container">
<test-Input title="elems.title" data="elems.id" ng-if="Ctrl.myId==" >/test-Input>
</div>
</div>
JS :
var Elems = [
{
title : "First",
id : 1
},
{
title : "Second",
id : 2
},
{
title : "Third",
id : 3
}
];
var myApp = angular.module('myApp', []);
myApp.controller('someController', function($scope) {
var self = this;
self.elem = Elems;
self.myId = false;
self.click = function(data){
self.myId = data;
};
});
myApp.directive('testInput',function(){
return {
restrict: 'E',
scope: {
myTitle: '=title',
myId: '=data'
},
template: '<div>{{myTitle}}</div>',
controller: function($scope) {
}
};
});
I'm new to angular js. when I click the "click me" div then I want to make ng-if = true result. then show (not ng-show it will renders every elements) the directive. is there any ways to do it angular way?
Here is a fiddle: http://jsfiddle.net/4L6qbpoy/5/
You can use something like:
<test-Input title="elems.title" data="elems.id" ng-if="elems.isVisible"></test-Input>
and toggle that on click
Check out this jsfiddle
You need to have the ng-if evaluate to true within the ng-repeat. So you need a condition that evaluates the unique value for each object in the array.
ng-if="Ctrl.myId==elems.title"
To understand, each instance of ng-repeat falls under the controller's scope. That means the ng-repeated elements are pushed to the boundaries and are only evaluated within your template. Within those bounds, you have access to a tiny local scope which includes $index, $first, $odd, etc..
You can read more here: https://docs.angularjs.org/api/ng/directive/ngRepeat

How to create a new directive that will compile other directive attributes in Angularjs?

I am trying to create buttons (using ng-repeat) that when clicked will create other buttons that when these are clicked will display the information I am looking for. I was told that an Angular Directive would do the trick. I have created a custom directive and am trying to incorporate the ng-repeat directive inside of my new directive. I have already looked at this StackOverflow discussion StackOverflow Discussion 2, but I am still having some confusion on how to best get his implemented. As it stands the new directive is being made, but no text is being appended to the button. Also only one button is being generated instead of two in this case. Below is my code (HTML and JavaScript)
HTML:
<div ng-app="anniversaries" class="row" ng-controller="annList">
<yearbuttons></yearbuttons>
</div>
JavaScript:
var annApp = angular.module('anniversaries', []);
annApp.controller('annList', function ($scope) {
$scope.anns = [
//January
{'date':'January 2015','year': '45',
'names': ['Sample Name']},
{'date':'January 2015','year': '34',
'names': ['Sample Name2']}
];
});
annApp.directive('yearbuttons',function(){
return {
restrict: "E",
compile: function compile(element, attrs)
{
element.attr('ng-repeat', 'years in anns');
element.attr('class', 'btn btn-default');
element.append('{{year}} Years');
}
}
});
I personally wouldn't use a directive for this at all unless it needed to be used repeatedly. But if it does, this could easily be converted to a directive just by sticking the template and controller into a directive. Directives can take controllers instead of link functions.
JavaScript
(function(){
angular.module('myApp', [])
.controller('BunchOfButtonsController', BunchOfButtonsController);
BunchOfButtonsController.$inject = ['$scope', '$log'];
function BunchOfButtonsController($scope, $log){
$scope.buttons = [];
$scope.nextLabel = "Some Text";
$scope.firstButtonClick = function(labelValue){
$scope.buttons.push({
label: labelValue,
click: function(){
$log.info(labelValue + " was clicked.");
}
});
$scope.nextLabel = "Another " + $scope.nextLabel;
};
}
})();
HTML
<div ng-app="myApp" ng-controller="BunchOfButtonsController">
Label: <input type="text" ng-model="nextLabel"/>
<button ng-click="firstButtonClick(nextLabel)">Click Me!</button>
<button ng-repeat="button in buttons" ng-click="button.click()">{{button.label}}</button>
</div>

Angularjs - ngModel.$setViewValue is not a function

Here is my plunker and the code I can't get to work starts on line 32
http://plnkr.co/edit/pmCjQL39BWWowIAgj9hP?p=preview
I am trying to apply an equivalent to markdown filter onto a directive... I created the filter and tested with manually applying the filter and it works that way,, but I should only use the filter conditionally when the type of content on directive is set to markdown.
I am trying to accomplish this by updating ng-model >>> ngModel.$setViewValue(html) but I am getting an error
ngModel.$setViewValue is not a function.. which makes me thing that the controller is not recognized although it is required by the directive.
Here is a working controller:
var app = angular.module('testOne', ["ngResource", "ngSanitize"]);
app.controller('testOneCtrl', function ($scope) {
$scope.product = {
id:12,
name:'Cotton T-Shirt, 2000',
description:'### markdown\n - list item 1\n - list item 2',
price:29.99
};
});
app.directive("myText", function ($parse) {
return {
restrict: "E",
require: "?ngModel",
scope:{
css: "#class", type: "#type"
},
controller: function ($scope, $element, $attrs) {},
templateUrl: "template.html",
compile: function(elm, attrs, ngModel){
var expFn = $parse(attrs.contentType + '.' + attrs.value);
return function(scope,elm,attrs){
scope.$parent.$watch(expFn, function(val){
scope.exp = { val: val };
if ( attrs.type == 'markdown'){
var converter = new Showdown.converter();
var html = converter.makeHtml(val);
//scope.exp.val = html;
ngModel.$setViewValue(html);
ngModel.$render();
}
})
scope.$watch('exp.val', function(val){
expFn.assign(scope.$parent, val)
})
}
}
}
})
This is a filter for markdown which works when applied.. (I would consider using the filter if I could figure out the way to conditionally apply it to existing directive but I'd rather do it with ng-model)
/*
app.filter('markdown', function ($sce) {
var converter = new Showdown.converter();
return function (value) {
var html = converter.makeHtml(value || '');
return $sce.trustAsHtml(html);
};
});
*/
Here is the directive template
<div ng-class="{{css}}"
ng-click="view = !view"
ng-bind-html="exp.val">
</div>
<div>
<textarea rows="4" cols="30" ng-model="exp.val"></textarea>
</div>
This is the directive in use:
<mb-text ng-cloak
type="markdown"
content-type="product"
value="description"
class="test-one-text-2">
</mb-text>
Why ngModel is empty?
When using require on a directive the controller is passed as the 4th argument to the linking function. In you code you try to reference it as an argument of the compile function. The controller is only instantiated before the linking phase so it could never be passed into the compile function anyway.
The bigger issue is that require can only get a controller of the same element ({ require: 'ngModel' }), or parent elements ({ require: '^ngmodel' } ). But you need to reference a controller from a child element (within the template).
How to get ngModel?
Do not use require at all as you cannot get child element's controller with it.
From angular.element docs:
jQuery/jqLite Extras
controller(name) - retrieves the controller of the current element or its parent. By default retrieves controller associated with the ngController directive. If name is provided as camelCase directive name, then the controller for this directive will be retrieved (e.g. 'ngModel').
Inside the linking function you can get the hold of the controller like so:
var ngModel = elm.find('textarea').controller('ngModel');
I fixed your directive:
here is a plunker: http://plnkr.co/edit/xFpK7yIYZtdgGNU5K2UR?p=preview
template:
<div ng-class="{{css}}" ng-bind-html="exp.preview"> </div>
<div>
<textarea rows="4" cols="30" ng-model="exp.val"></textarea>
</div>
Directive:
app.directive("myText", function($parse) {
return {
restrict: "E",
templateUrl: "template.html",
scope: {
css: "#class",
type: "#type"
},
compile: function(elm, attrs) {
var expFn = $parse(attrs.contentType + '.' + attrs.value);
return function(scope, elm, attrs) {
scope.exp = {
val: '',
preview: null
};
if (attrs.type == 'markdown') {
var converter = new Showdown.converter();
var updatePreview = function(val) {
scope.exp.preview = converter.makeHtml(val);
return val;
};
var ngModel = elm.find('textarea').controller('ngModel');
ngModel.$formatters.push(updatePreview);
ngModel.$parsers.push(updatePreview);
}
scope.$parent.$watch(expFn, function(val) {
scope.exp.val = val;
});
scope.$watch('exp.val', function(val) {
expFn.assign(scope.$parent, val);
});
};
}
};
});

Is it possible to compile angular template to final html string?

Is it possible to compile this html template string:
"<p>List of products from {{supplier.name}}</p>
<p ng-repeat="ref in refs">{{ref}}</p>"
directly to an html string like:
"<p>List of products from Some Supplier</p>
<p>a0120</p>
<p>a0241</p>
<p>z1242</p>
<p>z3412</p>"
or at least the less clean version:
"<p class="ng-scope ng-binding">List of product from Duval</p>
<!-- ngRepeat: ref in refs track by $index -->
<p ng-repeat="ref in refs track by $index" class="ng-scope ng-binding">a0120</p>
<p ng-repeat="ref in refs track by $index" class="ng-scope ng-binding">a0241</p>
<p ng-repeat="ref in refs track by $index" class="ng-scope ng-binding">z1242</p>
<p ng-repeat="ref in refs track by $index" class="ng-scope ng-binding">z3412</p>"
I tried using $compile(templateStr)($scope) but the dom elements returned are not fully processed.
However I managed no compile it to a page element using the following directive and and inspecting that element I can see it has the final html I'm looking for:
app.directive('compile', function($compile) {
return{
restrict: 'A',
scope: {
compile: '=compile',
data: '=ngData'
},
link: function(scope, element, attrs) {
scope.$watch('data',
function(value) {
for (var k in scope.data)
scope[k] = scope.data[k];
}
)
scope.$watch('compile',
function(value) {
element.html(value);
var a = $compile(element.contents())(scope);
}
)
}
}
})
Is there any way I can get that final html directly from the template?
Thanks
PS:
What I'm trying to achieve here is to edit a template directly in CKEditor (in text mode, not source)
and only eventually goint to source mode to add some "ng-repeat" attributes.
Using template engines like Handlebars require placeholders outside html elements and are automaticaly erased by CKEditor since it only deals with html.
POSSIBLE SOLUTION (hacky):
One possible way is to use the compile directive on an hidden element and read the element's content after view is loaded on the controller:
$scope.$on('$viewContentLoaded', $scope.onLoaded);
$timeout(function() {
var el =$("#text div")[0]
cleanAngularStuff(el)
$scope.currMailTemplate.processed = el.innerHTML
});
The cleanAngularStuff function is just to clean extra angular directives and classes.
I'll post it here if someone wants to use it or improve it.
Any better way to do this without adding an element to the page?
What you need to do is access the compiled element after a $digest cycle.
So within a $digest cycle you can do:
templateString = '<some-template-code/>';
...
var compiled = $compile(templateString)(scope);
// scope.$digest // only call this if not within a $digest cycle
// you can do a $timeout to let the previous digest cycle complete
$timeout(function(){
var theHtml = compiled[0].outerHTML;
console.log('the html with the variables', theHtml);
});
If you aren't already within a digest cycle then you need to manually call scope.$digest(). You can see the embedded sample below.
(function(){
"use strict";
var app = angular.module('soApp', []);
app.directive('showCompiledTemplate',[
'$compile','$timeout',
function($compile , $timeout) {
return {
restrict: 'E',
template: '<div class="compiled-template">' +
'<div><textarea cols="40" rows="15" readonly></textarea></div>' +
'<div class="output"></div>' +
'</div>',
scope: {
data: '=',
template: '='
},
link: function(scope,elem,attrs) {
var textarea = elem.find('textarea')[0];
var output = elem.children().children().eq(1);
var updateOutput = function(tpl) {
var compiled = $compile(tpl)(scope);
$timeout(function(){
var theHtml = compiled[0].outerHTML;
textarea.value = theHtml;
output.html(theHtml);
});
};
scope.$watch("template",function(tpl){
updateOutput(tpl);
});
scope.$watch("data",function(){
updateOutput(scope.template);
},true);
}
};
}
]);
app.controller('MainCtrl', function() {
this.data = {
name: 'John',
list: ['one duck','two ducks','three ducks']
};
//this.template = "<div>hi</div>";
var template = '';
template += '<div>\n';
template += ' <p>{{data.name}}</p>\n';
template += ' <ul>\n';
template += ' <li ng-repeat="item in data.list">{{item}}</li>\n';
template += ' </ul>\n';
template += '</div>\n';
this.template = template;
});
})();
.form-field {
padding-bottom: 10px;
}
.form-field span {
width: 70px;
display: inline-block;
}
.compiled-template {
display: -webkit-flex;
display: flex;
-webkit-flex-direction: row;
flex-direction: row;
}
.compiled-template textarea {
background-color: #eee;
margin-right: 10px;
}
.compiled-template .output {
border: 1px solid #ccc;
padding: 10px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.6/angular.js"></script>
<div ng-app="soApp">
<div ng-controller="MainCtrl as main">
<div class="form-field">
<span class="form-label">Name:</span>
<input type="text" ng-model="main.data.name" /> <br/>
</div>
<div class="form-field">
<span class="form-label">Template:</span>
<textarea ng-model="main.template" cols="40" rows="8"></textarea> <br/>
</div>
<div>
<show-compiled-template data="main.data" template="main.template" />
<div>
</div>
</div>
It can be done by providing template to your directive like below.
app.directive('compile', function($compile) {
return{
restrict: 'A',
template: '<p>List of products from {{supplier.name}}</p>
<p ng-repeat="ref in refs">{{ref}}</p>'
scope: {
refs: '='
supplier: '='
},
link: function(scope, element, attrs) {
# Your code goes here
}
}
})

Categories

Resources