Strange behaviour when adding directive dynamically in ngrepeat - angularjs

I am trying to add a directive dynamically inside ngrepeat. Please refer to the following fiddle link: Fiddle Link
Code:
// Code goes here
var app = angular.module('myApp',[]);
// myDir Directive
app.directive('myDir', function() {
var controller = ['$scope','$compile', function ($scope,$compile) {
$scope.names=[{id:'1',directive:'subDir1'},{id:'2',directive:'subDir2'}];
$scope.loadDynamicDir = function(id, directive) {
var newScope = $scope.$new(true);
var html = $compile('<div class="' + directive + '"></div>')(newScope);
angular.element(document.getElementById('div' + id)).append(html);
}
}]
return {
controller:controller,
templateUrl:'myDirTemplate.html'
}
})
// subDir1 Directive
app.directive('subDir1', function() {
return {
restrict:'C',
template: 'subDir1'
}
});
// subDir2 Directive
app.directive('subDir2', function() {
return {
restrict:'C',
template: 'subDir2'
}
});
Unfortunatley, each directive is added 3 times. Can anyone explain the exact behaviour?

It's fairly simple. EVERY function added to template will run at least twice as $digest cycle runs twice - first after model change, and second to check if first cycle changed any model
so your function loadDynamicDir will run everytime there is $digest and since you APPEND the html everytime it creates more nodes
I've added button to your plunker showing the behaviour
https://plnkr.co/edit/4hpVDOPJm2BMzSwRcm5N?p=preview
<body ng-app="myApp">
<my-dir></my-dir>
<button ng-click="$digest()">do digest</button>
</body>

Related

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

AngularJS Directive doesn't update scope for callback

I am using directives to create a component library in AngularJS 1.5. Hence, my directives need to have isolate scopes.
Some of my directives have callbacks so you can pass in a function to get invoked by the directive. However, when that callback is invoked by the directive, it doesn't seem like the changes to $scope attributes are fully updated like I would expect them to be.
Here is a Plunker that shows this behavior:
http://embed.plnkr.co/Rg15FHtHgCDExxOYNwNa/
Here is what the code looks like:
<script>
var app = angular.module('myApp', []);
app.controller('Controller', ['$scope',function($scope) {
// initialize the value to something obvious
$scope.clickersValue = "BEFORE";
// when this call back is called we would expect the value to be updated by updated by the directive
$scope.clickersCallback = function() {
//$scope.$apply(); // $apply is not allowed here
$scope.clickersValueRightAfterCall = $scope.clickersValue;
console.log("clickersCallback: scope.clickersValue", $scope.clickersValue);
};
}
]);
app.directive('clicker', [function() {
return {
restrict: 'EA',
template: '<div ng-click="clicked()">click me!</div>',
controller: ['$scope', function($scope) {
$scope.clicked = function() {
console.log("you clicked me.");
$scope.newValue = 'VALID';
$scope.myUpdate();
}
}],
scope: {
"newValue": "=",
"myUpdate": "&"
}
};
}]);
</script>
So when clickersCallback gets invoked the clickersValue attribute still has the old value. I have tried using $scope.$apply but of course it isn't allowed when another update is happening. I also tried using controller_bind but got the same effect.
Wrap the code inside clickersCallback function in a $timeout function.
$timeout(function() {
$scope.clickersValueRightAfterCall = $scope.clickersValue;
console.log("clickersCallback: scope.clickersValue", $scope.clickersValue);
});
Updated plunker
The $timeout does not generate error like „$digest already in progress“ because $timeout tells Angular that after the current cycle, there is a timeout waiting and this way it ensures that there will not any collisions between digest cycles and thus output of $timeout will execute on a new $digest cycle.
source
Edit 1: As the OP said below, the user of the directive should not have to write any "special" code in his callback function.
To achieve this behavior I changed the $timeout from de controller to the directive.
Controller callback function (without changes):
$scope.clickersCallback = function() {
$scope.clickersValueRightAfterCall = $scope.clickersValue;
console.log("clickersCallback: scope.clickersValue", $scope.clickersValue);
};
Directive code (inject $timeout in the directive):
$scope.clicked = function() {
console.log("you clicked me.");
$scope.newValue = 'VALID';
$timeout(function() {
$scope.myUpdate();
});
}
Updated plunker

update angularjs model from jquery [duplicate]

I have this simple scenario:
Input element which value is changed by jQuery's val() method.
I am trying to update the angular model with the value that jQuery set. I tried to write a simple directive, but it's not doing what I want.
Here's the directive:
var myApp = angular.module('myApp', []);
myApp.directive('testChange', function() {
return function(scope, element, attrs) {
element.bind('change', function() {
console.log('value changed');
})
}
})
this is the jQuery part:
$(function(){
$('button').click(function(){
$('input').val('xxx');
})
})
and html:
<div ng-app="myApp">
<div ng-controller="MyCtrl">
<input test-change ng-model="foo" />
<span>{{foo}}</span>
</div>
</div>
<button>clickme</button>
Here is the fiddle with my try:
http://jsfiddle.net/U3pVM/743/
Can someone please point me in the right direction?
ngModel listens for "input" event, so to "fix" your code you'd need to trigger that event after setting the value:
$('button').click(function(){
var input = $('input');
input.val('xxx');
input.trigger('input'); // Use for Chrome/Firefox/Edge
input.trigger('change'); // Use for Chrome/Firefox/Edge + IE11
});
For the explanation of this particular behaviour check out this answer that I gave a while ago: "How does AngularJS internally catch events like 'onclick', 'onchange'?"
But unfortunately, this is not the only problem you have. As pointed out with other post comments, your jQuery-centric approach is plain wrong. For more info take a look at this post: How do I “think in AngularJS” if I have a jQuery background?).
Hope this is useful for someone.
I was unable to get the jQuery('#myInputElement').trigger('input') event to be picked up my angular app.
I was however, able to get angular.element(jQuery('#myInputElement')).triggerHandler('input') to be picked up.
The accepted answer which was triggering input event with jQuery didn't work for me. Creating an event and dispatching with native JavaScript did the trick.
$("input")[0].dispatchEvent(new Event("input", { bubbles: true }));
I don't think jQuery is required here.
You can use $watch and ng-click instead
<div ng-app="myApp">
<div ng-controller="MyCtrl">
<input test-change ng-model="foo" />
<span>{{foo}}</span>
<button ng-click=" foo= 'xxx' ">click me</button>
<!-- this changes foo value, you can also call a function from your controller -->
</div>
</div>
In your controller :
$scope.$watch('foo', function(newValue, oldValue) {
console.log(newValue);
console.log(oldValue);
});
You have to use the following code in order to update the scope of the specific input model as follows
$('button').on('click', function(){
var newVal = $(this).data('val');
$('select').val(newVal).change();
var scope = angular.element($("select")).scope();
scope.$apply(function(){
scope.selectValue = newVal;
});
});
I made modifications on only controller initialization by adding listener on action button:
$(document).on('click', '#action-button', function () {
$timeout(function () {
angular.element($('#input')).triggerHandler('input');
});
});
Other solutions did not work in my case.
I know it's a bit late to answer here but maybe I may save some once's day.
I have been dealing with the same problem. A model will not populate once you update the value of input from jQuery. I tried using trigger events but no result.
Here is what I did that may save your day.
Declare a variable within your script tag in HTML.
Like:
<script>
var inputValue="";
// update that variable using your jQuery function with appropriate value, you want...
</script>
Once you did that by using below service of angular.
$window
Now below getData function called from the same controller scope will give you the value you want.
var myApp = angular.module('myApp', []);
app.controller('imageManagerCtrl',['$scope','$window',function($scope,$window) {
$scope.getData = function () {
console.log("Window value " + $window.inputValue);
}}]);
I've written this little plugin for jQuery which will make all calls to .val(value) update the angular element if present:
(function($, ng) {
'use strict';
var $val = $.fn.val; // save original jQuery function
// override jQuery function
$.fn.val = function (value) {
// if getter, just return original
if (!arguments.length) {
return $val.call(this);
}
// get result of original function
var result = $val.call(this, value);
// trigger angular input (this[0] is the DOM object)
ng.element(this[0]).triggerHandler('input');
// return the original result
return result;
}
})(window.jQuery, window.angular);
Just pop this script in after jQuery and angular.js and val(value) updates should now play nice.
Minified version:
!function(n,t){"use strict";var r=n.fn.val;n.fn.val=function(n){if(!arguments.length)return r.call(this);var e=r.call(this,n);return t.element(this[0]).triggerHandler("input"),e}}(window.jQuery,window.angular);
Example:
// the function
(function($, ng) {
'use strict';
var $val = $.fn.val;
$.fn.val = function (value) {
if (!arguments.length) {
return $val.call(this);
}
var result = $val.call(this, value);
ng.element(this[0]).triggerHandler('input');
return result;
}
})(window.jQuery, window.angular);
(function(ng){
ng.module('example', [])
.controller('ExampleController', function($scope) {
$scope.output = "output";
$scope.change = function() {
$scope.output = "" + $scope.input;
}
});
})(window.angular);
(function($){
$(function() {
var button = $('#button');
if (button.length)
console.log('hello, button');
button.click(function() {
var input = $('#input');
var value = parseInt(input.val());
value = isNaN(value) ? 0 : value;
input.val(value + 1);
});
});
})(window.jQuery);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div ng-app="example" ng-controller="ExampleController">
<input type="number" id="input" ng-model="input" ng-change="change()" />
<span>{{output}}</span>
<button id="button">+</button>
</div>
If you are using IE, you have to use: input.trigger("change");
add .change() after setting the value.
example:('id').val.('value').change();
also don't forget to add onchange or ng-change tag in html
I did this to be able to update the value of ngModel from the outside with Vanilla/jQuery:
function getScope(fieldElement) {
var $scope = angular.element(fieldElement).scope();
var nameScope;
var name = fieldElement.getAttribute('name');
if($scope) {
if($scope.form) {
nameScope = $scope.form[name];
} else if($scope[name]) {
nameScope = $scope[name];
}
}
return nameScope;
}
function setScopeValue(fieldElement, newValue) {
var $scope = getScope(fieldElement);
if($scope) {
$scope.$setViewValue(newValue);
$scope.$validate();
$scope.$render();
}
}
setScopeValue(document.getElementById("fieldId"), "new value");
Not what OP asked, but for any soul that might be as well writing an userscript that goes through input fields and fills the required details. Nothing (fully) worked for me, but finally managed to get it done this way:
var el = $('#sp_formfield_fw_ip');
el.val("some value");
angular.element(el).triggerHandler('focus');
angular.element(el).triggerHandler('input');
angular.element(el).triggerHandler('change');
angular.element(el).triggerHandler('blur');
Open developer tools, and inspect input field for added events. There I found all of them (in my case): focus, input, change and blur.

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