├── .gitignore ├── LICENSE ├── README.md ├── angular.json ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.e2e.json ├── ionic.config.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 │ ├── components │ │ ├── components.module.ts │ │ └── preload-image │ │ │ ├── preload-image.component.html │ │ │ ├── preload-image.component.scss │ │ │ └── preload-image.component.ts │ ├── home │ │ ├── home.module.ts │ │ ├── home.page.html │ │ ├── home.page.scss │ │ ├── home.page.spec.ts │ │ └── home.page.ts │ └── web-components │ │ ├── app-initialize.ts │ │ ├── coffee-icon │ │ └── coffee-icon.component.ts │ │ └── web-components.module.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 /.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 IonicThemes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web Components in Ionic Framework 2 | This project is an Ionic app showcasing Angular custom components and Stencil custom web components. This repo is part of the post: [Mastering Web Components in Ionic Framework](https://ionicthemes.com/tutorials/about/ionic-4-tutorial-mastering-web-components-in-ionic-4). 3 | 4 | 5 | ### Run the project 6 | Install the dependencies: 7 | `npm install` 8 | 9 | Run the project: 10 | `ionic serve` 11 | 12 | ### Demo 13 | ![](https://media.giphy.com/media/1swuqPMEqALdgm3NJ8/giphy.gif) 14 | 15 | 16 | ### Web Components, Shadow DOM, CSS Variables. Understanding the new components architecture in Ionic. 17 | In our previous post [Ionic 4 vs Ionic 3 — What you need to know about Ionic 4](https://ionicthemes.com/tutorials/about/ionic-4-vs-ionic-3) we surfaced the main differences between Ionic 3 and Ionic 4. 18 | 19 | Ionic 4 introduced a lot of changes that you need understand because they changed the way Ionic Framework components work. 20 | 21 | Then, when Ionic 5 was released, we created the post [What's new in Ionic 5 - Migration and Free Starter](https://ionicthemes.com/tutorials/about/ionic5-tutorial-migration-and-starter) where we explain how to take advantage of the new benefits from Ionic 5. 22 | 23 | In this series of posts we are going to go deeper on the structure and core concepts of Ionic apps and explore more advanced topics, so if you are new to Ionic, I strongly recommend that you first read our [Introduction to Ionic tutorial](https://ionicthemes.com/tutorials/about/ionic-framework-introduction-and-key-components). 24 | 25 | Covering everything from styling and customizing Ionic components, to building custom web components with Stencil and then use them in your Ionic projects (or any other framework, as web components are framework agnostic!). 26 | 27 | ### Get a premium Ionic 5 Starter App 28 | Did you know that we recently released [Ionic 5 Full Starter App](https://ionicthemes.com/product/ionic5-full-starter-app-pro-version)? It's an ionic 5 template that you can use to jump start your Ionic app development and save yourself hundreds of hours of design and development. 29 | 30 | It is also a PWA and uses Ionic Web Components. 31 | Try it on your phone to see how it works! 32 | 33 | 34 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular-devkit/core/src/workspace/workspace-schema.json", 3 | "version": 1, 4 | "defaultProject": "app", 5 | "projects": { 6 | "app": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "progress": false, 17 | "outputPath": "www", 18 | "index": "src/index.html", 19 | "main": "src/main.ts", 20 | "polyfills": "src/polyfills.ts", 21 | "tsConfig": "src/tsconfig.app.json", 22 | "assets": [ 23 | { 24 | "glob": "**/*", 25 | "input": "src/assets", 26 | "output": "assets" 27 | }, 28 | { 29 | "glob": "**/*.svg", 30 | "input": "node_modules/@ionic/angular/dist/ionic/svg", 31 | "output": "./svg" 32 | } 33 | ], 34 | "styles": [ 35 | { 36 | "input": "src/theme/variables.scss" 37 | }, 38 | { 39 | "input": "src/global.scss" 40 | } 41 | ], 42 | "scripts": [] 43 | }, 44 | "configurations": { 45 | "production": { 46 | "fileReplacements": [ 47 | { 48 | "replace": "src/environments/environment.ts", 49 | "with": "src/environments/environment.prod.ts" 50 | } 51 | ], 52 | "optimization": true, 53 | "outputHashing": "all", 54 | "sourceMap": false, 55 | "extractCss": true, 56 | "namedChunks": false, 57 | "aot": true, 58 | "extractLicenses": true, 59 | "vendorChunk": false, 60 | "buildOptimizer": true 61 | } 62 | } 63 | }, 64 | "serve": { 65 | "builder": "@angular-devkit/build-angular:dev-server", 66 | "options": { 67 | "browserTarget": "app:build" 68 | }, 69 | "configurations": { 70 | "production": { 71 | "browserTarget": "app:build:production" 72 | } 73 | } 74 | }, 75 | "extract-i18n": { 76 | "builder": "@angular-devkit/build-angular:extract-i18n", 77 | "options": { 78 | "browserTarget": "app:build" 79 | } 80 | }, 81 | "test": { 82 | "builder": "@angular-devkit/build-angular:karma", 83 | "options": { 84 | "main": "src/test.ts", 85 | "polyfills": "src/polyfills.ts", 86 | "tsConfig": "src/tsconfig.spec.json", 87 | "karmaConfig": "src/karma.conf.js", 88 | "styles": [ 89 | "styles.css" 90 | ], 91 | "scripts": [], 92 | "assets": [ 93 | { 94 | "glob": "favicon.ico", 95 | "input": "src/", 96 | "output": "/" 97 | }, 98 | { 99 | "glob": "**/*", 100 | "input": "src/assets", 101 | "output": "/assets" 102 | } 103 | ] 104 | } 105 | }, 106 | "lint": { 107 | "builder": "@angular-devkit/build-angular:tslint", 108 | "options": { 109 | "tsConfig": [ 110 | "src/tsconfig.app.json", 111 | "src/tsconfig.spec.json" 112 | ], 113 | "exclude": [ 114 | "**/node_modules/**" 115 | ] 116 | } 117 | }, 118 | "ionic-cordova-build": { 119 | "builder": "@ionic/ng-toolkit:cordova-build", 120 | "options": { 121 | "browserTarget": "app:build" 122 | }, 123 | "configurations": { 124 | "production": { 125 | "browserTarget": "app:build:production" 126 | } 127 | } 128 | }, 129 | "ionic-cordova-serve": { 130 | "builder": "@ionic/ng-toolkit:cordova-serve", 131 | "options": { 132 | "cordovaBuildTarget": "app:ionic-cordova-build", 133 | "devServerTarget": "app:serve" 134 | }, 135 | "configurations": { 136 | "production": { 137 | "cordovaBuildTarget": "app:ionic-cordova-build:production", 138 | "devServerTarget": "app:serve:production" 139 | } 140 | } 141 | } 142 | } 143 | }, 144 | "app-e2e": { 145 | "root": "e2e/", 146 | "projectType": "application", 147 | "architect": { 148 | "e2e": { 149 | "builder": "@angular-devkit/build-angular:protractor", 150 | "options": { 151 | "protractorConfig": "e2e/protractor.conf.js", 152 | "devServerTarget": "app:serve" 153 | } 154 | }, 155 | "lint": { 156 | "builder": "@angular-devkit/build-angular:tslint", 157 | "options": { 158 | "tsConfig": "e2e/tsconfig.e2e.json", 159 | "exclude": [ 160 | "**/node_modules/**" 161 | ] 162 | } 163 | } 164 | } 165 | } 166 | }, 167 | "cli": { 168 | "defaultCollection": "@ionic/schematics-angular" 169 | }, 170 | "schematics": { 171 | "@ionic/schematics-angular:component": { 172 | "styleext": "scss" 173 | }, 174 | "@ionic/schematics-angular:page": { 175 | "styleext": "scss" 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /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-4-custom-components", 3 | "integrations": {}, 4 | "type": "angular" 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ionic-4-custom-components", 3 | "version": "0.0.1", 4 | "author": "https://ionicthemes.com", 5 | "contributors": [ 6 | "Agustín " 7 | ], 8 | "homepage": "https://ionicthemes.com", 9 | "scripts": { 10 | "ng": "ng", 11 | "start": "ng serve", 12 | "build": "ng build", 13 | "test": "ng test", 14 | "lint": "ng lint", 15 | "e2e": "ng e2e" 16 | }, 17 | "private": true, 18 | "dependencies": { 19 | "@angular/common": "~6.1.1", 20 | "@angular/core": "~6.1.1", 21 | "@angular/forms": "~6.1.1", 22 | "@angular/http": "~6.1.1", 23 | "@angular/platform-browser": "~6.1.1", 24 | "@angular/platform-browser-dynamic": "~6.1.1", 25 | "@angular/router": "~6.1.1", 26 | "@ionic-native/core": "5.0.0-beta.15", 27 | "@ionic-native/splash-screen": "5.0.0-beta.15", 28 | "@ionic-native/status-bar": "5.0.0-beta.15", 29 | "@ionic/angular": "4.0.0-beta.5", 30 | "c-coffee-icon": "0.0.2", 31 | "core-js": "^2.5.3", 32 | "rxjs": "6.2.2", 33 | "zone.js": "^0.8.26" 34 | }, 35 | "devDependencies": { 36 | "@angular/cli": "~6.1.1", 37 | "@angular/compiler": "~6.1.1", 38 | "@angular/compiler-cli": "~6.1.1", 39 | "@angular/language-service": "~6.1.1", 40 | "@angular-devkit/architect": "~0.7.2", 41 | "@angular-devkit/build-angular": "~0.7.2", 42 | "@angular-devkit/core": "~0.7.2", 43 | "@angular-devkit/schematics": "~0.7.2", 44 | "@ionic/ng-toolkit": "^1.0.0", 45 | "@ionic/schematics-angular": "^1.0.0", 46 | "@types/jasmine": "~2.8.6", 47 | "@types/jasminewd2": "~2.0.3", 48 | "@types/node": "~10.9.2", 49 | "codelyzer": "~4.4.2", 50 | "jasmine-core": "~2.99.1", 51 | "jasmine-spec-reporter": "~4.2.1", 52 | "karma": "~3.0.0", 53 | "karma-chrome-launcher": "~2.2.0", 54 | "karma-coverage-istanbul-reporter": "~2.0.0", 55 | "karma-jasmine": "~1.1.1", 56 | "karma-jasmine-html-reporter": "^0.2.2", 57 | "protractor": "~5.4.0", 58 | "ts-node": "~7.0.0", 59 | "tslint": "~5.11.0", 60 | "typescript": "~2.9.2" 61 | }, 62 | "description": "An Ionic project showcasing Angular custom components and Stencil custom web components" 63 | } 64 | -------------------------------------------------------------------------------- /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 | @Component({ 8 | selector: 'app-root', 9 | templateUrl: 'app.component.html' 10 | }) 11 | export class AppComponent { 12 | constructor( 13 | private platform: Platform, 14 | private splashScreen: SplashScreen, 15 | private statusBar: StatusBar 16 | ) { 17 | this.initializeApp(); 18 | } 19 | 20 | initializeApp() { 21 | this.platform.ready().then(() => { 22 | this.statusBar.styleDefault(); 23 | this.splashScreen.hide(); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { RouterModule, RouteReuseStrategy, Routes } 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 { WebComponentsModule } from './web-components/web-components.module'; 10 | 11 | import { AppComponent } from './app.component'; 12 | import { AppRoutingModule } from './app-routing.module'; 13 | 14 | @NgModule({ 15 | declarations: [AppComponent], 16 | entryComponents: [], 17 | imports: [ 18 | BrowserModule, 19 | IonicModule.forRoot(), 20 | WebComponentsModule.forRoot(), 21 | AppRoutingModule 22 | ], 23 | providers: [ 24 | StatusBar, 25 | SplashScreen, 26 | { provide: RouteReuseStrategy, useClass: IonicRouteStrategy } 27 | ], 28 | bootstrap: [AppComponent] 29 | }) 30 | export class AppModule {} 31 | -------------------------------------------------------------------------------- /src/app/components/components.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { IonicModule } from '@ionic/angular'; 4 | 5 | import { PreloadImageComponent } from './preload-image/preload-image.component'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | CommonModule, 10 | IonicModule.forRoot(), 11 | ], 12 | declarations: [ 13 | PreloadImageComponent 14 | ], 15 | exports: [ 16 | PreloadImageComponent 17 | ], 18 | entryComponents: [], 19 | }) 20 | export class ComponentsModule {} 21 | -------------------------------------------------------------------------------- /src/app/components/preload-image/preload-image.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/app/components/preload-image/preload-image.component.scss: -------------------------------------------------------------------------------- 1 | $white: #FFFFFF; 2 | $grey: #9e9e9e; 3 | 4 | $preload-image-bg: rgba(darken($white, 10), .25); 5 | 6 | $spinner-size: 28px; 7 | $spinner-color: $grey; 8 | 9 | preload-image { 10 | display: block; 11 | background-color: $preload-image-bg; 12 | overflow: hidden; 13 | position: relative; 14 | width: 100%; 15 | 16 | .spinner { 17 | position: absolute; 18 | top: calc(50% - #{ ($spinner-size/2) }); 19 | left: calc(50% - #{ ($spinner-size/2) }); 20 | width: $spinner-size; 21 | height: $spinner-size; 22 | font-size: $spinner-size; 23 | line-height: $spinner-size; 24 | color: $spinner-color; 25 | } 26 | 27 | ion-img { 28 | position: absolute; 29 | top: 0; 30 | left: 0; 31 | transition: visibility 0s linear, opacity .5s linear; 32 | opacity: 0; 33 | visibility: hidden; 34 | width: 100%; 35 | height: 100%; 36 | } 37 | 38 | &.img-loaded { 39 | background-color: transparent; 40 | border: 0; 41 | 42 | ion-img { 43 | opacity: 1; 44 | visibility: visible; 45 | } 46 | 47 | .spinner { 48 | display: none; 49 | visibility: hidden; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/app/components/preload-image/preload-image.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, ElementRef, Renderer2, ViewEncapsulation, ViewChild, OnChanges, PLATFORM_ID, Inject } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'preload-image', 5 | templateUrl: './preload-image.component.html', 6 | styleUrls: [ 7 | './preload-image.component.scss' 8 | ], 9 | encapsulation: ViewEncapsulation.None 10 | }) 11 | export class PreloadImageComponent implements OnChanges { 12 | _src = ''; 13 | _ratio: { w: number, h: number }; 14 | 15 | constructor( 16 | private _elementRef: ElementRef, 17 | private _renderer: Renderer2 18 | ) {} 19 | 20 | @Input() set src(val: string) { 21 | this._src = (val !== undefined && val !== null) ? val : ''; 22 | } 23 | 24 | @Input() set ratio(ratio: { w: number, h: number }) { 25 | this._ratio = ratio || {w: 1, h: 1}; 26 | } 27 | 28 | ngOnChanges() { 29 | const ratio_height = (this._ratio.h / this._ratio.w * 100) + '%'; 30 | // Conserve aspect ratio (see: http://stackoverflow.com/a/10441480/1116959) 31 | this._renderer.setStyle(this._elementRef.nativeElement, 'padding', '0px 0px ' + ratio_height + ' 0px'); 32 | this._update(); 33 | } 34 | 35 | _update() { 36 | this._loaded(false); 37 | } 38 | 39 | _loaded(isLoaded: boolean) { 40 | if (isLoaded) { 41 | setTimeout(() => { 42 | this._renderer.addClass(this._elementRef.nativeElement, 'img-loaded'); 43 | }, 500); 44 | } else { 45 | this._renderer.removeClass(this._elementRef.nativeElement, 'img-loaded'); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/app/home/home.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { IonicModule } from '@ionic/angular'; 4 | import { FormsModule } from '@angular/forms'; 5 | import { RouterModule } from '@angular/router'; 6 | 7 | import { ComponentsModule } from '../components/components.module'; 8 | import { WebComponentsModule } from '../web-components/web-components.module'; 9 | 10 | import { HomePage } from './home.page'; 11 | 12 | @NgModule({ 13 | imports: [ 14 | CommonModule, 15 | FormsModule, 16 | IonicModule, 17 | RouterModule.forChild([ 18 | { 19 | path: '', 20 | component: HomePage 21 | } 22 | ]), 23 | ComponentsModule, 24 | WebComponentsModule 25 | ], 26 | declarations: [HomePage], 27 | schemas: [CUSTOM_ELEMENTS_SCHEMA] 28 | }) 29 | export class HomePageModule {} 30 | -------------------------------------------------------------------------------- /src/app/home/home.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Ionic Custom Components 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/app/home/home.page.scss: -------------------------------------------------------------------------------- 1 | c-coffee-icon { 2 | --handle-color: #1f2bac; 3 | --cup-color: #2f3fff; 4 | --smoke-color: #a5acbd; 5 | 6 | margin: 20px; 7 | } 8 | -------------------------------------------------------------------------------- /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 | 3 | @Component({ 4 | selector: 'app-home', 5 | templateUrl: 'home.page.html', 6 | styleUrls: ['home.page.scss'], 7 | }) 8 | export class HomePage { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/app/web-components/app-initialize.ts: -------------------------------------------------------------------------------- 1 | import { defineCustomElements } from 'c-coffee-icon'; 2 | 3 | export function appInitialize() { 4 | return () => { 5 | const win = window as any; 6 | if (typeof win !== 'undefined') { 7 | // Define all of our custom elements 8 | defineCustomElements(win); 9 | } 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/web-components/coffee-icon/coffee-icon.component.ts: -------------------------------------------------------------------------------- 1 | import 'c-coffee-icon'; 2 | 3 | import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core'; 4 | 5 | @Component({ 6 | selector: 'c-coffee-icon', 7 | changeDetection: ChangeDetectionStrategy.OnPush, 8 | encapsulation: ViewEncapsulation.None, 9 | template: '' 10 | }) 11 | export class CoffeeIconComponent { 12 | } 13 | -------------------------------------------------------------------------------- /src/app/web-components/web-components.module.ts: -------------------------------------------------------------------------------- 1 | import { APP_INITIALIZER, ModuleWithProviders, CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { appInitialize } from './app-initialize'; 5 | 6 | import { CoffeeIconComponent } from './coffee-icon/coffee-icon.component'; 7 | 8 | @NgModule({ 9 | imports: [ 10 | CommonModule 11 | ], 12 | declarations: [ 13 | CoffeeIconComponent 14 | ], 15 | exports: [ 16 | CoffeeIconComponent 17 | ], 18 | entryComponents: [], 19 | schemas: [CUSTOM_ELEMENTS_SCHEMA] 20 | }) 21 | export class WebComponentsModule { 22 | static forRoot(): ModuleWithProviders { 23 | return { 24 | ngModule: WebComponentsModule, 25 | providers: [ 26 | { 27 | provide: APP_INITIALIZER, 28 | useFactory: appInitialize, 29 | multi: true 30 | } 31 | ] 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/assets/icon/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ionicthemes/ionic-custom-components/7b6f9d660200312be6a345203b327c65cb2fc263/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/normalize.css"; 3 | @import "~@ionic/angular/css/structure.css"; 4 | @import "~@ionic/angular/css/typography.css"; 5 | @import "~@ionic/angular/css/colors.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/docs/ts/latest/guide/browser-support.html 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/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following for the Reflect API. */ 41 | // import 'core-js/es6/reflect'; 42 | 43 | 44 | /** Evergreen browsers require these. **/ 45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 46 | import 'core-js/es7/reflect'; 47 | 48 | 49 | /** 50 | * Required to support Web Animations `@angular/platform-browser/animations`. 51 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation 52 | **/ 53 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 54 | 55 | 56 | 57 | /*************************************************************************************************** 58 | * Zone JS is required by Angular itself. 59 | */ 60 | import 'zone.js/dist/zone'; // Included with Angular CLI. 61 | 62 | 63 | 64 | /*************************************************************************************************** 65 | * APPLICATION IMPORTS 66 | */ 67 | 68 | /** 69 | * Date, currency, decimal and percent pipes. 70 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 71 | */ 72 | // import 'intl'; // Run `npm install --save intl`. 73 | /** 74 | * Need to import at least one locale-data with intl. 75 | */ 76 | // import 'intl/locale-data/jsonp/en'; 77 | -------------------------------------------------------------------------------- /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 | /** primary **/ 7 | --ion-color-primary: #488aff; 8 | --ion-color-primary-rgb: 72,138,255; 9 | --ion-color-primary-contrast: #fff; 10 | --ion-color-primary-contrast-rgb: 255,255,255; 11 | --ion-color-primary-shade: #3f79e0; 12 | --ion-color-primary-tint: #5a96ff; 13 | 14 | /** secondary **/ 15 | --ion-color-secondary: #32db64; 16 | --ion-color-secondary-rgb: 50,219,100; 17 | --ion-color-secondary-contrast: #fff; 18 | --ion-color-secondary-contrast-rgb: 255,255,255; 19 | --ion-color-secondary-shade: #2cc158; 20 | --ion-color-secondary-tint: #47df74; 21 | 22 | /** tertiary **/ 23 | --ion-color-tertiary: #f4a942; 24 | --ion-color-tertiary-rgb: 244,169,66; 25 | --ion-color-tertiary-contrast: #fff; 26 | --ion-color-tertiary-contrast-rgb: 255,255,255; 27 | --ion-color-tertiary-shade: #d7953a; 28 | --ion-color-tertiary-tint: #f5b255; 29 | 30 | /** success **/ 31 | --ion-color-success: #10dc60; 32 | --ion-color-success-rgb: 16,220,96; 33 | --ion-color-success-contrast: #fff; 34 | --ion-color-success-contrast-rgb: 255,255,255; 35 | --ion-color-success-shade: #0ec254; 36 | --ion-color-success-tint: #28e070; 37 | 38 | /** warning **/ 39 | --ion-color-warning: #ffce00; 40 | --ion-color-warning-rgb: 255,206,0; 41 | --ion-color-warning-contrast: #000; 42 | --ion-color-warning-contrast-rgb: 0,0,0; 43 | --ion-color-warning-shade: #e0b500; 44 | --ion-color-warning-tint: #ffd31a; 45 | 46 | /** danger **/ 47 | --ion-color-danger: #f53d3d; 48 | --ion-color-danger-rgb: 245,61,61; 49 | --ion-color-danger-contrast: #fff; 50 | --ion-color-danger-contrast-rgb: 255,255,255; 51 | --ion-color-danger-shade: #d83636; 52 | --ion-color-danger-tint: #f65050; 53 | 54 | /** light **/ 55 | --ion-color-light: #f4f4f4; 56 | --ion-color-light-rgb: 244,244,244; 57 | --ion-color-light-contrast: #000; 58 | --ion-color-light-contrast-rgb: 0,0,0; 59 | --ion-color-light-shade: #d7d7d7; 60 | --ion-color-light-tint: #f5f5f5; 61 | 62 | /** medium **/ 63 | --ion-color-medium: #989aa2; 64 | --ion-color-medium-rgb: 152,154,162; 65 | --ion-color-medium-contrast: #000; 66 | --ion-color-medium-contrast-rgb: 0,0,0; 67 | --ion-color-medium-shade: #86888f; 68 | --ion-color-medium-tint: #a2a4ab; 69 | 70 | /** dark **/ 71 | --ion-color-dark: #222; 72 | --ion-color-dark-rgb: 34,34,34; 73 | --ion-color-dark-contrast: #fff; 74 | --ion-color-dark-contrast-rgb: 255,255,255; 75 | --ion-color-dark-shade: #1e1e1e; 76 | --ion-color-dark-tint: #383838; 77 | } -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------