I am developing an online store for a client using Angular/Spring Boot. To keep things simple and to the point, I am storing the users shopping cart data in localStorage which is working fine. However, for the shopping cart to be updated with the users products, I have to refresh the page. I would like the app to just update without having to refresh the page. Im sure this is probably very trivial, but I cant get it to work!
I have tried looking at a few different sources :
Refreshing Page with Angular
Angular Source
I have tried using the this.router.routeReuseStrategy.shouldReuseRoute = function () { return false; }; within the components constructor but again didnt have any luck.
The main bits of code are as follows:
CartService - On construction we fetch the data from localStorage:
constructor(private http: HttpServiceService) {
this.getCartDetailsByUser();
}
getCartDetailsByUser() {
let data = JSON.parse(localStorage.getItem("products"));
this.cartObj = data;
if (data !== null) {
this.cartQty = data.length;
console.log(data);
}
}
When the user clicks "Add to Cart", this function is called, which adds the data to the local storage, but i need to update the DOM with whats in the cart and the cart length!
addCart(product) {
let products = [];
console.log(product);
if (localStorage.getItem("products")) {
products = JSON.parse(localStorage.getItem("products"));
}
products.push({
productId: product.productId,
image: product.image,
price: product.price,
});
localStorage.setItem("products", JSON.stringify(products));
}
I dont want to manually refresh the whole page for something so simple, I would just like the DOM to update with no need to refresh.
The header.component.html is what displays the shopping cart with its quantity etc
<div class="cart cart box_1 checkout-count-wrap">
<form action="#" method="post" class="last">
<button
class="w3view-cart"
type="submit"
(click)="openCheckoutModel()"
name="submit"
value=""
>
<p class="total_count_checkout">{{cart_qty}}</p>
<i class="fa fa-cart-arrow-down" aria-hidden="true"></i>
</button>
</form>
</div>
With the linked .ts class (header.component.ts). In the constructor, I get the qty which gets passed to the html above.
constructor(
private router: Router,
private cartService: CartServiceService,
private http: HttpServiceService
) {
this.cartService.cartServiceEvent.subscribe((data) => {
this.cart_qty = this.cartService.getQty();
});
}
When the user clicks on the shopping cart on the DOM, the following code is executed which gives a popup with the products etc in the cart:
openCheckoutModel() {
this.cartObj = this.cartService.getCartOBj();
this.cartTotalPrice = this.cartService.cartTotalPrice;
this.mainDialogType = "checkout";
}
Which in turn then displays the data in the shopping cart.
ALL of this works correctly apart from the cart not updating on the fly!!
Any help would be greatly appreciated!! :D
**** EDITED ANSWER ****
OK, so from the answer on the question, I have made a few small changes with the Observable pattern.. This half works - It updates the quantity of the cart on the fly (incrementing the number + 1 when a user clicks 'Add to Cart'). It also stores the product in the json object in localStorage as it did before. However, the actual items in the cart now do not show on the DOM, where as they did before. The data definitely exists with the correct products added being stored in localStorage, but now there seems to be some issues with the DOM displaying what's in it (productName, price etc)
Will this be due to another Observable being needed to track the item data in the cart? This may become clearer when I share my code (full classes)..
So here is the checkout-component.ts with the method being highlighted with *****
import { Component, OnInit } from "#angular/core";
import { CartServiceService } from "../service/cart-service.service";
import { HttpServiceService } from "../http-service.service";
import { Router } from "#angular/router";
#Component({
selector: "app-checkout",
templateUrl: "./checkout.component.html",
styleUrls: ["./checkout.component.css"],
})
export class CheckoutComponent implements OnInit {
cartObj = [];
cartTotalPrice: any;
pay_type = "cash_on_delivery";
delivery_address = "";
constructor(
private router: Router,
private cartService: CartServiceService,
private http: HttpServiceService
) {}
ngOnInit() {
this.getCartDetailsByUser();
//below function will be triggerd from when removing and qty is changing..
this.cartService.cartServiceEvent.subscribe((data) => {
this.cartObj = this.cartService.getCartOBj();
this.cartTotalPrice = this.cartService.cartTotalPrice;
});
}
qtyChange(qty, cartObj) {
var request = {
cartId: cartObj.id,
quantity: qty,
price: cartObj.price * qty,
};
this.http
.postRequestWithToken("api/addtocart/updateQtyForCart", request)
.subscribe(
(data: any) => {
this.cartService.getCartDetailsByUser(); //for updating in the application..
},
(error) => {
alert("Error while fetching the cart Details");
}
);
}
getCartDetailsByUser() {
let data = JSON.parse(localStorage.getItem("products"));
this.cartObj = data;
this.cartTotalPrice = this.getTotalAmounOfTheCart();
console.log("Cart Obj", this.cartObj);
console.log("Total", this.cartTotalPrice);
}
// getCartDetailsByUser(){
// this.http.postRequestWithToken("api/addtocart/getCartsByUserId",{}).subscribe((data:any)=>{
// this.cartObj = data;
// this.cartTotalPrice = this.getTotalAmounOfTheCart();
// },error=>{
// alert("Error while fetching the cart Details");
// })
// }
getTotalAmounOfTheCart() {
let obj = this.cartObj;
let totalPrice = 0;
for (var o in obj) {
totalPrice = totalPrice + parseFloat(obj[o].price);
}
return totalPrice.toFixed(2);
}
removeCartById(cartObj) {
if (confirm("Are you sure want to delete..?")) {
let id = cartObj.id;
this.cartService.removeCart(id);
}
}
checkoutCart() {
if (this.delivery_address == "") {
alert("Delivery address should not be empty");
return;
}
if (this.pay_type == "cash_on_delivery") {
let request = {
total_price: this.cartTotalPrice,
pay_type: "COD",
deliveryAddress: this.delivery_address,
};
this.http
.postRequestWithToken("api/order/checkout_order", request)
.subscribe(
(data: any) => {
alert("checkout process completed.Your Order is processed..");
this.cartService.getCartDetailsByUser();
this.router.navigate([""]);
},
(error) => {
alert("Error while fetching the cart Details");
}
);
} else {
alert("Payment Integration is not yet completed.");
}
}
}
Then the corresponding checkout-component.html
<div style="display: block;" id="w3lssbmincart">
<ul>
<li *ngFor="let cart of cartObj" class="sbmincart-item sbmincart-item-changed">
<div class="sbmincart-details-name">
<a class="sbmincart-name">{{cart.name}}</a>
</div>
<div class="sbmincart-details-quantity">
<select [(ngModel)]="cart.qty" (change)="qtyChange($event.target.value,cart)">
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
<option>5</option>
<option>6</option>
<option>7</option>
<option>8</option>
<option>9</option>
<option>10</option>
</select>
</div>
<div class="sbmincart-details-remove">
<button (click)="removeCartById(cart)" type="button" class="sbmincart-remove" data-sbmincart-idx="0">×</button>
</div>
<div class="sbmincart-details-price">
<span class="sbmincart-price">{{cart.price}}</span>
</div>
</li>
</ul>
<div class="sbmincart-footer">
<div class="sbmincart-subtotal radio-wrap">
<span><input [(ngModel)]="pay_type" value="cash_on_delivery" type="radio" name="pay_type" /><span class="radio_text">Cash on Delivery</span></span>
<span><input [(ngModel)]="pay_type" value="online" name="pay_type" type="radio"/><span class="radio_text">Online</span></span>
</div>
<div class="sbmincart-subtotal">
<textarea placeholder="Enter the Delivery address" [(ngModel)]="delivery_address"></textarea>
</div>
<div class="sbmincart-subtotal">
Subtotal: <span class="price">${{cartTotalPrice}}</span>
</div>
<div>
<button (click)="checkoutCart()">Place Order</button>
</div>
</div>
<input type="hidden" name="bn" value="sbmincart_AddToCart_WPS_US">
</div>
</div>
</div>
</div>
Which simply loops over the cartObj stored within the service class.
I modified the header-component.ts (where the cart exists) to look like this:
import { Component, OnInit } from "#angular/core";
import { HttpServiceService } from "../http-service.service";
import { CartServiceService } from "../service/cart-service.service";
import { timingSafeEqual } from "crypto";
import { Router } from "#angular/router";
#Component({
selector: "app-header",
templateUrl: "./header.component.html",
styleUrls: ["./header.component.css"],
})
export class HeaderComponent implements OnInit {
isOpenLoginDialog = false;
currentDropDownMenu = "";
dialogType = "login";
mainDialogType = "";
isLogin = false;
mobile = "123456789";
password = "test";
cartObj = [];
cart_qty = 0;
cartTotalPrice = 0;
register = { name: "", email: "", mobile: "", password: "", re_password: "" };
welcomeUsername = "";
items$ = this.cartService.items$;
constructor(
private router: Router,
private cartService: CartServiceService,
private http: HttpServiceService
) {
let request = {};
this.http.postRequest("api/status", request).subscribe(
(data) => {
console.log("test", data);
},
(error) => {
alert("Server connection error " + error);
}
);
this.cartService.cartServiceEvent.subscribe((data) => {
this.cart_qty = this.cartService.getQty();
this.cartObj = this.cartService.getCartOBj();
});
}
logout() {
this.http.logout();
this.isLogin = false;
}
ngOnInit() {}
checkout_btn() {
this.router.navigate(["checkout"]);
}
openCheckoutModel() {
this.cartObj = this.cartService.getCartOBj();
this.cartTotalPrice = this.cartService.cartTotalPrice;
this.mainDialogType = "checkout";
}
openDialog() {
this.mainDialogType = "login";
}
dialogTypeInside(type) {
if (this.dialogType != type) this.dialogType = type;
}
closeDialog() {
this.mainDialogType = "";
}
curentDropDown(currentDropdownMenuName) {
if (this.currentDropDownMenu == currentDropdownMenuName) {
this.currentDropDownMenu = "";
} else {
this.currentDropDownMenu = currentDropdownMenuName;
}
}
}
Notice the openCheckoutModel() method which gets called which should pass the data down into the html component.
The data definitely exists as I am printing it out via console.log in the service class. The screen shot is below:
The front end DOM also shows there are 4 items in the cart:
However, the html does not display the actual data on the cart page:
From what I can see, i am looping over the data that definitely exists, however it does not show on the browser. I also get no errors :(
I hope this makes sense!
For peace of mind and for performance reasons, I would recommend you to go the RxJS way and turn things observable.
Short answer:
Here is a stackblitz example I could quickly make for you:
https://stackblitz.com/edit/angular-ivy-kgpicq
Long Answer:
I would recommend you structure your class as:
class CartService {
constructor() {
let cartItems = JSON.parse(localstorage.getItem('products'));
if (!cartItems) {
cartItems = []
}
this.itemsSubject.next(cartItems);
}
private itemsSubject = new BehaviorSubject<Product[]>([]);
items$ = itemsSubject.asObservable();
addToCart(item: Product) {
this.items$.pipe(
take(1),
map((products) => {
products.push(item);
localstorage.setItem('products', JSON.stringify(products));
},
).subscribe();
}
}
In your component class:
class ProductsPageComponent {
constructor(private cartService: CartService) {}
items$ = this.cartService.items$;
}
In your template:
<div class="cart cart box_1 checkout-count-wrap">
<form action="#" method="post" class="last">
<button class="w3view-cart" type="submit" (click)="openCheckoutModel()" name="submit" value="">
<p class="total_count_checkout">{{(items$ | async).length}}</p>
<i class="fa fa-cart-arrow-down" aria-hidden="true"></i>
</button>
</form>
</div>
AJAX
You can achieve this with ajax in plain javascript or with jQuery. My preference would go to jQuery.
Some code:
$.get( "/your_link", function( data ) {
$( ".result" ).html( data );
});
I would say you could write a another function in your controller that responds with the cart items. Then you could call the $.get whenever you would like to refresh your items. There's a lot of info on this so don't hesitate to look it up :)
https://api.jquery.com/jQuery.get/
Subscribe
Another solution might be to put a timeout on your subscribe. (this is some code from a project of mine which needed similar functionality to yours.)
In component.ts:
getUsers(): void {
// polling
timer(0, 2500)
.subscribe(() => {
this.userService.getUsers()
.subscribe(data => this.users = data);
});
}
In service.ts:
getUsers(): Observable<User[]> {
return this.http.get<User[]>(this.usersUrl);
}
I'm trying to add the rows of an Angular 2 Data Table ( https://material.angular.io/components/table/overview) dynamically.
I got a service ("ListService") which gives me the columns("meta.attributes") to display and i can retrieve my data from it.
The problem is, if I change the displayed columns later, after I loaded the dataSource and and the meta.attributes array gets entries (so the rows should exist in the html), it gives me this error:
Error: cdk-table: Could not find column with id "id".
Looks like the header can't find the given rows. Any ideas to fix that?
.html file:
<md-table #table [dataSource]="dataSource" mdSort>
<ng-container *ngFor="let attr of meta.attributes">
<ng-container [cdkColumnDef]="attr.name">
<md-header-cell *cdkHeaderCellDef md-sort-header>{{attr.label}}</md-header-cell>
<md-cell *cdkCellDef="let row">
{{row[attr.name]}}
</md-cell>
</ng-container>
</ng-container>
<md-header-row *cdkHeaderRowDef="displayedColumns"></md-header-row>
<md-row *cdkRowDef="let row; columns: displayedColumns;"></md-row>
</md-table>
.ts file:
export class ListComponent implements OnInit {
displayedColumns = [];
exampleDatabase = new ExampleDatabase();
dataSource: ExampleDataSource | null;
meta: any = {
attributes: []
};
constructor(private service: ListService) {
//If i do it here it works
//this.meta.attributes.push({label: "ID", name: "id"});
}
ngOnInit() {
this.dataSource = new ExampleDataSource(this.exampleDatabase);
this.service.getMeta(this.name).subscribe(meta => {
//not here
this.meta.attributes.push({label: "ID", name: "id"});
this.service.getTableData(this.name).subscribe(data => {
this.exampleDatabase.loadData(data);
let cols = [];
for (let i = 0; i < this.meta.attributes.length; i++)
cols.push(this.meta.attributes[i].name);
this.displayedColumns = cols;
});
});
}
}
...exampleDatabase etc., same as from Angular Website
Thanks for help!
I was able to fix it by a workaround... I just added an *ngIf to the table and enable everything when service (meta) finished loading.
this.showTable = true;
console.log('table set exists');
setTimeout(() => { // necessary waiting for DOM
this.displayedColumns = ['id'];
console.log('nameCol set shown');
}, 1);
I had the same issue where it wasn't displaying. I solved it by adding an empty constructor: constructor(){} into the class or it won't set up the table properly
I have a kendo grid that is filtered by pushing values from a dropdownlist into the built in kendo filters. I can search the grid using the same method when I type values in a textbox and search. This is my kendo grid and the dropdown
#(Html.Kendo().DropDownListFor(model => model.MyObject.ID)
.Name("Objects").DataTextField("Value").DataValueField("Key")
.BindTo(#Model.MyObjectList).AutoBind(true)
.HtmlAttributes(new { id = "selectedObject" })
<a class="button" onclick="searchGrid()" id="search">Search</a>
#(Html.Kendo().Grid<MyViewModel>()
.Name("MyGrid").HtmlAttributes(new { style = " overflow-x:scroll;" })
.Columns(columns =>
{
columns.Bound(a => a.MyObject.Name).Title("Field 1");
columns.Bound(a => a.Column2).Title("Field 2");
}
.Pageable(page => page.PageSizes(true))
.Scrollable(src => src.Height("auto"))
.Sortable()
.Filterable()
.Reorderable(reorder => reorder.Columns(true))
.ColumnMenu()
.DataSource(dataSource => dataSource
.Ajax()
.PageSize(10)
.Read(read => read.Action("GetList_Read", "MyController"))
)
)
<script>
function searchGrid()
{
selectedObject = $("#selectedObject").data("kendoDropDownList");
gridFilter = = { filters: [] };
if ($.trim(selectedRecipient).length > 0) {
gridListFilter.filters.push({ field: "Field 1", operator: "eq", value: selectedObject});
}
}
var grid = $("#MyGrid").data("kendoGrid");
grid.dataSource.filter(gridFilter);
</script>
My View model looks like
public class MyViewModel
{
public MyObject myObj {get;set;}
public string Column2 {get;set;}
}
The above function work when the search field is a textbox but it doesnt work when I am using a dropdown. I think it is because I am pushing the id of 'MyObject' into the grid filter while the grid is populated with the name of 'MyObject'. Can anyone show me how I can fix this. Thank you!!
There are two ways of handling this issue as I've found out. One is by pushing the selected values into the built in Kendo Filters or by passing a value to the controller action and filtering on the server side. First store the selected value of the dropdown on-change event to an object called 'selectedDropDownValue'
Filtering Client Side (Pushing values to kendo filters)
function searchGrid()
{
var gridListFilter = { filters: [] };
var gridDataSource = $("#MyGrid").data("kendoGrid").dataSource;
gridListFilter.logic = "and"; // a different logic 'or' can be selected
if ($.trim(selectedDropDownValue).length > 0) {
gridListFilter.filters.push({ field: "MyObject.MyObjectID", operator: "eq", value: parseInt(selectedDropDownValue) });
}
gridDataSource.filter(gridListFilter);
gridDataSource.read();
}
This pushes the selected value of the drop down to the built-in kendo grid filter
Filtering Server-side
Edit the DataSource read line by adding data
.Read(read => read.Action("GetApportionmentList_Read", "Apportionment").Data("AddFilter"))
Then create a javascript function to add the filter
function AddFilter()
{
return {filter:selectedDropDownValue};
}
Then inside the search grid JS function start with
function searchGrid()
{
var gridListFilter = { filters: [] };
var gridDataSource = $("#MyGrid").data("kendoGrid").dataSource;
gridDataSource.read();
}
After the read call you can still add client-side filters, apply the filter and then make the read recall afterwards.
The contoller signature should look like this
public JsonResult GetList_Read([DataSourceRequest] DataSourceRequest request, string filter)
filter will contain the value of the drop down selected
In your filter you are setting
value: selectedObject
but selectedObject is the actual Kendo DropDownList widget instance.
You need to get the value out of the widget using .value() or .text()
selectedObject = $("#selectedObject").data("kendoDropDownList").value();
I'm using Angular UI's typeahead in combination with Web Api to load results from a search.
My Angular is version 1.2.2 and bootstrap is 3.1.0. I should mention that I'm using typescript as well.
When I type into the search box I expect drop down menu with suggestions to fall down from the input. When I check the console I see my returned data, problem is that there is no drop down menu appearing to display it.
Here is my HTML + angular directives:
<div class="input-group">
<input type="text" class="form-control" placeholder="Search" ng-model="selected"
data-typeahead="searchResult as searchResult for searchResult in search($viewValue) | filter:$viewValue" >
<div class="input-group-btn">
<button class="btn btn-default" type="submit" ng-click="search(Text)"><i class=" glyphicon glyphicon-search"></i></button>
</div>
</div>
Here is my js function located in my controller:
$scope.search = function (criteria) {
controller.dataService.search($scope.employeeId, criteria, function (data) {
$scope.searchResult = data;
});
Here is the search function in my data service.
export interface ICommonDataService extends IBaseDataService {
search(employeeId: string, criteria: string, successCallback: (data: SearchResult) => any);
}
dataservice.ts information:
export class DataServicBase {
public httpService: ng.IHttpService;
public serviceBase = '/services/api';
public static $inject = [
'$http'
];
constructor($http: any) {
this.httpService = $http;
}
}
export class CommonDataService extends DataServicBase implements ICommonDataService {
public serviceUrl = this.serviceBase + '/common';
search(employeeId: string, criteria: string, successCallback: (data: SearchResult) => any) {
this.httpService.get(this.serviceUrl + '/' + employeeId + '/search/' + criteria)
.success(function (data) {
successCallback(data);
});
}
}
This is what SearchResult looks like:
// Class
export class SearchResult {
// Constructor
constructor(
public Employees: Employee[],
public Filters: Employee[],
public Projects: Project[]
) {
}
}
Error i get is this:
Error: matches is undefined
.link/getMatchesAsync/<#https://web.plank.local/Scripts/ui-bootstrap-tpls-0.10.0.js:3186
qFactory/defer/deferred.promise.then/wrappedCallback#https://web.plank.local/scripts/angular.js:10655
qFactory/defer/deferred.promise.then/wrappedCallback#https://web.plank.local/scripts/angular.js:10655
qFactory/ref/<.then/<#https://web.plank.local/scripts/angular.js:10741
$RootScopeProvider/this.$get</Scope.prototype.$eval#https://web.plank.local/scripts/angular.js:11634
$RootScopeProvider/this.$get</Scope.prototype.$digest#https://web.plank.local/scripts/angular.js:11479
$RootScopeProvider/this.$get</Scope.prototype.$apply#https://web.plank.local/scripts/angular.js:11740
textInputType/listener#https://web.plank.local/scripts/angular.js:15739
jQuery.event.dispatch#https://web.plank.local/scripts/jquery-2.1.0.js:4371
jQuery.event.add/elemData.handle#https://web.plank.local/scripts/jquery-2.1.0.js:4057
aM#https://cdn.qbaka.net/reporting.js:78
https://web.plank.local/scripts/angular.js
Line 9159
The main issue is that the list of data that you want your typeahead to work on is not a list (or not being populated).
Looks to me that your function search in the controller should return the data, whereas it is just a promise. You could try a return of the data, and to make it a bit more robust you could add a limitToFilter. Instead of placing it in the shared scope.searchResult which is not being picked up at all by your typeahead.
I am working on asp.net mvc3 application and have many records coming from database. I want to display only 10 records first then user can click on button to see next 10 records and so on. Like facebook wall posting more records. How can I implement this thing in my application ? I am using this to get 10 records but I want to display all records using more record button
This should get you going...
Assuming:
public class PostsViewModel
{
public IEnumerable<PostViewModel> Posts { get; set; }
}
Your controller might look like:
public class BlogController
{
public ActionResult Index()
{
PostsViewModel model = new PostsViewModel
{
Posts = postService.GetPosts(resultsPerPage: 10, page: 1)
};
return View(model);
}
public PartialViewResult More(Int32 page = 1)
{
PostsViewModel model = new PostsViewModel
{
Posts = postService.GetPosts(resultsPerPage: 10, page: page)
};
return PartialView(model);
}
}
And Views something like:
~/Views/Blog/Index.cshtml
#model PostsViewModel
#* Other page content *#
#Html.DisplayFor(x => x.Posts)
<div id="more"></div>
#Ajax.ActionLink("Read More", "More", "Blog", new AjaxOptions {
InsertionMode = InsertionMode.InsertBefore,
UpdateTargetId = "more"
})
#* Other page content *#
~/Views/Blog/More.cshtml
#model PostsViewModel
#Html.DisplayFor(x => x.Posts)
~/Views/Blog/DisplayTemplates/PostViewModel.cshtml
#model PostViewModel
#* Display post itself *#