Angularjs binding array splice - arrays

I have an issue with splicing an array:
I m displaying my array like this :
Email:<input type="text" ng-model="newcontact"/>
<button ng-click="add(newcontact)">Add</button>
<h2>Contacts</h2>
<ul id="todo-list">
<li ng-repeat="contact in contacts">
{{contact.libelle}}
del
</li>
</ul>
My controller is :
angular.module('angulartestApp')
.controller('MainCtrl', function ($scope,$log) {
var $scope.contacts = [{libelle:'test'},{libelle:'test2'}];
$scope.add = function(newcontact) {
$scope.contacts.push({libelle:newcontact});
$scope.newcontact = '';
};
$scope.del = function (idx){
$scope.contacts.splice(idx, 1);
for (var i=0;i<$scope.contacts.length;i++)
{
$log.info($scope.contacts[i].libelle+',');
}
};
});
If I add two items ('a' and 'b'), my view displays the good items in my list ('test', 'test2', 'a','b').
If I delete the 'a' item, my view displays only the two initial items of my list ('test', 'test2').
But in the console everything is good, 'test', 'test2' and 'b' gets well displayed.
I don't understand why. If someone can show me the good way to do it...
Thank you.

Is the page being reloaded when you click the del link? Try using a button instead of to verify. This may also help:
href causes unintended page reload with Angularjs and Twitter Bootstrap

Related

Change vm variable value after clicking anywhere apart from a specific element

When I click anywhere in the page apart from ul element (where countries are listed) and the suggestion-text input element (where I type country name), vm.suggested in controller should be set to null. As a result ul element will be closed automatically. How can I do this?
I've seen Click everywhere but here event and AngularJS dropdown directive hide when clicking outside where custom directive is discussed but I couldn't work out how to adapt it to my example.
Markup
<div>
<div id="suggestion-cover">
<input id="suggestion-text" type="text" ng-model="vm.countryName" ng-change="vm.countryNameChanged()">
<ul id="suggest" ng-if="vm.suggested">
<li ng-repeat="country in vm.suggested" ng-click="vm.select(country)">{{ country }}</li>
</ul>
</div>
<table class="table table-hover">
<tr>
<th>Teams</th>
</tr>
<tr ng-if="vm.teams">
<td><div ng-repeat="team in vm.teams">{{ team }}</div></td>
</tr>
</table>
<!-- There are many more elements here onwards -->
</div>
Controller
'use strict';
angular
.module('myApp')
.controller('readController', readController);
function readController() {
var vm = this;
vm.countryNameChanged = countryNameChanged;
vm.select = select;
vm.teams = {.....};
vm.countryName = null;
vm.suggested = null;
function countryNameChanged() {
// I have a logic here
}
function select(country) {
// I have a logic here
}
}
I solved the issue by calling controller function from within the directive so when user clicks outside (anywhere in the page) of the element, controller function gets triggered by directive.
View
<ul ng-if="vm.suggested" close-suggestion="vm.closeSuggestion()">
Controller
function closeSuggestion() {
vm.suggested = null;
}
Directive
angular.module('myApp').directive('closeSuggestion', [
'$document',
function (
$document
) {
return {
restrict: 'A',
scope: {
closeSuggestion: '&'
},
link: function (scope, element, attributes) {
$document.on('click', function (e) {
if (element !== e.target && !element[0].contains(e.target)) {
scope.$apply(function () {
scope.closeSuggestion();
});
}
});
}
}
}
]);
This is just an example but you can simply put ng-click on body that will reset your list to undefined.
Here's example:
http://plnkr.co/edit/iSw4Fqqg4VoUCSJ00tX4?p=preview
You will need on li elements:
$event.stopPropagation();
so your html:
<li ng-repeat="country in vm.suggested" ng-click="vm.select(country); $event.stopPropagation()">{{ country }}</li>
and your body tag:
<body ng-app="myWebApp" ng-controller="Controller01 as vm" ng-click="vm.suggested=undefined;">
UPDATE:
As I said it's only an example, you could potentially put it on body and then capture click there, and broadcast 'closeEvent' event throughout the app. You could then listen on your controller for that event - and close all. That would be one way to work around your problem, and I find it pretty decent solution.
Updated plunker showing communication between 2 controllers here:
http://plnkr.co/edit/iSw4Fqqg4VoUCSJ00tX4?p=preview
LAST UPDATE:
Ok, last try - create a directive or just a div doesn't really matter, and put it as an overlay when <li> elements are open, and on click close it down. Currently it's invisible - you can put some background color to visualize it.
Updated plunker:
http://plnkr.co/edit/iSw4Fqqg4VoUCSJ00tX4?p=preview
And finally totally different approach
After some giving it some thought I actually saw that we're looking at problem from the totally wrong perspective so final and in my opinion best solution for this problem would be to use ng-blur and put small timeout on function just enough so click is taken in case someone chose country:
on controller:
this.close = function () {
$timeout(()=>{
this.suggested = undefined;
}, 200);
}
on html:
<input id="suggestion-text" type="text" ng-model="vm.countryName" ng-change="vm.countryNameChanged()" ng-blur="vm.close()">
This way you won't have to do it jQuery way (your solution) which I was actually trying to avoid in all of my previous solutions.
Here is plnker: http://plnkr.co/edit/w5ETNCYsTHySyMW46WvO?p=preview

ng-repeat only updating after clicking another tab or typing in a textbox

I'm pretty new to angular and have been having a bit of a problem in trying to create a basic "to-do" list sort of app.
There are various categories in the sidebar, and the user can click a button that brings up a modal prompting the user for the name of a new category. This name is used to create a new category, which is pushed onto the preexisting array.
However, the new category is only appearing after I start typing in another text-box on the screen or click on another tab.
The code that should be relevant:
var list = this;
$(document).on("click", ".prompt", function(e) {
bootbox.prompt("What do you want your new category to be?", function(result) {
if(result !== null) {
list.addCategory(result);
}
});
});
this.addCategory = function(result) {
if(result.trim() != "") {
var newCategory = new Category(result);
list.categories.push(newCategory);
this.setCategory(newCategory);
}
};
I can't seem to figure out how to post HTML as a code block, but here's the directives used to list out the categories (with categoryCtrl being the controller in question): ng-class="{active: categoryCtrl.isSet(category) }" ng-repeat="category in categoryCtrl.categories" ng-init="categoryCtrl.currCategory = categoryCtrl.categories[0]"
I know that the array is being updated immediately - if I add an alert 'bootbox.alert(list.categories[list.categories.length-1].name)' the alert gives me whatever the input was like it's supposed to. It's just not showing up in the ng-repeat.
Another interesting observations is that the ng-init overrides the this.setCategory(newCategory) - so it seems that when the list does update, it is reverting to its ng-init value.
Other places where I have an ng-repeat of an array, it's updated automatically when something new is pushed onto it. I'm wondering if it may have something to do with the modal/usage of bootbox - everywhere else things are added either by a checkbox or keying something into a textbox on screen, this is the only place where a modal is used.
Here is a working plunker based on your code.
The app looks like below. I initialize an array with dummy data for the example, but an empty array would work too. I used the vm = this syntax similar to what you have. When calling $nbBootbox.prompt it returns a promise so you need to use the then() syntax like below:
var app = angular.module('plunker', ['ngBootbox']);
app.controller('MainCtrl', ['$scope', '$ngBootbox', function($scope, $ngBootbox) {
var vm = this;
vm.name = 'World';
vm.categories = ['Category 1', 'Category 2'];
vm.prompt = function() {
$ngBootbox.prompt('Enter a new category?')
.then(function(result) {
console.log('Prompt returned: ' + result);
vm.categories.push(result);
}, function() {
console.log('Prompt dismissed!');
});
}
}]);
To make your HTML more angular like I changed it to this and also use the ControllerAs syntax:
<body ng-controller="MainCtrl as vm">
<p>Hello {{vm.name}} !</p>
<ul>
<li ng-repeat="c in vm.categories">{{c}}</li>
</ul>
Add Category
</body>
So, the link calls the prompt() function... it opens the modal and if you enter in the category, I push it to the categories array and it is added automatically to the page as a new bullet point in the list of categories.
From the documentation:
$ngBootbox.prompt(msg)
Returns a promise that is resolved when submitted and rejected if dismissed.
Example
$ngBootbox.prompt('Enter something')
.then(function(result) {
console.log('Prompt returned: ' + result);
}, function() {
console.log('Prompt dismissed!');
});
Hope this helps. let us know.

angularjs move element/directive from one list to another without reinitialization

Lets image that we have two lists of elements, where each element itself is a directive.
List 1:
- A
- B
- C
List 2:
- X
- Y
- Z
I want to move element "Y" from list 2 to list 1 without reinitializing the directive. Hence, I want to simply move the contents/directive from list 2 to list 1.
I made a simple example to show what I mean: http://jsfiddle.net/HB7LU/24171/
As you can see from the example, the console prints 'init: Y' again, after moving the element. But I don't want that. I just want to have it moved.
How can I do this without reinitializing the directive?
template.html
<div ng-controller="MyCtrl">
<h2>List 1:</h2>
<ul>
<li ng-repeat="entry in list1">
<span my-directive="entry"></span>
</li>
</ul>
<h2>List 2:</h2>
<ul>
<li ng-repeat="entry in list2">
<span my-directive="entry"></span>
</li>
</ul>
<button ng-click="move()">
Move 'Y' to List 1
</button>
</div>
app.js
var myApp = angular.module('myApp',[]);
myApp.directive('myDirective', function() {
return {
restrict: 'A',
scope: {
myDirective: '='
},
template: '{{myDirective}}',
controller: function($scope) {
console.log('init: ' + $scope.myDirective);
}
};
});
function MyCtrl($scope) {
$scope.list1 = ['A', 'B', 'C'];
$scope.list2 = ['X', 'Y', 'Z'];
$scope.move = function() {
var entries = $scope.list2.splice(1,1);
$scope.list1.push(entries[0]);
};
}
Since your directive is used inside and ng-repeat, every item addition will create a new scope and thus directive controller will be initialized every time. You can however do it the other way around, i.e. bind your list to the directive and have ng-repeat within the template. See the Updated Fiddle. Let me know if that works for you.

AngularFire - Remove Single Item

Here is the relevant code in my view:
p(ng-repeat="t in todos")
input(
type="checkbox",
ng-model="t.done",
ng-click="clearItem($event)"
)
{{t.text}} done? {{t.done}}
When the checkbox is clicked, I want the appropriate object in the todos array to be removed from the database.
My clearItem function is as follows:
$scope.clearItem = function(event) {
todoRef.remove($scope.t);
}
However, this removes all the entries in my database. I want it to remove only the specific object in question. Is there anyway for me to do this?
Ok, figured it out.
When looping using ng-repeat, use (id, t) in todos. This allows you to send id as the parameter to the ng-click function, and $scope.todos.$remove(id) works just fine.
To provide a more complete example for anyone else that lands here, according to Firebase's documentation for AngularFire this would be the preferred way, and I believe the easiest way to remove an object:
// Create an app. Synch a Firebase array inside a controller
var myApp = angular.module("myApp", ["firebase"]);
// inject $firebaseArray
myApp.controller("TodoCtrl", ["$scope", "$firebaseArray", function($scope, $firebaseArray) {
// bind $scope.todos to Firebase database
$scope.todos = $firebaseArray(myFirebaseRef.child("todo"));
// create a destroy function
$scope.removeTodo = function(todo) {
$scope.todos.$remove(todo);
};
}]);
In your view, you could do something like below. Note that you could bind the removeTodo function to a checkbox as the question specifies, or a regular old <a href> element:
// In your view
<div ng-controller="TodoCtrl">
<ul>
<li ng-repeat="todo in todos">
{{ todo.text }} : <a href ng-click="removeTodo(todo)">X</a>
</li>
</ul>
</div>
Hope that helps!
A better solution would be to have $scope.clearItem() take the object t as an argument, instead of $event.
HTML - <p ng-repeat="t in todos"><input... ng-click="clearItem(t)">
JS - $scope.clearItem = function(obj) {todoRef.$remove(obj)};
The only way I'm able to remove the item is using a loop on the array we get from firebase.
var ref= new Firebase('https://Yourapp.firebaseio.com/YourObjectName');
var arr_ref=$firebaseArray(ref);
for(var i=0;i<arr_ref.length;i++){
if(key==arr_ref[i].$id){
console.log(arr_ref[i]);
arr_ref.$remove(i);
}
}
The easiest way to remove the object would be
scope.clearItem = function(event) {
todoRef.$loaded().then(function(){
todoRef.$remove($scope.t)
});
The asynchronous nature of the beast has gotten me a few times.

ng-repeat's element not updated on array modification

When I trying to update my array on ng-click in order to update my ng-repeat's directive items:
<li ng-repeat="itemL1 in mainMenuL1Arr"
ng-click="selectL2menu(itemL1.name)">
<i ng-class="getClass(itemL1.icon)"></i>
</li>
selectL2menu() defined on controller as:
$scope.selectL2menu = function selectL2menu(itemL1name){
$scope.$apply(function () {
$scope.mainMenuL2CurArr = $scope.mainMenuL2Obj[itemL1name];
});
}
Right after click on 1st level menu item it should reveal 2nd level menu block with correspondent elements (by updating the array with correspondent values, see below).
Here is 2nd level menu block I need to update on click:
<li ng-repeat="itemL2 in mainMenuL2CurArr"
class="subMenuElemBlock"
ng-class="{active: itemL2.selected}">
<a href="#">
<i ng-class="getClass(itemL2.icon)"></i>
<span>{{itemL2.name}}</span>
</a>
</li>
While click event triggered - my array successfully updated. Nevertheless ng-repeat directive not updated my 2nd level menu (base on updating array). I've tried to with (and without) using $apply function - same result (though using $apply error message appearing Error: $apply already in progress).
So, why my array being successfully updated on click not revealed on ng-repeat directive menu element?
I've read related posts (link, link, link) but did't find any working decision.
Without seeing more of your controller code, it is difficult to determine what the problem might be. Here is a simplified working fiddle. I suggest you compare it to what you have.
Note that you don't need to call $scope.$apply() because the ng-click directive will do that for us automatically.
HTML:
<ul>
<li ng-repeat="itemL1 in mainMenuL1Arr" ng-click="selectL2menu(itemL1.name)">
{{itemL1.name}}</li>
</ul>
<ul style="margin-left: 20px">
<li ng-repeat="itemL2 in mainMenuL2CurArr"><a href="#">
<span>{{itemL2.name}}</span>
</a>
</li>
</ul>
JavaScript:
function MyCtrl($scope) {
$scope.mainMenuL1Arr = [ {name: 'one'}, {name: 'two'} ];
$scope.mainMenuL2Obj = {
one: [ {name: '1.1'}, {name: '1.2'} ],
two: [ {name: '2.1'}, {name: '2.2'} ] };
$scope.mainMenuL2CurArr = $scope.mainMenuL2Obj['one'];
$scope.selectL2menu = function (itemL1name) {
console.log(itemL1name);
$scope.mainMenuL2CurArr = $scope.mainMenuL2Obj[itemL1name];
};
}
Can you try to make the $scope.$apply() after you add item in array like
$scope.array.push({id: 1, name: 'test'});
$scope.$apply();
Works fine for me, probably you have another function of a plugin or something like this, that blocking the scope apply, in my case I have a select2 and when select in field the apply is not fired
Have you tried changing the selectL2Menu function to:
$scope.selectL2menu = function (itemL1name){
$scope.mainMenuL2CurArr = $scope.mainMenuL2Obj[itemL1name];
}

Resources