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

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

Related

Why can`t I access scope via controller?

I am working on passion project. And I cant access scope to pass data to back-end frame work.
Here is my index file
<div id="main-menu" ng-controller="appCtrl">
//some other code
<div id="includedDocumentsFilter" style="float:right; display:none; padding-right: 10px;">
<my-documents validate-options="validateDialogOptions()" call-dialog="showDialog()"> </my-documents>
</div>
//some other code
</div>
My custom directive
'use strict';
dbApp
.directive('myDocuments', [
function () {
var documentTemplate =
' <div class="caption-row">' +
'<kendo-button style="width:62px" ng-click="changeDocument(true)"> Ok </kendo-button>'+
'<kendo-button style="width:62px" ng-click="changeDocument(false)" > Revert changes </kendo-button>'+
'</div>'
}
return {
scope: true,
template: documentTemplate
}
}]
)
My controller
$scope.changeDocument = function (applyFilter) {
if (applyFilter === true) {
//Here is where I cant access $scope
}
}
Firstly, I see a extra closing curly braces in your directive. Secondly in your html code there is display:none in div with id "includedDocumentsFilter". Just wondering if you are hiding the div, how will you be able to see the template defined in your directive. I have added a working jsfiddle link below using your above mentioned code
dbApp.directive('myDocuments', [
function () {
var documentTemplate =
' <div class="caption-row">' +
'<kendo-button style="width:62px" ng-click="changeDocument(true)"> Ok </kendo-button>'+
'<kendo-button style="width:62px" ng-click="changeDocument(false)" > Revert changes </kendo-button>'+
'</div>'
return {
scope: true,
template: documentTemplate
}
}]
)
JsFiddle link: https://jsfiddle.net/anilsarkar/gk2dfh1p/21/
Note: I have replaced kendo-button with span in jsfiddle

Dynamically change html element in Angular directive

Let's say have have a simple template for a directive like this:
<section class="card {{width}} recipe-list-card">
<div class="card-top">
<h3>{{headerText}}</h3>
</div>
<div class="card-bottom">
<div ng-transclude></div>
</div>
</section>
In some cases I'd like to use an h2 and in others and h3. Is there a good way to change the element with a directive?
Here's what I have in my directive:
module.exports = function(app) {
app.directive('cardDirective', function() {
return {
restrict: 'AC',
replace: true,
transclude: true,
templateUrl: '/templates/card_template.html',
scope: {
header: '=',
headerText: '#',
width: '#' //two columns, three columns, etc
}
}
})
}
I'd like to assign the header variable to h2, h3 etc. So far I've only been able to get escaped html (the actual tag rendered out like <h2> in the browser).
You can create a directive for your heading tag, like this:
angular.module('myApp')
.directive('myHeading', myHeading);
function myHeading() {
return {
transclude: true,
template: function(tElement, tAttrs) {
var level = Number(tAttrs.level);
if (level < 1 || level > 6) level = 1; // default
return '<h' + level + '><ng-transclude></ng-transclude></h' + level + '>';
}
};
}
Then you could use it in your template like this:
<my-heading level="2">{{headerText}}</my-heading>
You can do it by following the code as follows
Change HTML as follows:
<section class="card {{width}} recipe-list-card">
<div class="card-top">
<h3 ng-show="h3">{{headerText}}</h3>
<h2 ng-show="h2">{{headerText}}</h2>
</div>
<div ng-click="updateh2h3()">Check h2h3 changes</div>
<div class="card-bottom">
<div ng-transclude></div>
</div>
</section>
And modify controller as follows:
module.exports = function(app) {
app.directive('cardDirective', function() {
return {
restrict: 'AC',
replace: true,
transclude: true,
templateUrl: '/templates/card_template.html',
scope: {
header: '=',
headerText: '#',
width: '#' //two columns, three columns, etc
},
controller: function($scope) {
$scope.h2 = false;
$scope.h3 = true;
$scope.updateh2h3 = function(){
if($scope.h2){
$scope.h2 = false;
$scope.h3 = true;
} else {
$scope.h2 = true;
$scope.h3 = false;
}
}
}
}
})
}
You can simply add an attribute to the directive and use ng-switch to setup the header you need for your card. take a look this demo i've done.
<div class="card">
<div class="card--Title" ng-switch on="headline">
<h1 ng-switch-when="h1">{{::headerText}}</h1>
<h2 ng-switch-when="h2">{{::headerText}}</h2>
<span ng-switch-default>{{::headerText}}</span>
</div>
</div>
http://embed.plnkr.co/simYTj/
I think you are looking at this issue the wrong way. Instead of switching tags around, you can just define two classes for h2 and h3, and then you can use ng-class to switch between. Dynamically compiling tags and manipulating dom for this is very expensive operation.

Use link function in directives to append different HTML elements

Fiddle
I have two buttons. When pressed it displays a modal, with som text. But I also want to add some html dynamically depending on which button is pressed.
I have tried both $observe and $watch methods, but I'm having problems making it work.
here is my code.
angular.module('TM', [])
.controller('protocolCtrl', function(){
this.text = 'Now looking at the protocol part';
this.modalId = 'protocolModal';
})
.controller('categoryCtrl', function(){
this.text = 'Now looking at the category part';
this.modalId = "categoryModal";
})
.directive('modalDirective', function(){
return {
restrict: 'E',
scope: {
ctrl: '=',
modalId: '#',
},
template: ['<div id="{{modalId}}" class="modal fade" role="dialog">',
'<div class="modal-dialog">',
'<div class="modal-content">',
'<div class="modal-header">',
'<h4 class="modal-title">Modal Header</h4>',
'</div>',
'<div class="modal-body">',
'<p> {{ ctrl.text }} </p>',
'</div>',
'<div class="modal-footer">',
'<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>',
'</div>',
'</div>',
'</div>',
'</div>'].join(''),
link: function(scope, element, attrs) {
element.$observe('modalId', function(){
var modal = element.find('#{{modalId}}');
if(modal == 'protocolModal'){
element.find('#{{modalId}}').append('<div>this is a protocol test...</div>');
} else {
element.find('#{{modalId}}').append('<div>this is a category test...</div>');
}
});
}
}
});
I don't think there is element.$observe - there is attrs.$observe and scope.$watch. You already have modelId on the scope, so let's use that.
Also, instead of the wonky .find by id, inject an element as a placeholder for the template and replaceWith it accordingly:
template: '<div id="{{modalId}}">\
...\
<div class="modal-body">\
<template-placeholder></template-placeholder>\
</div>\
</div>",
link: function(scope, element){
// ...
var unwatch = scope.$watch("modalId", function(val){
var placeholder = element.find('template-placeholder');
if(val == 'protocolModal'){
placeholder.replaceWith('<div>this is a protocol test...</div>');
} else {
placeholder.replaceWith('<div>this is a category test...</div>');
}
unwatch(); // seems like you don't really need to set it again
}
}
See i updated your Fiddle
Use attr in link function. because you already given attribute to
your html i.e: modal-id="{{pctrl.modalId}}
<modal-directive ctrl="pctrl" modal-id="{{pctrl.modalId}}"></modal-directive>
if(attrs.modalId == 'protocolModal'){
element.find('#{{modalId}}').append('<div>this is a protocol test...</div>');
} else {
element.find('#{{modalId}}').append('<div>this is a category test...</div>');
}
Edit :
use $timeout
$timeout(function () {
if (attrs.modalId == 'protocolModal') {
element.find('#' + attrs.modalId).append('<div>this is a protocol test...</div>');
} else {
element.find('#' + attrs.modalId).append('<div>this is a category test...</div>');
}
}, 1000)
Now why $timeout because you are applying template and and at same
time your link function append your div so it will not apply your div
.So first apply template and then in template append your div
And if you want to show that div in popup content the use this selector.
see this example: https://jsfiddle.net/kevalbhatt18/o76hxj69/6/
element.find('#'+attrs.modalId +' .modal-body').append('<div>this is a protocol test...</div>');
If your two DOM structure diverses, you can consider using two different templates depending on some parameter value.
templateUrl can be specified as a function, such as:
angular.module('Joy', [])
.controller('ProfileCtrl', ['$scope', function ($scope) {
$scope.user = {
name: 'Elit'
};
}])
.directive('profile', [function () {
return {
restrict: 'A',
templateUrl: function (elem, attrs) {
return 'style-' + attrs.color + '.html';
}
};
}]);
And use the directive as:
<div ng-app="Joy" id="play-ground" ng-controller="ProfileCtrl">
<div profile color="red"></div>
<div profile color="green"></div>
</div>
In this case, if the color is red, the directive will load template from style-red.html. Otherwise from style-green.html.
In your case, you might maintain a flag in the outside controller. Clicking either button will change this flag value and pass in to the directive. The directive will load different templates accordingly.

Custom directive: How evaluate bindings with dynamic HTML

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>

AngularJS : Customizing the template within a Directive that includes references to scoped attributes

I'm trying to customize a template within a directive and include references to attributes in the parent scope. I'm pretty new to Angular but I've done a fair bit of searching and I've based my attempts on Customizing the template within a Directive. However if I pass a reference to a parent scoped variable as an attribute to the directive it doesn't get resolved, possibly because it's still undefined at the time the compile function is called.
My directive definition looks like this:
app.directive('sectionHeader', function() {
return {
restrict: 'EC',
replace: true,
transclude: true,
scope: {sectionName:'#sectionName', imageUrl:'#imageUrl'},
compile: function(element, attrs) {
var imageHtml = attrs.hasOwnProperty('imageUrl') ? '<div style="float: left; padding-right: 5px;"><img class="float_left" src="' + attrs.imageUrl + '" alt=""/></div>' : '';
var htmlText =
'<div>' + imageHtml + '<h1 class="float-left">' + attrs.sectionName + '</h1>' +
'<div class="clear"></div>' +
'<div class="modal_hr"></div></div>';
element.replaceWith(htmlText);
},
};
});
And I'm using the directive like this:
<div class="section-header" section-name="{{currentFeatureName}}"></div>
The problem is the {{currentFeatureName}} variable from my controller doesn't appear to be defined when the compile function is called on the directive.
One way I've considered to get around this is within the compile function set up an observer function on the sectionName attribute that updates h1 element content when it sees a change. This seems a little clunky and I was wondering if there is a better or more elegant way of doing this.
Check out the $observe function in Directive docs.
But beside that, there actually seems to be no need to do what you were trying to do. See:
var app = angular.module('plunker', []);
app.controller('AppController',
[
'$scope',
function($scope) {
$scope.currentFeatureName = 'Current Feature Name';
$scope.imageUrl = "https://lh3.googleusercontent.com/GYSBZh5RpCFwTU6db0JlHfOr_f-RWvSQwP505d0ZjWfqoovT3SYxIUPOCbUZNhLeN9EDRK3b2g=s128-h128-e365";
}
]
);
app.directive('sectionHeader', function() {
return {
restrict: 'EC',
replace: true,
transclude: true,
scope: {
sectionName:'#',
imageUrl:'#'
},
template: '<div><div style="float: left; padding-right: 5px;" ng-show="imageUrl"><img class="float_left" ng-src="{{imageUrl}}" alt=""/></div><h1 class="float-left">{{sectionName}}</h1><div class="clear"></div><div class="modal_hr"></div></div>'
};
});
HTML:
<div ng-controller="AppController">
<div class="section-header" section-name="{{currentFeatureName}}" image-url="{{imageUrl}}"></div>
</div>
Plunker.
You are correct on why this isn't working. Interpolated attributes are not available when the compile and link functions run because no digest has occurred yet to resolve the interpolation to a value. You can read more about this here. You are also right about the solution: use attrs.$observe( 'sectionName', function ( val ) { ... });
However, it doesn't look like you need a dynamic template. If this was your template:
<div>
<div style="float: left; padding-right: 5px;" ng-show="{{imageUrl}}">
<img class="float_left" ng-src="{{imageUrl}}" alt="" />
</div>
<h1 class="float-left">{{sectionName}}</h1>
<div class="clear"></div>
<div class="modal_hr"></div>
</div>
Then you wouldn't need any logic in a compile or link function. Perhaps this pattern will also help you.

Categories

Resources