$digest rendering ng-repeat as a comment - javascript

I'm writing a test for a directive, when executing the test the template (which is loaded correctly) is rendered just as <!-- ng-repeat="foo in bar" -->
For starters the relevant parts of the code:
Test
...
beforeEach(inject(function ($compile, $rootScope, $templateCache) {
var scope = $rootScope;
scope.prop = [ 'element0', 'element1', 'element2' ];
// Template loading, in the real code this is done with html2js, in this example
// I'm gonna load just a string (already checked the problem persists)
var template = '<strong ng-repeat="foo in bar"> <p> {{ foo }} </p> </strong>';
$templateCache.put('/path/to/template', [200, template, {}]);
el = angular.element('<directive-name bar="prop"> </directive-name>');
$compile(el)(scope);
scope.$digest(); // <--- here is the problem
isolateScope = el.isolateScope();
// Here I obtain just the ng-repeat text as a comment
console.log(el); // <--- ng-repeat="foo in bar" -->
}));
...
Directive
The directive is fairly simple and it's not the problem (outside the test everything works just fine):
app.directive('directiveName', function () {
return {
restrict : 'E',
replace : true,
scope : {
bar : '='
},
templateUrl : '/path/to/template', // Not used in this question, but still...
});
A few more details:
The directive, outside the test, works fine
If I change the template to something far more simple like: <h3> {{ bar[0] }} </h3> the test works just fine
The rootScope is loaded correctly
The isolateScope results as undefined

If you have a look at the generated output that angular creates for ng-repeat you will find comments in your html. For example:
<!-- ngRepeat: foo in bars -->
These comments are created by the compile function - see the angular sources:
document.createComment(' ' + directiveName + ': '+templateAttrs[directiveName]+' ')
What you get if you call console.log(el); is that created comment. You may check this if you change the output in this way: console.log(el[0].parentNode). You will see that there are a lot of childNodes:
If you use the directive outside of a test you will not be aware of this problem, because your element directive-name will be replaced by the complete created DocumentFragment. Another way to solve the problem is using a wrapping element for your directive template:
<div><strong ng-repeat="foo in bar"> <p> {{ foo }} </p> </strong></div>
In this case you have access to the div element.

Related

How to get passed attributes from custom directive and collect them in an array to show into template

I have created a custom directive which should display a slider with the data entered to the custom directive. I need to be able to display the image Url and redirect Link via directive attributes for example:
<div class="sliderBanner" imgUrl="http://example.com/img1.jpg" imgLink="example.com"></div>
<div class="sliderBanner" imgUrl="http://example.com/img2.jpg" imgLink="example.com"></div>
<div class="sliderBanner" imgUrl="http://example.com/img3.jpg" imgLink="example.com"></div>
Now I want to collect those data and place them inside an array within directive scope and use ng-repeat inside directive template to show them.
e
PS: I'm using Swiper angular directive for slider purpose.
var app = angular.module('APP',['ksSwiper']);
app.directive('sliderBanner',function($http){
return{
scope:true,
restrict:'C',
link: function(scope,element,attr){
scope.data = [];
scope.data.push({
"id": attr.id,
"imgUrl": attr.imgUrl,
"imgRef": attr.imgRef
});
},
templateUrl: 'http://www.lajmislam.com/wp-content/themes/Newspaper/ng-templates/sliderBanner.html'
}
});
This is my directive template:
<ks-swiper-container autoplay="3000" show-nav-buttons="true" pagination-is-active="true" swiper="swiper">
<ks-swiper-slide ng-repeat="item in data">
{{item.id}},{{item.imgUrl}}
</ks-swiper-slide>
</ks-swiper-container>
<div ng-repeat="item in data">
{{item.id}},{{item.imgUrl}}
</div>
What errors are you seeing?
What you have here appears to be working. I tested it out here: https://github.com/styonsk/StackOverflowSolutions/tree/master/35997232

AngularJS directive: template with scope value (ng-bind-html)

I have such directive:
...
template: function(element, attrs) {
var htmlTemplate = '<div class="start-it" ng-if="isVisible">\
<p ng-bind-html="\'{{customDynamicText}}\' | translate"></p>\
</div>';
return htmlTemplate;
},
...
(as you can see also i'm using translate plugin)
and there i have a problem: in scope this value is changing, but it doesn't change in directive(
when i'm using attrs-params (sure, if customDynamicText is a static string - all works) - but i have a dynamic variable customDynamicText
How can i use this dynamic variable in directive template with ng-bind-html.
Is it possible?
Simply clever...
I forgot to remove some quote-chars...
So works:
...
<p ng-bind-html="' + attrs.customDynamicText + ' | translate"></p>\
...

Issues with calling function from template

In one case I have a problem with running a function on the Controller from the template. The value becomes a string containing the function signature, not the value that should be returned from the function.
When I use {{ getSomeObject(d) }} in my template markup it works fine, and it prints the object values, meaning that the function got called on the Controller.
I have tried with and without the {{ }}.
Pseudo code:
<div class"xyz" data-lav-fact="getSomeObject(d)"> <!-- Does not work here -->
{{ getSomeObject(d) }} <!-- Works here -->
</div>
And of course the function is added to the scope in the Controller:
$scope.getSomeObject = function(data) {
return { key: "test" };
};
This works in other parts of the application and I don't know what wrong in this case.
Does anyone know what typically can be wrong here?
Since you are trying to set an attribute with a $scope function, you'll need to {{ interpolate }} and use ngAttr attribute bindings. Here is a simple example that shows this in action. Examine the difference between the elements logged out. As you dig, you'll see your { key: 'test' } value being set
<div id="without" data-lav-fact="getSomeObject()">without</div>
<div id="with" ng-attr-data-lav-fact="{{ getSomeObject() }}">with</div>
app.controller('ctrl', ['$scope', function($scope) {
$scope.getSomeObject = function() {
return { key: 'test' };
}
var w = angular.element(document.getElementById('with'));
var wo = angular.element(document.getElementById('without'));
console.log(w[0].attributes); // has value
console.log(wo[0].attributes); // does not have value
}]);
JSFiddle Link

Evaluate an expression from within a JSON file

I have an Angular app where data is loaded in through JSON files. For the various objects, one of the properties is a "Description". In my app I pop it in my html via {{item.Description}}. My problem is that the string in the JSON file has values that need to be adjusted based on a variable. For example, "The value is 160 (+20 per var)". I would like this description to read out 160 plus 20 times the value of the provided variable.
Unfortunately I can't just put {{160+(20*var)}} in the description, because it just prints out the expression as string.
Is there anyway to create that binding in angular so it updates dynamically based on the other variable?
Update
As per request I'm adding as much code as I can.
In my file's head I'm including a JSON file with:
<script src="path/to/file.json"></script>
Then, I have my controller:
app.controller('itemController', function(){
this.items = Items //Items is declared in the JSON file as the JSON object.
});
Then in my HTML I call:
<div ng-controller="itemController as ctrl">
<span class="description" ng-repeat="item in ctrl.items">
{{item.Description}}
</span>
</div>
The problem is, that item.Description has expressions I would like to evaluate. I would normally just do {{160+(20*ctrl.var)}}, but since that expression is contained in the item.Description string, Angular doesn't evaluate it normally.
It appears you can do this by replacing {{item.Description}} with {{$eval(item.Description)}}, which will evaluate a string as an Angular expression. See the Angular docs for expressions, or a StackOverflow post about $eval.
Edit: OP has clarified that item.Description may contain mixed Angular expressions and other text, for example "The value is {{85 + 22 * ctrl.var}}". Fortunately the Angular docs for $compile contain an example that solves this exact problem! Here is a brief demo.
angular.module('app', [])
.directive('compile', function($compile) {
// directive factory creates a link function
return function(scope, element, attrs) {
scope.$watch(
function(scope) {
// watch the 'compile' expression for changes
return scope.$eval(attrs.compile);
},
function(value) {
// when the 'compile' expression changes
// assign it into the current DOM
element.html(value);
// compile the new DOM and link it to the current
// scope.
// NOTE: we only compile .childNodes so that
// we don't get into infinite loop compiling ourselves
$compile(element.contents())(scope);
}
);
};
})
.controller('itemController', function() {
this.var = 5;
this.items = [
{Description: "What's 1+1? It's {{1+1}}"},
{Description: "The value is {{85+22*ctrl.var}}"},
{Description: "He{{'llo'}} World!"}
];
});
body {
font-family: sans-serif;
}
#main > span {
background-color: #ddd;
padding: 8px 16px;
margin: 8px;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<body ng-app="app" class="ng-scope">
<h1>Compile Test!</h1>
<div ng-controller="itemController as ctrl" id="main">
<span class="description ng-scope" ng-repeat="item in ctrl.items" compile="item.Description"></span>
</div>
</body>

How do I leave static content if property is empty in angular?

I have a simple angular controller in my app. And I wish to enhance a page containing static content, with angular. This will allow me to have a good, content rich page as fall back for those without JS (who?!) and web crawlers and readers.
So far I have identified a few ways to do this, possibly using custom directives, or by doubling up content in 'ng-bind-template' directive (which is obviously not ideal).
I am fairly new to the world of angular so would like to try and garner the best practice for this scenario.
What is the best approach for this?
Controller:
app.controller('TestArticle',function($scope, feed){
$scope.initValue = "This is angular injecting this"
$scope.activate = function(){
$scope.eTitle = "Dynamic title";
$scope.eContent = "Dynamic content";
};
});
Markup:
(The problem here is that 'Static content' is replaced by '' if angular initializes)
<div ng-controller="TestArticle">
<div ng-cloak ng-bind-template='{{eTitle}}'>Static content</div>
<div ng-cloak ng-bind-template='{{eContent}}'>Some more content</div>
<div ng-cloak>{{initValue}}</div>
<a href ng-click="activate()" ng-cloak>click to activate</a>
</div>
EDIT
I should be clear that even though angular is bootstrapping, The aim is to leave the default content intact, and not replace it with dynamic content. That only wants to be achieved once the activate button is clicked. I have tried the following but it involves doubling up the content which could get bulky if it is a whole article.
<div ng-controller="TestArticle">
<div ng-cloak ng-bind-template='{{eTitle || "Static Content"}}'>Static content</div>
<div ng-cloak ng-bind-template='{{eContent || "Some more content"}}'>Some more content</div>
<div ng-cloak>{{initValue}}</div> <a href ng-click="activate()" ng-cloak>click to activate</a>
</div>
You can create a simple directive to create the html structure that you want.
I propose this to be your new html:
<div ng-cloak sk-custom-bind="eTitle">Static content</div>
Notice how you don't have to specify the or clause with the static content.
Now you can create a directive that will build the html template for you:
app.directive('skCustomBind',function() {
return {
restrict: 'A',
template: function(elem, attrs) {
var templ = "<div ";
templ += "ng-bind-template='{{";
templ += attrs.skCustomBind;
templ += ' || "';
templ += elem.text();
templ += '"';
templ += "}}'></div>";
return templ;
},
replace: true
};
});
And as simple as that you have the functionality that you are looking for without the duplication.
See this plunker to see a working sample.
The thing here is that when you define the controller, angular will parse through the html to find all the variables and link them with the corresponding controller items using $scope.
So, when angular initializes, since eTitle and eContent are not having any values (since you initialize them in the function only), which is called on click of the last item(where on your application works fine).
So until you click the button, the static text is replaced by $scope.eTitle and $scope.eContent both of which are undefined. Hence no text inside the tags.
So the solution,
1. Either you initialize the variables inside the controller with the static text, like you did for 'This is angular injecting this'(reason only this came!):
app.controller('TestArticle',function($scope, feed){
$scope.initValue = "This is angular injecting this";
$scope.eTitle = "Dynamic title";
$scope.eContent = "Dynamic content";
$scope.activate = function(){
$scope.eTitle = "Dynamic title";
$scope.eContent = "Dynamic content";
};
});
Or you can initialize the variables in the html using ng-init, it will initialize the variable in html only:
<div ng-controller="TestArticle">
<div ng-init="eTitle = 'Static Content'" ng-cloak ng-bind-template='{{eTitle}}'>Static content</div>
<div ng-init="eContent = 'Some more content'" ng-cloak ng-bind-template='{{eContent}}'>Some more content</div>
<div ng-cloak>{{initValue}}</div>
<a href ng-click="activate()" ng-cloak>click to activate</a>
</div>
The thing is when you start using angular, the html will get kind of hijacked and angular will start using its own variables.
Thanks to #JoseM for putting me on to right track and helping me find the following solution...
SOLUTION
My Markup:
NOTE: the custom directive and properties ('cg-dyn' and 'cg-content'). These will not initialise any values when angular bootstraps. And further down you will see how these map from controller scope via an isolated scope.
<div ng-controller="TestArticle">
<div ng-cloak cg-dyn cg-content="eTitle">Static content</div>
<div ng-cloak cg-dyn cg-content="eContent">Some more content</div>
<div ng-cloak>{{initValue}}</div>
<a href ng-click="upDateTitle()" ng-cloak>Update title</a> | <a href ng-click="upDateContent()" ng-cloak>Update content</a>
</div>
My Controller:
app.controller('TestArticle',function($scope, feed){
$scope.initValue = "This is angular injecting this"
$scope.upDateTitle = function(){
$scope.eTitle = "Dynamic title";
};
$scope.upDateContent = function(){
$scope.eContent = "Dynamic Content Lorem ipsum";
};
});
And finally the magic in My Directive
app.directive('cgDyn',function() {
return {
restrict: 'A',
scope:{
'content':'=cgContent'
// this is a two way binding to the property defined in 'cg-content
},
link: function(scope, elem, attrs) {
// below is a watcher keyed on a combination content
scope.$watch('content',function(){
if(scope.content){
elem.html(scope.content);
// and here is the bit, normally plumbed by default, but instead we only replace the html if the value has been set
}
});
},
controller: 'TestArticle',
replace: false
};
});
Thanks for everyone's help. I am sure there is an even better solution out there so keep the suggestions coming!

Categories

Resources