├── src ├── app │ ├── pages │ │ └── cart-modal │ │ │ ├── cart-modal.page.scss │ │ │ ├── cart-modal.module.ts │ │ │ ├── cart-modal.page.ts │ │ │ └── cart-modal.page.html │ ├── home │ │ ├── home.page.scss │ │ ├── home.module.ts │ │ ├── home.page.html │ │ └── home.page.ts │ ├── app-routing.module.ts │ ├── app.component.ts │ ├── app.module.ts │ └── services │ │ └── cart.service.ts ├── polyfills.ts ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── assets │ ├── icon │ │ └── favicon.png │ └── shapes.svg ├── zone-flags.ts ├── main.ts ├── test.ts ├── index.html ├── global.scss └── theme │ └── variables.scss ├── .markdownlint.json ├── img ├── cart.png └── main-screen.png ├── ionic.config.json ├── typings └── cordova-typings.d.ts ├── e2e ├── src │ ├── app.po.ts │ └── app.e2e-spec.ts ├── tsconfig.json └── protractor.conf.js ├── tsconfig.app.json ├── tsconfig.spec.json ├── browserslist ├── .gitignore ├── tsconfig.json ├── karma.conf.js ├── LICENSE ├── package.json ├── tslint.json ├── README.md └── angular.json /src/app/pages/cart-modal/cart-modal.page.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | import 'zone.js'; // Included with Angular CLI. 2 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "MD010": false, 3 | "MD007": false, 4 | "MD013": false 5 | } -------------------------------------------------------------------------------- /img/cart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-cart/HEAD/img/cart.png -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /img/main-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-cart/HEAD/img/main-screen.png -------------------------------------------------------------------------------- /ionic.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ionic-angular-cart", 3 | "integrations": {}, 4 | "type": "angular" 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/icon/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-cart/HEAD/src/assets/icon/favicon.png -------------------------------------------------------------------------------- /typings/cordova-typings.d.ts: -------------------------------------------------------------------------------- 1 | 2 | /// 3 | /// -------------------------------------------------------------------------------- /src/zone-flags.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Prevents Angular change detection from 3 | * running with certain Web Component callbacks 4 | */ 5 | (window as any).__Zone_disable_customElements = true; 6 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.deepCss('app-root ion-content')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "include": [ 8 | "src/**/*.ts" 9 | ], 10 | "exclude": [ 11 | "src/test.ts", 12 | "src/**/*.spec.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('new App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should be blank', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toContain('The world is your oyster.'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/app/home/home.page.scss: -------------------------------------------------------------------------------- 1 | ion-fab-button { 2 | height: 70px; 3 | width: 70px; 4 | } 5 | 6 | .cart-icon { 7 | font-size: 50px; 8 | } 9 | 10 | .cart-length { 11 | color: var(--ion-color-primary); 12 | position: absolute; 13 | top: 23px; 14 | left: 25px; 15 | font-weight: 600; 16 | font-size: 1em; 17 | min-width: 25px; 18 | z-index: 10; 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/zone-flags.ts", 13 | "src/polyfills.ts" 14 | ], 15 | "include": [ 16 | "src/**/*.spec.ts", 17 | "src/**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.log(err)); 13 | -------------------------------------------------------------------------------- /browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.angular/cache 2 | # Specifies intentionally untracked files to ignore when using Git 3 | # http://git-scm.com/docs/gitignore 4 | 5 | *~ 6 | *.sw[mnpcod] 7 | .tmp 8 | *.tmp 9 | *.tmp.* 10 | *.sublime-project 11 | *.sublime-workspace 12 | .DS_Store 13 | Thumbs.db 14 | UserInterfaceState.xcuserstate 15 | $RECYCLE.BIN/ 16 | 17 | *.log 18 | log.txt 19 | npm-debug.log* 20 | 21 | /.idea 22 | /.ionic 23 | /.sass-cache 24 | /.sourcemaps 25 | /.versions 26 | /.vscode 27 | /coverage 28 | /dist 29 | /node_modules 30 | /platforms 31 | /plugins 32 | /www 33 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | // First, initialize the Angular testing environment. 11 | getTestBed().initTestEnvironment( 12 | BrowserDynamicTestingModule, 13 | platformBrowserDynamicTesting(), { 14 | teardown: { destroyAfterEach: false } 15 | } 16 | ); 17 | -------------------------------------------------------------------------------- /src/app/home/home.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { CommonModule } from "@angular/common"; 3 | import { IonicModule } from "@ionic/angular"; 4 | import { FormsModule } from "@angular/forms"; 5 | import { RouterModule } from "@angular/router"; 6 | 7 | import { HomePage } from "./home.page"; 8 | 9 | @NgModule({ 10 | imports: [ 11 | CommonModule, 12 | FormsModule, 13 | IonicModule, 14 | RouterModule.forChild([ 15 | { 16 | path: "", 17 | component: HomePage, 18 | }, 19 | ]), 20 | ], 21 | declarations: [HomePage], 22 | }) 23 | export default class HomePageModule {} 24 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { PreloadAllModules, RouterModule, Routes } from "@angular/router"; 3 | 4 | const routes: Routes = [ 5 | { path: "", redirectTo: "home", pathMatch: "full" }, 6 | { 7 | path: "home", 8 | loadChildren: () => import("./home/home.module"), 9 | }, 10 | { 11 | path: "cart-modal", 12 | loadChildren: () => import("./pages/cart-modal/cart-modal.module"), 13 | }, 14 | ]; 15 | 16 | @NgModule({ 17 | imports: [ 18 | RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules }), 19 | ], 20 | exports: [RouterModule], 21 | }) 22 | export class AppRoutingModule {} 23 | -------------------------------------------------------------------------------- /src/app/pages/cart-modal/cart-modal.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { Routes, RouterModule } from '@angular/router'; 5 | 6 | import { IonicModule } from '@ionic/angular'; 7 | 8 | import { CartModalPage } from './cart-modal.page'; 9 | 10 | const routes: Routes = [ 11 | { 12 | path: '', 13 | component: CartModalPage 14 | } 15 | ]; 16 | 17 | @NgModule({ 18 | imports: [ 19 | CommonModule, 20 | FormsModule, 21 | IonicModule, 22 | RouterModule.forChild(routes) 23 | ], 24 | declarations: [CartModalPage] 25 | }) 26 | export default class CartModalPageModule {} 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "skipLibCheck": false, 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "importHelpers": true, 14 | "target": "ES2022", 15 | "typeRoots": [ 16 | "node_modules/@types" 17 | ], 18 | "lib": [ 19 | "ES2022", 20 | "dom" 21 | ], 22 | "useDefineForClassFields": false 23 | }, 24 | "angularCompilerOptions": { 25 | "fullTemplateTypeCheck": true, 26 | "strictInjectionParameters": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@angular/core"; 2 | 3 | import { Platform } from "@ionic/angular"; 4 | import { SplashScreen } from "@ionic-native/splash-screen/ngx"; 5 | import { StatusBar } from "@ionic-native/status-bar/ngx"; 6 | 7 | @Component({ 8 | selector: "app-root", 9 | template: ` 10 | 11 | `, 12 | }) 13 | export class AppComponent { 14 | constructor( 15 | private platform: Platform, 16 | private splashScreen: SplashScreen, 17 | private statusBar: StatusBar 18 | ) { 19 | this.initializeApp(); 20 | } 21 | 22 | initializeApp() { 23 | this.platform.ready().then(() => { 24 | this.statusBar.styleDefault(); 25 | this.splashScreen.hide(); 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ionic App 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from "@angular/core"; 2 | import { BrowserModule } from "@angular/platform-browser"; 3 | import { RouteReuseStrategy } from "@angular/router"; 4 | 5 | import { IonicModule, IonicRouteStrategy } from "@ionic/angular"; 6 | import { SplashScreen } from "@ionic-native/splash-screen/ngx"; 7 | import { StatusBar } from "@ionic-native/status-bar/ngx"; 8 | 9 | import { AppComponent } from "./app.component"; 10 | import { AppRoutingModule } from "./app-routing.module"; 11 | 12 | @NgModule({ 13 | declarations: [AppComponent], 14 | imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule], 15 | providers: [ 16 | StatusBar, 17 | SplashScreen, 18 | { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, 19 | ], 20 | bootstrap: [AppComponent], 21 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 22 | }) 23 | export class AppModule {} 24 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Andrew Bateman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/assets/shapes.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/pages/cart-modal/cart-modal.page.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from "@angular/core"; 2 | import { CartService } from "src/app/services/cart.service"; 3 | import { Product } from "../../services/cart.service"; 4 | import { ModalController } from "@ionic/angular"; 5 | 6 | @Component({ 7 | selector: "app-cart-modal", 8 | templateUrl: "./cart-modal.page.html", 9 | styleUrls: ["./cart-modal.page.scss"], 10 | }) 11 | export class CartModalPage implements OnInit { 12 | cart: Product[] = []; 13 | 14 | constructor( 15 | private cartService: CartService, 16 | private modalCtrl: ModalController 17 | ) {} 18 | 19 | ngOnInit() { 20 | this.cart = this.cartService.getCart(); 21 | } 22 | 23 | decreaseCartItem(product: Product): void { 24 | this.cartService.decreaseProduct(product); 25 | } 26 | 27 | increaseCartItem(product: Product): void { 28 | this.cartService.addProduct(product); 29 | } 30 | 31 | removeCartItem(product: Product): void { 32 | this.cartService.removeProduct(product); 33 | } 34 | 35 | getTotal(): number { 36 | return this.cart.reduce((i, j) => i + j.price * j.amount, 0); 37 | } 38 | 39 | close(): void { 40 | this.modalCtrl.dismiss(); 41 | } 42 | 43 | trackByFn(index: number, product: Product): number { 44 | return product.id; 45 | } 46 | 47 | checkout() {} 48 | } 49 | -------------------------------------------------------------------------------- /src/global.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * App Global CSS 3 | * ---------------------------------------------------------------------------- 4 | * Put style rules here that you want to apply globally. These styles are for 5 | * the entire app and not just one component. Additionally, this file can be 6 | * used as an entry point to import other CSS/Sass files to be included in the 7 | * output CSS. 8 | * For more information on global stylesheets, visit the documentation: 9 | * https://ionicframework.com/docs/layout/global-stylesheets 10 | */ 11 | 12 | /* Core CSS required for Ionic components to work properly */ 13 | @import "~@ionic/angular/css/core.css"; 14 | 15 | /* Basic CSS for apps built with Ionic */ 16 | @import "~@ionic/angular/css/normalize.css"; 17 | @import "~@ionic/angular/css/structure.css"; 18 | @import "~@ionic/angular/css/typography.css"; 19 | @import '~@ionic/angular/css/display.css'; 20 | 21 | /* Optional CSS utils that can be commented out */ 22 | @import "~@ionic/angular/css/padding.css"; 23 | @import "~@ionic/angular/css/float-elements.css"; 24 | @import "~@ionic/angular/css/text-alignment.css"; 25 | @import "~@ionic/angular/css/text-transformation.css"; 26 | @import "~@ionic/angular/css/flex-utils.css"; 27 | @import '~animate.css/animate.min.css'; 28 | 29 | .cart-modal { 30 | --height: 50%; 31 | --border-radius: 10px; 32 | padding: 25px; 33 | } -------------------------------------------------------------------------------- /src/app/home/home.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ionic Shopping 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
{{ cartItemCount | async }}
12 | 13 |
14 |
15 | 16 | 17 | 18 | 19 | 20 | {{ p.name }} 21 | 22 | 23 | 24 | 25 | 26 | {{ p.price | currency:'EUR' }} 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ionic-angular-cart", 3 | "version": "0.0.1", 4 | "author": "Ionic Framework", 5 | "homepage": "https://ionicframework.com/", 6 | "scripts": { 7 | "ng": "ng", 8 | "start": "ng serve", 9 | "build": "ng build", 10 | "test": "ng test", 11 | "lint": "ng lint", 12 | "e2e": "ng e2e" 13 | }, 14 | "private": true, 15 | "dependencies": { 16 | "@angular/common": "^17.3.8", 17 | "@angular/compiler": "^17.3.8", 18 | "@angular/core": "^17.3.8", 19 | "@angular/forms": "^17.3.8", 20 | "@angular/platform-browser": "^17.3.8", 21 | "@angular/platform-browser-dynamic": "^17.3.8", 22 | "@angular/router": "^17.3.8", 23 | "@ionic-native/core": "^5.36.0", 24 | "@ionic-native/splash-screen": "^5.36.0", 25 | "@ionic-native/status-bar": "^5.36.0", 26 | "@ionic/angular": "^8.1.1", 27 | "ajv": "^8.13.0", 28 | "animate.css": "^4.1.1", 29 | "core-js": "^3.37.0", 30 | "rxjs": "~7.8.1", 31 | "tslib": "^2.6.2", 32 | "zone.js": "^0.14.5" 33 | }, 34 | "devDependencies": { 35 | "@angular-devkit/architect": "~0.1703.7", 36 | "@angular-devkit/build-angular": "~17.3.7", 37 | "@angular-devkit/core": "~17.3.7", 38 | "@angular-devkit/schematics": "~17.3.7", 39 | "@angular/cli": "~17.3.7", 40 | "@angular/compiler-cli": "~17.3.8", 41 | "@angular/language-service": "~17.3.8", 42 | "@ionic/angular-toolkit": "^11.0.1", 43 | "@types/jasmine": "~5.1.4", 44 | "@types/jasminewd2": "~2.0.13", 45 | "@types/node": "~20.12.11", 46 | "codelyzer": "^6.0.2", 47 | "jasmine-core": "~5.1.2", 48 | "jasmine-spec-reporter": "~7.0.0", 49 | "karma": "~6.4.3", 50 | "karma-chrome-launcher": "~3.2.0", 51 | "karma-coverage-istanbul-reporter": "~3.0.3", 52 | "karma-jasmine": "~5.1.0", 53 | "karma-jasmine-html-reporter": "^2.1.0", 54 | "protractor": "~7.0.0", 55 | "ts-node": "~10.9.2", 56 | "tslint": "~6.1.3", 57 | "typescript": "~5.4.5" 58 | }, 59 | "description": "An Ionic project" 60 | } -------------------------------------------------------------------------------- /src/app/services/cart.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { BehaviorSubject } from "rxjs"; 3 | 4 | export interface Product { 5 | id: number; 6 | name: string; 7 | price: number; 8 | amount: number; 9 | } 10 | 11 | @Injectable({ 12 | providedIn: "root", 13 | }) 14 | export class CartService { 15 | data: Product[] = [ 16 | { id: 0, name: "Pizza Salami", price: 8.99, amount: 0 }, 17 | { id: 1, name: "Pizza Classic", price: 5.49, amount: 0 }, 18 | { id: 2, name: "Sliced Bread", price: 4.99, amount: 0 }, 19 | { id: 3, name: "Salad", price: 6.99, amount: 0 }, 20 | ]; 21 | 22 | private cart = []; 23 | private cartItemCount = new BehaviorSubject(0); 24 | 25 | constructor() {} 26 | 27 | getProducts(): Product[] { 28 | return this.data; 29 | } 30 | 31 | getCart(): Product[] { 32 | console.log("this.cart: ", this.cart); 33 | return this.cart; 34 | } 35 | 36 | getCartItemCount(): BehaviorSubject { 37 | return this.cartItemCount; 38 | } 39 | 40 | addProduct(product: Product): void { 41 | let added = false; 42 | for (let p of this.cart) { 43 | if (p.id === product.id) { 44 | p.amount += 1; 45 | added = true; 46 | break; 47 | } 48 | } 49 | if (!added) { 50 | product.amount = 1; 51 | this.cart.push(product); 52 | console.log(`product ${product.name} pushed to cart`); 53 | } 54 | this.cartItemCount.next(this.cartItemCount.value + 1); 55 | } 56 | 57 | decreaseProduct(product: Product): void { 58 | for (let [index, p] of this.cart.entries()) { 59 | if (p.id === product.id) { 60 | p.amount -= 1; 61 | if (p.amount == 0) { 62 | this.cart.splice(index, 1); 63 | } 64 | } 65 | } 66 | this.cartItemCount.next(this.cartItemCount.value - 1); 67 | } 68 | 69 | removeProduct(product: Product): void { 70 | for (let [index, p] of this.cart.entries()) { 71 | if (p.id === product.id) { 72 | this.cartItemCount.next(this.cartItemCount.value - p.amount); 73 | this.cart.splice(index, 1); 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/app/home/home.page.ts: -------------------------------------------------------------------------------- 1 | import { CartService, Product } from "./../services/cart.service"; 2 | import { Component, OnInit, ViewChild, ElementRef } from "@angular/core"; 3 | import { ModalController } from "@ionic/angular"; 4 | import { BehaviorSubject } from "rxjs"; 5 | import { CartModalPage } from "../pages/cart-modal/cart-modal.page"; 6 | 7 | @Component({ 8 | selector: "app-home", 9 | templateUrl: "home.page.html", 10 | styleUrls: ["home.page.scss"], 11 | }) 12 | export class HomePage implements OnInit { 13 | cart = []; 14 | products = []; 15 | cartItemCount: BehaviorSubject; 16 | 17 | @ViewChild("cart", { static: false, read: ElementRef }) fab: ElementRef; 18 | 19 | constructor( 20 | private cartService: CartService, 21 | private modalCtrl: ModalController 22 | ) {} 23 | 24 | ngOnInit() { 25 | this.products = this.cartService.getProducts(); 26 | this.cart = this.cartService.getCart(); 27 | this.cartItemCount = this.cartService.getCartItemCount(); 28 | } 29 | 30 | addToCart(product: Product) { 31 | console.log(`add ${product.name} to cart`); 32 | this.animateCSS("jello"); 33 | this.cartService.addProduct(product); 34 | } 35 | 36 | async openCart() { 37 | this.animateCSS("bounceOutLeft", true); 38 | 39 | const modal = await this.modalCtrl.create({ 40 | component: CartModalPage, 41 | cssClass: "cart-modal", 42 | }); 43 | modal.onWillDismiss().then(() => { 44 | this.fab.nativeElement.classList.remove("animated", "bounceOutLeft"); 45 | this.animateCSS("bounceInLeft"); 46 | }); 47 | modal.present(); 48 | } 49 | 50 | // copied from animate.css github page: https://github.com/daneden/animate.css 51 | animateCSS(animationName: string, keepAnimated = false) { 52 | const node = this.fab.nativeElement; 53 | node.classList.add("animated", animationName); 54 | 55 | function handleAnimationEnd() { 56 | if (!keepAnimated) { 57 | node.classList.remove("animated", animationName); 58 | } 59 | node.removeEventListener("animationend", handleAnimationEnd); 60 | } 61 | node.addEventListener("animationend", handleAnimationEnd); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "codelyzer" 5 | ], 6 | "rules": { 7 | "array-type": false, 8 | "arrow-parens": false, 9 | "deprecation": { 10 | "severity": "warn" 11 | }, 12 | "import-blacklist": [ 13 | true, 14 | "rxjs/Rx" 15 | ], 16 | "indent": [ 17 | false, 18 | "tabs", 19 | 2 20 | ], 21 | "interface-name": false, 22 | "max-classes-per-file": false, 23 | "max-line-length": [ 24 | true, 25 | 140 26 | ], 27 | "member-access": false, 28 | "member-ordering": [ 29 | true, 30 | { 31 | "order": [ 32 | "static-field", 33 | "instance-field", 34 | "static-method", 35 | "instance-method" 36 | ] 37 | } 38 | ], 39 | "no-consecutive-blank-lines": false, 40 | "no-console": [ 41 | true, 42 | "debug", 43 | "info", 44 | "time", 45 | "timeEnd", 46 | "trace" 47 | ], 48 | "no-empty": false, 49 | "no-inferrable-types": [ 50 | true, 51 | "ignore-params" 52 | ], 53 | "no-non-null-assertion": true, 54 | "no-redundant-jsdoc": true, 55 | "no-switch-case-fall-through": true, 56 | "no-use-before-declare": true, 57 | "no-var-requires": false, 58 | "object-literal-key-quotes": [ 59 | true, 60 | "as-needed" 61 | ], 62 | "object-literal-sort-keys": false, 63 | "ordered-imports": false, 64 | "quotemark": [ 65 | true, 66 | "single" 67 | ], 68 | "trailing-comma": false, 69 | "no-output-on-prefix": true, 70 | "no-inputs-metadata-property": true, 71 | "no-host-metadata-property": true, 72 | "no-input-rename": true, 73 | "no-output-rename": true, 74 | "use-lifecycle-interface": true, 75 | "use-pipe-transform-interface": true, 76 | "one-variable-per-declaration": false, 77 | "component-class-suffix": [true, "Page", "Component"], 78 | "directive-class-suffix": true, 79 | "directive-selector": [ 80 | true, 81 | "attribute", 82 | "app", 83 | "camelCase" 84 | ], 85 | "component-selector": [ 86 | true, 87 | "element", 88 | "app", 89 | "page", 90 | "kebab-case" 91 | ] 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/theme/variables.scss: -------------------------------------------------------------------------------- 1 | // Ionic Variables and Theming. For more info, please see: 2 | // http://ionicframework.com/docs/theming/ 3 | 4 | /** Ionic CSS Variables **/ 5 | :root { 6 | /** primary **/ 7 | --ion-color-primary: #3880ff; 8 | --ion-color-primary-rgb: 56, 128, 255; 9 | --ion-color-primary-contrast: #ffffff; 10 | --ion-color-primary-contrast-rgb: 255, 255, 255; 11 | --ion-color-primary-shade: #3171e0; 12 | --ion-color-primary-tint: #4c8dff; 13 | 14 | /** secondary **/ 15 | --ion-color-secondary: #0cd1e8; 16 | --ion-color-secondary-rgb: 12, 209, 232; 17 | --ion-color-secondary-contrast: #ffffff; 18 | --ion-color-secondary-contrast-rgb: 255, 255, 255; 19 | --ion-color-secondary-shade: #0bb8cc; 20 | --ion-color-secondary-tint: #24d6ea; 21 | 22 | /** tertiary **/ 23 | --ion-color-tertiary: #7044ff; 24 | --ion-color-tertiary-rgb: 112, 68, 255; 25 | --ion-color-tertiary-contrast: #ffffff; 26 | --ion-color-tertiary-contrast-rgb: 255, 255, 255; 27 | --ion-color-tertiary-shade: #633ce0; 28 | --ion-color-tertiary-tint: #7e57ff; 29 | 30 | /** success **/ 31 | --ion-color-success: #10dc60; 32 | --ion-color-success-rgb: 16, 220, 96; 33 | --ion-color-success-contrast: #ffffff; 34 | --ion-color-success-contrast-rgb: 255, 255, 255; 35 | --ion-color-success-shade: #0ec254; 36 | --ion-color-success-tint: #28e070; 37 | 38 | /** warning **/ 39 | --ion-color-warning: #ffce00; 40 | --ion-color-warning-rgb: 255, 206, 0; 41 | --ion-color-warning-contrast: #ffffff; 42 | --ion-color-warning-contrast-rgb: 255, 255, 255; 43 | --ion-color-warning-shade: #e0b500; 44 | --ion-color-warning-tint: #ffd31a; 45 | 46 | /** danger **/ 47 | --ion-color-danger: #f04141; 48 | --ion-color-danger-rgb: 245, 61, 61; 49 | --ion-color-danger-contrast: #ffffff; 50 | --ion-color-danger-contrast-rgb: 255, 255, 255; 51 | --ion-color-danger-shade: #d33939; 52 | --ion-color-danger-tint: #f25454; 53 | 54 | /** dark **/ 55 | --ion-color-dark: #222428; 56 | --ion-color-dark-rgb: 34, 34, 34; 57 | --ion-color-dark-contrast: #ffffff; 58 | --ion-color-dark-contrast-rgb: 255, 255, 255; 59 | --ion-color-dark-shade: #1e2023; 60 | --ion-color-dark-tint: #383a3e; 61 | 62 | /** medium **/ 63 | --ion-color-medium: #989aa2; 64 | --ion-color-medium-rgb: 152, 154, 162; 65 | --ion-color-medium-contrast: #ffffff; 66 | --ion-color-medium-contrast-rgb: 255, 255, 255; 67 | --ion-color-medium-shade: #86888f; 68 | --ion-color-medium-tint: #a2a4ab; 69 | 70 | /** light **/ 71 | --ion-color-light: #f4f5f8; 72 | --ion-color-light-rgb: 244, 244, 244; 73 | --ion-color-light-contrast: #000000; 74 | --ion-color-light-contrast-rgb: 0, 0, 0; 75 | --ion-color-light-shade: #d7d8da; 76 | --ion-color-light-tint: #f5f6f9; 77 | } 78 | -------------------------------------------------------------------------------- /src/app/pages/cart-modal/cart-modal.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 |
8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | 22 | 23 | 24 | 25 | 26 | {{ product.amount }} 27 | 28 | 29 | 30 | 35 | 36 | 37 | 38 | 39 | 40 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | {{ product.name }} 54 | 55 | 56 | {{ product.amount * product.price | currency:'USD' }} 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | Total: 66 | 67 | {{ getTotal() | currency:'USD' }} 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | Checkout 76 |
77 |
78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :zap: Ionic Angular Cart 2 | 3 | * Ionic app to show a shopping cart where the user can select items and see them added to a cart. 4 | * Items can also be removed and the total price and product quantities will be updated. 5 | * This is another great tutorial from [Simon Grimm](https://www.youtube.com/channel/UCZZPgUIorPao48a1tBYSDgg) - see [:clap: Inspiration](#clap-inspiration) below. 6 | * **Note:** to open web links in a new window use: _ctrl+click on link_ 7 | 8 | ![GitHub repo size](https://img.shields.io/github/repo-size/AndrewJBateman/ionic-angular-cart?style=plastic) 9 | ![GitHub pull requests](https://img.shields.io/github/issues-pr/AndrewJBateman/ionic-angular-cart?style=plastic) 10 | ![GitHub Repo stars](https://img.shields.io/github/stars/AndrewJBateman/ionic-angular-cart?style=plastic) 11 | ![GitHub last commit](https://img.shields.io/github/last-commit/AndrewJBateman/ionic-angular-cart?style=plastic) 12 | 13 | ## :page_facing_up: Table of contents 14 | 15 | * [:zap: Ionic Angular Cart](#zap-ionic-angular-cart) 16 | * [:page\_facing\_up: Table of contents](#page_facing_up-table-of-contents) 17 | * [:books: General info](#books-general-info) 18 | * [:camera: Screenshots](#camera-screenshots) 19 | * [:signal\_strength: Technologies](#signal_strength-technologies) 20 | * [:floppy\_disk: Setup](#floppy_disk-setup) 21 | * [:computer: Code Examples](#computer-code-examples) 22 | * [:cool: Features](#cool-features) 23 | * [:clipboard: Status \& To-do list](#clipboard-status--to-do-list) 24 | * [:clap: Inspiration](#clap-inspiration) 25 | * [:file\_folder: License](#file_folder-license) 26 | * [:envelope: Contact](#envelope-contact) 27 | 28 | ## :books: General info 29 | 30 | * modal used to show shopping cart contents: product quantities can be increased or decreased and total price will be adjusted using a simple reduce function. 31 | * animate.css used to provide some fun visual effects when items are added to the cart and when the cart modal is activated and dismissed. There are options to control delays, speed of animation etc. 32 | 33 | ## :camera: Screenshots 34 | 35 | ![screenshot](./img/main-screen.png) 36 | ![screenshot](./img/cart.png) 37 | 38 | ## :signal_strength: Technologies 39 | 40 | * [Ionic/angular v8](https://ionicframework.com/) 41 | * [Angular v17](https://angular.io/) 42 | * [rxjs v7](https://angular.io/guide/rx-library) reactive programming. 43 | * [RxJS Behavior subject](http://reactivex.io/rxjs/manual/overview.html#behaviorsubject) to represent the event stream of product cart updates. 44 | * [animate.css v4](https://github.com/daneden/animate.css/) a library of CSS animations. 45 | 46 | ## :floppy_disk: Setup 47 | 48 | * `npm i` to install dependencies 49 | * `ionic serve` to start the server on _localhost://8100_ 50 | * To start the server on a mobile using Ionic devapp and connected via wifi, type: 'ionic serve --devapp' 51 | * The Ionic DevApp was installed on an Android device from the Google Play app store. 52 | 53 | ## :computer: Code Examples 54 | 55 | * Cart service: function to add a product to the shopping cart. 56 | 57 | ```typescript 58 | addProduct(product: Product) { 59 | let added = false; 60 | for (const item of this.cart) { 61 | if (item.id === product.id) { 62 | item.amount += 1; 63 | added = true; 64 | break; 65 | } 66 | } 67 | !added 68 | ? this.cart.push(product) 69 | : this.cartItemCount.next(this.cartItemCount.value + 1); 70 | } 71 | ``` 72 | 73 | ## :cool: Features 74 | 75 | * [Animate.css](https://github.com/daneden/animate.css) used to animate items. 76 | 77 | ## :clipboard: Status & To-do list 78 | 79 | * Status: Working. 80 | * To-do: add a backend product list. Add to functionality, including a checkout and payment function. 81 | 82 | ## :clap: Inspiration 83 | 84 | * [Simon Grimm of Devdactic, Youtube video 'How to Build a Shopping Cart with Ionic 4' (2019)](https://www.youtube.com/watch?v=ZFfVMBhJzVU). 85 | * [Written version of tutorial from Simon Grimm of Devdactic (2019)](https://devdactic.com/shopping-cart-ionic-4/). 86 | 87 | ## :file_folder: License 88 | 89 | * This project is licensed under the terms of the MIT license. 90 | 91 | ## :envelope: Contact 92 | 93 | * Repo created by [ABateman](https://github.com/AndrewJBateman), email: `gomezbateman@gmail.com` 94 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "app": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "www", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "tsconfig.app.json", 21 | "assets": [ 22 | { 23 | "glob": "**/*", 24 | "input": "src/assets", 25 | "output": "assets" 26 | }, 27 | { 28 | "glob": "**/*.svg", 29 | "input": "node_modules/ionicons/dist/ionicons/svg", 30 | "output": "./svg" 31 | } 32 | ], 33 | "styles": [ 34 | { 35 | "input": "src/theme/variables.scss" 36 | }, 37 | { 38 | "input": "src/global.scss" 39 | } 40 | ], 41 | "scripts": [], 42 | "aot": false, 43 | "vendorChunk": true, 44 | "extractLicenses": false, 45 | "buildOptimizer": false, 46 | "sourceMap": true, 47 | "optimization": false, 48 | "namedChunks": true 49 | }, 50 | "configurations": { 51 | "production": { 52 | "fileReplacements": [ 53 | { 54 | "replace": "src/environments/environment.ts", 55 | "with": "src/environments/environment.prod.ts" 56 | } 57 | ], 58 | "optimization": true, 59 | "outputHashing": "all", 60 | "sourceMap": false, 61 | "namedChunks": false, 62 | "aot": true, 63 | "extractLicenses": true, 64 | "vendorChunk": false, 65 | "buildOptimizer": true, 66 | "budgets": [ 67 | { 68 | "type": "initial", 69 | "maximumWarning": "2mb", 70 | "maximumError": "5mb" 71 | } 72 | ] 73 | }, 74 | "ci": { 75 | "progress": false 76 | } 77 | } 78 | }, 79 | "serve": { 80 | "builder": "@angular-devkit/build-angular:dev-server", 81 | "options": { 82 | "buildTarget": "app:build" 83 | }, 84 | "configurations": { 85 | "production": { 86 | "buildTarget": "app:build:production" 87 | }, 88 | "ci": { 89 | } 90 | } 91 | }, 92 | "extract-i18n": { 93 | "builder": "@angular-devkit/build-angular:extract-i18n", 94 | "options": { 95 | "buildTarget": "app:build" 96 | } 97 | }, 98 | "test": { 99 | "builder": "@angular-devkit/build-angular:karma", 100 | "options": { 101 | "main": "src/test.ts", 102 | "polyfills": "src/polyfills.ts", 103 | "tsConfig": "tsconfig.spec.json", 104 | "karmaConfig": "karma.conf.js", 105 | "styles": [], 106 | "scripts": [], 107 | "assets": [ 108 | { 109 | "glob": "favicon.ico", 110 | "input": "src/", 111 | "output": "/" 112 | }, 113 | { 114 | "glob": "**/*", 115 | "input": "src/assets", 116 | "output": "/assets" 117 | } 118 | ] 119 | }, 120 | "configurations": { 121 | "ci": { 122 | "progress": false, 123 | "watch": false 124 | } 125 | } 126 | }, 127 | "e2e": { 128 | "builder": "@angular-devkit/build-angular:protractor", 129 | "options": { 130 | "protractorConfig": "e2e/protractor.conf.js", 131 | "devServerTarget": "app:serve" 132 | }, 133 | "configurations": { 134 | "production": { 135 | "devServerTarget": "app:serve:production" 136 | }, 137 | "ci": { 138 | "devServerTarget": "app:serve:ci" 139 | } 140 | } 141 | }, 142 | "ionic-cordova-build": { 143 | "builder": "@ionic/angular-toolkit:cordova-build", 144 | "options": { 145 | "browserTarget": "app:build" 146 | }, 147 | "configurations": { 148 | "production": { 149 | "browserTarget": "app:build:production" 150 | } 151 | } 152 | }, 153 | "ionic-cordova-serve": { 154 | "builder": "@ionic/angular-toolkit:cordova-serve", 155 | "options": { 156 | "cordovaBuildTarget": "app:ionic-cordova-build", 157 | "devServerTarget": "app:serve" 158 | }, 159 | "configurations": { 160 | "production": { 161 | "cordovaBuildTarget": "app:ionic-cordova-build:production", 162 | "devServerTarget": "app:serve:production" 163 | } 164 | } 165 | } 166 | } 167 | } 168 | }, 169 | "cli": { 170 | "analytics": false, 171 | "schematicCollections": [ 172 | "@ionic/angular-toolkit" 173 | ] 174 | }, 175 | "schematics": { 176 | "@ionic/angular-toolkit:component": { 177 | "styleext": "scss" 178 | }, 179 | "@ionic/angular-toolkit:page": { 180 | "styleext": "scss" 181 | } 182 | } 183 | } 184 | --------------------------------------------------------------------------------