exploring Angular, I built a simple to-do list.
My issue is that when I use the checkbox to delete an item, say the very last one, the item is deleted but the checkbox above it becomes "checked." I just want to be able to delete the item via the checkbox and all the other checkboxes will remain unchecked. I can't figure the bug here. Dimly, I was thinking I needed to reset the "checked" variable, but that wasn't working.
angular.module('ToDo', [])
.controller('ToDoController', function ($scope, ToDoService) {
$scope.items = ToDoService.items;
$scope.checked = false;
$scope.add = function() {
ToDoService.add($scope.todo);
};
$scope.deleteItem = function() {
ToDoService.deleteItem();
};
$scope.remove = function(idx) {
this.items.splice(idx, 1);
console.log("test inside remove");
return this.items;
};
})
.factory('ToDoService', function () {
return {
items: [],
add: function(todo) {
this.items.push({todo: todo, time: new Date()});
},
deleteItem: function(idx) {
this.items.splice(idx, 1);
console.log("test inside deleteItem");
}
};
});
<html ng-app='ToDo'>
<head>
<title>My Angular To-Do App</title>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"></script>
<script src='app.js'></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<link rel="stylesheet" href="app.css">
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="#">My Angular To-Do App</a>
</div>
</div>
</nav>
<div class="container todos" ng-controller='ToDoController'>
<div class="starter-template">
<h1>My To-Do's</h1>
<div class="alert alert-info" role="alert" ng-show="!items.length">
No to-do items have been added yet.
</div>
<ul>
<li ng-repeat='item in items'>{{item.todo}} - {{item.time}} <a class="btn" ng-click="remove()">Delete</a>
<input type="checkbox" ng-model="checked" ng-change="deleteItem(checked)" >
</li>
</ul>
<form class="form-inline">
<input type='text' ng-model='todo' class="form-control" />
<button ng-click='add()' class="btn btn-default">Add</button>
</form>
</div>
</div>
</body>
</html>
You have complicated your app, using factory is unnecessary here. Another thing - your ng-repeat function didn't set id's of added todos - you were deleting always the first element every time you clicked on delete or on checkbox.
ng-repeat='(id, item) in items'
Lastly - I've added a new feature. With every submit of the todo, the todo input gets cleared.
$scope.todo = null;
Codepen
There are multiple bugs in your application. In your situation, you are calling ToDoService.deleteItem without any parameters when your checkbox change boolean state. So, basically you are doing a this.items.splice(undefined, 1); which removes the first element (indeed what happens in your codepen).
You should change the signature of your deleteItem methods in your service and controller to take the name of the todo. Then, you only have to search it and remove it from the list.
.factory('ToDoService', function() {
return {
....
deleteItem: function(itemStr) {
delete this.items[this.items.findIndexOf(function(item) {
return item ==== itemStr;
})];
},
}
})
then in your template you should call remove with the current todo
ng-change="remove(item.todo)"
Using Kind User answer is even simpler, because you have the direct index of your component,
.factory('ToDoService', function() {
return {
....
deleteItem: function(idx) {
delete this.items[idx];
},
}
})
Related
So I'm currently doing a Calorie Counter project that consists on giving the user the option to firstly, add items with the respective name and number of calories, remove items or update them when clicking on an edit icon next to the item, and finally removing all items at once.
The UI will basically display all the items that the user has added (including the name and the number of calories), where each item will have an edit icon next to it, and if the icon is clicked, it will give the user the option to edit them and delete them.
I still haven't gotten to the edit part because I'm currently stuck in the delete part.
Let's say I have 3 items in the list, when I click on the edit button and then delete, everything works out fine, the html element is deleted and it looks good. If I repeat the process one more time it still works, but when I repeat the process one last time, the problem happens.
For some reason, when I hit the edit button nothing happens, I've checked and apparently the item array is completely empty, even though I only deleted 2 out of the 3 items.
I've tried everything and I've been completely stuck for 3 days straight.
// Item Controller
const ItemController = function() {
// Hard coded items
data = [{
name: "Hamburguer",
id: 0,
calories: 1000
},
{
name: "Pasta",
id: 1,
calories: 700
},
{
name: "Apple",
id: 2,
calories: 70
}
]
return {
getItems: function() {
return data;
},
deleteAllItems: function() {
data.items = [];
UIController().clearItems();
},
getTotalCalories: function() {
totalCalories = 0;
this.getItems().forEach(item => {
totalCalories += parseInt(item.calories)
});
UIController().changeToTotalCalories(totalCalories);
},
removeSingleItem: function(item, li) {
// Getting the index of the item
indexItem = items.getItems().indexOf(item);
// Deleting item from array
items.getItems().splice(indexItem, 1);
// Deleting li item from UI
li.remove();
console.log(items.getItems());
}
}
};
const items = ItemController();
// UI controller
const UIController = function() {
return {
displayItems: function(itemsPresented) {
itemsPresented.forEach(function(item) {
itemList = document.getElementById("item-list");
itemList.innerHTML += `
<li class="collection-item" id="${item.id}">
<strong>${item.name}: </strong><em>${item.calories} calories</em>
<a href="#" class="secondary-content">
<i class="edit-item fa fa-pencil">
</i>
</a>
</li>
`;
})
},
clearItems: function() {
itemList = document.getElementById("item-list");
itemList.innerHTML = "";
items.getTotalCalories();
},
changeToTotalCalories: function(totalCalories) {
document.querySelector(".total-calories").textContent = totalCalories;
},
}
}
const uiCtrl = UIController();
// So when the page loads, the hard coded items can be represented
uiCtrl.displayItems(items.getItems());
// To delete all the items at once
clearAllBtn = document.querySelector(".clear-btn");
clearAllBtn.addEventListener("click", (e) => {
items.deleteItems();
e.preventDefault();
})
// Getting the li element (The one that has all the hard-coded items)
itemList = document.getElementById("item-list");
itemList.addEventListener("click", e => {
// Checking if the user is clicking the Edit Icon
if (e.target.classList.contains("edit-item")) {
items.getItems().forEach(item => {
li = e.target.parentElement.parentElement;
// Getting the item that has the edit icon that the user clicked
if (item.id === parseInt(e.target.parentElement.parentElement.id)) {
// Putting the name and the calories of the item that is being edited in the input fields
document.getElementById("item-name").value = item.name;
document.getElementById("item-calories").value = item.calories;
// Changing the buttons so when the user edits an item, they have the options Update and Delete
document.querySelector(".add-btn").style.display = "none";
document.querySelector(".update-btn").style.display = "block";
document.querySelector(".delete-btn").style.display = "block";
document.querySelector(".back-btn").style.display = "none";
// If the user clicks the delete button
document.querySelector(".delete-btn").addEventListener("click", e => {
// Changing all the buttons back to normal
document.querySelector(".add-btn").style.display = "block";
document.querySelector(".update-btn").style.display = "none";
document.querySelector(".delete-btn").style.display = "none";
document.querySelector(".back-btn").style.display = "block";
// Clearing out the input fields
document.getElementById("item-name").value = "";
document.getElementById("item-calories").value = "";
// Deleting item
items.removeSingleItem(item, li);
// Updating the calories
items.getTotalCalories();
e.preventDefault();
});
}
});
}
})
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
<nav>
<div class="nav-wrapper blue">
<div class="container">
<a href="#" class="brand-logo center">
Tracalorie
</a>
<ul class="right">
<li>
<a href="#" class="clear-btn btn blue lighten-3">
Clear All
</a>
</li>
</ul>
</div>
</div>
</nav>
<br>
<div class="container">
<!-- Form Card -->
<div class="card">
<div class="card-content">
<span class="card-title">
Add Meal / Food Item
</span>
<form class="col">
<div class="row">
<div class="input-field col s6">
<input type="text" id="item-name" placeholder="Add item">
<label for="item-name">Meal</label>
</div>
<div class="input-field col s6">
<input type="text" id="item-calories" placeholder="Add calories">
<label for="item-calories">Calories</label>
</div>
<button class="add-btn btn blue darken-3"><i class="fa fa-plus"></i>
Add Meal</button>
<button style="display: none;" class="update-btn btn orange" display=><i class="fa fa-pencil-square-o"></i>
Update Meal</button>
<button style="display: none;" class="delete-btn btn red"><i class="fa fa-remove"></i>
Delete Meal</button>
<button class="back-btn btn grey pull-right"><i class="fa fa-chevron-circle-left"></i>
Back</button>
</div>
</form>
</div>
</div>
<!-- Calorie Count -->
<h3 class="center-align">Total Calories: <span class="total-calories">
0
</span></h3>
<!-- Item list -->
<ul id="item-list" class="collection">
</ul>
</div>
It seems like you add an eventListener to the delete button every single time a user clicks on the edit pencil. You never remove these eventListeners. So when the first edit is done, there is one delete event and one items gets deleted. The next time a user clicks on the edit button, a second event gets added to the same html element, thus two items gets deleted (both events will trigger one after the other). This becomes apparent when your hardcoded list would contain 10 items, you would see 1,2,3 and lastly 4 items disappear. I suggest you look into resetting/removing eventlisteners.
I am developing an application and I am using Vue 2 as my javascript framework, I tried to declare some components and use them in my html pages
this is my html:
<!DOCTYPE html>
<html>
<head>
<title></title>
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.3.1/css/bulma.css" />
</head>
<body>
<div id="modal_element" >
<modal v-if="showModal" ></modal>
<button #click="showModal = true" >Show Modal</button>
</div>
<div id="root">
<ul>
<li v-for="task in incompeletedTasks" >
{{ task.description }}
</li>
</ul>
</div>
</body>
<script src="https://unpkg.com/vue#2.1.10/dist/vue.js" ></script>
<script src="main.js"></script>
<script src="modal.js" ></script>
<script>
let main_data = {
tasks : [
{ description : "Go to the store ", completed : true },
{ description : "Leave the store" , completed : false }
]
}
new Vue({
el : "#root",
data : main_data,
computed : {
incompeletedTasks() {
return this.tasks.filter(task => !task.completed);
}
}
});
and this the modal.js file:
Vue.component('modal',{
template : '<div id="modal_element">
<div class="modal is-active">
<div class="modal-background"></div>
<div class="modal-content box">
<p>
Some Modal Text here ...
</p>
</div>
<button class="modal-close" #click="showModal = false" >
</button>
</div>',
data : function(){
return {
showModal : false
};
}
});
new Vue({
el : '#modal_element',
});
but the modal is not displayed, and I am getting the following error in the chrome console
[Vue warn]: Property or method "showModal" is not defined on the instance
but referenced during render. Make sure to declare reactive data
properties in the data option.
Question:
what modification do I have to make to get the code working? and html page successfully displays modal?
I think there are a couple of things.
You are creating 2 vue instances in this example (#root and #modal-element), so the data will not be able to be shared unless you have some store. Much better to have just a single instance and put components in that.
You will need to pass the component into the vue instance in order for it to be aware of the component.
Here is an example with alot of the stuff trimmed out.
https://jsfiddle.net/Austio/vhgztp59/2/
The gist of it is
var component = ...createComponentStuff
new Vue({
...otherVueStuff,
components: [component]
})
Here is my piece of code, a function to add a meal template:
vm.addMealTemplate = function() {
$scope.mealCount++;
$compile( $(document.createElement('div')).attr("id", 'mealDiv' + $scope.mealCount).addClass("mealDiv"+$scope.mealCount).after().html(
'<select ng-options="(option.name ) for option in mealOptions" ng-model="selectedOption'+ $scope.mealCount+'" />' +
'<input type="text" placeholder="Meal timings" id="time'+ $scope.mealCount +'"/>' +
'<a id="mealCount" ng-class="mealCount" ng-click="addItemCategory()" uib-tooltip="Add category" tooltip-trigger="mouseenter" tooltip-placement="bottom"><i class="fa fa-plus"></i></a>'
).appendTo("#meals")
// $("#meals").append(newMealDiv)
)($scope);
}
On clicking calling the addItemCategory() for the specific div, I want another div to get added as a child of that div. There can be mutiple meal templates, and for each template I can call the addItemCategory mutliple times, and I want the category to be added to the same div for which the function has been called. How do I achieve this?
Currently I am using mealCount variable from scope to have the context, but once it gets increased, I have no way to access the divs added previously, to add the new element to that div. Any way using jQuery or AngularJs?
You can use ng-repeat
For example:
angular.module('app', []).
controller('ctrl', function($scope) {
$scope.meals = [];
});
.meal {
border:1px solid;
padding:10px;
margin-bottom:10px;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
<div ng-repeat="meal in meals" class="meal">
<select ng-model="meal.count">
<option>1</option>
<option>2</option>
<option>3</option>
</select>
<input type="text" placeholder="Meal timings" ng-model="meal.timing" />
<div>
<div>Categories:</div>
<div ng-repeat="cat in meal.categories track by $index">
<input type="text" ng-model="meal.categories[$index]" />
</div>
<button ng-click="meal.categories.push('')">Add Category</button>
</div>
</div>
<button ng-click="meals.push({categories:[]})">Add meal</button>
<hr />
{{meals | json}}
</div>
Note: I changed the models etc. it's just example..
Here's an example Plunker on how your problem can be solved with ng-repeat in Angular:
http://plnkr.co/edit/POt7nFc4GqFU67SWdMp7?p=preview
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="style.css">
<script src="https://code.angularjs.org/1.5.5/angular.js"></script>
<script src="script.js"></script>
</head>
<body ng-app="restaurant" ng-controller="meal-controller">
<div ng-repeat="meal in meals track by $index">
<select ng-options="option.name for option in mealOptions" ng-model="meal.selectedMeal"></select>
<input type="text" placeholder="Meal timings" id="{{'time'+ $index }}" ng-model="meal.timing"/>
<a id="mealCount" ng-class="mealCount" ng-click="addItemCategory()" uib-tooltip="Add category" tooltip-trigger="mouseenter" tooltip-placement="bottom"><i class="fa fa-plus"></i></a>
</div>
<button ng-click="addMeal()">Add meal</button>
</body>
</html>
-
// Script.js
angular.module('restaurant', [])
.run( ['$rootScope', function( $rootScope ) {
}]
);
angular.module('restaurant').controller('meal-controller', [ '$scope', function( $scope ) {
$scope.meals = [];
$scope.mealOptions = [{ name: "option1"}, { name: "option2"}, { name: "option3" } ];
$scope.addMeal = function() {
$scope.meals.push({ selectedMeal: null, timing: "timing" });
}
$scope.addItemCategory = function() {
}
}]);
I am not entirely sure about how you want the logic to work, but I am sure that you can modify this example to fit your needs.
Ng-repeat works like a loop which prints the content it is wrapping until the end of the array. Ng-repeat has a lot of features like tracking elements in the array by index, this can used to (like in the example) give each input an unique id.
If you need to make some specific changes to a meal, you can pass it as an argument, for example if you want to delete a specific meal you can have a button inside the ng-repeat like this:
<button ng-click="deleteMeal(meal)">Delete Meal</button>
This means that you do not have to access the meal specifically by, for example, its id with jQuery.
I would say it is not recommended to mix angular and jQuery the way you are doing now. Try to stick with Angular and avoid jQuery. In some special cases where you need to do element specific modifications it can be achieved by using jQlite:
https://docs.angularjs.org/api/ng/function/angular.element
I am making a web application using AngularJS and Laravel. The application is meant to allow the user to post a note on a board. With the code I have, when submitting the note it gets saved to the database but it does not display on the page.
angulartest.blade.php:
<!doctype html>
<html lang="en" ng-app="app">
<title>Test angular</title>
<link rel="stylesheet" href="css/bootstrap.css">
<body>
<div class="container" ng-controller="NoteController">
<h3>Add note</h3>
<form ng-submit="addNote()">
<input type="text" ng-model="newNote.content">
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
<ul>
<li ng-repeat="note in notes">
#{{ note.content }}
</li>
</ul>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.28/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.0rc1/angular-route.min.js"></script>
<script type="text/javascript" src="js/app.js"></script>
</body>
</html>
app.js
var app = angular.module('app', ['ngRoute']);
app.factory('Data', function Data($http) {
return {
getNotes: function getNotes() { return $http.get('/notes/all'); },
addNote: function addNote(data) { return $http.post('/notes', data); },
removeNote: function removeNote(id) { return $http.delete('/notes?id='+ id); }
}
});
app.controller('NoteController', function NoteController($scope, Data) {
Data.getNotes().success(parseNotes);
function parseNotes(data) {
$scope.notes = data;
}
$scope.newNote = { content: '', poster: '' };
$scope.addNote = function addNote() {
Data.addNote({
content: $scope.newNote.content,
poster: $scope.newNote.post
})
.success(noteAddSuccess).error(noteAddError);
}
function noteAddSuccess(data) {
$scope.error = null;
$scope.notes.push(data);
console.log($scope.notes);
$scope.newNote = { content: '', poster: '' };
}
function noteAddError(data) {
$scope.error = data;
}
$scope.removeNote = function removeNote(id) {
if (confirm('Do you really want to remove this note?')) {
Data.removeNote(id).success(noteRemoveSuccess);
}
}
function noteRemoveSuccess(data) {
var i = $scope.notes.length;
while (i--) {
if ($scope.notes[i].id == data) {
$scope.notes.splice(i, 1);
}
}
}
});
I believe this is all the relevant code. I'm not sure why it is not displaying note.content
Thank you
Since the data update is not triggered from UI, i.e on user clicks or similar activity the scope might be unaware of the changes. In your code you are updating the data from the service, thus my first suggestion will be is to use $scope.$apply() to propagate the changes on the model to the UI.
function parseNotes(data) {
$scope.notes = data;
if (!$scope.$$phase) {
$scope.$apply();
}
}
This might help. If not then, please post back
I found my error, really simple. I was closing div tag before I was requesting {{note.content}}. It should look like:
<div class="container" ng-controller="NoteController">
<h3>Add note</h3>
<form ng-submit="addNote()">
<input type="text" ng-model="newNote.content">
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<ul>
<li ng-repeat="note in notes">
#{{ note.content }}
</li>
</ul>
</div>
thank you for the replies!
Assuming a controller is there to manipulate some data on the scope, what is the best practice to supply the controller with that data?
For example, if I have a list of items I might want a ListController to manipulate the list, and an ItemController to manipulate an individual item. But how do I give each ItemController an item to use?
In the case below, how would doSomethingToItem access the item. When nested in the ngRepeat the item is $scope.item, but in the selection view the item we want to manipulate is $scope.selection.
angular.module('MyApp', [])
.controller('ListController', function($scope, $http) {
$scope.list = $http.get(...);
$scope.selection = null;
})
.controller('ItemController', function($scope) {
$scope.doSomethingToItem = function() {
...
};
})
<div ng-controller="ListController">
<div ng-repeat="item in list" ng-click="selection = item">
<div ng-controller="ItemController">
<button ng-click="doSomethingToItem()">Do Something</button>
</div>
</div>
<div ng-show="selection"
<div ng-controller="ItemController">
<button ng-click="doSomethingToItem()">Do Something</button>
</div>
</div>
</div>
Isn't this a common structure, or is my thinking backwards?
You should understand that Angular would create n+1 ItemController in your case. N for items and 1 for the selection section.
To make passing the object that needs to be worked on easier you can change the method signature to
doSomethingToItem(item) and in html do
<button ng-click="doSomethingToItem(item)">Do Something</button> at both places.
Or else for the repeat case the item variable contains your object that you can access in ItemController
selection variable contains the reference to the select controller, which can be reference from the instance of the controller defined under selection section.
Update: The expression in ng-repeat and selection would differ
<button ng-click="doSomethingToItem(item)">Do Something</button>
and
<div ng-show="selection"
<div ng-controller="ItemController">
<button ng-click="doSomethingToItem(selection)">Do Something</button>
</div>
</div>
You could pass the item data model like this:
<div ng-init="instance = item" ng-controller="ItemController">
</div>
"instance" will be a reference to list array data model item in "ListController".
And you could access its property in your ItemController function closure:
.controller("ItemController", function($scope){
$scope.instance={};
$scope.doSomething = function(){
console.log($scope.instance.name);
}
$scope.$watch('instance',function(){
console.log("iitem changed");
},true);
});
I'm not quite sure what feature do you want to achieve in your "selection" implementation.
I think you want to implement a selected list and list item will be added to it when user clicked the list item. You could try to create a "selected list" model to control the selected list view if you want to add the selected item to a list.
ListController.js
.controller("ListController", function($scope){
$scope.selectedList = [];
$scope.addItem = function(item){
$scope.selectedList.push(item);
}
});
HTML
<div ng-repeat="selected in selectedList">
<div ng-init="instance = selected" ng-controller="ItemController">
<span>{{instance.name}}</span>
<button ng-click="doSomething()">selectedAction</button>
</div>
</div>
I wrote a simple multi-selected list example as following:
HTML
<!doctype html>
<html lang="en" ng-app="myApp">
<head>
<meta charset="UTF-8">
<title>Nested controller</title>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.6/angular.min.js"></script>
<script src="js/nestedController.js"></script>
</head>
<body>
<div ng-controller="parentCtrl">
<h2>List</h2>
<div ng-repeat="item in list">
<input type="checkbox" ng-model="item.selected" ng-checked="item.selected"/>
<div ng-init="instance = item" ng-controller="childCtrl">
<span>{{instance.name}}</span>
<button ng-click="doSomething()">doSomething</button>
</div>
</div>
<h2>Selected</h2>
<div ng-repeat="selected in selectedList">
<div ng-init="instance = selected" ng-controller="childCtrl">
<span>{{instance.name}}</span>
<button ng-click="selectedAction()">selectedAction</button>
</div>
</div>
</div>
JS
angular.module("myApp",[])
.controller("parentCtrl",function($scope){
//test data
$scope.list = [{name:'item1',age:'12',selected:false},{name:'item2',age:'18',selected:false}];
//use model to control selected list view
$scope.selectedList = [];
//refresh the selected list model when the list checked stauts has been updated
$scope.$watch('list',function(){
console.log("parent controller detected change");
$scope.selectedList = [];
$scope.list.forEach(function(elem,index,array){
if(elem.selected===true){
$scope.selectedList.push(elem);
}
});
},true);
})
.controller("childCtrl",function($scope){
$scope.instance={}
$scope.doSomething = function(){
alert("I'm the item: "+$scope.instance.name);
}
$scope.selectedAction = function(){
alert("I'm the selected item: "+$scope.instance.name);
}
//could register a watcher to monitor the model status
$scope.$watch('instance',function(){
console.log("child controller detected change");
},true);
});
Here is the jsFiddle demo
Hope this is helpful.