├── src
├── app
│ ├── home
│ │ ├── home.page.scss
│ │ ├── home.page.spec.ts
│ │ ├── home.module.ts
│ │ ├── home.page.html
│ │ └── home.page.ts
│ ├── app.component.html
│ ├── models
│ │ └── stored.request.model.ts
│ ├── modals
│ │ └── add-dog-modal
│ │ │ ├── add.dog.modal.scss
│ │ │ ├── add.dog.modal.ts
│ │ │ └── add.dog.modal.html
│ ├── services
│ │ ├── api.service.spec.ts
│ │ ├── network.service.spec.ts
│ │ ├── offline-manager.service.spec.ts
│ │ ├── api.service.ts
│ │ ├── offline-manager.service.ts
│ │ └── network.service.ts
│ ├── app-routing.module.ts
│ ├── app.module.ts
│ ├── app.component.ts
│ └── app.component.spec.ts
├── environments
│ ├── environment.prod.ts
│ └── environment.ts
├── assets
│ └── icon
│ │ └── favicon.png
├── tsconfig.app.json
├── tsconfig.spec.json
├── main.ts
├── global.scss
├── test.ts
├── index.html
├── karma.conf.js
├── theme
│ └── variables.scss
└── polyfills.ts
├── ionic.config.json
├── capacitor.config.json
├── typings
└── cordova-typings.d.ts
├── e2e
├── tsconfig.e2e.json
├── src
│ ├── app.po.ts
│ └── app.e2e-spec.ts
└── protractor.conf.js
├── tsconfig.json
├── .gitignore
├── README.md
├── package.json
├── tslint.json
├── json-server
└── db.json
└── angular.json
/src/app/home/home.page.scss:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true
3 | };
4 |
--------------------------------------------------------------------------------
/ionic.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ionic-offline-mode",
3 | "integrations": {},
4 | "type": "angular"
5 | }
--------------------------------------------------------------------------------
/src/assets/icon/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abritopach/ionic-offline-mode/HEAD/src/assets/icon/favicon.png
--------------------------------------------------------------------------------
/capacitor.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "appId": "com.offlinemode.app",
3 | "appName": "ionic-offline-mode",
4 | "bundledWebRuntime": false,
5 | "webDir": "www"
6 | }
7 |
--------------------------------------------------------------------------------
/typings/cordova-typings.d.ts:
--------------------------------------------------------------------------------
1 |
2 | ///
3 | ///
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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)
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
48 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------