Angularjs addmore directive and scope management - angularjs

I am trying to create a directive which has addmore button.
In the controller I have a tickets obj which is empty. I am trying to add new tickets (multiple) to this obj while clicking "Add Tickets".
What I need is
Each ticket should increment by 1.
The ticket price should be updated in tickets array when a price is
updated in input box (two way binding).
Should be able to update the quantity from controller or directive
which should update in quantity input (one way binding).
I have created a plunker , any help would be appreciated, have spend lot of time in this.

Fancy solution with components
I forked your plunker here with a component version.
When you can, I recommand you the use of components over directives.
I splitted it in two components, that's way easier to separate the functionnalities. One components for the list, and an other for one item representation.
tickets component
It handles the ng-repeat on components, the add button and the sum functions.
app.component('tickets', {
template: '<div ng-repeat="ticket in $ctrl.tickets">#{{ticket.id}}<ticket ng-change="$ctrl.recompute()" assign="$ctrl.assign" ng-model="ticket"/></div><hr><button ng-click="$ctrl.addOne()">Add One</button><br>Total price : {{$ctrl.price}}<br>Total quantity : {{$ctrl.quantities}}',
controller: class Ctrl {
$onInit() {
this.tickets = [];
this.price = 0;
this.quantities = 0;
}
count() {
return this.tickets.length;
}
addOne() {
this.tickets.push({
id: this.count(),
price: 0,
color: 'red',
quantity: 1
});
this.recompute();
}
recompute() {
this.price = 0;
this.quantities = 0;
this.tickets.forEach((ticket) => {
this.price += ticket.price;
this.quantities += ticket.quantity;
});
}
assign(ticket) {
console.log("ticket Assigned : ", ticket);
}
}
});
ticket component
It handles the representation and actions for one ticket.
app.component('ticket', {
template: '<div><label>Quantity</label><input type="number" ng-model="$ctrl.ticket.quantity" ng-change="$ctrl.ngChange"/><br><label>Price</label><input type="number" ng-model="$ctrl.ticket.price" ng-change="$ctrl.ngChange"/><br><button ng-click="$ctrl.assignMe()">Assign</button></div>',
bindings: {
ngModel: '=',
ngChange: '<',
assign: '<'
},
controller: class Ctrl {
$onInit() {
this.ticket = this.ngModel;
}
assignMe() {
this.assign(this.ticket);
}
addOne() {
this.tickets.push({
price: 0,
color: 'red',
quentity: 0
});
}
}
});
Let me now if you want a version with directive.

Related

Printing shopping cart total price

Hello I am trying to teach myself angular framework.
I have built a simple shopping cart where the products can be added to the basket by using a tutorial. The problem I am having is that I can't seem to get the total price to show. any one who could show me the direction to go in or what to do would be greatly appreciated. At the moment my total cost function has the same function as the total number of objects as price so I need to change the total cost function but wouldn't know what to attempt.
I have shared my full project below rather than paste a lot of code.
also two bonus questions, currently I have an array of items. how would I approach removing the items and calling them from an API instead?
and how would i add a 10% discount should the user have three or more items in the basket?
shopping-cart.component.ts
import { Component, Input, Output, EventEmitter } from '#angular/core';
#Component({
selector: 'shopping-cart',
template: `
<h1>Shopping Cart ({{products.length}})</h1>
<h2>Total Cost £{{calcTotal()}}</h2>
<cart-product *ngFor="let product of products" [product]="product" (productRemoved)="removeProduct($event)"><cart-product>
`,
})
export class ShoppingCartComponent {
#Input() products: any[] = [];
#Output() productRemoved = new EventEmitter();
calcTotal() {
if (!this.isArrayEmpty()) {
if (this.products.length < 2) {
const targetProduct = this.products[0];
return targetProduct.price * targetProduct.num;
} else {
return this.products.reduce((a, b) => (a.num * a.price) + (b.num * b.price));
}
} else {
return 0;
}
}
isArrayEmpty(): boolean {
return this.products.length < 1;
}
removeProduct(product) {
this.productRemoved.emit(product)
}
}
app.component.ts
import { Component } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
productList = [
{name: 'Array Item One', price: 10},
{name: 'Array Item Two', price: 20},
{name: 'Array Item Three', price: 30}
];
cartProductList = [];
addProductToCart(product) {
const productExistInCart = this.cartProductList.find((productInCart) => productInCart.name === product.name);
if (!productExistInCart) {
this.cartProductList.push({...product, num:1});
return;
}
productExistInCart.num += 1;
console.log(this.cartProductList);
}
removeProduct(product) {
this.cartProductList = this.cartProductList.filter(({name}) => name !== product.name)
}
}
If you want to show the total price, you will just have to introduce some changes to calcPrice methode inside shopping-cart.component.ts.
instead of just returning the number of products, you return the number of products (for each product) multiplied by the price of each product, hence prod.price * prod.num
just like the following:
calcPrice() {
return this.products.reduce((acc, prod) => acc+= prod.price * prod.num ,0)
}
If you want to add a discount to your total price, you will have to introduce more change to calcPrice methode again.
instead of returning the total price of products inside the cart, you first check if there is 3 or more products in it and apply the discount if true, otherwise, if false, your just return the normal price with no discount.
just like the following:
calcPrice() {
return this.products.reduce((acc, prod) => {
if (this.calcTotal() >= 3) {
// 0.9 means 90% of the total price which make it a 10% discount
return acc+= prod.price * prod.num * 0.9
} else {
return acc+= prod.price * prod.num
}
},0)
}
If you want to retrieve products from an API, you will have to use Angular's HttpClientModule, here you can find a link to a beginner friendly tutorial on how to use it.

animating number change in directive in angular

I have a directive which I have included jquery's animate functionality in to. I'd like for a particular variable's number to change with easing animation. The issue is that then the directive loads, the initial number is shown but doesn't show the number changing with the animation effect.
I have created a similar version in Plunkr to make it easy to see what's going on.
If I trigger $apply() from elsewhere the final numbers show, skipping the whole animated sqeuqnce of numbers. Also, in the code when I try to do apply on each step, it throws an 'in progress' error.
This plugin almost does what I need it to, except that it doesn't increment over decimal places and doesn't use easing. http://sparkalow.github.io/angular-count-to/
scope.$watch('difference', function(newVal, oldVal) {
jQuery({someValue: oldVal}).animate({someValue: newVal}, {
duration: 1000,
easing:'swing',
step: function(e) {
scope.display = e.toFixed(2);
scope.$parent.$apply();
}
});
});
and..
template: function(scope, element, attrs) {
return '<h3>' +
'<i class="fa progress-arrow" ng-class="[{\'fa-caret-up\': direction_up}, {\'fa-caret-down\': direction_down}]" aria-hidden="true"></i> ' +
'{{ display }}' +
'</div>' +
'</h3>' +
'<label>{{ label }} (lbs)</label>';
The answer was to use the angular $timeout function in conjunction with scope.$apply().
Here's the updated code that does in fact work:
scope.$watch('difference', function(newVal, oldVal) {
jQuery({someValue: oldVal}).animate({someValue: newVal}, {
duration: 500,
easing:'swing',
step: function(e) {
$timeout(function () {
scope.$apply(function () {
scope.display = e.toFixed(2);
});
});
}
});
And here it is in Plunkr
create directive
export class IncrementCounterDirective implements AfterViewInit {
#Input('appIncrementCounter') to: number = 0;
constructor(private elRef: ElementRef, private renderer: Renderer2) {}
ngAfterViewInit(): void {
this.counterFunc(this.to, 2000);
}
private counterFunc(end: number, duration: number = 3000) {
let range, current: number, step, timer: any;
range = end - 0;
current = end - 150;
step = Math.abs(Math.floor(duration / range));
// console.log(`step`, step);
timer = setInterval(() => {
current += 1;
this.setText(current);
if (current >= end) {
clearInterval(timer);
}
}, step);
}
setText(n: number) {
this.renderer.setProperty(this.elRef.nativeElement, 'innerText', `${n}`);
}
}
To use
<h3 class="stat-count" [appIncrementCounter]="607">000</h3>

How can I display one attribute of a set of data just once?

I'm doing some testing with Angular to see if I can replicate what I already have in PHP more efficiently.
I have a set of data stored in JSON:
[
{
"name":"Blue Widget",
"description":"blue-widget",
"snippet":"The best blue widget around!",
"category":"Home Widgets",
"popular":true
},
{
"name":"Red Widget",
"description":"red-widget",
"snippet":"The best red widget around!",
"category":"Outdoor Widgets",
"popular":true
},
{
"name":"Green Widget",
"description":"green-widget",
"snippet":"The best green widget around!",
"category":"Work Widgets",
"popular":true
},
{
"name":"Yellow Widget",
"description":"yellow-widget",
"snippet":"The best yellow widget around!",
"category":"Home Widgets",
"popular":true
}
]
I'm grabbing this in my controller and adding it to my view in a fairly standard way (yes, I know not to use $http directly in a controller in production):
widgetApp.controller('widgetListCtrl', function($scope,$http){
$http.get('widgets/widgets.json').success(function(data){
$scope.widgets = data
})
})
If I use:
<li ng-repeat="widget in widgets">{{widget.category}}</li>
Then naturally it will just go through and list:
Home Widgets
Outdoor Widgets
Work Widgets
Home Widgets
What I'd like to do is generate a list of each widget.category but with each category only appearing once, so a user could then click on a category and be shown all the widgets in that category. How can I go about this? Sorry, I haven't got anything to go on because I pretty much have no idea where to start.
You can use the existing 'unique' filter from AngularUI.
<li ng-repeat="widget in widgets | unique: 'widget.category' ">{{widget.category}}</li>
Be sure to include a reference to the filters module in your app as well (e.g. angular.module('yourModule', ['ui', 'ui.filters']);).
You'd have to build a list of unique categories:
widgetApp.controller('widgetListCtrl', function($scope,$http){
$http.get('widgets/widgets.json').success(function(data){
$scope.uniqueCategories = [];
for (var i = 0; i < $scope.widgets.length; i++) {
if ($scope.uniqueCategories.indexOf($scope.widgets[i].category) === -1)
$scope.uniqueCategories.push($scope.widgets[i].category);
}
});
});
Make a dropdown with the model set to the category:
<select ng-model="categoryFilter" ng-options="category as category for category in uniqueCategories"></select>
And use a filter on your repeat:
<li ng-repeat="widget in widgets | filter: { category: categoryFilter }">{{widget.category}}</li>
Create a filter
app.filter('unique', function() {
return function (arr, field) {
return _.uniq(arr, function(a) { return a[field]; });
};
});
In Markup
<li ng-repeat="widget in widgets | unique:'category'">{{widget.category}}</li>
Create a distinct filiter and use it on your view:
angular.filter("distinct", function () {
return function (data, propertyName) {
if (angular.isArray(data) && angular.isString(propertyName)) {
var results = [];
var keys = {};
for (var i = 0; i < data.length; i++) {
var val = data[i][propertyName];
if (angular.isUndefined(keys[val]) && val != null) {
keys[val] = true;
results.push(val);
};
};
return results;
}
else {
return data;
}
}
})
<li ng-repeat="widget in widgets | distinct:'category'">{{widget.category}}</li>

select box : display text 'error' if value not exist in array

I have a key value pair defined as below, which is being used for select using ng-options
$scope.BucketEnum = [
{ display: 'Error', value: 0 },
{ display: '1', value: 1 },
{ display: '2', value: 2 },
{ display: '3', value: 3 },
{ display: '4', value: 4 },
{ display: '5', value: 5 },
{ display: 'Flows', value: 125 },
{ display: 'Recovery', value: 151 }
];
I am using this key value pair to display select box in ng-options
<select ng-model="selectedBucket" ng-options="row.value as rows.display for row in BucketEnum" multiple="multiple" ></select>
now if I set ng-model i.e. $scope.selectedBucket = 10, I want to display the text Error. Is it possible to show value Error for all the values which are not there in $scope.BucketEnum array.
NOTE
I am looking at a more generic way to do this e.g a filter for doing this
SCENARIO
There is certain historical data in database, which has some garbage and some good data.
For each garbage value, i need to show the current garbage value as well as the valid values to select from, so for the end users to fix it.
Would this fit your needs ?
jsfiddle
app.filter('bootstrapValues', function(){
return function(initial, baseBucket){
var result = [];
for(var i=0; i<initial.length; i++){
var flag = false;
for(var j=1; j<baseBucket.length; j++){ //from 1 or 0.. you call
if(initial[i] === baseBucket[j].value){
flag = true;
result.push(baseBucket[j]);
break; // if there are repeated elements
}
}
if(!flag)
result.push(baseBucket[0])
}
return result;
};
});
Using it to start the selectedBucket, in your controller:
// setting initials
$scope.selectedBucket = $filter('bootstrapValues')(initialSet, $scope.bucketEnum);
Does it help?
Edit: Here is other jsfiddle with little modifications, if the value is not in the bucket it add the element to the list with Error display and as a selected value.
Using ng-options generates multiple HTML <select> elements for each item in your BucketEnum array and 'returns' the selected value in your ng-model variable: selectedBucket. I think the only way to display the options without an additional blank entry is to ensure the value of selectedBucket is a valid entry in BucketEnum.
Your question states:
if I set ng-model i.e. $scope.selectedBucket = 10, I want to display
the text Error.
I assume you want to display the value: {{BucketEnum[selectedBucket].display}}
So... starting with $scope.selectedBucket = 10, we want some generic way of implementing a select using ng-options which will reset this value to a default.
You could do this by implementing an attribute directive, allowing you to write:
<select ng-model="selectedBucket" select-default="BucketEnum"
ng-options="row.value as row.display for row in BucketEnum"
multiple="multiple">
An example of this approach is shown below. Note that this assumes the default value is zero and does not handle multiple selections (you'd have to iterate over the selections when comparing to each item in BucketEnum and decide what to do if there is a mix of valid and invalid selections).
app.directive("selectDefault",function(){
return{
restrict: 'A',
scope: false,
link:function(scope,element,attrs){
var arr= scope[attrs.selectDefault]; // array from attribute
scope.$watch(attrs.ngModel,function(){
var i, ok=false;
var sel= scope[attrs.ngModel]; // ng-model variable
for( i=0; i<arr.length; i++){ // variable in array ?
if( arr[i].value == sel ) // nasty '==' only for demo
ok= true;
}
if( ! ok )
scope[attrs.ngModel]=0; // set selectedBucket to 0
});
}
};
});
I've run up a jsfiddle of this here
The downside of this is that I've used a $watch on the ng-model which causes side-effects, i.e. any assignment of the named variable will trigger the $watch function.
If this is the sort of solution you were looking for, you could expand the directive in all sorts of ways, for example:
<select ng-model="selectResult"
select-default="99" array="BucketEnum" initial="selectedBucket"
ng-options="row.value as row.display for row in BucketEnum"
multiple="multiple">
...the idea being that the select-default directive would read the default value ("99" here), the array and an initial value then set selectResult accordingly
You would need to code for this explicitly. Scan the choices you want to set against the choices that are present. If you don't find it, select the Error value too.
Note also that you need to pass an array for selectedBucket and it needs to include the actual option objects not just the values inside them.
<div ng-app="myApp">
<div ng-controller="myController">
<p>Select something</p>
<select ng-model="selectedBucket"
ng-options="row as row.display for row in bucketEnum" multiple="multiple">
</select>
</div>
</div>
.
var app = angular.module('myApp', []);
app.controller('myController', function ($scope) {
var initialSet = [1, 5, 10];
$scope.bucketEnum = [
{ display: 'Error', value: 0 },
{ display: '1', value: 1 },
{ display: '2', value: 2 },
{ display: '3', value: 3 },
{ display: '4', value: 4 },
{ display: '5', value: 5 },
{ display: 'Flows', value: 125 },
{ display: 'Recovery', value: 151 }
];
var selected = [];
var error = $scope.bucketEnum[0];
angular.forEach(initialSet, function(item) {
var found;
angular.forEach($scope.bucketEnum, function (e) {
if (+item == +e.value) {
console.log('Found ', e);
found = item;
selected.push(e);
}
});
if (typeof found === 'undefined') {
selected.push(error);
}
$scope.selectedBucket = selected;
console.log(selected);
});
});

kendo ui get id of checkbox when unchecked

i am using kendo ui tree view with check box
i want the check box's id when it is getting unchecked
this is kendo ui mine code
// var homogeneous contains data
$("#treeview").kendoTreeView({
checkboxes: {
checkChildren: false,
template:"# if(!item.hasChildren){# <input type='hidden' id='#=item.id#' parent_id='#=item.parent_id#' d_text='#=item.value#'/> <input type='checkbox' id_a='#= item.id #' name='c_#= item.id #' value='true' />#}else{# <div id='#=item.id#' style='display:none;' parent_id='#=item.parent_id#' d_text='#=item.value#'/> #}#",
},
dataSource: homogeneous,
dataBound: ondata,
dataTextField: "value"
});
function ondata() {
//alert("databound");
}
// function that gathers IDs of checked nodes
function checkedNodeIds(nodes, checkedNodes) {
//console.log(nodes);
for (var i = 0; i < nodes.length; i++) {
if (nodes[i].checked) {
checkedNodes.push(nodes[i].id);
}
if (nodes[i].hasChildren) {
checkedNodeIds(nodes[i].children.view(), checkedNodes);
}
}
}
// show checked node IDs on datasource change
$("#treeview").data("kendoTreeView").dataSource.bind("change", function() {
var checkedNodes = [],
treeView = $("#treeview").data("kendoTreeView"),
message;
checkedNodeIds(treeView.dataSource.view(), checkedNodes);
if (checkedNodes.length > 0) {
message = "IDs of checked nodes: " + checkedNodes.join(",");
} else {
message = "No nodes checked.";
}
$("#result").html(message);
});
in this code i am not getting checkbox's id when it is unchecked so i have tried this
jquery code
$('input[type=checkbox]').click(function() {
if($(this).is(':checked')) {
alert('checked');
} else {
alert('not checked');
}
});
this code is only working in js fiddle but not in my case http://jsfiddle.net/NPUeL/
if i use this code then i can get the number of count but i dont know how to use it
var treeview = $("[data-role=treeview]").data("kendoTreeView");
treeview.dataSource.bind("change", function (e) {
if (e.field == "checked") {
console.log("Recorded Selected: " + $("[data-role=treeview] :checked").length);
}
});
what changed i need to do in data source so i can get id
thanks in adavance
If you want to get the id you might do:
$('input[type=checkbox]').click(function (e) {
var li = $(e.target).closest("li");
var id = $("input:hidden", li).attr("id");
var node = treeView.dataSource.get(id);
if (node.checked) {
console.log('checked');
} else {
console.log('not checked');
}
});
What I do in the event handler is:
find the closest li element that is the node of the tree that has been clicked.
the id is in an HTML input element that is hidden (this is the way that I understood that you have stored it).
Get item from dataSource using dataSource.get method.
See your code modified and running here
i made the small change and its working now
function ondata() {
$('input[type=checkbox]').click(function() {
if($(this).is(':checked')) {
alert('checked');
} else {
alert('not checked');
}
});
}

Resources