├── 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 | 
9 | 
10 | 
11 | 
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 | 
36 | 
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 |
--------------------------------------------------------------------------------