Modifying styles of a compiled html element in Angularjs - angularjs

UPDATE
I think I found the issue, the template variable is lossing it's value, I don't get why yet, I've changed the code a bit:
var template;
$templateRequest("ng-templates/app/cart-counter.html").then(function(html){
template = angular.element(html);
element.append(template);
$compile(template)(scope);
console.log("template: " + template); // This returns the template object
});
var unbindWatcher = scope.$watch(
"clickCounter",
function(newClickCounter){
console.log("template: " + template); // This returns undefined
if (newClickCounter >= 5) {
var cartButton = this.template.children('.btn');
cartButton.toggleClass('btn-success'); // this throws undefined error
unbindWatcher();
}
}
);
My question now would be why is the template variable undefined when it had a value earlier and what should I do to fix it?
ORIGINAL QUESTION
I am playing around with Angular, trying to change some elements classes by compiling an html adding it to the DOM and when an event happens, I am trying to use angularElement to access the childs of the html I compiled and toggling some classes.
This is not giving me an error, but the changes in the classes are not happening and I can't find what Im doing wrong, please help.
This is the code for the directive:
store.directive("appCartCounter", ['$templateRequest', '$compile', function($templateRequest, $compile){
var link = function(scope, element){
this.messages = [
"Sorry, the shopping cart is not implemented",
"Hey, I told you, it's not ready",
"Stop that! It's anoying",
"I'm getting really really angry",
"YEarghhh!!!!"
];
scope.messages = this.messsages;
scope.clickCounter = 0;
scope.incrementCount = function(){
scope.clickCounter++;
};
$templateRequest("ng-templates/app/cart-counter.html").then(function(html){
this.template = angular.element(html);
element.append(template);
$compile(template)(scope);
});
var unbindWatcher = scope.$watch(
"clickCounter",
function(newClickCounter){
console.log("I've been watching you... alalalong");
if (newClickCounter >= 5) {
var cartButton = this.template.children('.btn');
var messageElement = this.template.children('.text-info');
cartButton.toggleClass('btn-success');
cartButton.toggleClass('btn-danger');
cartButton.toggleClass('btn-lg');
messageElement.toggleClass('text-info');
messageElement.toggleClass('text-danger');
messageElement.toggleClass('text-capitalize');
messageElement.toggleClass('lead');
unbindWatcher();
console.log("I'm blind!!");
}
}
);
};
return {
restrict: 'E',
scope: {
'addedProducts' : '='
},
replace: 'true',
link: link
};
}]);
cart-counter.html:
<button
class="btn btn-success"
data-ng-show="addedProducts.length"
data-ng-click="incrementCount()"
data-ng-cloak>
<i class="glyphicon glyphicon-shopping-cart"></i> {{addedProducts.length}}
</button>
<p data-ng-show="clickCounter" class="text-info">
{{messages[clickCounter]}}
</p>
html using the directive:
<app-cart-counter data-added-products="storeCtrl.addedProducts"></app-cart-counter>
I'll try to get a simpler example in a plunker.
Thanks!!

In the end I managed to "fix" the problem by saving the template to the scope, altough I still don't understand why the variable or the saving it to "this." didn't worked.
As a sidenote, selecting the children via tagname didn't worked, I also tried with the children number and classname (even though I have jquery imported in the solution). To solve that, I had to access the elements via the template object's array, and wrap that in an angular.element(), eg:
var cartButton = angular.element(scope.template[0]);
var messageElement = angular.element(scope.template[2]);
PS: scope.template[1] returned me a text node because of the linebreak, I hadn't expected that.

Related

ngRepeat error when removing child directive from parent directive

I´m having some issues with a gallery manager component where you can add/remove the pictures.
This is the html code of the gallery handler:
<img src = '{{snapshot}}' >
<div class = 'md-button l3' is-file ng-model = 'picture' blob-url = 'snapshot'>Upload</div>
<div class = 'md-button l3' ng-click = 'article.pictures.push(picture)'>Add</div>
<div gallery-manager = 'article.pictures'></div>
Below, the directives:
.directive("galleryManager", function($compile){
var controllerFn = function($scope, $element, $attrs){
var self = this;
self.removeItemAt = function($index){
self.pictures.splice($index, 1);
$compile($element)($scope); <--Note this
}
}
var linkFn = function($scope, $element, $attrs){
}
return {
template:"<div gallery-item = 'picture' ng-repeat = 'picture in galleryManagerCtrl.pictures track by $index'></div>",
restrict:"A",
controller:controllerFn,
controllerAs:"galleryManagerCtrl",
bindToController:{
pictures:"=galleryManager",
}
}
})
.directive("galleryItem", function(FileService){
var linkFn = function($scope, $element, $attrs, galleryManagerCtrl){
$scope.galleryItemCtrl.galleryManagerCtrl = galleryManagerCtrl;
}
var controllerFn = function($scope, $element, $attrs){
var self = this;
if ( self.item instanceof File ){
FileService.buildBlobUrl(self.item).then(function(blobUrl){
self.thumb = blobUrl;
})
}
}
return{
template:"<img src = '{{galleryItemCtrl.thumb}}'>"+
"<a class = 'delete' ng-click = 'galleryItemCtrl.galleryManagerCtrl.removeItemAt($index)'>&times</span></a>",
restrict:"A",
require:"^galleryManager",
link:linkFn,
controller:controllerFn,
bindToController:{
item:"=galleryItem",
},
controllerAs:"galleryItemCtrl"
}
})
Right now, the directive is working well when adding elements, but problems come when removing items; before using: $compile($element)($scope) after the deletion, in the gallery, always dissapeared the last item, although the pictures array removed the correct item, so I added the $compile line after deleting an item.
The problem is that, although the gallery now does what I want to, it keeps throwing an error after compiling (post the full trace, maybe it can help someone):
angular.js:13920 TypeError: Cannot read property 'insertBefore' of null
at after (http://localhost/www/project/admin/bower_components/angular/angular.js:3644:13)
at JQLite.(anonymous function) [as after] (http://localhost/www/project/admin/bower_components/angular/angular.js:3728:17)
at domInsert (http://localhost/www/project/admin/bower_components/angular/angular.js:5282:35)
at Object.move (http://localhost/www/project/admin/bower_components/angular/angular.js:5488:9)
at ngRepeatAction (http://localhost/www/project/admin/bower_components/angular/angular.js:29865:26)
at $watchCollectionAction (http://localhost/www/project/admin/bower_components/angular/angular.js:17385:13)
at Scope.$digest (http://localhost/www/project/admin/bower_components/angular/angular.js:17524:23)
at ChildScope.$apply (http://localhost/www/project/admin/bower_components/angular/angular.js:17790:24)
at HTMLAnchorElement.<anonymous> (http://localhost/www/project/admin/bower_components/angular/angular.js:25890:23)
at defaultHandlerWrapper (http://localhost/www/project/admin/bower_components/angular/angular.js:3497:11)
That seems to come from watchCollection at ngRepeatDirective.
I have the feeling that I´m missing something basic, but can´t see what is right now, so, here I come to ask before digging into angular code.
Thank you in advance.
EDIT
Added a working sample:
http://codepen.io/sergio0983/pen/rMEMoJ?editors=1010
EDIT 2
Removed $compile from working sample, it makes it work, yes, but throws errors; and besides, I think the real problem is elsewhere. In the working sample, you can see how file names get updated when you delete an item, but the pictures keep their original order.
add addPicture() function to your mainController (which will add constant unique ID ro the picture object):
.controller("mainController", function($scope){
$scope.article = {pictures:[]};
$scope.addPicture = function addPicture (picture) {
// set unique ID
picture.id = $scope.article.pictures.length;
$scope.article.pictures.push(picture);
};
})
change add button HTML to:
<div class='md-button l3' ng-click='addPicture(picture)'>Add</div>
change template of galleryManager to track by picture.id:
<div gallery-item='picture'
ng-repeat='picture in galleryManagerCtrl.pictures track by picture.id'></div>
modify removeItemAt() function, ($compile not needed here):
self.removeItemAt = function removeItemAt (id) {
// find index for the picture with given id
id = self.pictures.findIndex((item) => item.id === id);
self.pictures.splice(id, 1);
}
modified codepen: http://codepen.io/anon/pen/mrZOre?editors=1010

angularjs textarea with colors (with html5 contenteditable)

I'm trying to create an editor which does "syntax highlighting",
it is rather simple:
yellow -> <span style="color:yellow">yellow</span>
I'm also using <code contenteditable> html5 tag to replace <textarea>, and have color output.
I started from angularjs documentation, and created the following simple directive. It does work, except it do not update the contenteditable area with the generated html.
If I use a element.html(htmlTrusted) instead of ngModel.$setViewValue(htmlTrusted), everything works, except the cursor jumps to the beginning at each keypress.
directive:
app.directive("contenteditable", function($sce) {
return {
restrict: "A", // only activate on element attribute
require: "?ngModel", // get ng-model, if not provided in html, then null
link: function(scope, element, attrs, ngModel) {
if (!ngModel) {return;} // do nothing if no ng-model
element.on('blur keyup change', function() {
console.log('app.directive->contenteditable->link->element.on()');
//runs at each event inside <div contenteditable>
scope.$evalAsync(read);
});
function read() {
console.log('app.directive->contenteditable->link->read()');
var html = element.html();
// When we clear the content editable the browser leaves a <br> behind
// If strip-br attribute is provided then we strip this out
if ( attrs.stripBr && html == '<br>' ) {
html = '';
}
html = html.replace(/</, '<');
html = html.replace(/>/, '>');
html = html.replace(/<span\ style=\"color:\w+\">(.*?)<\/span>/g, "$1");
html = html.replace('yellow', '<span style="color:yellow">yellow</span>');
html = html.replace('green', '<span style="color:green">green</span>');
html = html.replace('purple', '<span style="color:purple">purple</span>');
html = html.replace('blue', '<span style="color:yellow">blue</span>');
console.log('read()-> html:', html);
var htmlTrusted = $sce.trustAsHtml(html);
ngModel.$setViewValue(htmlTrusted);
}
read(); // INITIALIZATION, run read() when initializing
}
};
});
html:
<body ng-app="MyApp">
<code contenteditable
name="myWidget" ng-model="userContent"
strip-br="true"
required>This <span style="color:purple">text is purple.</span> Change me!</code>
<hr>
<pre>{{userContent}}</pre>
</body>
plunkr: demo (type yellow, green or blue into the change me input area)
I tried scope.$apply(), ngModel.$render() but has no effect. I must miss something really obvious...
The links I already read through:
others' plunker demo 1
others' plunker demo 2
angularjs documentation's example
$sce.trustAsHtml stackoverflow question
setViewValue stackoverflow question
setViewValue not updating stackoverflow question
Any help is much appreciated. Please see the plunker demo above.
After almost a year, I finally settled to Codemirror, and I was never happier.
I'm doing side-by-side markdown source editing with live update (with syntax highlighting, so even a bit more advanced than stackoverflow's editing page.)
I created a simple codeEditor angular directive, which requires codeMirror, and uses it.
For completeness, here is the component sourcecode:
$ cat components/codeEditor/code-editor.html
<div class="code-editor"></div>
$ cat codeEditor.js
'use strict';
angular.module('myApp')
.directive('codeEditor', function($timeout, TextUtils){
return {
restrict: 'E',
replace: true,
require: '?ngModel',
transclude: true,
scope: {
syntax: '#',
theme: '#'
},
templateUrl: 'components/codeEditor/code-editor.html',
link: function(scope, element, attrs, ngModelCtrl, transclude){
// Initialize Codemirror
var option = {
mode: scope.syntax || 'xml',
theme: scope.theme || 'default',
lineNumbers: true
};
if (option.mode === 'xml') {
option.htmlMode = true;
}
scope.$on('toedit', function () { //event
//This is required to correctly refresh the codemirror view.
// otherwise the view stuck with 'Both <code...empty.' initial text.
$timeout(function() {
editor.refresh();
});
});
// Require CodeMirror
if (angular.isUndefined(window.CodeMirror)) {
throw new Error('codeEditor.js needs CodeMirror to work... (o rly?)');
}
var editor = window.CodeMirror(element[0], option);
// Handle setting the editor when the model changes if ngModel exists
if(ngModelCtrl) {
// Timeout is required here to give ngModel a chance to setup. This prevents
// a value of undefined getting passed as the view is rendered for the first
// time, which causes CodeMirror to throw an error.
$timeout(function(){
ngModelCtrl.$render = function() {
if (!!ngModelCtrl.$viewValue) {
// overwrite <code-editor>SOMETHING</code-editor>
// if the $scope.content.code (ngModelCtrl.$viewValue) is not empty.
editor.setValue(ngModelCtrl.$viewValue); //THIRD happening
}
};
ngModelCtrl.$render();
});
}
transclude(scope, function(clonedEl){
var initialText = clonedEl.text();
if (!!initialText) {
initialText = TextUtils.normalizeWhitespace(initialText);
} else {
initialText = 'Both <code-editor> tag and $scope.content.code is empty.';
}
editor.setValue(initialText); // FIRST happening
// Handle setting the model if ngModel exists
if(ngModelCtrl){
// Wrap these initial setting calls in a $timeout to give angular a chance
// to setup the view and set any initial model values that may be set in the view
$timeout(function(){
// Populate the initial ng-model if it exists and is empty.
// Prioritize the value in ngModel.
if(initialText && !ngModelCtrl.$viewValue){
ngModelCtrl.$setViewValue(initialText); //SECOND happening
}
// Whenever the editor emits any change events, update the value
// of the model.
editor.on('change', function(){
ngModelCtrl.$setViewValue(editor.getValue());
});
});
}
});
// Clean up the CodeMirror change event whenever the directive is destroyed
scope.$on('$destroy', function(){
editor.off('change');
});
}
};
});
There is also inside the components/codeEditor/vendor directory the full codemirror sourcecode.
I can highly recommend codeMirror. It is a rocksolid component, works in
every browser combination (firefox, firefox for android, chromium).

AngularJS directive with ng-show not toggling with change in variable

I'm trying to create a simple html5 video playlist app. I've got an overlay div on top of the html5 video that should appear/disappear when stopping and starting the video.
I've got ng-show and a variable to trigger it, but it's not changing when I look using ng-inspector.
My events might not be quite correct, either - but I can't seem to find much information on putting events on different elements within the same directive. Is this a clue that I should break this up into multiple directives?
(function() {
'use strict';
angular
.module('app')
.controller('campaignController', campaignController)
.directive('myVideo', myvideo);
function campaignController($log,Campaign) {
var vm = this;
vm.overlay = true;
Campaign.getCampaign().success(function(data) {
vm.campaign = data[0];
vm.item = vm.campaign.videos[0];
});
vm.select = function(item) {
vm.item = item;
};
vm.isActive = function(item) {
return vm.item === item;
};
};
function myvideo() {
return {
restrict: 'E',
template: ['<div class="video-overlay" ng-show="vm.overlay">',
'<p>{{ vm.campaign.name}}</p>',
'<img class="start" src="play.png">',
'</div>',
'<video class="video1" controls ng-src="{{ vm.item.video_mp4_url | trusted }}" type="video/mp4"></source>',
'</video>' ].join(''),
link: function(scope, element, attrs) {
scope.video = angular.element(document.getElementsByClassName("video1")[0]);
scope.startbutton = angular.element(document.getElementsByClassName("start")[0]);
scope.startbutton.on('click', function() {
scope.vm.overlay = false;
scope.video[0].play();
});
scope.video.on('click', function() {
scope.video[0].pause();
scope.vm.overlay = true;
});
}
};
}
})();
From my personal experience angular expression evaluation does not work as javascript. so try ng-show="vm.overlay==true".
Furthermore you bind click using native javascript.
Either don't do that and use ng-click or call scope.$apply() in the click event t callbackas last intruction (even though i'm not sure if it's really important).

Initializing an Angular Directive in JavaScript

I have a directive in my template. It's working great:
<ul class="activity-stream">
<my-activity-stream-item ng-repeat="activity in vm.activities" activity="activity"></my-activity-stream-item>
</ul>
I'd basically like to include the same HTML in that template as a popup in a Leaflet Map, but I have no idea how to create that in code. Here's what I tried:
for (i = 0; i < activities.length; i++) {
var activity = activities[i];
var marker = L.marker([activity.location.lat, activity.location.lng]);
marker.type = activity.type;
marker.bindPopup( '<my-activity-stream-item activity="activity"></my-activity-stream-item>' );
marker.addTo( map );
}
I didn't really expect that to work, I feel like I have to pass the scope in somehow... but I'm at a complete loss as to how to do it.
var app = angular.module('myPortal');
app.factory('TemplateService', TemplateService);
app.directive('myActivityStreamItem', myActivityStreamItem);
function myActivityStreamItem( $compile, TemplateService ) {
return {
restrict: 'E',
link: linker,
transclude: true,
scope: {
activity: '='
}
};
function linker(scope, element, attrs) {
scope.rootDirectory = 'images/';
TemplateService.getTemplate( 'activity-' + scope.activity.type ).then(function(response) {
element.html( response.data );
$compile(element.contents())(scope);
});
}
}
function TemplateService( $http ) {
return {
getTemplate: getTemplate
};
function getTemplate( templateName ) {
return $http.get('/templates/' + templateName + '.html');
}
}
(Note - I've only been using Angular for about a week, so please let me know if you think I've done this completely wrong)
EDIT: I took Chandermani's advice and switched my directive to an ngInclude:
<ul class="activity-stream">
<li ng-repeat="activity in vm.activities" ng-include="'/templates/activity-' + activity.type + '.html'"></li>
</ul>
This works great! I also tried to use Josh's advice to compile the HTML in JavaScript, however I'm not quite there...
var link = $compile('<li ng-include="\'/templates/activity-' + activity.type + '.html\'"></li>');
var newScope = $rootScope.$new();
newScope.activity = activity;
var html = link( newScope );
marker.bindPopup( html[0] );
This results in the popup appearing, but the HTML contained within the popup is a comment: <!-- ngInclude: '/templates/activity-incident.html' -->
Do I have to pass it the activity in the li somehow?
Edit 2: Got it! As noted in Issue #4505, you need to wrap the snippet in something, so I wrapped my ngInclude in a div:
var link = $compile( '<div><ng-include src="\'/templates/activity-incident.html\'"></ng-include></div>' );
Not sure i have understood your problem, but what you can do is to use ng-include directive and it can take a template expression to dynamically load a template. Something like:
<ul class="activity-stream">
<li ng-repeat="activity in vm.activities" ng-include="'/templates/activity-' + activity.type + '.html'"></li>
</ul>
You may not require a directive here.
Anytime you want to add raw HTML to the page and have Angular process it, you need to use the $compile service.
Calling $compile on a template will return a linking function which can then be used to bind a scope object to.
var link = $compile('<span>{{someObj}}</span>');
Linking that function to a scope object will result in an element that can then be appended into the DOM.
//Or the scope provided by a directive, etc...
var newScope = $rootScope.$new();
var elem = link(newScope);
//Could also be the element provided by directive
$('someSelector').append(elem);
That's the basic flow you need to be able to tell Angular to process your DOM element. Usually this is done via a directive, and that's probably what you need in this case as well.

Angular.js $compile returns array of html but not actual html

I have the following code:
app.directive('mySample', function($compile) {
return {
//template:"<input type='text' ng=model='sampleData'/> {{sampleData}} <br/>"
link: function(scope, element, atts, controller) {
var markup = "<input type='text' ng=model='sampleData'/> {{sampleData}} <br/>";
angular.element(element).html($compile(markup)(scope));
console.log($compile(markup)(scope));
}
};
});
And I would expect it to generate an input, some span that's coupled via the scope and a break. However I get this output:
[[object HTMLInputElement], [object HTMLSpanElement], [object HTMLBRElement]]
I also tried the template, in comment here, separately and then commenting out the link part. That generates the input and break elements but not the span that shows the coupled model input sampleData.
I have a non-working sample at http://jsfiddle.net/KvdM/nwbsT/ that demonstrates it.
Try this:
element.html(markup);
$compile(element.contents())(scope);
Running the function returned by the $compile service gives you DOM elements rather than html.
So you need to insert them into your page using append (or equivalent):
angular.element(element).append($compile(markup)(scope));
Maybe the easiest way is to use a hard-coded template rather than a dynamic generated one
<div ng-app="myApp">
<my-sample sample-data="'test'"></my-sample>
</div>
var app = angular.module('myApp', []);
app.directive('mySample', function ($compile) {
return {
restrict: 'E',
scope: {
sampleData: '=sampleData'
},
template: "<input type='text'/> {{sampleData}} <br/>",
};
});
FIDDLE
Depends on what kind of data should to be compiled, some times Angular returns a comment node type.
The relevant node that we want to use is the next() (its first sibling).
var tpl = '<div class="myWidget" ng-include="'templates/myWidget.html'"></div>;
var _myWidget = $compile(tpl)(scope);
var myWidget = null;
scope.$on('$includeContentLoaded', function () {
myWidget = _myWidget.next();
});
You just needed to add the jquery to use ".html" and fixed the naming of ng-model

Resources