How to add multiple items to a list - javascript

I'm building an app where users can add items to a list and I decided, for the sake of learning, to use Angular (which I'm very new to). So far, I've been able to successfully add a single item to that list without any issues. Unfortunately, whenever I try to add more than one without a page refresh, I get an error - specifically a "Undefined is not a function."
I've spent more time than I care to think about trying to resolve this issue and I'm hoping an expert out there can give me a hand. Here's what I have so far:
Controllers:
angular.module('streakApp')
.controller('StreakController', function($scope) {
// Removed REST code since it isn't relevant
$scope.streaks = Streak.query();
// Get user info and use it for making new streaks
var userInfo = User.query(function() {
var user = userInfo[0];
var userName = user.username;
$scope.newStreak = new Streak({
'user': userName
});
});
})
.controller('FormController', function($scope) {
// Works for single items, not for multiple items
$scope.addStreak = function(activity) {
$scope.streaks.push(activity);
$scope.newStreak = {};
};
});
View:
<div class="streaks" ng-controller="FormController as formCtrl">
<form name="streakForm" novalidate >
<fieldset>
<legend>Add an activity</legend>
<input ng-model="newStreak.activity" placeholder="Activity" required />
<input ng-model="newStreak.start" placeholder="Start" type="date" required />
<input ng-model="newStreak.current_streak" placeholder="Current streak" type="number" min="0" required />
<input ng-model="newStreak.notes" placeholder="Notes" />
<button type="submit" ng-click="addStreak(newStreak)">Add</button>
</fieldset>
</form>
<h4>Current streaks: {{ streaks.length }}</h4>
<div ng-show="newStreak.activity">
<hr>
<h3>{{ newStreak.activity }}</h3>
<h4>Current streak: {{ newStreak.current_streak }}</h4>
<p>Start: {{ newStreak.start | date }}</p>
<p>Notes: {{ newStreak.notes }}</p>
<hr>
</div>
<div ng-repeat="user_streak in streaks">
<!-- Removed most of this for simplicity -->
<h3>{{ user_streak.fields }}</h3>
</div>
</div>

Could you post the html of StreakController too? Your solution works fine in this fiddle:
http://jsfiddle.net/zf9y0yyg/1/
.controller('FormController', function($scope) {
$scope.streaks = [];
// Works for single items, not for multiple items
$scope.addStreak = function(activity) {
$scope.streaks.push(activity);
$scope.newStreak = {};
};
});
The $scope inject in each controller is different, so you have to define the "streaks" in FormController.

Your problems comes from :
.controller('FormController', function($scope) {
// Works for single items, not for multiple items
$scope.addStreak = function(activity) {
$scope.streaks.push(activity);
^^^^^^
// Streaks is initialized in another controller (StreakController)
// therefore, depending of when is instantiated StreakController,
// you can have an error or not
$scope.newStreak = {};
};
});
A better design would be to implement a StreakService, and to inject that service in the controller you need it. Of course, initializing $scope.streaks in FormController will make your code work, but that's not the responsibility of FormController to initialize this data.

I assume FormController is a nested controller of StreakController, so they share the same scope.
if that works for single object, it should work for mulitiple objects, the problems is you can't just use push to push an array of object to the streaks, you can for loop the array and add them individually or use push.apply trick. I thought the reason of Undefined is not a function. is because the Stack.query() return an element instead of an array of elements so, the method push doesn't exists on the $scope.streaks.
http://jsbin.com/jezomutizo/2/edit

Related

Tracking many dynamic forms with ng-model

I'm generating dozens of forms on my page. Each form has several parameters (not the same for each form). I'm generating my forms as such (simplified):
<div ng-repeat='module in modules'>
<form ng-submit='submitModule(module)'>
<div ng-repeat='arg in module.args'>
<input ng-model='models[module.name][arg.name]' id="{{ arg.name }}">
</div>
</form>
</div>
You can see I'm trying to assign a unique ng-model to each input parameter by using a two dimensional array models[module.name][arg.name].
Because I am planning on submitting this as JSON, the idea was that I could just do models[some_module] in my controller to get the full JSON, and then just post along.
Unfortunately this isn't working, when trying models['test_module'] I get undefined, instead of my object. There are no errors elsewhere in the code, I've tested extensively. The problem comes from the use of multi-dimensional arrays here which is apparently a big no-no.
How should I handle my situation? IE: several forms, several inconsistent parameters, and a need to POST every param together as JSON.
EDIT: For info, my controller looks like:
angular.module('app')
.controller('InputCtrl', function($scope, InputSvc) {
$scope.models = {};
InputSvc.list().success(function(modules) {
$scope.modules = modules;
$scope.models['test_module'] = {}
});
$scope.submitModule = function(module) {
console.log($scope.models['test_module']);
};
});
Perhaps you could give each form a controller so the model is scoped to the form instance rather than the parent:
<div ng-repeat='module in modules'>
<form ng-controller="FormCtrl" ng-submit='submitModule(module)'>
<div ng-repeat='arg in module.args'>
<input ng-model='formData[arg.name]' id="{{ arg.name }}">
</div>
</form>
</div>
Then your FormCtrl would have the submit method and the model:
angular.module('app')
.controller('FormCtrl', function($scope) {
$scope.formData = {};
$scope.submitModule = function(module) {
console.log($scope.formData);
};
});
Here is a Codepen

Get $id from nested ng-repeat

I have a post/comment system in Angularjs and Firebase, I can loop without problem to show all the post with ng-repeat and its comments as well. The problem start when I tried to get the $id from the second nested ng-repeat to be able to save the replies into the comment. Let's see the code I have:
<div class="posts" ng-repeat="post in posts">
<div>{{post.text}}</div>
<div>
<input type="text" placeholder="Comment here..." ng-model="comment">
<button ng-click="addComment(post, comment)"></button>
</div>
<div class="comments" ng-repeat="cmt in post.comments">
<p>{{cmt.text}}</p>
<div>
<input type="text" ng-model="answer">
<button ng-click="addAnswer(cmt, post)"></button>
</div>
<div ng-repeat="answer in cmt.answers">
<p>{{answer.text}}</p>
</div>
</div>
</div>
app.js
$scope.addComment = function(post, comment){
var ref = new Firebase("https://url.firebaseio.com/users/" + post.$id + "/comments");
var comments = $firebaseArray(ref);
comments.$add({
text: comment
});
}
$scope.addAnswer = function(cmt, post){
var refanswers = new Firebase("https://url.firebaseio.com/users/" + post.$id + "/comments/" + cmt.$id + "/answers");
var answers = $firebaseArray(refanswers);
answers.$add({
text: answer
});
}
To test, first I remove the code above inside addAnswer function and write two console.log to check if Im getting the values
console.log(post.$id);
console.log(cmt.$id);
I get only the post.$id but no the second one (undefined), am I missing something about nested ng-repeat?
Edited: I changed the code to avoid the $scope conflict following the answers below but still does not work.
Any advice is welcome, thanks :)
comment being passed into addAnswer() is likely not the instance object of the ng-repeat as you are thinking, rather it could be clashing with the ng-model='comment' already defined above. You have a scope issue.
This may be fixed by changing ng-repeat="comment in post.comments" to ng-repeat="cmnt in post.comments"or whatever you want to name it, as long as it doesn't conflict with previously defined $scope objects.

Strange binding permanence between controllers

I've got a project in which you write a note in a formulary. Then, you submit that note into an information container (now it's just an array for testing purposes, but it's intended to be a DB later).
The formulary has the following controller:
app.controller('controlFormulario', ['$scope', 'SubmitService', function($scope, submitService) {
$scope.formData = {
"titulo":"",
"texto":"",
"fecha": new Date()
};
$scope.submit = function() {
var temp = $scope.formData;
submitService.prepForBroadcast(temp);
}
// more things we don't need now
... which is bound to this part of the DOM, which is added into it, via a directive:
<form ng-controller="controlFormulario as formCtrl">
<div class="element">
<div class="form-group" ng-class="{'has-error': formData.titulo.length > 50 }">
<label for="inputTitulo">Título</label>
<input type="titulo" class="form-control" id="inputTitulo" ng-model="formData.titulo">
<span ng-show="formData.titulo.length > 50" id="helpBlock" class="help-block">El título no puede exceder los 50 caracteres.</span>
</div>
<div class="form-group">
<label for="inputTexto">Texto</label>
<textarea class="form-control" id="inputTexto" ng-model="formData.texto"></textarea>
</div>
<div class="form-group">
<label for="fecha">Fecha</label>
<input type="fecha" class="form-control" id="fecha" ng-model="formData.fecha" disabled>
</div>
<div class="form-group" >
<button class="btn btn-primary" style="height:35px;width:100px;float:right;" id="submit"
ng-disabled="isDisabled()" ng-click="submit()">
Enviar
</button>
</div>
</div>
<div class="note" ng-show="formData.titulo.length > 0">
<div class="title" ng-model="formData.titulo" class="title">{{formData.titulo | limitTo:50}}</div>
<div class="text" ng-model="formData.texto" class="text">{{formData.texto}}</div>
<div class="date" ng-model="formData.fecha" class="date">{{formData.fecha | date}}</div>
</div>
</form>
This is my directive (I don't think it's really needed, but just in case):
app.directive('formulario', [function() {
return {
restrict: 'E', // C: class, E: element, M: comments, A: attributes
templateUrl: 'modules/formulario.html',
};
}]);
I use a service for passing the data between the previous controller, and the note controller (which controls the note objects of the array). This is the service:
app.factory('SubmitService', function($rootScope) {
var data = {};
data.prepForBroadcast = function(recvData) {
data.data = recvData;
this.broadcastItem();
};
data.broadcastItem = function() {
$rootScope.$broadcast('handleBroadcast');
};
return data;
});
... and I receive it in this part of my note controller:
app.controller('noteController', ['$scope', 'SubmitService', function($scope, submitService) {
var nc = this;
$scope.$on('handleBroadcast', function() {
nc.pruebaNota.push(submitService.data);
$scope.formData.titulo = "";
$scope.formData.texto= "";
$scope.formData.fecha = new Date();
});
// more things, the array, etc...
Ok. This should work, and it does, but something strange happens: as you can see, the preview note is binded with ng-model to the form. That works great, ok. But when I submit the form, the new note object keeps bound to the form (so if I delete the form text, the note appears in blank, and if I write something, it gets automatically updated both in the preview note, and the new note), when there isn't any relation between them. The new note, which appears dynamically on the screen, shouldn't be bound to anything.
Am I doing something wrong? Some help would be really nice!
You are forgetting something really important. Memory address. So, the rought idea is something like: imagine that $scope.formData is in the address 123123. You first create a temp var pointing to 123123 then you send it to the service and the service holds the same address 123123 into data.data.
Then in your second controller you say: hey, I want to work with that data.data (AKA your data in 123123) you have SubmitService.
Now when you modify $scope.formData again, you are updating what you have in that 123123 and everything that is "looking" into that address will be updated.
That is the rough idea. To point it simple, you're modifying the same piece of information everywhere.
See it here: http://plnkr.co/edit/zcEDQLHFWxYg4D7FqlmP?p=preview
As a AWolf suggested, to fix this issue, you can use angular.copy like this:
nc.pruebaNota.push(angular.copy(submitService.data));

How can I take one large object, use a sub-object of that as the data for multiple instances of the same controller, and keep them both in sync?

Apologies for the ambiguity of the question :-P
I have a single JavaScript object which contains all my data. And I have a controller which I will use multiple times throughout the application. So the controllers are all working on the same data, the data is added to the application using a service.
To support a read-only/edit mode interaction, I make two copies of the original data source in the service. When the user manipulates data, they are manipulating the edit mode data source. They can then press a button to save the data to the edit mode data source to the read-only mode data source (using angular.copy).
I would also like to have the instances of the controller work on just part of the data source rather than the whole thing.
The behavior I am seeing is angularjs is able to update the parts, keeping them both in sync; but when I press the button to perform the angular.copy, it seems to reassign the variable rather than adjust the value of where it was pointing.
Code below and here's a jsfiddle http://jsfiddle.net/q5ca5quq/1/
<html ng-app='app'>
<body>
<div ng-controller='a_controller as ctrl_1'>
read_mode_inner = {{ ctrl_1.read_mode_inner }}<br>
edit_mode_inner = {{ ctrl_1.edit_mode_inner }}<br>
<br>
<input ng-model='ctrl_1.edit_mode_inner[0].a'>
</div>
<br><br><br>
<div ng-controller='a_controller as ctrl_2'>
read_mode_inner = {{ ctrl_2.read_mode_inner }}<br>
edit_mode_inner = {{ ctrl_2.edit_mode_inner }}<br>
<br>
Change this and press button below <input ng-model='ctrl_2.edit_mode_inner[0].a'> <br>
<button ng-click='ctrl_2.change()'>Copy edit_mode_inner into read_mode_inner</button>
</div>
<script src='https://ajax.googleapis.com/ajax/libs/angularjs/1.2.21/angular.min.js'></script>
<script type='text/javascript'>
angular.module('app',[])
.factory('DataService', [function() {
data = {
xx : [{'a':1}, {'b':2}, {'c':3}],
yy : [{'a':1}, {'b':2}, {'c':3}]
}
return {
read_mode : data,
edit_mode : angular.copy(data)
}
}])
.controller('a_controller', ['DataService', function(DataService) {
var self = this;
window.s = self; // For debugging
self.read_mode = DataService.read_mode;
self.edit_mode = DataService.edit_mode;
self.read_mode_inner = self.read_mode.xx;
self.edit_mode_inner = self.edit_mode.xx;
self.change = function(){
self.read_mode_inner = angular.copy(self.edit_mode_inner);
}
}]);
</script>
</body>
</html>
http://jsfiddle.net/q5ca5quq/2/
You can share data between controllers using a service, but if you want all controller instances to update every time the value in service is changed, you'll need something like $watch inside your controller:
$scope.change = function(){
DataService.read_mode.xx = angular.copy($scope.edit_mode_inner);
}
$scope.$watch(function(){return DataService}, function(dataService){
$scope.read_mode_inner = dataService.read_mode.xx;
}, true);
For this to work you need to use angular $scope instead of this/self reference. Notice how using $scope also simplified angular notation in HTML. You don't need to name controllers separately:
instead of:
<div ng-controller='a_controller as ctrl_1'>
read_mode_inner = {{ ctrl_1.read_mode_inner }}<br>
edit_mode_inner = {{ ctrl_1.edit_mode_inner }}<br>
</div>
You only have to:
<div ng-controller='a_controller'>
read_mode_inner = {{ read_mode_inner }}<br>
edit_mode_inner = {{ edit_mode_inner }}<br>
</div>
because $scope takes care of the rest.

AngularFire 3-way data binding is not updating firebase when a checkbox change

I'm developing a simple todo app with Angular and Firebase using AngularFire module.
So I have a boolean attribute in my model represented by a checkbox in the template, the problem is that I'm trying to use the three way data binding from AngularFire using the $bind method to keep the all changes syncronized (firebase data, DOM and ng-model) but the firebase data is not updating when I select a checkbox.
Here's my controller where I'm using the AngularFire $bind method:
angular.module('singularPracticeApp')
.controller('TodoCtrl', ['$scope', 'TodoService', function ($scope, todoService) {
$scope.todos = todoService;
$scope.todos.$bind($scope, 'todo.done');
$scope.addTodo = function () {
$scope.todos.$add({text: $scope.todoText, done:false});
$scope.todoText = '';
};
$scope.remaining = function () {
var count = -11;
angular.forEach($scope.todos, function(todo){
count += todo.done? 0 : 1;
});
return count;
};
$scope.clear = function (id) {
$scope.todos.$remove(id);
};
}]);
And here is the tempalte file:
<div ng-controller="TodoCtrl">
<h4>Task runner</h4>
<span>{{remaining()}} todos left.</span>
<ul>
<li ng-repeat="(id, todo) in todos">
<input type="checkbox" ng-model="todo.done">
<span ng-if="todo.done" style="color: #ddd;">{{todo.text}}</span>
<span ng-if="todo.done == false">{{todo.text}}</span>
<small ng-if="todo.done">clear</small>
</li>
</ul>
<form ng-submit="addTodo()">
<input type="text" ng-model="todoText" placeholder="New todo item">
<input type="submit" class="btn btn-primary" value="add">
</form>
</div>
Am I missing something? Is really possible to make this work with a simple checkbox?
Thanks in advance.
You haven't included todoService here so it's going to be difficult to give you an accurate answer. I'll assume that todoService returns a $firebase instance containing the todos since that seems likely. Keep in mind that the problem could be in that code as well.
Several problems you can address, which may resolve your issue:
Your TodoCtrl is not per-item
You seem to be using TodoCtrl as if it were created per-item in the ng-repeat. However, it exists outside the scope of ng-repeat and is only created once for the entire list.
Ng-repeat does not re-use your existing controller scope.
Directives operate in an isolate scope. That means that they do not share scope with your controller. So when you do ng-repeat="todo in todos" you do not add todo into your controller's scope.
This makes sense since each ng-repeat iteration would overwrite the same todo object.
You are trying to double-bind to a synchronized object
You are trying to create a three-way binding $scope.todos.[$todo].done, but you have already created a three-way binding on $scope.todos. Instead, let $scope.todos take care of synchronization.
You've attempted to bind $scope.todos to a property in itself
When you call $bind, you are binding $scope.todos to $scope.todos.todo.done. Obviously this self-referential statement isn't what you intended. I can't tell what is returned by your service but maybe you meant this:
todoService.$bind($scope, 'todos');
If you don't want to automatically push changes on the entire todos list, you can add a $save call instead of using $bind:
$scope.todos = todoService;
<input type="checkbox" ng-model="todo.done" ng-change="$parent.todos.$save(id)">
All together:
angular.module('singularPracticeApp')
.service('todoService', function($firebase) {
return $firebase( new Firebase(URL_TO_TODOS_LIST) );
});
.controller('TodoCtrl', function($scope, todoService) {
todoService.$bind($scope, 'todos');
$scope.addTodo = function () {
$scope.todos.$add({text: $scope.todoText, done:false});
$scope.todoText = '';
};
/** ... and so on ... **/
});

Categories

Resources