ngRepeat error when removing child directive from parent directive - angularjs

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

Related

Render a custom directive when loaded dynamically from controller

I have three custom directive, say <sample-grid></sample-grid> , <sample-form></sample-form> and <sample-view></sample-view>. These are defined in the following way:
angular.module("MyApp").directive('sampleGrid', [ function() {
return {
scope : {},
restrict: 'EA',
templateUrl : 'view/templates/sample-grid.tmpl.html',
controller : 'SampleGridCtrl',
scope: {
scnid: '=',
}
};
} ]);
I have a main view where I am using <md-tabs> from angular material in following way -
<md-tabs>
<md-tab ng-repeat="tab in myCtrl.pageTabs track by $index">
<md-tab-label>{{tab.tabName}}</md-tab-label>
<md-tab-body>
<div id="tab.tabName" ng-bind-html="tab.tabHTML"></div>
</md-tab-body>
</md-tab>
</md-tabs>
also the controller for this view defined as
angular.module("MyApp").controller("myCtrl",['$log','$scope','$sce', '$compile', function myCtrl ($log,$scope,$sce,$compile) {
var homeScope = this;
homeScope.pageTabs = [];
var newTab = {};
newTab.tabName = "Sample";
newTab.tabHTML = $sce.trustAsHtml("<sample-grid></sample-grid>");
newTab.tabAction = "grid";
homeScope.pageTabs.push(newTab);
var template = angular.element(document.querySelector('#Sample'));
$compile(template)(homeScope);
}])
The requirement is: user will be able to add new tabs in view, and I have to show one of the three custom directives in the view based on the selection. So I have to just push new tab objects into homeScope.pageTabs with the required property.
The problem is : After pushing the tab object, new tab is created but the custom directive does not render into view. I can see it in the console like :
<div id="tab.tabName" ng-bind-html="tab.tabHTML">
<sample-grid></sample-grid>
</div>
I have seen answers here such that I have to use $compile to render it correctly. I tried that, But I could not get the proper solution.
So my question is how I can achieve this functionality? And, is there any other easy and possible way to achieve this?
I have changed your controller logic a little bit. You have to use compile a little bit different.
angular.module("MyApp").controller("myCtrl",['$log','$scope','$sce', '$compile', function myCtrl ($log,$scope,$sce,$compile) {
var homeScope = this;
homeScope.pageTabs = [];
var template = $sce.trustAsHtml("<sample-grid></sample-grid>");
var linkFn = $compile(template);
var content = linkFn(scope);
var newTab = {};
newTab.tabName = "Sample";
newTab.tabHTML = content;
newTab.tabAction = "grid";
homeScope.pageTabs.push(newTab);
}])

Dynamically loaded AngularJs code via ASP MVC are not bound

I'm loading a Partial view which contains AngularJS code, using the code below:
http.post("/Admin/LoadPartial", {
path: "~/Views/Admin/Urchin/Modals/_ChangeHero.cshtml",
model: self.newID
}).then(function (res) {
//res.data is the .cshtml
var element = angular.element(res.data);
var modal = $compile(element)(scope);
self.newSlides.push({
"ID": self.newID,
"Data": self.newData,
"Modal": modal.html()
});
scope.$emit("ngRepeatFinished");
Notify.Show("Saul goodman", "notice");});
This is how I render the partial:
<div ng-repeat="item in h.newSlides"
ng-bind-html="item.Modal | to_trusted" id="Hey-{{item.ID}}"></div>
And the filter:
.filter('to_trusted', ['$sce', function ($sce) {
return function (text) {
return $sce.trustAsHtml(text);
};
}])
The problem:
The rendered partial loads as HTML, but it contains code like this:
<button id="bgProg-#Model" class="progress-button" ng-click="h.EditBG()">
where h is the controller that loaded the .cshtml, and no click event is bound to the button.
Any ideas as to where I'm doing things wrong are greatly appreciated.
Progress
Thank you #user1620220 for the response.
I added this right after Notify.Show(.. :
timeout(function () {
var what = document.getElementById("Hey-" + self.newID);
var element = angular.element(what);
var modal = $compile(element)(scope);
what.innerHTML = content;}, 0, false);
and still no bindings are happening.
You are using $compile to generate a compiled node, but then you are calling html() to convert the node back to a string. ng-bind-html then converts the string into an uncompiled node and adds it to the DOM.
Instead, just pass res.data to ng-bind-html, allow the digest loop to run, then compile the in-situ DOM node.
Edit: After reading your edit, it occurred to me you need to use the cloneAttachFn returned by $compile. Here is my new proposed solution:
HTML:
<div ng-repeat="item in h.newSlides">
<div id="Hey-{{item.ID}}"><!--place holder--></div>
</div>
JS:
var linkFn = $compile(res.data);
timeout(function () {
self.newSlides.forEach((slide) => {
var what = document.getElementById("Hey-" + slide.ID);
linkFn(scope,(clone) => {
what.parentNode.replaceChild(clone, what);
});
});
}, 0, false);

Modifying styles of a compiled html element in 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.

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.

AngularJS: ng-repeat list is not updated when a model element is spliced from the model array

I have two controllers and share data between them with an app.factory function.
The first controller adds a widget in the model array (pluginsDisplayed) when a link is clicked. The widget is pushed into the array and this change is reflected into the view (that uses ng-repeat to show the array content):
<div ng-repeat="pluginD in pluginsDisplayed">
<div k2plugin pluginname="{{pluginD.name}}" pluginid="{{pluginD.id}}"></div>
</div>
The widget is built upon three directives, k2plugin, remove and resize. The remove directive adds a span to the template of the k2plugin directive. When said span is clicked, the right element into the shared array is deleted with Array.splice(). The shared array is correctly updated, but the change is not reflected in the view. However, when another element is added, after the remove, the view is refreshed correctly and the previously-deleted element is not shown.
What am I getting wrong? Could you explain me why this doesn't work?
Is there a better way to do what I'm trying to do with AngularJS?
This is my index.html:
<!doctype html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.5/angular.min.js">
</script>
<script src="main.js"></script>
</head>
<body>
<div ng-app="livePlugins">
<div ng-controller="pluginlistctrl">
<span>Add one of {{pluginList.length}} plugins</span>
<li ng-repeat="plugin in pluginList">
<span>{{plugin.name}}</span>
</li>
</div>
<div ng-controller="k2ctrl">
<div ng-repeat="pluginD in pluginsDisplayed">
<div k2plugin pluginname="{{pluginD.name}}" pluginid="{{pluginD.id}}"></div>
</div>
</div>
</div>
</body>
</html>
This is my main.js:
var app = angular.module ("livePlugins",[]);
app.factory('Data', function () {
return {pluginsDisplayed: []};
});
app.controller ("pluginlistctrl", function ($scope, Data) {
$scope.pluginList = [{name: "plugin1"}, {name:"plugin2"}, {name:"plugin3"}];
$scope.add = function () {
console.log ("Called add on", this.plugin.name, this.pluginList);
var newPlugin = {};
newPlugin.id = this.plugin.name + '_' + (new Date()).getTime();
newPlugin.name = this.plugin.name;
Data.pluginsDisplayed.push (newPlugin);
}
})
app.controller ("k2ctrl", function ($scope, Data) {
$scope.pluginsDisplayed = Data.pluginsDisplayed;
$scope.remove = function (element) {
console.log ("Called remove on ", this.pluginid, element);
var len = $scope.pluginsDisplayed.length;
var index = -1;
// Find the element in the array
for (var i = 0; i < len; i += 1) {
if ($scope.pluginsDisplayed[i].id === this.pluginid) {
index = i;
break;
}
}
// Remove the element
if (index !== -1) {
console.log ("removing the element from the array, index: ", index);
$scope.pluginsDisplayed.splice(index,1);
}
}
$scope.resize = function () {
console.log ("Called resize on ", this.pluginid);
}
})
app.directive("k2plugin", function () {
return {
restrict: "A",
scope: true,
link: function (scope, elements, attrs) {
console.log ("creating plugin");
// This won't work immediately. Attribute pluginname will be undefined
// as soon as this is called.
scope.pluginname = "Loading...";
scope.pluginid = attrs.pluginid;
// Observe changes to interpolated attribute
attrs.$observe('pluginname', function(value) {
console.log('pluginname has changed value to ' + value);
scope.pluginname = attrs.pluginname;
});
// Observe changes to interpolated attribute
attrs.$observe('pluginid', function(value) {
console.log('pluginid has changed value to ' + value);
scope.pluginid = attrs.pluginid;
});
},
template: "<div>{{pluginname}} <span resize>_</span> <span remove>X</span>" +
"<div>Plugin DIV</div>" +
"</div>",
replace: true
};
});
app.directive("remove", function () {
return function (scope, element, attrs) {
element.bind ("mousedown", function () {
scope.remove(element);
})
};
});
app.directive("resize", function () {
return function (scope, element, attrs) {
element.bind ("mousedown", function () {
scope.resize(element);
})
};
});
Whenever you do some form of operation outside of AngularJS, such as doing an Ajax call with jQuery, or binding an event to an element like you have here you need to let AngularJS know to update itself. Here is the code change you need to do:
app.directive("remove", function () {
return function (scope, element, attrs) {
element.bind ("mousedown", function () {
scope.remove(element);
scope.$apply();
})
};
});
app.directive("resize", function () {
return function (scope, element, attrs) {
element.bind ("mousedown", function () {
scope.resize(element);
scope.$apply();
})
};
});
Here is the documentation on it: https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$apply
If you add a $scope.$apply(); right after $scope.pluginsDisplayed.splice(index,1); then it works.
I am not sure why this is happening, but basically when AngularJS doesn't know that the $scope has changed, it requires to call $apply manually. I am also new to AngularJS so cannot explain this better. I need too look more into it.
I found this awesome article that explains it quite properly.
Note: I think it might be better to use ng-click (docs) rather than binding to "mousedown". I wrote a simple app here (http://avinash.me/losh, source http://github.com/hardfire/losh) based on AngularJS. It is not very clean, but it might be of help.
I had the same issue. The problem was because 'ng-controller' was defined twice (in routing and also in the HTML).
Remove "track by index" from the ng-repeat and it would refresh the DOM
There's an easy way to do that. Very easy. Since I noticed that
$scope.yourModel = [];
removes all $scope.yourModel array list you can do like this
function deleteAnObjectByKey(objects, key) {
var clonedObjects = Object.assign({}, objects);
for (var x in clonedObjects)
if (clonedObjects.hasOwnProperty(x))
if (clonedObjects[x].id == key)
delete clonedObjects[x];
$scope.yourModel = clonedObjects;
}
The $scope.yourModel will be updated with the clonedObjects.
Hope that helps.

Resources