├── .gitignore ├── README.md ├── angular.json ├── capacitor.config.json ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.e2e.json ├── ionic.config.json ├── json-server └── db.json ├── package-lock.json ├── package.json ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── home │ │ ├── home.module.ts │ │ ├── home.page.html │ │ ├── home.page.scss │ │ ├── home.page.spec.ts │ │ └── home.page.ts │ ├── modals │ │ └── add-dog-modal │ │ │ ├── add.dog.modal.html │ │ │ ├── add.dog.modal.scss │ │ │ └── add.dog.modal.ts │ ├── models │ │ └── stored.request.model.ts │ └── services │ │ ├── api.service.spec.ts │ │ ├── api.service.ts │ │ ├── network.service.spec.ts │ │ ├── network.service.ts │ │ ├── offline-manager.service.spec.ts │ │ └── offline-manager.service.ts ├── assets │ └── icon │ │ └── favicon.png ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── global.scss ├── index.html ├── karma.conf.js ├── main.ts ├── polyfills.ts ├── test.ts ├── theme │ └── variables.scss ├── tsconfig.app.json └── tsconfig.spec.json ├── tsconfig.json ├── tslint.json └── typings └── cordova-typings.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Specifies intentionally untracked files to ignore when using Git 2 | # http://git-scm.com/docs/gitignore 3 | 4 | *~ 5 | *.sw[mnpcod] 6 | *.log 7 | *.tmp 8 | *.tmp.* 9 | log.txt 10 | *.sublime-project 11 | *.sublime-workspace 12 | .vscode/ 13 | npm-debug.log* 14 | 15 | .idea/ 16 | .ionic/ 17 | .sourcemaps/ 18 | .sass-cache/ 19 | .tmp/ 20 | .versions/ 21 | coverage/ 22 | www/ 23 | node_modules/ 24 | tmp/ 25 | temp/ 26 | platforms/ 27 | plugins/ 28 | plugins/android.json 29 | plugins/ios.json 30 | $RECYCLE.BIN/ 31 | 32 | .DS_Store 33 | Thumbs.db 34 | UserInterfaceState.xcuserstate 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IonicOfflineMode 2 | 3 | Sample project that shows how to build an Dogs breeds APP with offline mode that caches API data so it can be used as a fallback later. Also, we create an offline manager which stores requests made during that time so we can later send out the calls one by one when we are online again. 4 | 5 | This project is an example created in the [Devdactic Blog](https://devdactic.com/ionic-4-offline-mode/) that have been modified by me. This project has been developed to practice my skills with the tech stack shown above. 6 | 7 | This project shows you how to: 8 | 9 | * Use Capacitor in Ionic 4. 10 | * Show dogs breeds list. 11 | * Add new dog breed. 12 | * Handling network changes. 13 | * Storing API requests locally. 14 | * Making API requests with local caching. 15 | 16 | Technologies: Ionic, Capacitor, TypeScript. 17 | 18 | ## Start fake json server 19 | 20 | ```bash 21 | $ cd json-server 22 | $ json-server --watch db.json 23 | ``` 24 | 25 | ## Running 26 | 27 | Before you go through this example, you should have at least a basic understanding of Ionic concepts. You must also already have Ionic installed on your machine. 28 | 29 | * Test in localhost: 30 | 31 | To run it, cd into `ionic-offline-mode` and run: 32 | 33 | ```bash 34 | npm install 35 | ionic serve 36 | ``` 37 | 38 | ## Capacitor: Add Platforms 39 | 40 | ``` bash 41 | $ npx cap add ios 42 | $ npx cap add android 43 | ``` 44 | 45 | ## Capacitor: Syncing your app 46 | Every time you perform a build (e.g. npm run build) that changes your web directory (default: www), you'll need to copy those changes down to your native projects: 47 | 48 | ``` bash 49 | $ npx cap copy 50 | ``` 51 | 52 | ## Capacitor: Open IDE to build 53 | 54 | ``` bash 55 | $ npx cap open ios 56 | $ npx cap open android 57 | ``` 58 | 59 | ## Requirements 60 | 61 | * [Node.js](http://nodejs.org/) 62 | * [Ionic](https://ionicframework.com/getting-started#cli) -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular-devkit/core/src/workspace/workspace-schema.json", 3 | "version": 1, 4 | "defaultProject": "app", 5 | "newProjectRoot": "projects", 6 | "projects": { 7 | "app": { 8 | "root": "", 9 | "sourceRoot": "src", 10 | "projectType": "application", 11 | "prefix": "app", 12 | "schematics": {}, 13 | "architect": { 14 | "build": { 15 | "builder": "@angular-devkit/build-angular:browser", 16 | "options": { 17 | "progress": false, 18 | "outputPath": "www", 19 | "index": "src/index.html", 20 | "main": "src/main.ts", 21 | "polyfills": "src/polyfills.ts", 22 | "tsConfig": "src/tsconfig.app.json", 23 | "assets": [ 24 | { 25 | "glob": "**/*", 26 | "input": "src/assets", 27 | "output": "assets" 28 | }, 29 | { 30 | "glob": "**/*.svg", 31 | "input": "node_modules/@ionic/angular/dist/ionic/svg", 32 | "output": "./svg" 33 | } 34 | ], 35 | "styles": [ 36 | { 37 | "input": "src/theme/variables.scss" 38 | }, 39 | { 40 | "input": "src/global.scss" 41 | } 42 | ], 43 | "scripts": [] 44 | }, 45 | "configurations": { 46 | "production": { 47 | "fileReplacements": [ 48 | { 49 | "replace": "src/environments/environment.ts", 50 | "with": "src/environments/environment.prod.ts" 51 | } 52 | ], 53 | "optimization": true, 54 | "outputHashing": "all", 55 | "sourceMap": false, 56 | "extractCss": true, 57 | "namedChunks": false, 58 | "aot": true, 59 | "extractLicenses": true, 60 | "vendorChunk": false, 61 | "buildOptimizer": true 62 | } 63 | } 64 | }, 65 | "serve": { 66 | "builder": "@angular-devkit/build-angular:dev-server", 67 | "options": { 68 | "browserTarget": "app:build" 69 | }, 70 | "configurations": { 71 | "production": { 72 | "browserTarget": "app:build:production" 73 | } 74 | } 75 | }, 76 | "extract-i18n": { 77 | "builder": "@angular-devkit/build-angular:extract-i18n", 78 | "options": { 79 | "browserTarget": "app:build" 80 | } 81 | }, 82 | "test": { 83 | "builder": "@angular-devkit/build-angular:karma", 84 | "options": { 85 | "main": "src/test.ts", 86 | "polyfills": "src/polyfills.ts", 87 | "tsConfig": "src/tsconfig.spec.json", 88 | "karmaConfig": "src/karma.conf.js", 89 | "styles": [], 90 | "scripts": [], 91 | "assets": [ 92 | { 93 | "glob": "favicon.ico", 94 | "input": "src/", 95 | "output": "/" 96 | }, 97 | { 98 | "glob": "**/*", 99 | "input": "src/assets", 100 | "output": "/assets" 101 | } 102 | ] 103 | } 104 | }, 105 | "lint": { 106 | "builder": "@angular-devkit/build-angular:tslint", 107 | "options": { 108 | "tsConfig": [ 109 | "src/tsconfig.app.json", 110 | "src/tsconfig.spec.json" 111 | ], 112 | "exclude": [ 113 | "**/node_modules/**" 114 | ] 115 | } 116 | }, 117 | "ionic-cordova-build": { 118 | "builder": "@ionic/angular-toolkit:cordova-build", 119 | "options": { 120 | "browserTarget": "app:build" 121 | }, 122 | "configurations": { 123 | "production": { 124 | "browserTarget": "app:build:production" 125 | } 126 | } 127 | }, 128 | "ionic-cordova-serve": { 129 | "builder": "@ionic/angular-toolkit:cordova-serve", 130 | "options": { 131 | "cordovaBuildTarget": "app:ionic-cordova-build", 132 | "devServerTarget": "app:serve" 133 | }, 134 | "configurations": { 135 | "production": { 136 | "cordovaBuildTarget": "app:ionic-cordova-build:production", 137 | "devServerTarget": "app:serve:production" 138 | } 139 | } 140 | } 141 | } 142 | }, 143 | "app-e2e": { 144 | "root": "e2e/", 145 | "projectType": "application", 146 | "architect": { 147 | "e2e": { 148 | "builder": "@angular-devkit/build-angular:protractor", 149 | "options": { 150 | "protractorConfig": "e2e/protractor.conf.js", 151 | "devServerTarget": "app:serve" 152 | } 153 | }, 154 | "lint": { 155 | "builder": "@angular-devkit/build-angular:tslint", 156 | "options": { 157 | "tsConfig": "e2e/tsconfig.e2e.json", 158 | "exclude": [ 159 | "**/node_modules/**" 160 | ] 161 | } 162 | } 163 | } 164 | } 165 | }, 166 | "cli": { 167 | "defaultCollection": "@ionic/angular-toolkit" 168 | }, 169 | "schematics": { 170 | "@ionic/angular-toolkit:component": { 171 | "styleext": "scss" 172 | }, 173 | "@ionic/angular-toolkit:page": { 174 | "styleext": "scss" 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /capacitor.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "appId": "com.offlinemode.app", 3 | "appName": "ionic-offline-mode", 4 | "bundledWebRuntime": false, 5 | "webDir": "www" 6 | } 7 | -------------------------------------------------------------------------------- /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: 'e2e/tsconfig.e2e.json' 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /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 display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toContain('The world is your oyster.'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /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.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ionic.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ionic-offline-mode", 3 | "integrations": {}, 4 | "type": "angular" 5 | } -------------------------------------------------------------------------------- /json-server/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "dogs": [ 3 | { 4 | "id": "d9c8bc3c-9e15-4ee4-a90e-82f8eb76feb5", 5 | "breed": "Parson russell terrier", 6 | "description": "Formando parte del grupo de los terriers, encontramos a los parson russell terrier, una variante de los conocidos jack russells. Estos graciosos y simpáticos canes destacan por su dinamismo y sus dotes para aprender nuevos trucos con el que deleitar a todos los que le rodean.", 7 | "image": "https://t1.ea.ltmcdn.com/es/razas/0/4/5/img_540_parson-russell-terrier_0_300_square.jpg" 8 | }, 9 | { 10 | "id": "3280c4f4-d1e8-45b7-924f-829b773ac9c9", 11 | "breed": "Cocker spaniel americano", 12 | "description": "Con un porte regio y elegante, gracias al que luce un aspecto realmente señorial, combinado con un carácter sumamente dulce y equilibrado, este animal conseguirá ganarse nuestro corazón con esa mirada tan tierna en milésimas de segundo. Estos canes resultan los compañeros ideales para todos aquellos dispuestos a brindarles su cariño.", 13 | "image": "https://t1.ea.ltmcdn.com/es/razas/9/1/6/img_619_cocker-spaniel-americano_0_300_square.jpg" 14 | }, 15 | { 16 | "id": "3d769bd7-af9f-4d29-bf78-ef70f4c26fdd", 17 | "breed": "Bóxer", 18 | "description": "El perro bóxer (Deutscher Boxer), también conocido como bóxer alemán o simplemente bóxer es una de las razas caninas de tipo moloso más populares del mundo y nace del cruce entre un Brabant bullenbeisser y un Bulldog, razas extintas actualmente. Debemos saber que la raza bóxer apareció por primera vez en Múnich (Alemania) en criadero conocido como Von Dom y que más adelante el perro bóxer fue utilizado durante la Segunda Guerra Mundial como perro de guerra mensajero, entregando cables de comunicación, y como perro ambulancia, transportando cuerpos de soldados heridos.", 19 | "image": "https://t1.ea.ltmcdn.com/es/razas/0/1/1/img_110_boxer_1_300_square.jpg" 20 | }, 21 | { 22 | "id": "1c4e0acd-d9cb-488c-8c0b-be18334e8fd5", 23 | "breed": "Chihuahua", 24 | "description": "El chiuahua o chihuahueño es una raza de perro pequeña muy popular por su reducido tamaño. Además de ser una mascota adorable se trata de un compañero inteligente, inquieto y curioso que ofrecerá todo su amor a quienes cuiden de él.", 25 | "image": "https://t2.ea.ltmcdn.com/es/razas/1/0/0/img_1_chihuahua_0_300_square.jpg" 26 | }, 27 | { 28 | "breed": "Rottweiler", 29 | "description": "El rottweiler es un perro fuerte, robusto y atlético. De talla mediana a grande, y con una apariencia que no esconde su gran poder, el rottweiler inspira una enorme admiración entre sus simpatizantes y un temor casi mítico entre quienes no lo conocen. Es indudable que la sola presencia de estos perros impone respeto y es fácil asustarse de un perro tan poderoso. No en vano la raza fue la elegida para encarnar al \"perro del diablo\" en la serie de películas \"La Profecía\".", 30 | "image": "https://t1.ea.ltmcdn.com/es/razas/2/1/1/img_112_rottweiler_0_300_square.jpg", 31 | "id": "39c69e8e-24e1-49db-9753-64fff23e96a2" 32 | }, 33 | { 34 | "breed": "Harrier", 35 | "description": "El harrier es una de las razas de perros de caza más populares de Gran Bretaña y suele confundirse con el beagle y el beagle harrier, aunque uno de sus parientes mas próximos es el foxhound inglés, siendo una \"versión reducida\" de este. El perro harrier destaca como sabueso por su increíble y potente olfato, que le ha convertido actualmente en uno de los perros detectores de sustancias olorosas más cualificados.", 36 | "image": "https://t2.ea.ltmcdn.com/es/razas/3/1/6/img_613_harrier_0_300_square.jpg", 37 | "id": "d55857e8-4a8e-400e-958f-e305e49dec5d" 38 | } 39 | ] 40 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ionic-offline-mode", 3 | "version": "0.0.1", 4 | "author": "Ionic Framework", 5 | "homepage": "http://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": "~8.0.0", 17 | "@angular/core": "~8.0.0", 18 | "@angular/forms": "~8.0.0", 19 | "@angular/http": "~7.2.15", 20 | "@angular/platform-browser": "~8.0.0", 21 | "@angular/platform-browser-dynamic": "~8.0.0", 22 | "@angular/router": "~8.0.0", 23 | "@capacitor/cli": "^1.0.0", 24 | "@capacitor/core": "^1.0.0", 25 | "@ionic-native/core": "5.7.0", 26 | "@ionic-native/splash-screen": "5.7.0", 27 | "@ionic-native/status-bar": "5.7.0", 28 | "@ionic/angular": "4.4.2", 29 | "@ionic/storage": "^2.2.0", 30 | "core-js": "^2.6.2", 31 | "rxjs": "6.5.2", 32 | "zone.js": "^0.9.1" 33 | }, 34 | "devDependencies": { 35 | "@angular-devkit/architect": "~0.800.1", 36 | "@angular-devkit/build-angular": "~0.800.1", 37 | "@angular-devkit/core": "~8.0.1", 38 | "@angular-devkit/schematics": "~8.0.1", 39 | "@angular/cli": "~8.0.1", 40 | "@angular/compiler": "~8.0.0", 41 | "@angular/compiler-cli": "~8.0.0", 42 | "@angular/language-service": "~8.0.0", 43 | "@ionic/angular-toolkit": "^1.5.1", 44 | "@types/jasmine": "~3.3.13", 45 | "@types/jasminewd2": "~2.0.6", 46 | "@types/node": "~12.0.4", 47 | "@webcomponents/webcomponentsjs": "^2.2.10", 48 | "codelyzer": "~5.0.1", 49 | "jasmine-core": "~3.4.0", 50 | "jasmine-spec-reporter": "~4.2.1", 51 | "karma": "~4.1.0", 52 | "karma-chrome-launcher": "~2.2.0", 53 | "karma-coverage-istanbul-reporter": "~2.0.5", 54 | "karma-jasmine": "~2.0.1", 55 | "karma-jasmine-html-reporter": "^1.4.2", 56 | "protractor": "~5.4.2", 57 | "ts-node": "~8.2.0", 58 | "tslint": "~5.17.0", 59 | "typescript": "~3.4.5" 60 | }, 61 | "description": "An Ionic project" 62 | } 63 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | const routes: Routes = [ 5 | { path: '', redirectTo: 'home', pathMatch: 'full' }, 6 | { path: 'home', loadChildren: './home/home.module#HomePageModule' }, 7 | ]; 8 | 9 | @NgModule({ 10 | imports: [RouterModule.forRoot(routes)], 11 | exports: [RouterModule] 12 | }) 13 | export class AppRoutingModule { } 14 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { TestBed, async } from '@angular/core/testing'; 3 | 4 | import { Platform } from '@ionic/angular'; 5 | import { SplashScreen } from '@ionic-native/splash-screen/ngx'; 6 | import { StatusBar } from '@ionic-native/status-bar/ngx'; 7 | 8 | import { AppComponent } from './app.component'; 9 | 10 | describe('AppComponent', () => { 11 | 12 | let statusBarSpy, splashScreenSpy, platformReadySpy, platformSpy; 13 | 14 | beforeEach(async(() => { 15 | statusBarSpy = jasmine.createSpyObj('StatusBar', ['styleDefault']); 16 | splashScreenSpy = jasmine.createSpyObj('SplashScreen', ['hide']); 17 | platformReadySpy = Promise.resolve(); 18 | platformSpy = jasmine.createSpyObj('Platform', { ready: platformReadySpy }); 19 | 20 | TestBed.configureTestingModule({ 21 | declarations: [AppComponent], 22 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 23 | providers: [ 24 | { provide: StatusBar, useValue: statusBarSpy }, 25 | { provide: SplashScreen, useValue: splashScreenSpy }, 26 | { provide: Platform, useValue: platformSpy }, 27 | ], 28 | }).compileComponents(); 29 | })); 30 | 31 | it('should create the app', () => { 32 | const fixture = TestBed.createComponent(AppComponent); 33 | const app = fixture.debugElement.componentInstance; 34 | expect(app).toBeTruthy(); 35 | }); 36 | 37 | it('should initialize the app', async () => { 38 | TestBed.createComponent(AppComponent); 39 | expect(platformSpy.ready).toHaveBeenCalled(); 40 | await platformReadySpy; 41 | expect(statusBarSpy.styleDefault).toHaveBeenCalled(); 42 | expect(splashScreenSpy.hide).toHaveBeenCalled(); 43 | }); 44 | 45 | // TODO: add more tests! 46 | 47 | }); 48 | -------------------------------------------------------------------------------- /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 | import { NetworkService, ConnectionStatus } from './services/network.service'; 8 | import { OfflineManagerService } from './services/offline-manager.service'; 9 | 10 | @Component({ 11 | selector: 'app-root', 12 | templateUrl: 'app.component.html' 13 | }) 14 | export class AppComponent { 15 | constructor( 16 | private platform: Platform, 17 | private splashScreen: SplashScreen, 18 | private statusBar: StatusBar, 19 | private networkService: NetworkService, 20 | private offlineManager: OfflineManagerService 21 | ) { 22 | this.initializeApp(); 23 | } 24 | 25 | initializeApp() { 26 | this.platform.ready().then(() => { 27 | this.statusBar.styleDefault(); 28 | this.splashScreen.hide(); 29 | this.networkService.onNetworkChange().subscribe((status: ConnectionStatus) => { 30 | if (status === ConnectionStatus.Online) { 31 | console.log('status in app.component', status); 32 | this.offlineManager.checkForEvents().subscribe(); 33 | } 34 | }); 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { 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 | import { HttpClientModule } from '@angular/common/http'; 13 | import { IonicStorageModule } from '@ionic/storage'; 14 | 15 | @NgModule({ 16 | declarations: [AppComponent], 17 | entryComponents: [], 18 | imports: [ 19 | BrowserModule, 20 | IonicModule.forRoot(), 21 | AppRoutingModule, 22 | HttpClientModule, 23 | IonicStorageModule.forRoot( 24 | { 25 | name: '__mydb', 26 | driverOrder: ['indexeddb', 'sqlite', 'websql'] 27 | } 28 | ) 29 | ], 30 | providers: [ 31 | StatusBar, 32 | SplashScreen, 33 | { provide: RouteReuseStrategy, useClass: IonicRouteStrategy } 34 | ], 35 | bootstrap: [AppComponent] 36 | }) 37 | export class AppModule {} 38 | -------------------------------------------------------------------------------- /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, ReactiveFormsModule } from '@angular/forms'; 5 | import { RouterModule } from '@angular/router'; 6 | 7 | import { HomePage } from './home.page'; 8 | import { AddDogModalComponent } from './../modals/add-dog-modal/add.dog.modal'; 9 | 10 | 11 | @NgModule({ 12 | imports: [ 13 | CommonModule, 14 | FormsModule, 15 | ReactiveFormsModule, 16 | IonicModule, 17 | RouterModule.forChild([ 18 | { 19 | path: '', 20 | component: HomePage 21 | } 22 | ]) 23 | ], 24 | declarations: [HomePage, AddDogModalComponent], 25 | entryComponents: [AddDogModalComponent] 26 | }) 27 | export class HomePageModule {} 28 | -------------------------------------------------------------------------------- /src/app/home/home.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Ionic Offline Mode Dogs Breeds App 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |

{{ dog.breed }}

26 |

{{ dog.description }}

27 |
28 |
29 |
30 |
31 | -------------------------------------------------------------------------------- /src/app/home/home.page.scss: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/home/home.page.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { HomePage } from './home.page'; 5 | 6 | describe('HomePage', () => { 7 | let component: HomePage; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ HomePage ], 13 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 14 | }) 15 | .compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(HomePage); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/home/home.page.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { NetworkService } from './../services/network.service'; 3 | import { ApiService } from './../services/api.service'; 4 | import { AddDogModalComponent } from './../modals/add-dog-modal/add.dog.modal'; 5 | import { ModalController } from '@ionic/angular'; 6 | 7 | 8 | @Component({ 9 | selector: 'app-home', 10 | templateUrl: 'home.page.html', 11 | styleUrls: ['home.page.scss'], 12 | }) 13 | export class HomePage { 14 | 15 | dogs: any; 16 | 17 | constructor(private networkService: NetworkService, private apiService: ApiService, private modalCtrl: ModalController) { 18 | console.log('HomePage::constructor() | method called'); 19 | this.loadData(true); 20 | } 21 | 22 | loadData(refresh = false, refresher?) { 23 | this.apiService.getDogs(refresh).subscribe(res => { 24 | this.dogs = res; 25 | if (refresher) { 26 | refresher.target.complete(); 27 | } 28 | }); 29 | } 30 | 31 | async presentModal() { 32 | console.log('HomePage::presentModal | method called'); 33 | const componentProps = { modalProps: { title: 'Add Dog Modal', buttonText: 'Add'}}; 34 | const modal = await this.modalCtrl.create({ 35 | component: AddDogModalComponent, 36 | componentProps: componentProps 37 | }); 38 | await modal.present(); 39 | 40 | const {data} = await modal.onWillDismiss(); 41 | if (data) { 42 | console.log('data', data); 43 | } 44 | } 45 | 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/app/modals/add-dog-modal/add.dog.modal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{ modal.title }} 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | Breed * 19 | 20 | 21 | 22 |
23 | This field is required. 24 |
25 |
26 | 27 | Description * 28 | 29 | 30 | 31 |
32 | This field is required. 33 |
34 |
35 | 36 | Image * 37 | 38 | 39 | 40 |
41 | This field is required. 42 |
43 |
44 |
45 | {{ modal.buttonText }} 46 | Clear 47 |
48 |
-------------------------------------------------------------------------------- /src/app/modals/add-dog-modal/add.dog.modal.scss: -------------------------------------------------------------------------------- 1 | .has-error { 2 | border-bottom: solid 1px var(--ion-color-danger); 3 | } 4 | 5 | .error-message { 6 | color: var(--ion-color-danger)!important; 7 | } -------------------------------------------------------------------------------- /src/app/modals/add-dog-modal/add.dog.modal.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewEncapsulation, OnInit } from '@angular/core'; 2 | import { ModalController, NavParams } from '@ionic/angular'; 3 | 4 | import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms'; 5 | import { ApiService } from './../../services/api.service'; 6 | 7 | import { v4 as uuid } from 'uuid'; 8 | 9 | @Component({ 10 | selector: 'app-add-dog-modal', 11 | templateUrl: 'add.dog.modal.html', 12 | styleUrls: ['./add.dog.modal.scss'], 13 | encapsulation: ViewEncapsulation.None 14 | }) 15 | export class AddDogModalComponent implements OnInit { 16 | 17 | modal: any = { 18 | title: '', 19 | buttonText: '' 20 | }; 21 | 22 | addDogForm: FormGroup; 23 | 24 | constructor(private formBuilder: FormBuilder, private modalCtrl: ModalController, public navParams: NavParams, 25 | private apiService: ApiService) { 26 | this.createForm(); 27 | } 28 | 29 | createForm() { 30 | this.addDogForm = this.formBuilder.group({ 31 | breed: new FormControl('', Validators.required), 32 | description: new FormControl('', Validators.required), 33 | image: new FormControl('', Validators.required), 34 | }); 35 | } 36 | 37 | ngOnInit() { 38 | this.modal = { ...this.navParams.data.modalProps}; 39 | } 40 | 41 | dismiss(data?: any) { 42 | // Using the injected ModalController this page 43 | // can "dismiss" itself and pass back data. 44 | // console.log('dismiss', data); 45 | this.modalCtrl.dismiss(data); 46 | } 47 | 48 | addDogFormSubmit() { 49 | console.log('AddDogModalComponent::addDogFormSubmit() | method called'); 50 | this.addDogForm.value.id = uuid(); 51 | console.log(this.addDogForm.value); 52 | this.apiService.addDog(this.addDogForm.value).subscribe(res => { 53 | console.log('Added dog', res); 54 | this.dismiss(); 55 | }); 56 | } 57 | 58 | clearAddDogForm() { 59 | console.log('AddDogModalComponent::clearAddDogForm() | method called'); 60 | this.addDogForm.reset(); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/app/models/stored.request.model.ts: -------------------------------------------------------------------------------- 1 | export interface StoredRequest { 2 | url: string; 3 | type: string; 4 | data: any; 5 | time: number; 6 | id: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/services/api.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ApiService } from './api.service'; 4 | 5 | describe('ApiService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: ApiService = TestBed.get(ApiService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/app/services/api.service.ts: -------------------------------------------------------------------------------- 1 | import { NetworkService, ConnectionStatus } from './network.service'; 2 | import { Injectable } from '@angular/core'; 3 | import { Storage } from '@ionic/storage'; 4 | import { Observable, from, throwError } from 'rxjs'; 5 | import { tap, map, catchError } from 'rxjs/operators'; 6 | import { HttpClient } from '@angular/common/http'; 7 | 8 | import { OfflineManagerService } from './offline-manager.service'; 9 | 10 | const API_STORAGE_KEY = 'specialkey'; 11 | const API_URL = 'http://localhost:3004'; 12 | 13 | @Injectable({ 14 | providedIn: 'root' 15 | }) 16 | export class ApiService { 17 | 18 | constructor(private networkService: NetworkService, private http: HttpClient, private storage: Storage, 19 | private offlineManager: OfflineManagerService) { } 20 | 21 | getDogs(forceRefresh: boolean = false): Observable { 22 | if (this.networkService.getCurrentNetworkStatus() === ConnectionStatus.Offline || !forceRefresh) { 23 | // Return the cached data from Storage 24 | return from(this.getLocalData('dogs')); 25 | } else { 26 | // Return real API data and store it locally 27 | return this.http.get(`${API_URL}/dogs`).pipe( 28 | tap(res => { 29 | console.log('res', res); 30 | this.setLocalData('dogs', res); 31 | }), 32 | catchError((x, caught) => { 33 | return throwError(x); 34 | }), 35 | ); 36 | } 37 | } 38 | 39 | addDog(data): Observable { 40 | const url = `${API_URL}/dogs/`; 41 | if (this.networkService.getCurrentNetworkStatus() === ConnectionStatus.Offline) { 42 | return from(this.offlineManager.storeRequest(url, 'POST', data)); 43 | } else { 44 | return this.http.post(url, data).pipe( 45 | catchError(err => { 46 | this.offlineManager.storeRequest(url, 'POST', data); 47 | throw new Error(err); 48 | }) 49 | ); 50 | } 51 | } 52 | 53 | // Save result of API requests 54 | private setLocalData(key, data) { 55 | console.log('ApiService::setLocalData(key, data) | method called', key, data); 56 | this.storage.ready().then(() => { 57 | this.storage.set(`${API_STORAGE_KEY}-${key}`, data); 58 | }); 59 | } 60 | 61 | // Get cached API result 62 | private getLocalData(key) { 63 | console.log('ApiService::getLocalData(key) | method called', key); 64 | return this.storage.get(`${API_STORAGE_KEY}-${key}`); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/app/services/network.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { NetworkService } from './network.service'; 4 | 5 | describe('NetworkService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: NetworkService = TestBed.get(NetworkService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/app/services/network.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject, Observable } from 'rxjs'; 3 | import { ToastController, LoadingController } from '@ionic/angular'; 4 | 5 | import { Plugins, Capacitor } from '@capacitor/core'; 6 | 7 | const { Network } = Plugins; 8 | 9 | export enum ConnectionStatus { 10 | Online, 11 | Offline 12 | } 13 | 14 | @Injectable({ 15 | providedIn: 'root' 16 | }) 17 | export class NetworkService { 18 | 19 | private status: BehaviorSubject = new BehaviorSubject(ConnectionStatus.Offline); 20 | private loading: any = null; 21 | 22 | constructor(private toastController: ToastController, private loadingCtrl: LoadingController) { 23 | console.log('NetworkService::constructor | method called'); 24 | 25 | let status = ConnectionStatus.Offline; 26 | if (Capacitor.platform === 'web') { 27 | console.log('WEB'); 28 | console.log('navigator.onLine', navigator.onLine); 29 | this.addConnectivityListenersBrowser(); 30 | status = navigator.onLine === true ? ConnectionStatus.Online : ConnectionStatus.Offline; 31 | } else { // Native: use capacitor network plugin 32 | console.log('NATIVE'); 33 | this.addConnectivityListernerNative(); 34 | // status = Network.getStatus(); 35 | } 36 | 37 | this.status.next(status); 38 | } 39 | 40 | onOnline() { 41 | if (this.status.getValue() === ConnectionStatus.Offline) { 42 | console.log('Network connected!'); 43 | console.log('navigator.onLine', navigator.onLine); 44 | this.dismissLoading(); 45 | this.updateNetworkStatus(ConnectionStatus.Online); 46 | } 47 | } 48 | 49 | onOffline() { 50 | if (this.status.getValue() === ConnectionStatus.Online) { 51 | console.log('Network was disconnected :-('); 52 | console.log('navigator.onLine', navigator.onLine); 53 | this.presentLoading(); 54 | this.updateNetworkStatus(ConnectionStatus.Offline); 55 | } 56 | } 57 | 58 | addConnectivityListenersBrowser() { 59 | window.addEventListener('online', this.onOnline.bind(this)); 60 | window.addEventListener('offline', this.onOffline.bind(this)); 61 | } 62 | 63 | addConnectivityListernerNative() { 64 | 65 | const handler = Network.addListener('networkStatusChange', (status) => { 66 | console.log('Network status changed', status); 67 | }); 68 | } 69 | 70 | private async updateNetworkStatus(status: ConnectionStatus) { 71 | console.log('updateNetworkStatus', status); 72 | this.status.next(status); 73 | 74 | const connection = status === ConnectionStatus.Offline ? 'Offline' : 'Online'; 75 | const toast = await this.toastController.create({ 76 | message: `You are now ${connection}`, 77 | duration: 3000, 78 | position: 'bottom' 79 | }); 80 | toast.present(); 81 | } 82 | 83 | public onNetworkChange(): Observable { 84 | return this.status.asObservable(); 85 | } 86 | 87 | public getCurrentNetworkStatus(): ConnectionStatus { 88 | return this.status.getValue(); 89 | } 90 | 91 | private async presentLoading() { 92 | this.loading = await this.loadingCtrl.create({ 93 | message: 'Waiting for connection...', 94 | }); 95 | 96 | return await this.loading.present(); 97 | } 98 | 99 | private async dismissLoading() { 100 | if ((this.loading !== null) && (typeof this.loading !== 'undefined')) { 101 | this.loading.dismiss(); 102 | this.loading = null; 103 | } 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/app/services/offline-manager.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { OfflineManagerService } from './offline-manager.service'; 4 | 5 | describe('OfflineManagerService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: OfflineManagerService = TestBed.get(OfflineManagerService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/app/services/offline-manager.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Storage } from '@ionic/storage'; 3 | import { from, of, forkJoin } from 'rxjs'; 4 | import { switchMap, finalize } from 'rxjs/operators'; 5 | import { HttpClient } from '@angular/common/http'; 6 | 7 | import { ToastController } from '@ionic/angular'; 8 | 9 | import { StoredRequest } from './../models/stored.request.model'; 10 | 11 | const STORAGE_REQ_KEY = 'storedreq'; 12 | 13 | @Injectable({ 14 | providedIn: 'root' 15 | }) 16 | export class OfflineManagerService { 17 | 18 | constructor(private storage: Storage, private toastCtrl: ToastController, private http: HttpClient) { } 19 | 20 | checkForEvents() { 21 | return from(this.storage.get(STORAGE_REQ_KEY)).pipe( 22 | switchMap(storedOperations => { 23 | const storedObj = JSON.parse(storedOperations); 24 | if (storedObj && storedObj.length > 0) { 25 | return this.sendRequests(storedObj).pipe( 26 | finalize(() => { 27 | this.presentToast(`Local data succesfully synced to API!`); 28 | this.storage.remove(STORAGE_REQ_KEY); 29 | }) 30 | ); 31 | } else { 32 | console.log('no local events to sync'); 33 | return of(false); 34 | } 35 | }) 36 | ); 37 | } 38 | 39 | sendRequests(operations: StoredRequest[]) { 40 | const obs = []; 41 | 42 | for (const op of operations) { 43 | console.log('Make one request: ', op); 44 | const data = JSON.stringify(op.data); 45 | console.log('JSON.stringify(op.data)', data); 46 | const oneObs = this.http.request(op.type, op.url, {body: op.data}); 47 | obs.push(oneObs); 48 | } 49 | 50 | // Send out all local events and return once they are finished. 51 | return forkJoin(obs); 52 | } 53 | 54 | async presentToast(message) { 55 | const toast = await this.toastCtrl.create({ 56 | message: message, 57 | duration: 3000, 58 | position: 'bottom' 59 | }); 60 | toast.present(); 61 | } 62 | 63 | storeRequest(url, type, data) { 64 | this.presentToast(`Your data is stored locally because you seem to be offline.`); 65 | 66 | const action: StoredRequest = { 67 | url: url, 68 | type: type, 69 | data: data, 70 | time: new Date().getTime(), 71 | id: Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 5) 72 | }; 73 | // https://stackoverflow.com/questions/1349404/generate-random-string-characters-in-javascript 74 | 75 | return this.storage.get(STORAGE_REQ_KEY).then(storedOperations => { 76 | let storedObj = JSON.parse(storedOperations); 77 | 78 | if (storedObj) { 79 | storedObj.push(action); 80 | } else { 81 | storedObj = [action]; 82 | } 83 | // Save old & new local transactions back to Storage 84 | return this.storage.set(STORAGE_REQ_KEY, JSON.stringify(storedObj)); 85 | }); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/assets/icon/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abritopach/ionic-offline-mode/efd113ccea91f7a63ede80572a1aed9f8d9e02bc/src/assets/icon/favicon.png -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * In development mode, to ignore zone related error stack frames such as 11 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can 12 | * import the following file, but please comment it out in production mode 13 | * because it will have performance impact when throw error 14 | */ 15 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 16 | -------------------------------------------------------------------------------- /src/global.scss: -------------------------------------------------------------------------------- 1 | // http://ionicframework.com/docs/theming/ 2 | @import "~@ionic/angular/css/core.css"; 3 | @import "~@ionic/angular/css/normalize.css"; 4 | @import "~@ionic/angular/css/structure.css"; 5 | @import "~@ionic/angular/css/typography.css"; 6 | 7 | @import "~@ionic/angular/css/padding.css"; 8 | @import "~@ionic/angular/css/float-elements.css"; 9 | @import "~@ionic/angular/css/text-alignment.css"; 10 | @import "~@ionic/angular/css/text-transformation.css"; 11 | @import "~@ionic/angular/css/flex-utils.css"; 12 | 13 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ionic App 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/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'], 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/regexp'; 32 | // import 'core-js/es6/map'; 33 | // import 'core-js/es6/weak-map'; 34 | // import 'core-js/es6/set'; 35 | 36 | /** 37 | * If your app need to indexed by Google Search, your app require polyfills 'core-js/es6/array' 38 | * Google bot use ES5. 39 | * FYI: Googlebot uses a renderer following the similar spec to Chrome 41. 40 | * https://developers.google.com/search/docs/guides/rendering 41 | **/ 42 | // import 'core-js/es6/array'; 43 | 44 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 45 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 46 | 47 | /** IE10 and IE11 requires the following for the Reflect API. */ 48 | // import 'core-js/es6/reflect'; 49 | 50 | 51 | /** Evergreen browsers require these. **/ 52 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 53 | import 'core-js/es7/reflect'; 54 | 55 | 56 | /** 57 | * Web Animations `@angular/platform-browser/animations` 58 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 59 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 60 | **/ 61 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 62 | 63 | /** 64 | * By default, zone.js will patch all possible macroTask and DomEvents 65 | * user can disable parts of macroTask/DomEvents patch by setting following flags 66 | */ 67 | 68 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 69 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 70 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 71 | 72 | /* 73 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 74 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 75 | */ 76 | // (window as any).__Zone_enable_cross_context_check = true; 77 | 78 | /*************************************************************************************************** 79 | * Zone JS is required by default for Angular itself. 80 | */ 81 | import 'zone.js/dist/zone'; // Included with Angular CLI. 82 | 83 | 84 | 85 | /*************************************************************************************************** 86 | * APPLICATION IMPORTS 87 | */ 88 | 89 | import '@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js'; 90 | -------------------------------------------------------------------------------- /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/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /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 | 7 | /** primary **/ 8 | --ion-color-primary: #3880ff; 9 | --ion-color-primary-rgb: 56,128,255; 10 | --ion-color-primary-contrast: #ffffff; 11 | --ion-color-primary-contrast-rgb: 255,255,255; 12 | --ion-color-primary-shade: #3171e0; 13 | --ion-color-primary-tint: #4c8dff; 14 | 15 | /** secondary **/ 16 | --ion-color-secondary: #0cd1e8; 17 | --ion-color-secondary-rgb: 12,209,232; 18 | --ion-color-secondary-contrast: #ffffff; 19 | --ion-color-secondary-contrast-rgb: 255,255,255; 20 | --ion-color-secondary-shade: #0bb8cc; 21 | --ion-color-secondary-tint: #24d6ea; 22 | 23 | /** tertiary **/ 24 | --ion-color-tertiary: #7044ff; 25 | --ion-color-tertiary-rgb: 112,68,255; 26 | --ion-color-tertiary-contrast: #ffffff; 27 | --ion-color-tertiary-contrast-rgb: 255,255,255; 28 | --ion-color-tertiary-shade: #633ce0; 29 | --ion-color-tertiary-tint: #7e57ff; 30 | 31 | /** success **/ 32 | --ion-color-success: #10dc60; 33 | --ion-color-success-rgb: 16,220,96; 34 | --ion-color-success-contrast: #ffffff; 35 | --ion-color-success-contrast-rgb: 255,255,255; 36 | --ion-color-success-shade: #0ec254; 37 | --ion-color-success-tint: #28e070; 38 | 39 | /** warning **/ 40 | --ion-color-warning: #ffce00; 41 | --ion-color-warning-rgb: 255,206,0; 42 | --ion-color-warning-contrast: #ffffff; 43 | --ion-color-warning-contrast-rgb: 255,255,255; 44 | --ion-color-warning-shade: #e0b500; 45 | --ion-color-warning-tint: #ffd31a; 46 | 47 | /** danger **/ 48 | --ion-color-danger: #f04141; 49 | --ion-color-danger-rgb: 245,61,61; 50 | --ion-color-danger-contrast: #ffffff; 51 | --ion-color-danger-contrast-rgb: 255,255,255; 52 | --ion-color-danger-shade: #d33939; 53 | --ion-color-danger-tint: #f25454; 54 | 55 | /** dark **/ 56 | --ion-color-dark: #222428; 57 | --ion-color-dark-rgb: 34,34,34; 58 | --ion-color-dark-contrast: #ffffff; 59 | --ion-color-dark-contrast-rgb: 255,255,255; 60 | --ion-color-dark-shade: #1e2023; 61 | --ion-color-dark-tint: #383a3e; 62 | 63 | /** medium **/ 64 | --ion-color-medium: #989aa2; 65 | --ion-color-medium-rgb: 152,154,162; 66 | --ion-color-medium-contrast: #ffffff; 67 | --ion-color-medium-contrast-rgb: 255,255,255; 68 | --ion-color-medium-shade: #86888f; 69 | --ion-color-medium-tint: #a2a4ab; 70 | 71 | /** light **/ 72 | --ion-color-light: #f4f5f8; 73 | --ion-color-light-rgb: 244,244,244; 74 | --ion-color-light-contrast: #000000; 75 | --ion-color-light-contrast-rgb: 0,0,0; 76 | --ion-color-light-shade: #d7d8da; 77 | --ion-color-light-tint: #f5f6f9; 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "baseUrl": "./", 6 | "module": "es2015" 7 | }, 8 | "exclude": [ 9 | "test.ts", 10 | "**/*.spec.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "types": [ 8 | "jasmine", 9 | "node" 10 | ] 11 | }, 12 | "files": [ 13 | "test.ts" 14 | ], 15 | "include": [ 16 | "polyfills.ts", 17 | "**/*.spec.ts", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "es5", 11 | "lib": [ 12 | "es2017", 13 | "dom" 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-spacing": true, 20 | "indent": [ 21 | true, 22 | "spaces" 23 | ], 24 | "interface-over-type-literal": true, 25 | "label-position": true, 26 | "max-line-length": [ 27 | true, 28 | 140 29 | ], 30 | "member-access": false, 31 | "member-ordering": [ 32 | true, 33 | { 34 | "order": [ 35 | "static-field", 36 | "instance-field", 37 | "static-method", 38 | "instance-method" 39 | ] 40 | } 41 | ], 42 | "no-arg": true, 43 | "no-bitwise": true, 44 | "no-console": [ 45 | true, 46 | "debug", 47 | "info", 48 | "time", 49 | "timeEnd", 50 | "trace" 51 | ], 52 | "no-construct": true, 53 | "no-debugger": true, 54 | "no-duplicate-super": true, 55 | "no-empty": false, 56 | "no-empty-interface": true, 57 | "no-eval": true, 58 | "no-inferrable-types": [ 59 | true, 60 | "ignore-params" 61 | ], 62 | "no-misused-new": true, 63 | "no-non-null-assertion": true, 64 | "no-shadowed-variable": true, 65 | "no-string-literal": false, 66 | "no-string-throw": true, 67 | "no-switch-case-fall-through": true, 68 | "no-trailing-whitespace": true, 69 | "no-unnecessary-initializer": true, 70 | "no-unused-expression": true, 71 | "no-use-before-declare": true, 72 | "no-var-keyword": true, 73 | "object-literal-sort-keys": false, 74 | "one-line": [ 75 | true, 76 | "check-open-brace", 77 | "check-catch", 78 | "check-else", 79 | "check-whitespace" 80 | ], 81 | "prefer-const": true, 82 | "quotemark": [ 83 | true, 84 | "single" 85 | ], 86 | "radix": true, 87 | "semicolon": [ 88 | true, 89 | "always" 90 | ], 91 | "triple-equals": [ 92 | true, 93 | "allow-null-check" 94 | ], 95 | "typedef-whitespace": [ 96 | true, 97 | { 98 | "call-signature": "nospace", 99 | "index-signature": "nospace", 100 | "parameter": "nospace", 101 | "property-declaration": "nospace", 102 | "variable-declaration": "nospace" 103 | } 104 | ], 105 | "unified-signatures": true, 106 | "variable-name": false, 107 | "whitespace": [ 108 | true, 109 | "check-branch", 110 | "check-decl", 111 | "check-operator", 112 | "check-separator", 113 | "check-type" 114 | ], 115 | "directive-selector": [ 116 | true, 117 | "attribute", 118 | "app", 119 | "camelCase" 120 | ], 121 | "component-selector": [ 122 | true, 123 | "element", 124 | "app", 125 | "page", 126 | "kebab-case" 127 | ], 128 | "no-output-on-prefix": true, 129 | "use-input-property-decorator": true, 130 | "use-output-property-decorator": true, 131 | "use-host-property-decorator": true, 132 | "no-input-rename": true, 133 | "no-output-rename": true, 134 | "use-life-cycle-interface": true, 135 | "use-pipe-transform-interface": true, 136 | "directive-class-suffix": true 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /typings/cordova-typings.d.ts: -------------------------------------------------------------------------------- 1 | 2 | /// 3 | /// --------------------------------------------------------------------------------