I am pulling my hair out trying to figure this out, I feel like I've tried everything to get this working.
I am creating essentially a todo app. I am using AngularFire in a service. There is an item directive that is repeating items that each have a checkbox. When a checkbox is checked/unchecked it should call a method on the DataService to update the item object {completed: false} to {completed: true} or vice versa each time it's checked/unchecked. ngChange passes in the individual object from an array of objects (objects are representing a unique item). Everything seems to work except being able to save the updated item after the checkbox is checked/unchecked. Everything I've tried has given for example, this error: "Invalid record; could determine key for somekey"
In the screenshot below, the console says it updates the object, but throws an error on trying to save the updated entry to firebase. Everything I've tried passing in has given the same error. What am I doing wrong here? Please take a look at the code below. I've spent a long time fiddling with this, so any help would be greatly appreciated. Please if anything doesn't make sense, ask me instead of downvoting, I'm really trying to explain this the best I can and really need help. Thank You!
Service Code:
var app = angular.module('dataService', ['firebase']);
app.factory('DataService', ["$firebaseArray",
function ($firebaseArray) {
//create, retrieve, update, destroy data from angularfire
var url = 'https://myFirebase.firebaseio.com/item';
var fireRef = new Firebase(url);
var factory = {};
factory.complete = function (data) {
var completeGoal = $firebaseArray(fireRef);
console.log("complete, fired in factory");
var itemID = data.$id;
console.debug("this item ID: " + itemID);
if(data.completed === true) {
console.log("this item data.completed: " + data.completed)
data.completed = false;
console.log("this item data.completed: is NOW " + data.completed)
}
else {
console.debug("this item data.completed: " + data.completed)
data.completed = true;
console.debug("this item data.completed: is NOW " + data.completed)
}
completeGoal.$save(itemID);
console.log(completeGoal.$save(itemID));
}
return factory;
}]);
Directive Code:
app.directive('item', function() {
return {
scope: {
item: '=set',
onClick: '&',
listType: '=',
complete: '='
},
controller: function() {},
controllerAs: 'ctrl',
bindToController: true,
transclude: true,
replace: true,
restrict: 'EA',
templateUrl: 'directives/items.html',
link: function(scope, elem, attrs) {
scope.$parent.onChange = function (data) {
scope.ctrl.complete(data);
}
//filter the items by type
var typeFilter = {type: scope.ctrl.listType}
scope.filterExpr = typeFilter;
}
}
});
Item Directive Template:
<div class="row goal-item" ng-repeat="item in ctrl.item | filter:filterExpr">
<div class="col-xs-12 col-sm-8 col-sm-offset-2">
<ul>
<li>
<label class="checkbox" ng-click="onChange(item)">
<input type="checkbox" class="goal-checkbox" ng-click="null" />
</label>
<span class="goal-title goal-completed">{{item.text}}</span>
</li>
</ul>
</div>
</div>
Controller:
app.controller('mainCtrl', ['$scope','DataService', function($scope, DataService) {
this.complete = DataService.complete;
}])
I think the problem here is that you're using the actual firebase key to try and save the data. According to the angularfire docs the $save method actually accepts 1 of 2 parameters:
$save(recordOrIndex)
So either the record - which would be the entire object or the index wich would be the indexed location inside of your local array [0,1,2,3,...]
I think your best bet would be to try and save the entire object opposed to using the unique key. So like this:
completeGoal.$save(data);
Hopefully it's that easy!
Here's the link to the angularfire api I'm referencing:
https://www.firebase.com/docs/web/libraries/angular/api.html#angularfire-firebasearray-saverecordorindex
Related
I'm retrieving a list of objects (item) from a Django API.
my_app.factory('list_of_items', function($resource) {
return $resource(
'/api/petdata/') });
Then I display everything in a html page within a ng-repeat:
<div ng-controller="ModalDemoCtrl">
<div ng-repeat="item in items | filter:{display:'1'} | orderBy: 'item_name'">
<div class="box box-widget widget-user">
{{ item.pet_name }}{% endverbatim %}
<button type="button" class="btn btn-box-tool" ng-click="askDelete(item)" href="#"><i class="fa fa-times"></i></button>
</div>
<div>
Everything's fine so far.
Then I want the user to be able to delete one of the item by clicking on the button from the html page.
What means deleting here :
1. Update the API database by changing the property "display:1" to "display:0".
2. Remove the item from the ng-repeat.
I want to make a "Are you sure" modal to confirm the delete process.
This is the askDelete function.
angular.module('djangular-demo').controller('Ctrl_List_Of_Pets', function($scope, $http, $window,$filter,list_of_pets,pet_by_id,$uibModal) {
$scope.items = list_of_items.query()
$scope.askDelete = function (idx,item,size,parentSelector) {
// console.log("PET",$scope.pet_to_be_undisplayed);
var parentElem = parentSelector ?
angular.element($document[0].querySelector('.modal-demo ' + parentSelector)) : undefined;
var modalInstance = $uibModal.open({
animation: true,
ariaLabelledBy: 'LOL',
ariaDescribedBy: 'modal-body',
templateUrl: "myModalContent.html",
controller: function($scope) {
$scope.ok = function() {
modalInstance.close();
};
$scope.cancel = function() {
modalInstance.dismiss('cancel');
};
},
size: size,
appendTo: parentElem,
resolve: {
}
});
modalInstance.result.then(function() {
reallyDelete(item);
});
};
var reallyDelete = function(item) {
$scope.entry = items_by_id.get({ id: item.id }, function() {
// $scope.entry is fetched from server and is an instance of Entry
$scope.entry.display = 0;
$scope.entry.$update({id: $scope.entry.id},function() {
//updated in the backend
});
});
$scope.items = window._.remove($scope.items, function(elem) {
return elem != item;
});
};
});
What works :
Updating the DB works with a PUT request (code hasn't been provided).
What doesn't work :
Removing the item from the ng-repeat never works. Or it throws me an error like here because it doesn't know window._.remove or it doesn't know $scope.items. It depends from what I try. Or the modal close and there is no update of the ng-repeat list, no refresh and every items remain whereas the PUT request to update worked.
I read every article on scope inheritance and I think I didn't make any mistake here but I'm might be wrong. I've been struggling for too long so I post here !
Would you suggest anything to make it work ?
Thank you for your rime.
First:
$scope.askDelete = function (idx,item,size,parentSelector) receives the item index, the item, size, and parent selector... and you are calling ng-click="askDelete(item)"
I assume you are attempting to pass the item, but in askDelete you are receiving as first parameter the index (maybe you should do ng-click="askDelete($index)"?)
Second:
In reallyDelete why are you removing the items array like this:
$scope.items = window._.remove($scope.items, function(elem) {
return elem != item;
});
?
IMHO, it would be a much cleaner code if we just do:
$scope.items.splice(idx, 1) //<- idx would be the idx of the entry in the items
You may want to take a look at Splice
EDIT/UPDATE
I have added a plunker for this issue:
http://plnkr.co/edit/mlYKJQc7zQR0dsvDswMo
I am loading in data from the previous page into $scope.visit in my page controller. I would like to have a directive that constructs and loads select elements on the page with options from the database. Here is what I have so far:
app.directive('popList', ['$http', function($http) {
'use strict';
var directive = {
restrict: 'EA',
link: link,
scope: {
popList: '='
},
template: function(elem,attrs) {
return '<select '+
'ng-model="'+attrs.model+'" '+
'ng-options="option.name for option in ddlOpts track by option.id" '+
'required '+
'></select>';
}
};
return directive;
function link(scope, elem, attrs, ctrl) {
var data = {
sTableName: attrs.tbl
};
$http.post('ddl.asmx/populateDDL',data).
then(function(response) {
console.log('This is the value I want to set it to '+scope.$parent.visit.data.state_id);
//ddlOpts loads with no problem
scope.ddlOpts = response.data.d;
});
}
}]);
Then the HTML looks like this:
<div style="width: 5%" class="tableLabel">State</div>
<div
style="width: 27%;"
class="tableInput"
model="visit.data.state_id"
tbl="tblStates"
pop-list="states"
>
</div>
Essentially I pass in the name of the table that holds my drop down options. I also pass in the model which will tell the select what value is the default (if it has one.)
I can get the data loaded into the select element as options with no problem. I have checked the ng-model to ensure that I have the correct value to match to the ng-value. I have tried ng-repeat then started all over with ng-options but I cannot for the life of me get the select option to set the default to the ng-model value.
I am starting to think its because the when the select is constructed its in a different scope than the where the controller set the data to $scope.visit. Can someone explain why the two scopes don't recognize one another or just explain why I can't set my default value to the value stored in the ng-model?
Here is the page controller just in case you need for reference:
app.controller('demographicsFormCtrl', function($rootScope, $scope, $cookies, $http, $window, $route) {
if (
typeof $cookies.get('visitTrack') === 'undefined' ||
typeof $cookies.get('visitInfo') === 'undefined' ||
typeof $cookies.get('visitData') === 'undefined'
){
window.location.href = "#/";
return false;
}
$scope.visit = {};
$scope.visit.track = JSON.parse($cookies.get('visitTrack'));
$scope.visit.data = JSON.parse($cookies.get('visitData'));
$scope.visit.info = JSON.parse($cookies.get('visitInfo'));
$rootScope.preloader = true;
console.log('controller set');
});
template: function(elem,attrs) {
return '<select '+
'ng-model="'+attrs.model+'" '+
'ng-options="option.name for option in ddlOpts track by option.id" '+
'required '+
'></select>';
}
In the above template you have ddlOpts, which I believe is an array of objects. And you are displaying name in drop down and storing option in ng-model when user selects one of the name.
For attrs.model, you are passing data from the main view model="visit.data.state_id". So, you are passing just an id to set the default option.
If you want to set an default option, you have to pass an object matching one of the objects of ddlOpts.
Also note that if you try to pass an object to model="visit.data.state_id", I don't think you can read an object inside directive by attrs.model, since model="" holds a string.
Probably, you have add one more isolated scope binding in your directive.
scope: {
popList: '=',
model: '='
},
In HTML
template: function(elem,attrs) {
return '<select '+
'ng-model="'+model+'" '+
'ng-options="option.name for option in ddlOpts track by option.id" '+
'required '+
'></select>';
}
First, thanks to Ravi as I couldn't have found the answer without him...
OK so here is the skinny on doing this in a directive. The database was sending down an object with just a key and an integer value. Angular, when using ng-options, was creating an object with k/v pairs of id-int, name-string... As you can see here in this plunk when I converted the database data (MANUALLY) to match the outcome of the selected option it worked like a charm...
See it here: http://plnkr.co/edit/mlYKJQc7zQR0dsvDswMo
Essentially when trying to change the value of your directive created just make sure the data object in ng-model matches the data object created by ng-options...
or if you have the same issue with the served data you can write in something after you set ddlOpts to set based on the int sent from the DB like so:
app.directive('popList', ['$http', function($http) {
'use strict';
var directive = {
restrict: 'EA',
scope: {
model: '='
},
template: function(elem,attrs) {
return '<select '+
'ng-model="model" '+
'ng-options="option.name for option in ddlOpts track by option.id" '+
'required '+
'></select>';
},
link: link
};
return directive;
function link(scope, elem, attrs) {
var defaultInt;
if (typeof scope.model === "number"){
defaultInt = scope.model;
}else{
defaultInt = parseInt(scope.model);
}
var data = {
sTableName: attrs.tbl
};
$http.post('/emr4/ws/util.asmx/populateDDL',data).
then(function(response) {
scope.ddlOpts = response.data.d;
// THIS LINE BELOW HERE SETS THE VALUE POST COMPILE
angular.forEach(scope.ddlOpts, function (v, i) {
if (v.id === defaultInt) {
scope.model = v;
}
});
});
}
}]);
iTS ALWAYS BETTER AS A RULE OF THUMB TO HAVE YOUR DATA COMING FROM THE db MATCH WHAT ANGULAR CREATES BUT THAT IS NOT ALWAYS SO...
I'm using a server side generated JSON to populate a custom view using different directives with Angular 1.2.29. I have a couple of questions regarding what is the proper way a doing this considering performance and good practices.
5 different types of directive will be involved for about 30 items
The JSON will stay the same about 90% and it's a bit bad to have to regenerate all the DOM elements between user tab switch.
I want to avoid creating watches but in since I'm using 1.2.X should I consider using angular-once
Since I'm going to reuse the same directive couple of time should I consider cloneAttachFn
function processItems(items) {
angular.forEach(items, function(item) {
switch(item.type) {
case 'directive1':
var newDirective = angular.element('<directive-one></directive-one>');
newDirective.attr('value', item.value);
var compiledHtml = $compile(newDirective)(scope);
element.append(compiledHtml);
break;
case 'directive2':
var newDirective = angular.element('<directive-two></directive-two>');
newDirective.attr('value', item.value);
var compiledHtml = $compile(newDirective)(scope);
element.append(compiledHtml);
break;
}
})
}
I created a Plunker to show you guys my current approach. Comments and answers are very welcome! https://plnkr.co/edit/Za4ANluUkXYP5RCcnuAb?p=preview
I have been through this problem many times when generating dynamic filter type functionality. Your code works but I would argue it's over engineered and not very readable. GenericItems directive isn't needed. I would try and move functionality to the view and make it clear what happens as the type changes. Here is my solution as a Plunker
Controller
<div ng-controller="appCtrl as app">
<p>{{app.name}}</p>
<button ng-click="app.add1()">Directive 1</button>
<button ng-click="app.add2()">Directive 2</button>
<button ng-click="app.remove()">Remove</button>
<div ng-repeat="item in app.items">
<directive-one value="item.value" ng-if="item.type==='directive1'"></directive-one>
<directive-two value="item.value" ng-if="item.type==='directive2'"></directive-two>
</div>
</div>
app.js
app.controller('appCtrl', function() {
var vm = this;
vm.items = [];
vm.name = 'Dynamic directive test';
vm.add1 = function() {
vm.items.push({type: 'directive1', value: Math.random()})
};
vm.add2 = function() {
vm.items.push({type: 'directive2', value: Math.random()})
};
vm.remove = function() {
vm.items.pop();
};
});
app.directive('directiveOne', function() {
return {
scope: {
value: '='
},
restrict: 'E',
template: '<p>d1: {{value}}</p>'
}
});
app.directive('directiveTwo', function() {
return {
scope: {
value: '='
},
restrict: 'E',
template: '<p>d2: {{value}}</p>'
}
});
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.
Trying to simulate a hotel cart.
Newbie here
Questions
1. How to add an item to orders when clicked on corresponding Add button
2. Is it correct to use a factory for serving both menuitems for menu directive and orderItems for cart directive
3. On click of add button, where should the to be called add function be written, in the factory or in the directive's controller
4. Is there any way to better this code and its logic?
For those who wish to see the plunkr demo can view the same here
HTML snippet
<menu></menu>
JS snippet
angular.module('myApp',[])
.factory('menuItems',function(){
return {
list:function(){
var items = [{'name':'kabab'},
{'name':'chicken'},
{'name':'egg'},
{'name':'noodles'}];
return items
}
};
})
.factory('cartItems',function(){
var orders = [];
return orders;
})
.directive('menu',function(){
return {
restrict:'E',
template:"<ul><li ng-repeat='item in menuItems'>"+
'{{item.name}}' +
"</li></ul>",
scope:{},
controllerAs:'menuCtrl',
controller:function($scope, menuItems){
console.log("I am in menuDirective and outputting menuItems")
console.log(menuItems);
$scope.menuItems = menuItems.list();
},
link:function(){
}
}
})
.directive('cart',function(){
return{
restrict:'E',
template:"<ul><li ng-repeat='order in cartItems'>"+
'{{order.name}}' +
"</li></ul>",
scope:{},
controller:function($scope,cartItems){
$scope.cartItems = cartItems.list();
},
link:function(){
}}
})
Plunker Demo
I think this is a fine way to do it. Since you need to access the same dataset from multiple directives, it makes sense to me to put the methods and data into a factory.
HTML:
<body ng-controller="MainCtrl">
<menu></menu>
<br />Cart:<br />
<cart></cart>
</body>
JS:
angular.module('plunker',[])
.controller('MainCtrl', function() {
})
.factory('menuItems',function(){
return {
list:function(){
var items = [{'name':'kabab'},
{'name':'chicken'},
{'name':'egg'},
{'name':'noodles'}];
return items
}
};
})
.factory('cartItems',function(){
return {
orders: [],
add: function(item) {
this.orders.push(item)
}
}
//return this.orders;
})
.directive('menu',function(cartItems){
return {
restrict:'E',
template:"<ul><li ng-repeat='item in menuItems'>"+
'{{item.name}}' +
"<button ng-click='addItem(item.name)'>Add</button></li></ul>",
scope:{},
controllerAs:'menuCtrl',
controller:function($scope, menuItems){
console.log("I am in menuDirective and outputting menuItems")
console.log(menuItems);
$scope.menuItems = menuItems.list();
$scope.addItem = function(item) {
cartItems.add(item);
console.log(cartItems.orders)
}
},
link:function(){
}
}
})
.directive('cart',function(){
return{
restrict:'E',
template:"<ul><li ng-repeat='order in cartItems track by $index'>"+
'{{order}}' +
"</li></ul>",
scope:{},
controller:function($scope,cartItems){
$scope.cartItems = cartItems.orders;
},
link:function(){
}}
})
Any methods relating to the orders array should be put in the factory. Your factory should encapsulate all logic related to that particular 'thing'. Keeping the logic out of your directives gives you good abstraction, so if you need to change something, you know where all the logic is kept, rather than spread out in a bunch of directives. So if you want to add an 'empty cart' method, or a 'delete from cart' method, I would put those into the factory, operate directly on the orders array, and return the updated array to whatever is calling the methods.