I'm attempting to solve an issue with the design of my models, view, and directive in AngularJS. Essentially I have a list of products being populated on the page by category with quantity fields being added for each one. I need these quantity fields to have bidirectional binding with an array of orderItem that I return from and post to the server.
Currently I can get the oc.order.orderItems array to update with new orderItem when I change the quantity field of a product. But when I try to populate a previous order, I am having trouble updating the model and view. Majority of my issue stems from how I should handle the ngModel attribute of the quantity field tied with my directive.
Here are some of my code samples (slightly stripped down). If anything needs clarification to help with the assist, because I know this sounds confusing, please let me know.
Product Model:
var product = { id: 1, category_id: 1, name: 'Apple' };
OrderItem Model:
var orderItem = { id: 1, order_id: 1, product_id: 1, quantity: 2 };
View:
<div ng-repeat="category in oc.categories">
<h2>{{ category.name }}<h2>
<div ng-repeat="product in category.products">
<h3>{{ product.name }}</h3>
<input type="number" placeholder="Quantity"
order-item="product" ng-model="oc.order.orderItems[$index]" />
</div>
</div>
Directive:
angular
.module('OrderApp')
.directive('orderItem', function ($window) {
return {
restrict: 'A',
scope : {
product: '=orderItem'
},
require: 'ngModel',
link: function (scope, element, attrs, ngModelCtrl) {
ngModelCtrl.$parsers.push(function (viewValue) {
if (!viewValue > 0) return;
if (!scope.id) scope.id = 0;
if (!scope.order_id) scope.order_id = 0;
return {
id: scope.id,
order_id: scope.order_id,
quantity: viewValue,
product_id: scope.product.id
};
});
ngModelCtrl.$formatters.push(function (modelValue) {
if (!modelValue) return;
if (!modelValue.id) scope.id = modelValue.id;
if (!modelValue.order_id) scope.order_id = modelValue.order_id;
return modelValue.quantity;
});
}
};
});
(Directive doesn't seem right at all and don't know where to put a view $render and scope $watch)
After looking at the problem differently, I found that my solution should not have been a directive but actually a service to share the data for each model between controllers.
Solution: Gist
Related
I have a form with multiple currency value inputfields and I have a directive which restricts the currency value to be entered as something like space for example USD 100.But I need to split the model value and seperate the currency and number and store it in the same model object.
So , USD 100 can be splitted into, USD , 100 as integer
$scope.formData = {
Fee : "USD 100"
}
this should become as
$scope.formData = {
fee : 100,
feeCurrency : USD
}
there are multiple fields in the form along with other type of data also.Please help me with any thoughts where I can implement this with service or directive so that I can easily apply to all the similar fields.
Here is one way you could do it with a directive. I've made a lot of assumptions here and this is not production worthy code, but it should give you a base to start from. Basically you pass the object to the directive and then watch a property on that object for changes. When the property changes you can split on a space character and fill in the appropriate values on the object. Since it's a directive you can use it anywhere.
js
angular.module('app', [])
.controller('ctrl', function($scope) {
$scope.feeData = {
fee: 0,
feeCurrency: 'N/A',
value: ''
};
})
.directive('feeDirective', function() {
return {
restrict: 'A',
scope: {
feeData: '='
},
link: function(scope, element, attrs) {
scope.$watch('feeData.value', function(newValue, oldValue) {
console.log(newValue);
var data = newValue.split(' ');
if (data.length === 2) {
console.log(data);
scope.feeData.fee = data[0];
scope.feeData.feeCurrency = data[1];
}
}, true);
}
}
});
html
<div ng-app="app" ng-controller="ctrl">
<div>
<input type="text" ng-model="feeData.value" fee-directive fee-data="feeData">
</div>
<div>
Value: {{feeData.value}}
<br/> Fee: {{feeData.fee}}
<br/> Currency: {{feeData.feeCurrency}}
</div>
</div>
Working JSFiddle that illustrates the above code.
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>'
}
});
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
Could you help me to find a way to set and get select element values which are placed inside my custom directive.
This is what I have:
<body ng-app="myApp">
<div ng-controller="MyCtrl">
<div ng-repeat="category in categories">
{{category.Name}}:
<status-selector items="statuses" ng-model="selectedStates[status.Id]"></status-selector>
</div>
</div>
</body>
I have two arrays: categories and statuses. Each category can have its own status. When a status for a category is chosen, it should be saved to selectedStatus to finally have something like [{CategoryId:1,StatusId:2},{CategoryId:2,StatusId:3}]. In case if selectedStatus was already initialized, I would like to see chosen statuses for corresponding categories, means that I also need to put values, not just read them.
myApp
.controller("MyCtrl", function($scope) {
$scope.categories = [{Id:1, Name: "Category1"}, {Id: 2, Name: "Category2"}];
$scope.statuses = [{Id: 1, Name: "Low"}, {Id: 2, Name: "Normal"}, {Id: 3, Name: "High"}]
$scope.selectedStates = {};
})
.directive('statusSelector', function() {
return {
restrict: 'E',
replace: true,
scope: { items: '=', ngModel: '='},
template: '<span><select class="select" ng-options="obj.Id as obj.Name for obj in items" ng-model="ngModel"></select></span>',
link: function(scope, element, attrs) {
}
}
});
Thank you.
Demo: Fiddle
You should have your own category model as ng-model. It probably make more sense.
<status-selector items="statuses" ng-model="category.Status"></status-selector>
To set the status, just bring the Status filled in the JSON.
// ...
.controller("MyCtrl", function($scope) {
$scope.categories = [{Id:1, Name: "Category1", Status: 1}, {Id: 2, Name: "Category2"}];
// ...
Obviouslly, to get user's selection, just grab the category.Status property. Here's the updated fiddle.
Also, just a side tip. You seem to come from a .Net background, where you're used to Pascal case. In javascript, it's common sense to use camel case, instead, so you'd have {id: ..., status: ...} instead of {Id: ..., Status: ...}.
After applying orderBy in my list of input boxes if i edit any field it starts sorting immediately and i loose focus. I tried to dug in the angular code and found out they have applied something like $watch on the orderBy's attributes therefore whenever the value changes it sorts the list.Is there any way to disable orderBy while editing? I dont want to let the orderBy sort data while editing text. Any help will be appreciated
Here is my plunker
Note: I want to use orderBy & don't need any alternative like sorting the list from any controller itself. I just want the orderBy to sort the list once on page load and then remain quite.
I was using orderBy on a list that could be re-ordered using angular-sortable, and ran into a similar issue.
My solution was to perform the ordering manually, by calling the orderBy filter inside the controller when the page was initialised, and then calling it subsequently when necessary, for example in a callback function after re-ordering the list.
You could override the directive to change the moment of the update to the moment you wish the reordering. You could also just not use ng-model and rely on a custom directive.
This thread discuss overriding the input directive to change the model update to be triggered by tge blur event. Take a look at the fiddle.
Although you might override the directive, you shouldn't do this, and the best solution, as explained and exemplified by #Liviu T. in the comments below would be to create a custom directive that removes the event keyup binding and adds a blur one. Here is directive code, and here is Liviu's plunker:
app.directive('modelChangeBlur', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elm, attr, ngModelCtrl) {
if (attr.type === 'radio' || attr.type === 'checkbox') return;
elm.unbind('input').unbind('keydown').unbind('change');
elm.bind('blur', function() {
scope.$apply(function() {
ngModelCtrl.$setViewValue(elm.val());
});
});
}
};
});
<input type="text" ng-model="variable" model-change-blur/>
Unfortunately, as Angular events are not namespaces, you will have to remove any previously added event.
A different approach may be to not loose focus to begin with. If your main problem is that your loosing focus, then instead of disabling orderBy, add this directive to your input:
app.directive("keepFocus", ['$timeout', function ($timeout) {
/*
Intended use:
<input keep-focus ng-model='someModel.value'></input>
*/
return {
restrict: 'A',
require: 'ngModel',
link: function ($scope, $element, attrs, ngModel) {
ngModel.$parsers.unshift(function (value) {
$timeout(function () {
$element[0].focus();
});
return value;
});
}
};
}])
Then just:
<input keep-focus ng-model="item.name"/>
I know this does not directly answer you question, but it might help solve the underlying problem.
Following #CaioToOn works but it breaks in Angular 1.2.4 (possibly the whole 1.2.X branch). To get it to work you also need to declare the priority of the custom directive to be 1 (the input directive is 0).
The directive then becomes:
app.directive('modelChangeBlur', function() {
return {
restrict: 'A',
priority: 1,
require: 'ngModel',
link: function(scope, elm, attr, ngModelCtrl) {
if (attr.type === 'radio' || attr.type === 'checkbox') return;
elm.unbind('input').unbind('keydown').unbind('change');
elm.bind('blur', function() {
scope.$apply(function() {
ngModelCtrl.$setViewValue(elm.val());
});
});
}
};
});
See http://plnkr.co/edit/rOIH7W5oRrijv46f4d9R?p=preview and https://stackoverflow.com/a/19961284/1196057 for related priority fix.
There's no easy or straightforward way to accomplish what you want, but there are some options. You would have to work without ng-model, and instead use the blur event:
JS:
var app = angular.module('myapp', []);
app.controller('MainCtrl', function($scope) {
$scope.items = [
{ name: 'foo', id: 1, eligible: true },
{ name: 'bar', id: 2, eligible: false },
{ name: 'test', id: 3, eligible: true }
];
$scope.onBlur = function($event, item){
// Here you find the target node in your 'items' array and update it with new value
_($scope.items).findWhere({id: item.id}).name = $event.currentTarget.value;
};
});
app.directive('ngBlur', function($parse) {
return function ( scope, element, attr ) {
var fn = $parse(attr.ngBlur);
element.bind( 'blur', function ( event, arg ) {
scope.$apply( function(){
fn(scope, {
$event : event,
arg: arg
});
});
});
};
});
HTML:
<div ng-controller="MainCtrl">
<div ng-repeat="item in items | orderBy:'name'" >
<input value="{{item.name}}" ng-blur="onBlur($event, item)"/>
</div>
</div>
Plunker.
But be aware that this would break the two-way binding between the model and the view.
You can create custom filter and call that only when necessary. Example when you click on 'Grid header' for sorting or after dynamically adding/removing values to array, or simply click of a button(Refresh Grid)
You need to dependency Inject Angular filter and sort filter
angular
.module('MyModule')
.controller('MyController', ['filterFilter', '$filter', MyContFunc])
function ExpenseSubmitter(funcAngularFilter, funcAngularFilterOrderBy) {
oCont = this;
oCont.ArrayOfData = [{
name: 'RackBar',
age: 24
}, {
name: 'BamaO',
age: 48
}];
oCont.sortOnColumn = 'age';
oCont.orderBy = false;
var SearchObj = {
name: 'Bama'
};
oCont.RefreshGrid = function() {
oCont.ArrayOfData = funcAngularFilter(oCont.ArrayOfData, SearchObj);
oCont.ArrayOfData = funcAngularFilterOrderBy('orderBy')(oCont.ArrayOfData, oCont.sortOnColumn, oCont.orderBy);
}
}
and call in HTML something like:
<table>
<thead>
<tr>
<th ng-click="oCont.sortOnColumn = 'age'; oCont.RefreshGrid()">Age</th>
<th ng-click="oCont.sortOnColumn = 'name'; oCont.RefreshGrid()">Name</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="val in oCont.ArrayOfData">
<td>{{val.age}}</td>
<td>{{val.name}}</td>
</tr>
</tbody>
</table>
You can freeze the current ordering while you are editing. Say your html looks like this:
<tbody ng-repeat="item in items | orderBy:orderBy:reverse">
<tr ng-click="startEdit()">
<td>{{item.name}}</td>
</tr>
</tbody>
In your controller you write:
var savedOrderBy, savedReverse;
$scope.startEdit() = function() {
$scope.items = $filter('orderBy')($scope.items, $scope.orderby, $scope.reverse);
for (var i = 0; i < $scope.items.length; ++i) {
if (i < 9999) {
$scope.items[i]['pos'] = ("000" + i).slice(-4);
}
}
savedOrderBy = $scope.orderBy;
savedReverse = $scope.reverse;
$scope.orderBy = 'pos';
$scope.reverse = false;
};
Before the user starts editing, you first sort the current items in exactly the same order that they currently appear in the page. You do that by calling the orderBy $filter() with the current sorting parameters.
Then you go over your - now sorted - items, and add an arbitrary property (here "pos") and set it to the current position. I zero-pad it so that position 0002 collates before 0011. Maybe that is not necessary, no idea.
You normally want to remember the current ordering, here in the scope variables "savedOrder" and "savedReverse".
And finally you tell angular to sort by that new property "pos" and voilĂ the table order is frozen, because that property simply does not change while editing.
When you are done editing, you have to do the opposite. You restore the old ordering from the scope variables "savedOrder" and "savedReverse":
$scope.endEdit = function() {
$scope.orderBy = savedOrderBy;
$scope.reverse = reverse;
};
If the order of the $scope.items array matters for you, you would also have to sort it again to its original ordering.
Try using a scope variable to change the order. In this case, when you are going to order, you call a function in your controller that changes the variable order value to the field you want to order by, and when you are editing, you reset it.
Example:
<li ng-repeat="item in filtered = (items | filter:search) | orderBy:order:rever" >
So here we have the variable "order" to tell the ng-repeat by which field the list must be ordered. A button calls a function in the controller that changes the "order" value.
<button type="button" id="orderName" click="changeOrder('item.name')" >Name order </button>
<button type="button" id="orderDate" click="changeOrder('item.date')" >Date order </button>`
And then, in the changeOrder function
$scope.order = param;
Where 'param' is the field you want to order by. If you don't do anything else, you are going to have the same problem you had, so then, after you have assigned the correct value to the order variable you go
$scope.order = "";
Which resets the ordering.
The result is that the ordering is just going to be effective when the button is pressed and then it wont order again unless you press the button again.
This can be changed so it orders again when, for example, you have finished editing an item, as you wanted.