Angular - push back to array after splice - angularjs

I'm using next directive to splice array if image src error.
myApp.directive("noImage", function () {
return {
link: function (scope, element, attrs) {
element.bind("error", function () {
var idx = scope.posts.indexOf(scope.post);
scope.$apply(function () {
scope.posts.splice(idx, 1);
});
});
}
};
});
And trying to push spliced items back with function from controller something like.
$scope.restorePosts = (function() {
return function() {
$scope.spliced_posts.forEach(function(x) {
return $scope.posts.push(x);
});
return $scope.spliced_posts = [];
};
});
The question is what i should add to directive?
scope.spliced_posts.push(scope.post)?
JSFIDDLE

If I'm correct, you want to only show the images that have no "error.jpg" in them.
Removing them and later adding them to your array might not be the best solution here. You can use a filter with your ng-repeat to filter out the images with error.jpg in it.
ng-repeat="post in posts | filter:{ image_url:'!http://example.com/error.jpg'}"
If anywhere else you want to add a class to images that DO have the error in it, you can use ng-class:
ng-class="{'no-img': post.image_url=='http://example.com/error.jpg'}" ng-src="{{post.image_url}}"
http://jsfiddle.net/jkrielaars/xxL0qyy6/3/
That way your array of posts remains intact. If you want to be able to toggle between showing the posts with and without error.jpg, you could toggle the filter instead of changing your posts array.
in your controller
$scope.myFilter = null;
$scope.toggleFilter = function(){
if( $scope.myFilter ){
$scope.myFilter = null;
}else{
$scope.myFilter = { image_url:'!http://example.com/error.jpg' };
}
}
and in the view:
<li ng-repeat="post in posts | filter:myFilter">
UPDATE
If you want to do dynamic checking to see if the image triggers an error, you can use a directive, much like the one you originally used, to add a class, or completely hide the element. I have also updated the JSfiddle. Something like this would add an error class to the parent. You could change this to whatever you like to style your parent. You could also choose to just hide the parent straight from the directive.
myApp.directive("hideNoImage", function () {
return {
link: function (scope, element, attrs) {
element.bind("error", function () {
element.parent().addClass("error");
});
}
};
});

Use angular.copy() to make a copy of the original array that you receive from your api and store in your service

Related

Floating title is not working in Angularjs

I have a list of item with two iterations. I want a sticky title when the title scroll up from the view area. I have done it with jquery, but can't able to do in angular. Created a fiddle https://jsfiddle.net/1vf5ska7/
I just to want to add a class in tag when the title is goes up to the view area.
angular.element(document.querySelector('#l-content__desc__split1__body')).on('scroll', function() {
});
And the important thing is it is not a window scroll. It's a div scroll
Please help me.
Thanks..
You need to include a directive and operate on it. If $window.pageYOffset is greater than the position of the element you apply a specific class to that element which is positioned fixed.
var app = angular.module('app', []);
app.directive('setClassOnTop', function ($window) {
var $win = angular.element($window); // wrap window object as jQuery object
return {
restrict: 'A',
link: function (scope, element, attrs) {
var title = angular.element(document.getElementById('sticky-title'));
var offsetTop = title[0].offsetTop;
$win.on('scroll', function (e) {
if ($window.pageYOffset > offsetTop) {
angular.element(title[0]).addClass('floating-title');
} else {
angular.element(title[0]).removeClass('floating-title');
}
});
}
};
});
And here is the updated fiddle:
https://jsfiddle.net/1vf5ska7/3/

select2, ng-model and angular

Using jquery-select2 (not ui-select) and angular, I'm trying to set the value to the ng-model.
I've tried using $watch and ng-change, but none seem to fire after selecting an item with select2.
Unfortunately, I am using a purchased template and cannot use angular-ui.
HTML:
<input type="hidden" class="form-control select2remote input-medium"
ng-model="contact.person.id"
value="{{ contact.person.id }}"
data-display-value="{{ contact.person.name }}"
data-remote-search-url="api_post_person_search"
data-remote-load-url="api_get_person"
ng-change="updatePerson(contact, contact.person)">
ClientController:
$scope.updatePerson = function (contact, person) {
console.log('ng change');
console.log(contact);
console.log(person);
} // not firing
$scope.$watch("client", function () {
console.log($scope.client);
}, true); // not firing either
JQuery integration:
var handleSelect2RemoteSelection = function () {
if ($().select2) {
var $elements = $('input[type=hidden].select2remote');
$elements.each(function(){
var $this = $(this);
if ($this.data('remote-search-url') && $this.data('remote-load-url')) {
$this.select2({
placeholder: "Select",
allowClear: true,
minimumInputLength: 1,
ajax: { // instead of writing the function to execute the request we use Select2's convenient helper
url: Routing.generate($this.data('remote-search-url'), {'_format': 'json'}),
type: 'post',
dataType: 'json',
delay: 250,
data: function (term, page) {
return {
query: term, // search term
};
},
results: function (data, page) { // parse the results into the format expected by Select2.
return {
results: $.map(data, function (datum) {
var result = {
'id': datum.id,
'text': datum.name
};
for (var prop in datum) {
if (datum.hasOwnProperty(prop)) {
result['data-' + prop] = datum[prop];
}
}
return result;
})
}
}
},
initSelection: function (element, callback) {
// the input tag has a value attribute preloaded that points to a preselected movie's id
// this function resolves that id attribute to an object that select2 can render
// using its formatResult renderer - that way the movie name is shown preselected
var id = $(element).val(),
displayValue = $(element).data('display-value');
if (id && id !== "") {
if (displayValue && displayValue !== "") {
callback({'id': $(element).val(), 'text': $(element).data('display-value')});
} else {
$.ajax(Routing.generate($this.data('remote-load-url'), {'id': id, '_format': 'json'}), {
dataType: "json"
}).done(function (data) {
callback({'id': data.id, 'text': data.name});
});
}
}
},
});
}
});
}
};
Any advice would be greatly appreciated! :)
UPDATE
I've managed to put together a plunk which seems to similarly reproduce the problem - it now appears as if the ng-watch and the $watch events are fired only when first changing the value.
Nevertheless, in my code (and when adding further complexity like dynamically adding and removing from the collection), it doesn't even seem to fire once.
Again, pointers in the right direction (or in any direction really) would be greatly appreciated!
There are a number of issues with your example. I'm not sure I am going to be able to provide an "answer", but hopefully the following suggestions and explanations will help you out.
First, you are "mixing" jQuery and Angular. In general, this really doesn't work. For example:
In script.js, you run
$(document).ready(function() {
var $elements = $('input[type=hidden].select2remote');
$elements.each(function() {
//...
});
});
This code is going to run once, when the DOM is initially ready. It will select hidden input elements with the select2remote class that are currently in the DOM and initialized the select2 plugin on them.
The problem is that any new input[type=hidden].select2remote elements added after this function is run will not be initialized at all. This would happen if you are loading data asynchronously and populating an ng-repeat, for example.
The fix is to move the select2 initialization code to a directive, and place this directive on each input element. Abridged, this directive might look like:
.directive('select2', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attr, ngModel) {
//$this becomes element
element.select2({
//options removed for clarity
});
element.on('change', function() {
console.log('on change event');
var val = $(this).value;
scope.$apply(function(){
//will cause the ng-model to be updated.
ngModel.setViewValue(val);
});
});
ngModel.$render = function() {
//if this is called, the model was changed outside of select, and we need to set the value
//not sure what the select2 api is, but something like:
element.value = ngModel.$viewValue;
}
}
}
});
I apologize that I'm not familiar enough with select2 to know the API for getting and setting the current value of the control. If you provide that to me in a comment I can modify the example.
Your markup would change to:
<input select2 type="hidden" class="form-control select2remote input-medium"
ng-model="contact.person.id"
value="{{ contact.person.id }}"
data-display-value="{{ contact.person.name }}"
data-remote-search-url="api_post_person_search"
data-remote-load-url="api_get_person"
ng-change="updatePerson(contact, contact.person)">
After implementing this directive, you could remove the entirety of script.js.
In your controller you have the following:
$('.select2remote').on('change', function () {
console.log('change');
var value = $(this).value;
$scope.$apply(function () {
$scope.contact.person.id = value;
});
});
There are two problems here:
First, you are using jQuery in a controller, which you really shouldn't do.
Second, this line of code is going to fire a change event on every element with the select2remote class in the entire application that was in the DOM when the controller was instatiated.
It is likely that elements added by Angular (i.e through ng-repeat) will not have the change listener registered on them because they will be added to the DOM after the controller is instantiated (at the next digest cycle).
Also, elements outside the scope of the controller that have change events will modify the state of the controller's $scope. The solution to this, again, is to move this functionality into the directive and rely on ng-model functionality.
Remember that anytime you leave Angular's context (i.e if you are using jQuery's $.ajax functionality), you have to use scope.$apply() to reenter Angular's execution context.
I hope these suggestions help you out.

AngularJS : directive two way data binding not working

I have a controller with following code snippet,
...
$scope.selected_contents = [];
$scope.$watch('selected_contents', function (sel_contents) {
console.log(sel_contents, 'selected contents');
}, true);
...
a directive,
commonDirectives.directive('chkbox', function() {
return {
restrict: 'A',
require: '?ngModel',
scope : {
item : '=item',
selection_pool: '=selectionPool'
},
link: function(scope, elem, attrs, ngModel) {
console.log('selected contents are', scope.selection_pool);
// watch selection_pool
scope.$watch('selection_pool', function (pool) {
console.log(pool, scope.selection_pool, 'pool updated');
if (_.contains(pool, scope.item)) {
elem.prop('checked', true);
}
else {
elem.prop('checked', false);
}
});
// toggle the selection of this component
var toggle_selection = function () {
if(_.indexOf(scope.selection_pool, scope.item) != -1) {
scope.selection_pool = _.without(scope.selection_pool , scope.item);
}
else {
scope.selection_pool.push(scope.item);
}
};
elem.on('click', toggle_selection);
}
};
});
and a template which uses the directive,
<tr ng-repeat="content in contents">
<td><input type="checkbox" selection_pool="selected_contents" item="content" chkbox></td>
</tr>
The problem is, changes in selection_pool in the directive is not reflected to selected_contents in the controller. What am i missing?
Update 1:
Following the suggestion from #mohamedrias I wrapped the changes in scope with scope.$apply. Doing so updates selected_contents in controller only while adding the content but not while removing it.
...
// toggle the selection of this component
var toggle_selection = function () {
if(_.indexOf(scope.selection_pool, scope.item) != -1) {
scope.$apply(function () {
scope.selection_pool = _.without(scope.selection_pool , scope.item);
});
}
else {
scope.$apply(function () {
scope.selection_pool.push(scope.item);
});
}
};
...
Angular uses name-with-dashes for attribute names and camelCase for
the corresponding directive name
From here.
The variable should be changed from this selection_pool:
<input type="checkbox" selection_pool="selected_contents" item="content" chkbox>
to selection-pool:
<input type="checkbox" selection-pool="selected_contents" item="content" chkbox>
And this selectionPool into the directive:
scope : {
item : '=item',
selectionPool: '=selectionPool'
}
EDIT: Because the selectionPool is an array, you should use $watchCollection:
scope.$watchCollection('selectionPool', function (pool)
And when you add/remove values from the array in toggle_selection function, should be wrapped within the $timeout function:
$timeout(function () {
if (_.indexOf(scope.selectionPool, scope.item) != -1) {
scope.selectionPool = _.without(scope.selectionPool, scope.item);
} else {
scope.selectionPool.push(scope.item);
}
});
This is to assure that a digest cycle is going to be applied afterwards.
Here's the code working on a jsfiddle: http://jsfiddle.net/0rvcguz0/3/
After researching for entire day, I ended up here. If someone is having any trouble with Angularjs scope, I highly encourage to read it.
The proper solution in my case was to wrap selected_contents by an object. e.g.
$scope.selected = {};
$scope.selected.contents = [];
Then in the template replace selcted_contents with selected.contents.
But still what I don't understand is, [] or an Array is also an object. My earlier code should have worked according to the information I found in the wiki. If anyone could explain me why I would really appreciate it :).

Angular.js ng-repeat not updating the dom elements when i destroy the scope object

I'm trying to delete elements in the ng-repeat from the controller for when a user scrolls down the screen to limit the amount of dom elements in view.
Heres what i have tried
$scope.someStuff = someobject..
delete $scope.friendsViewObject[SomeIndex];
And no success. If I delete the elements in the object alone the dom won't update.
And this is the directive that would call the controller.
<div myDirective="loadMyController()" ></div>
.directive('myDirective', function() {
return function(scope, elm, attr) {
var raw = elm[0];
elm.bind('scroll', function() {
if (((raw.scrollHeight - raw.offsetHeight) - raw.scrollTop) < 10 ) {
scope.$apply(attr.friendsWhenScrolled);
}
});
}
});
Instead of $apply on the attribute, when you change the DOM and you want angular to know about it, wrap the change in $apply.
So, your code should look something like this:
.directive('myDirective', function() {
return function(scope, elm, attr) {
var raw = elm[0];
elm.bind('scroll', function() {
if (((raw.scrollHeight - raw.offsetHeight) - raw.scrollTop) < 10 ) {
scope.$apply(function() {
// delete here
});
}
});
}
});
OK i discovered the problem..
I think this might be an angular.js bug with the ng-repeat..
When i destroy elements in the scopes array the actual data is removed from view BUT the dom elements are still there.. It looks like the ng-repeat goes off array.length property without any pre checking.. so it gets all the array counting wrong.. So i had to use the method shift() to re-arrange all the elements..
Solution:
Change this
delete $scope.friendsViewObject[SomeIndex];
To This
$scope.friendsViewObject.shift()

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