├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example.gif ├── gulpfile.js ├── package.json ├── src ├── auto-complete.scss ├── auto-complete.service.ts ├── autocomplete.component.ts ├── boldprefix.pipe.ts ├── index.ts ├── package.json └── tsconfig.es5.json ├── tools └── gulp │ └── inline-resources.js ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Node 2 | node_modules/* 3 | npm-debug.log 4 | 5 | # TypeScript 6 | *.js 7 | *.map 8 | *.d.ts 9 | !gulpfile.js 10 | # JetBrains 11 | .idea 12 | .project 13 | .settings 14 | .idea/* 15 | *.iml 16 | 17 | # Windows 18 | Thumbs.db 19 | Desktop.ini 20 | 21 | # Mac 22 | .DS_Store 23 | **/.DS_Store 24 | 25 | dist 26 | *.sh 27 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Node 2 | node_modules/* 3 | npm-debug.log 4 | 5 | # DO NOT IGNORE TYPESCRIPT FILES FOR NPM 6 | # TypeScript 7 | # *.js 8 | # *.map 9 | # *.d.ts 10 | 11 | # JetBrains 12 | .idea 13 | .project 14 | .settings 15 | .idea/* 16 | *.iml 17 | 18 | # Windows 19 | Thumbs.db 20 | Desktop.ini 21 | 22 | # Mac 23 | .DS_Store 24 | **/.DS_Store 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - '4.2.1' 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.5.0] 2017-05-17 2 | * New custom-templates mechanism (please refer to the README file) 3 | * Added [showResultsFirst] option - calls `getItems()` when the component is tapped 4 | * Added [useIonInput] option - use `` instead of `` 5 | 6 | ## [1.5.1] - beta 2017-06-26 7 | * fixed document click handler issue 8 | * support rxjs subject 9 | 10 | ## [1.5.2] - beta 2017-07-13 11 | * bug fixes 12 | * added setFocus() metho 13 | * added getSelection() methodd 14 | * added option to debounce the search 15 | 16 | ## [1.5.3] - beta 2017-09-30 17 | * bug fixes 18 | * support for ngModel (see docs) 19 | 20 | ## [1.6.2] - alpha 2017-12-16 21 | * bug fixes 22 | * support for Angular 5.0 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Mor Kadosh 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 | # Ionic2-auto-complete 2 | 3 | ## Disclaimer ## 4 | Due to a very little free time, I am not available for mainting and supporting this project, so contributions are very welcome!!! 5 | 6 | 7 | ## About ## 8 | This is a component based on Ionic's search-bar component, with the addition of auto-complete abillity. 9 | This component is super simple and light-weight. Just provide the data, and let the fun begin. 10 | 11 | This is a **free software** please feel free to contribute! :) 12 | 13 | ![](example.gif) 14 | 15 | ### Angular 5.0 Support 16 | 17 | Since Angular 5.0 was out, a several issues occured. 18 | Thanks to @CoreyCole, most of them are gone now :) 19 | 20 | If you encounter another issues regrading Angular 5, pleae file an issue! 21 | 22 | For more info: https://github.com/kadoshms/ionic2-autocomplete/issues/128 23 | 24 | ### Installation 25 | ``` 26 | $ npm install ionic2-auto-complete --save 27 | ``` 28 | 29 | #### Usage guide 30 | 31 | Open `app.module.ts` and add the following import statetment: 32 | 33 | `` 34 | import { AutoCompleteModule } from 'ionic2-auto-complete'; 35 | `` 36 | 37 | Then, add the `AutoCompleteModule` to the `imports` array: 38 | 39 | ``` 40 | @NgModule({ 41 | declarations: [ 42 | MyApp, 43 | HomePage, 44 | TabsPage, 45 | MyItem 46 | ], 47 | imports: [ 48 | BrowserModule, 49 | AutoCompleteModule, 50 | FormsModule, 51 | HttpModule, 52 | IonicModule.forRoot(MyApp) 53 | ], 54 | ... 55 | ... 56 | }) 57 | export class AppModule {} 58 | ``` 59 | Now let's import the styling file. Open `app.scss` and add the following: 60 | 61 | `@import "../../node_modules/ionic2-auto-complete/auto-complete";` 62 | 63 | Now, let's add the component to our app! 64 | 65 | Add the following tag to one of your pages, in this example I am using the Homepage: 66 | 67 | `` 68 | 69 | Now let's see what wev'e done so far by running `ionic serve`. 70 | 71 | Now, when everything is up and running you should see a nice search-bar component. Open the **developer console** and try to type something. 72 | 73 | Oh no! something is wrong. You probably see an excpetion similiar to : 74 | 75 | `EXCEPTION: Error in ./AutoCompleteComponent class AutoCompleteComponent - inline template:1:21` 76 | 77 | This is totally cool, for now. The exception shows up since we did not provide a **dataProvider** to the autocomplete component. 78 | 79 | **How does it work?** So, ionic2-auto-complete is not responsible for getting the data from the server. As a developer, you should implement your own service which eventually be responsible to get the data for the component to work, as well we determing how many results to show and/or their order of display. 80 | 81 | So there are two possibilities to provide data: 82 | 83 | 1. A simple function that returns an Array of items 84 | 2. An instance of 'AutocompleteService' (specified below) 85 | 86 | Let's start by creating the service: 87 | 88 | ``` 89 | import {AutoCompleteService} from 'ionic2-auto-complete'; 90 | import { Http } from '@angular/http'; 91 | import {Injectable} from "@angular/core"; 92 | import 'rxjs/add/operator/map' 93 | 94 | @Injectable() 95 | export class CompleteTestService implements AutoCompleteService { 96 | labelAttribute = "name"; 97 | 98 | constructor(private http:Http) { 99 | 100 | } 101 | getResults(keyword:string) { 102 | return this.http.get("https://restcountries.eu/rest/v1/name/"+keyword) 103 | .map( 104 | result => 105 | { 106 | return result.json() 107 | .filter(item => item.name.toLowerCase().startsWith(keyword.toLowerCase()) ) 108 | }); 109 | } 110 | } 111 | 112 | 113 | ``` 114 | 115 | By implementing an AutoCompleteService interface, you must implement two properties: 116 | 117 | 1. **labelAttribute** [string] - which is the name of the object's descriptive property (leaving it null is also an option for non-object results) 118 | 2. **getResults(keyword)** [() => any] - which is the method responsible for getting the data from server. 119 | 120 | The **getResults** method can return one of: 121 | - an Observable that produces an array 122 | - a Subject (like an Observable) 123 | - a Promise that provides an array 124 | - directly an array of values 125 | 126 | In the above example, we fetch countries data from the amazing https://restcountries.eu/ project, and we filter the results accordingly. 127 | 128 | **Important!** the above example is just an example! the best practice would be to let the server to the filtering for us! Here, since I used the countries-api, that's the best I could do. 129 | 130 | Now, we need to let ionic2-auto-complete that we want to use CompleteTestService as the data provider, edit *home.ts* and add `private completeTestService: CompleteTestService` to the constructor argument list. 131 | Should look like that: 132 | ``` 133 | import { Component } from '@angular/core'; 134 | import { NavController } from 'ionic-angular'; 135 | import { CompleteTestService } from '../../providers/CompleteTestService'; 136 | 137 | @Component({ 138 | selector: 'page-home', 139 | templateUrl: 'home.html' 140 | }) 141 | export class HomePage { 142 | 143 | constructor(public navCtrl: NavController, public completeTestService: CompleteTestService) { 144 | 145 | } 146 | 147 | } 148 | 149 | ``` 150 | 151 | Than, in *home.html* modify ``: 152 | ``` 153 | 154 | ``` 155 | 156 | Now, everything should be up and ready :) 157 | 158 | 159 | ---------------------------------------------------------------------------- 160 | 161 | ### Use auto-complete in Angular FormGroup ### 162 | 163 | #### Use labelAttribute as both label and form value (default behavior) #### 164 | 165 | By default, if your **dataProvider** provides an array of objects, the `labelAttribute` property is used to take the good field of each object to display in the suggestion list. For backward compatibility, if nothing is specified, this attribute is also used to grab the value used in the form. 166 | 167 | The page should look like this: 168 | 169 | ``` 170 | import { Component } from '@angular/core'; 171 | import { NavController } from 'ionic-angular'; 172 | import { CompleteTestService } from '../../providers/CompleteTestService'; 173 | import { FormGroup, Validators, FormControl } from '@angular/forms' 174 | 175 | 176 | @Component({ 177 | selector: 'page-home', 178 | templateUrl: 'home.html' 179 | }) 180 | export class HomePage { 181 | myForm: FormGroup 182 | 183 | constructor(public navCtrl: NavController, public completeTestService: CompleteTestService) { 184 | } 185 | 186 | ngOnInit(): void { 187 | this.myForm = new FormGroup({ 188 | country: new FormControl('', [ 189 | Validators.required 190 | ]) 191 | }) 192 | } 193 | 194 | submit(): void { 195 | let country = this.myForm.value.country 196 | } 197 | 198 | } 199 | ``` 200 | 201 | Then, in *home.html* place the auto-complete component in the form group and add the `formControlName` attribute: 202 | ``` 203 |
204 |
205 | 206 |
207 | 208 |
209 | ``` 210 | 211 | Now when the `submit` method is called, the `country` is the selected country **name**. 212 | 213 | **NOTE** As said above by default for backward compatibility, only the name is used as value not the country object. 214 | 215 | 216 | #### How to use another field as form value ? #### 217 | 218 | To indicate that you don't want the label as value but another field of the country object returned by the REST service, you can specify the attribute **formValueAttribute** on your dataProvider. For example, we want to use the country numeric code as value and still use the country name as label. 219 | 220 | Let's update the service (juste declare `formValueAttribute` property): 221 | 222 | ``` 223 | import {AutoCompleteService} from 'ionic2-auto-complete'; 224 | import { Http } from '@angular/http'; 225 | import {Injectable} from "@angular/core"; 226 | import 'rxjs/add/operator/map' 227 | 228 | @Injectable() 229 | export class CompleteTestService implements AutoCompleteService { 230 | labelAttribute = "name"; 231 | formValueAttribute = "numericCode" 232 | 233 | constructor(private http:Http) { 234 | } 235 | 236 | getResults(keyword:string) { 237 | return this.http.get("https://restcountries.eu/rest/v1/name/"+keyword) 238 | .map( 239 | result => 240 | { 241 | return result.json() 242 | .filter(item => item.name.toLowerCase().startsWith(keyword.toLowerCase()) ) 243 | }); 244 | } 245 | } 246 | ``` 247 | 248 | Now when the `submit` method is called, the `country` is the selected country **numericCode**. The name is still used as the label. 249 | 250 | #### How to use the whole object as form value ? #### 251 | 252 | Simply set `formValueAttribute` to empty string: 253 | ``` 254 | import {AutoCompleteService} from 'ionic2-auto-complete'; 255 | import { Http } from '@angular/http'; 256 | import {Injectable} from "@angular/core"; 257 | import 'rxjs/add/operator/map' 258 | 259 | @Injectable() 260 | export class CompleteTestService implements AutoCompleteService { 261 | labelAttribute = "name"; 262 | formValueAttribute = "" 263 | 264 | constructor(private http:Http) { 265 | } 266 | 267 | getResults(keyword:string) { 268 | return this.http.get("https://restcountries.eu/rest/v1/name/"+keyword) 269 | .map( 270 | result => 271 | { 272 | return result.json() 273 | .filter(item => item.name.toLowerCase().startsWith(keyword.toLowerCase()) ) 274 | }); 275 | } 276 | } 277 | ``` 278 | 279 | 280 | ---------------------------------------------------------------------------- 281 | 282 | ### Styling ### 283 | 284 | Currently for best visual result, use viewport size / fixed size (pixels) if you are interested in resizing the component: 285 | ``` 286 | ion-auto-complete { 287 | width: 50vw; 288 | } 289 | ``` 290 | 291 | 327 | 328 | 329 | ### Custom Templates (for versions 1.5.0 and above) ### 330 | 331 | **NOTE** this feature uses ng-template which was introduced in Angular versions 4.0.0 and later, it might not work in earlier versions. 332 | 333 | Ionic2-auto-complete also supports custom templates for the list items. 334 | Actually, you can display any attribute associated with your data items by simply accessing it from the `data` input class member in the template. 335 | 336 | For example: 337 | 338 | Let's assume that in addition to the country name, we also wish to display the country flag. 339 | For that, we use the `ng-template` directive, which let's us pass the template as an input to the component. 340 | 341 | On the page where your `ion-auto-complete` is located: 342 | 343 | ``` 344 | 345 | 346 | 347 | 348 | ``` 349 | 350 | Please note that you must add the `let-attrs="attrs"` attribute to your template. 351 | 352 | With that, you can easily of **different templates for different components**! 353 | 354 | #### Old custom templates mechanism (depreacted) #### 355 | **NOTE** the following is depreacted! (versions less than 1.5.0) 356 | 357 | 358 | **DEPREACTED (applies for<1.5.0)** 359 | For that, we need to create a new file, let's call it for instance `comp-test-item.ts`: 360 | ``` 361 | import {AutoCompleteItem, AutoCompleteItemComponent} from 'ionic2-auto-complete'; 362 | 363 | @AutoCompleteItem({ 364 | template: ` ` 365 | }) 366 | export class CompTestItem extends AutoCompleteItemComponent{ 367 | 368 | } 369 | 370 | ``` 371 | 372 | And we must also add this component to our module: 373 | 374 | ``` 375 | @NgModule({ 376 | declarations: [ 377 | MyApp, 378 | AboutPage, 379 | ContactPage, 380 | HomePage, 381 | TabsPage, 382 | CompTestItem 383 | ], 384 | ... 385 | ... 386 | providers: [ 387 | StatusBar, 388 | SplashScreen, 389 | CompleteTestService, 390 | {provide: ErrorHandler, useClass: IonicErrorHandler} 391 | ] 392 | 393 | ``` 394 | 395 | What is going on above is very simple. 396 | In order to implement a custom Item component, you need to follow these steps: 397 | 398 | 1. Import all neccessary classes. 399 | 2. Use the `@AutoCompleteItem` decorator, which currently accepts `template` only (`templeteUrl` is currently not supported). 400 | 3. Extend the AutoCompleteItemComponent class with your own class. 401 | 402 | **DEPREACTED** 403 | 404 | ## Events ## 405 | 406 | **itemSelected($event)** - fired when item is selected (clicked) 407 | **itemsShown($event)** - fired when items are shown 408 | **itemsHidden($event)** - fired when items are hidden 409 | **ionAutoInput($event)** - fired when user inputs 410 | **autoFocus($event)** - fired when the input is focused 411 | **autoBlur($event)** - fired when the input is blured 412 | 413 | ## Searchbar options ## 414 | 415 | Ionic2-auto-complete supports the regular Ionic's Searchbar options, which are set to their default values as specified in the [docs](http://ionicframework.com/docs/v2/api/components/searchbar/Searchbar/). 416 | 417 | You can override these default values by adding the `[options]` attribute to the `` tag, for instance: 418 | 419 | ``` 420 | 421 | ``` 422 | Options include, but not limited to: 423 | 1. debounce (default is `250`) 424 | 2. autocomplete ("on" and "off") 425 | 3. type ("text", "password", "email", "number", "search", "tel", "url". Default "search".) 426 | 4. placeholder (default "Search") 427 | 428 | ## Component specific options 429 | 430 | In addition to the searchbar options, ion-auto-complete also supports the following option attributes: 431 | 432 | * **[template]** (TemplateRef) - custom template reference for your auto complete items (see below) 433 | * **[showResultsFirst]** (Boolean) - for small lists it might be nicer to show all options on first tap (you might need to modify your service to handle an empty `keyword`) 434 | * **[alwaysShowList]** (Boolean) - always show the list - defaults to false) 435 | * **[hideListOnSelection]** (Boolean) - if allowing multiple selections, it might be nice not to dismiss the list after each selection - defaults to true) 436 | 437 | Will set the Searchbar's placeholder to *Lorem Ipsum* 438 | 439 | ## Accessing Searchbar component ## 440 | 441 | By using the `@ViewChild()` decorator, and the built-in `getValue()` method we can easily access the actual value in the searchbar component. 442 | Just define a new property within the desired page, for instance (the chosen names are arbitrary): 443 | 444 | ``` 445 | @ViewChild('searchbar') 446 | searchbar: AutoCompleteComponent; 447 | ``` 448 | 449 | And then, in the component tag we need to add `#searchbar`: 450 | 451 | ``` 452 | 453 | ``` 454 | 455 | Available methods: 456 | 457 | 1. getValue(): `this.searchbar.getValue()` - get the string value of the selected item 458 | 2. getSelection(): `this.searchbar.getSelection()` - get the selected object 459 | 3. setFocus(): `this.searchbar.setFocus()` - focus on searchbar 460 | 461 | ## ngModel (since 1.5.3) ## 462 | 463 | Many thanks to [bushybuffalo](https://github.com/bushybuffalo) for contributing this cool feature. 464 | You can now bind the component with an ngModel. 465 | Please note that if you use an object as your model, the component will try to achieve the initial keyword value using the labelAttribute. 466 | For plain string models, it will just use the value itself. 467 | 468 | ## Contributing ## 469 | 470 | To contribute, clone the repo. Then, run `npm install` to get the packages needed for the library to work. Running `gulp` will run a series of tasks that builds the files in `/src` into `/dist`. Replace the `/dist` into whatever Ionic application's `node_modules` where you're testing your changes to continously improve the library. 471 | -------------------------------------------------------------------------------- /example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kadoshms/ionic2-autocomplete/9c0ed3148e17bcdd975b60b211a1a03bb68b2201/example.gif -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var gulp = require('gulp'), 3 | path = require('path'), 4 | ngc = require('@angular/compiler-cli/src/main').main, 5 | rollup = require('gulp-rollup'), 6 | rename = require('gulp-rename'), 7 | del = require('del'), 8 | runSequence = require('run-sequence'), 9 | inlineResources = require('gulp-inline-source'); 10 | 11 | const rootFolder = path.join(__dirname); 12 | const srcFolder = path.join(rootFolder, 'src'); 13 | const tmpFolder = path.join(rootFolder, '.tmp'); 14 | const buildFolder = path.join(rootFolder, 'build'); 15 | const distFolder = path.join(rootFolder, 'dist'); 16 | 17 | /** 18 | * 1. Delete /dist folder 19 | */ 20 | gulp.task('clean:dist', function () { 21 | return deleteFolders([distFolder]); 22 | }); 23 | 24 | /** 25 | * 2. Clone the /src folder into /.tmp. If an npm link inside /src has been made, 26 | * then it's likely that a node_modules folder exists. Ignore this folder 27 | * when copying to /.tmp. 28 | */ 29 | gulp.task('copy:source', function () { 30 | return gulp.src([`${srcFolder}/**/*`, `!${srcFolder}/node_modules`]) 31 | .pipe(gulp.dest(tmpFolder)); 32 | }); 33 | 34 | /** 35 | * 3. Inline template (.html) and style (.css) files into the the component .ts files. 36 | * We do this on the /.tmp folder to avoid editing the original /src files 37 | */ 38 | gulp.task('inline-resources', function () { 39 | return Promise.resolve() 40 | .then(() => inlineResources(tmpFolder)); 41 | }); 42 | 43 | 44 | /** 45 | * 4. Run the Angular compiler, ngc, on the /.tmp folder. This will output all 46 | * compiled modules to the /build folder. 47 | */ 48 | gulp.task('ngc', function () { 49 | return ngc({ 50 | project: `${tmpFolder}/tsconfig.es5.json` 51 | }) 52 | .then((exitCode) => { 53 | if (exitCode === 1) { 54 | // This error is caught in the 'compile' task by the runSequence method callback 55 | // so that when ngc fails to compile, the whole compile process stops running 56 | throw new Error('ngc compilation failed'); 57 | } 58 | }); 59 | }); 60 | 61 | /** 62 | * 5. Run rollup inside the /build folder to generate our Flat ES module and place the 63 | * generated file into the /dist folder 64 | */ 65 | gulp.task('rollup:fesm', function () { 66 | return gulp.src(`${buildFolder}/**/*.js`) 67 | // transform the files here. 68 | .pipe(rollup({ 69 | 70 | // Bundle's entry point 71 | // See https://github.com/rollup/rollup/wiki/JavaScript-API#entry 72 | entry: `${buildFolder}/index.js`, 73 | 74 | // A list of IDs of modules that should remain external to the bundle 75 | // See https://github.com/rollup/rollup/wiki/JavaScript-API#external 76 | external: [ 77 | '@angular/core', 78 | '@angular/common' 79 | ], 80 | 81 | // Format of generated bundle 82 | // See https://github.com/rollup/rollup/wiki/JavaScript-API#format 83 | format: 'es' 84 | })) 85 | .pipe(gulp.dest(distFolder)); 86 | }); 87 | 88 | /** 89 | * 6. Run rollup inside the /build folder to generate our UMD module and place the 90 | * generated file into the /dist folder 91 | */ 92 | gulp.task('rollup:umd', function () { 93 | return gulp.src(`${buildFolder}/**/*.js`) 94 | // transform the files here. 95 | .pipe(rollup({ 96 | 97 | // Bundle's entry point 98 | // See https://github.com/rollup/rollup/wiki/JavaScript-API#entry 99 | entry: `${buildFolder}/index.js`, 100 | 101 | // A list of IDs of modules that should remain external to the bundle 102 | // See https://github.com/rollup/rollup/wiki/JavaScript-API#external 103 | external: [ 104 | '@angular/core', 105 | '@angular/common' 106 | ], 107 | 108 | // Format of generated bundle 109 | // See https://github.com/rollup/rollup/wiki/JavaScript-API#format 110 | format: 'umd', 111 | 112 | // Export mode to use 113 | // See https://github.com/rollup/rollup/wiki/JavaScript-API#exports 114 | exports: 'named', 115 | 116 | // The name to use for the module for UMD/IIFE bundles 117 | // (required for bundles with exports) 118 | // See https://github.com/rollup/rollup/wiki/JavaScript-API#modulename 119 | name: 'ionic2-auto-complete', 120 | 121 | // See https://github.com/rollup/rollup/wiki/JavaScript-API#globals 122 | globals: { 123 | typescript: 'ts' 124 | } 125 | 126 | })) 127 | .pipe(rename('ionic2-auto-complete.umd.js')) 128 | .pipe(gulp.dest(distFolder)); 129 | }); 130 | 131 | /** 132 | * 7. Copy all the files from /build to /dist, except .js files. We ignore all .js from /build 133 | * because with don't need individual modules anymore, just the Flat ES module generated 134 | * on step 5. 135 | */ 136 | gulp.task('copy:build', function () { 137 | return gulp.src([`${buildFolder}/**/*`, `!${buildFolder}/**/*.js`]) 138 | .pipe(gulp.dest(distFolder)); 139 | }); 140 | 141 | /** 142 | * 8. Copy package.json from /src to /dist 143 | */ 144 | gulp.task('copy:manifest', function () { 145 | return gulp.src([`${srcFolder}/package.json`]) 146 | .pipe(gulp.dest(distFolder)); 147 | }); 148 | 149 | /** 150 | * 9. Copy README.md from / to /dist 151 | */ 152 | gulp.task('copy:readme', function () { 153 | return gulp.src([path.join(rootFolder, 'README.MD')]) 154 | .pipe(gulp.dest(distFolder)); 155 | }); 156 | 157 | /** 158 | * 10. Delete /.tmp folder 159 | */ 160 | gulp.task('clean:tmp', function () { 161 | return deleteFolders([tmpFolder]); 162 | }); 163 | 164 | /** 165 | * 11. Delete /build folder 166 | */ 167 | gulp.task('clean:build', function () { 168 | return deleteFolders([buildFolder]); 169 | }); 170 | 171 | gulp.task('scss', function() { 172 | return gulp.src(['src/auto-complete.scss', `dist/auto-complete.scss`]) 173 | .pipe(gulp.dest(distFolder)) 174 | }); 175 | 176 | gulp.task('compile', function () { 177 | runSequence( 178 | 'clean:dist', 179 | 'copy:source', 180 | 'inline-resources', 181 | 'ngc', 182 | 'rollup:fesm', 183 | 'rollup:umd', 184 | 'copy:build', 185 | 'copy:manifest', 186 | 'copy:readme', 187 | 'clean:build', 188 | 'clean:tmp', 189 | function (err) { 190 | if (err) { 191 | console.log('ERROR:', err.message); 192 | deleteFolders([distFolder, tmpFolder, buildFolder]); 193 | } else { 194 | console.log('Compilation finished succesfully'); 195 | } 196 | }); 197 | }); 198 | 199 | /** 200 | * Watch for any change in the /src folder and compile files 201 | */ 202 | gulp.task('watch', function () { 203 | gulp.watch(`${srcFolder}/**/*`, ['compile']); 204 | }); 205 | 206 | gulp.task('clean', ['clean:dist', 'clean:tmp', 'clean:build']); 207 | 208 | gulp.task('build', ['clean', 'compile','scss']); 209 | gulp.task('build:watch', ['build', 'watch']); 210 | gulp.task('default', ['build:watch']); 211 | 212 | /** 213 | * Deletes the specified folder 214 | */ 215 | function deleteFolders(folders) { 216 | return del(folders); 217 | } 218 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ionic2-auto-complete", 3 | "version": "1.6.2-alpha", 4 | "scripts": { 5 | "publish": "npm publish dist", 6 | "build": "gulp build", 7 | "build:watch": "gulp", 8 | "docs": "npm run docs:build", 9 | "docs:build": "compodoc -p tsconfig.json -n ionic2-auto-complete -d docs --hideGenerator", 10 | "docs:serve": "npm run docs:build -- -s", 11 | "docs:watch": "npm run docs:build -- -s -w", 12 | "lint": "tslint --type-check --project tsconfig.json src/**/*.ts", 13 | "test": "tsc && karma start" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/kadoshms/ionic2-autocomplete" 18 | }, 19 | "author": { 20 | "name": "mor", 21 | "email": "kadoshms@gmail.com" 22 | }, 23 | "keywords": [ 24 | "angular" 25 | ], 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/kadoshms/ionic2-autocomplete/issues" 29 | }, 30 | "devDependencies": { 31 | "@angular/common": "4.1.0", 32 | "@angular/compiler": "4.1.0", 33 | "@angular/compiler-cli": "4.1.0", 34 | "@angular/core": "4.1.0", 35 | "@angular/forms": "4.1.0", 36 | "@angular/http": "4.1.0", 37 | "@angular/platform-browser": "4.1.0", 38 | "@angular/platform-browser-dynamic": "4.1.0", 39 | "@compodoc/compodoc": "^1.0.0-beta.7", 40 | "@ionic-native/core": "3.7.0", 41 | "@ionic-native/splash-screen": "3.7.0", 42 | "@ionic-native/status-bar": "3.7.0", 43 | "@ionic/app-scripts": "1.3.7", 44 | "@ionic/cli-plugin-cordova": "1.0.0", 45 | "@ionic/cli-plugin-ionic-angular": "1.0.0", 46 | "@ionic/storage": "2.0.1", 47 | "@types/jasmine": "2.5.38", 48 | "@types/node": "~6.0.60", 49 | "codelyzer": "~2.0.0", 50 | "core-js": "^2.4.1", 51 | "del": "^2.2.2", 52 | "gulp": "^3.9.1", 53 | "gulp-inline-source": "^3.1.0", 54 | "gulp-rename": "^1.2.2", 55 | "gulp-rollup": "^2.15.0", 56 | "ionic-angular": "3.2.1", 57 | "ionic2-auto-complete": "^1.4.3-release", 58 | "ionicons": "3.0.0", 59 | "jasmine-core": "~2.5.2", 60 | "jasmine-spec-reporter": "~3.2.0", 61 | "karma": "~1.4.1", 62 | "karma-chrome-launcher": "~2.0.0", 63 | "karma-cli": "~1.0.1", 64 | "karma-coverage-istanbul-reporter": "^0.2.0", 65 | "karma-jasmine": "~1.1.0", 66 | "karma-jasmine-html-reporter": "^0.2.2", 67 | "node-sass": "^4.5.2", 68 | "node-watch": "^0.5.2", 69 | "protractor": "~5.1.0", 70 | "rollup": "^0.41.6", 71 | "run-sequence": "^1.2.2", 72 | "rxjs": "5.1.1", 73 | "sw-toolbox": "3.6.0", 74 | "ts-node": "~2.0.0", 75 | "tslint": "~4.5.0", 76 | "typescript": "~2.2.0", 77 | "zone.js": "0.8.10" 78 | }, 79 | "engines": { 80 | "node": ">=6.0.0" 81 | }, 82 | "dependencies": {} 83 | } 84 | -------------------------------------------------------------------------------- /src/auto-complete.scss: -------------------------------------------------------------------------------- 1 | ion-auto-complete { 2 | overflow : hidden !important; 3 | display: block; 4 | width: 90vw; 5 | display: inline-block; 6 | ion-searchbar { 7 | padding: 1px !important; 8 | } 9 | 10 | .hidden { 11 | display: none; 12 | } 13 | 14 | ul { 15 | position: absolute; 16 | width: inherit; 17 | margin-top: 0px; 18 | background: #FFF; 19 | list-style-type: none; 20 | padding:0px; 21 | left: 16px; 22 | z-index: 999; 23 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 1px 5px 0 rgba(0, 0, 0, 0.12); 24 | 25 | li { 26 | padding: 15px; 27 | border-bottom: 1px solid #c1c1c1; 28 | } 29 | 30 | ion-auto-complete-item { 31 | height: 40px; 32 | width: 100%; 33 | } 34 | 35 | li:last-child { 36 | border: none; 37 | } 38 | 39 | li:hover { 40 | cursor: pointer; 41 | background: #f1f1f1 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/auto-complete.service.ts: -------------------------------------------------------------------------------- 1 | export interface AutoCompleteService { 2 | 3 | /** 4 | * the literal name of the title attribute 5 | */ 6 | labelAttribute?: string; 7 | 8 | /** 9 | * the value of the field when used in a formGroup. If null, labelAttribute is used 10 | */ 11 | formValueAttribute?: any; 12 | 13 | /** 14 | * this method should return an array of objects (results) 15 | * @param term 16 | */ 17 | getResults(term: any): any; 18 | 19 | /** 20 | * this method parses each item of the results from data service. 21 | * the returned value is the displayed form of the result 22 | * @param item 23 | */ 24 | getItemLabel?(item: any): any; 25 | } -------------------------------------------------------------------------------- /src/autocomplete.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, Output, EventEmitter, TemplateRef, ViewChild, HostListener} from '@angular/core'; 2 | import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; 3 | import {noop} from 'rxjs/util/noop'; 4 | import {Observable} from 'rxjs/Observable'; 5 | import {Subject} from 'rxjs/Subject'; 6 | import {fromPromise} from 'rxjs/observable/fromPromise'; 7 | 8 | // searchbar default options 9 | const defaultOpts = { 10 | cancelButtonText: 'Cancel', 11 | showCancelButton: false, 12 | debounce: 250, 13 | placeholder: 'Search', 14 | autocomplete: 'off', 15 | autocorrect: 'off', 16 | spellcheck: 'off', 17 | type: 'search', 18 | value: '', 19 | noItems: '', 20 | clearOnEdit: false, 21 | clearInput: false 22 | }; 23 | 24 | @Component({ 25 | selector: 'ion-auto-complete', 26 | template: ` 27 | 42 | 43 | 63 | 64 | 65 | 66 | 67 |
    68 |
  • 69 | 79 |
  • 80 |
81 |

{{ options.noItems }}

82 | `, 83 | providers: [ 84 | {provide: NG_VALUE_ACCESSOR, useExisting: AutoCompleteComponent, multi: true} 85 | ] 86 | }) 87 | export class AutoCompleteComponent implements ControlValueAccessor { 88 | 89 | @Input() public dataProvider: any; 90 | @Input() public options: any; 91 | @Input() public disabled: any; 92 | @Input() public keyword: string; 93 | @Input() public showResultsFirst: boolean; 94 | @Input() public alwaysShowList: boolean; 95 | @Input() public hideListOnSelection: boolean = true; 96 | @Input() public template: TemplateRef; 97 | @Input() public useIonInput: boolean; 98 | @Output() public autoFocus: EventEmitter; 99 | @Output() public autoBlur: EventEmitter; 100 | @Output() public itemSelected: EventEmitter; 101 | @Output() public itemsShown: EventEmitter; 102 | @Output() public itemsHidden: EventEmitter; 103 | @Output() public ionAutoInput: EventEmitter; 104 | @ViewChild('searchbarElem') searchbarElem; 105 | @ViewChild('inputElem') inputElem; 106 | 107 | private onTouchedCallback: () => void = noop; 108 | private onChangeCallback: (_: any) => void = noop; 109 | public defaultOpts: any; 110 | public suggestions: any[]; 111 | public formValue: any; 112 | 113 | public get showList(): boolean { 114 | return this._showList; 115 | } 116 | 117 | public set showList(value: boolean) { 118 | if (this._showList === value) { 119 | return; 120 | } 121 | 122 | this._showList = value; 123 | this.showListChanged = true; 124 | } 125 | 126 | private _showList: boolean; 127 | 128 | private selection: any; 129 | private showListChanged: boolean = false; 130 | 131 | /** 132 | * create a new instace 133 | */ 134 | public constructor() { 135 | this.keyword = ''; 136 | this.suggestions = []; 137 | this._showList = false; 138 | this.itemSelected = new EventEmitter(); 139 | this.itemsShown = new EventEmitter(); 140 | this.itemsHidden = new EventEmitter(); 141 | this.ionAutoInput = new EventEmitter(); 142 | this.autoFocus = new EventEmitter(); 143 | this.autoBlur = new EventEmitter(); 144 | this.options = {}; 145 | 146 | // set default options 147 | this.defaultOpts = defaultOpts; 148 | } 149 | 150 | /** 151 | * handle tap 152 | * @param event 153 | */ 154 | public handleTap(event) { 155 | if (this.showResultsFirst || this.keyword.length > 0) { 156 | this.getItems(); 157 | } 158 | } 159 | 160 | public handleSelectTap($event, suggestion): boolean { 161 | this.select(suggestion); 162 | $event.srcEvent.stopPropagation(); 163 | $event.srcEvent.preventDefault(); 164 | return false; 165 | } 166 | 167 | public writeValue(value: any) { 168 | if (value !== this.selection) { 169 | this.selection = value || null; 170 | this.formValue = this.getFormValue(this.selection); 171 | this.keyword = this.getLabel(this.selection); 172 | } 173 | } 174 | 175 | public registerOnChange(fn: any) { 176 | this.onChangeCallback = fn; 177 | } 178 | 179 | public registerOnTouched(fn: any) { 180 | this.onTouchedCallback = fn; 181 | } 182 | 183 | public updateModel() { 184 | this.onChangeCallback(this.formValue); 185 | } 186 | 187 | ngAfterViewChecked() { 188 | if (this.showListChanged) { 189 | this.showListChanged = false; 190 | this.showList ? this.itemsShown.emit() : this.itemsHidden.emit(); 191 | } 192 | } 193 | 194 | /** 195 | * get items for auto-complete 196 | */ 197 | public getItems(e?: Event) { 198 | 199 | let result; 200 | 201 | if (this.showResultsFirst && this.keyword.trim() === '') { 202 | this.keyword = ''; 203 | } else if (this.keyword.trim() === '') { 204 | this.suggestions = []; 205 | return; 206 | } 207 | 208 | if (typeof this.dataProvider === 'function') { 209 | result = this.dataProvider(this.keyword); 210 | } else { 211 | result = this.dataProvider.getResults(this.keyword); 212 | } 213 | 214 | // if result is instanceof Subject, use it asObservable 215 | if (result instanceof Subject) { 216 | result = result.asObservable(); 217 | } 218 | 219 | if (result instanceof Promise) { 220 | result = fromPromise(result); 221 | } 222 | 223 | // if query is async 224 | if (result instanceof Observable) { 225 | result 226 | .subscribe( 227 | (results: any[]) => { 228 | this.suggestions = results; 229 | this.showItemList(); 230 | }, 231 | (error: any) => console.error(error) 232 | ) 233 | ; 234 | } else { 235 | this.suggestions = result; 236 | this.showItemList(); 237 | } 238 | 239 | // emit event 240 | this.ionAutoInput.emit(this.keyword); 241 | } 242 | 243 | /** 244 | * show item list 245 | */ 246 | public showItemList(): void { 247 | this.showList = true; 248 | } 249 | 250 | /** 251 | * hide item list 252 | */ 253 | public hideItemList(): void { 254 | this.showList = this.alwaysShowList; 255 | } 256 | 257 | /** 258 | * select item from list 259 | * 260 | * @param event 261 | * @param selection 262 | **/ 263 | public select(selection: any): void { 264 | this.keyword = this.getLabel(selection); 265 | this.formValue = this.getFormValue(selection); 266 | this.hideItemList(); 267 | 268 | // emit selection event 269 | this.updateModel(); 270 | 271 | if (this.hideListOnSelection) { 272 | this.hideItemList(); 273 | } 274 | 275 | // emit selection event 276 | this.itemSelected.emit(selection); 277 | this.selection = selection; 278 | } 279 | 280 | /** 281 | * get current selection 282 | * @returns {any} 283 | */ 284 | public getSelection(): any { 285 | return this.selection; 286 | } 287 | 288 | /** 289 | * get current input value 290 | * @returns {string} 291 | */ 292 | public getValue() { 293 | return this.formValue; 294 | } 295 | 296 | /** 297 | * set current input value 298 | */ 299 | public setValue(selection: any) { 300 | this.formValue = this.getFormValue(selection); 301 | this.keyword = this.getLabel(selection); 302 | return; 303 | } 304 | 305 | /** 306 | 307 | /** 308 | * clear current input value 309 | */ 310 | public clearValue(hideItemList: boolean = false) { 311 | this.keyword = ''; 312 | this.selection = null; 313 | this.formValue = null; 314 | 315 | if (hideItemList) { 316 | this.hideItemList(); 317 | } 318 | 319 | return; 320 | } 321 | 322 | /** 323 | * set focus of searchbar 324 | */ 325 | public setFocus() { 326 | if (this.searchbarElem) { 327 | this.searchbarElem.setFocus(); 328 | } 329 | } 330 | 331 | /** 332 | * fired when the input focused 333 | */ 334 | onFocus() { 335 | this.autoFocus.emit(); 336 | } 337 | 338 | /** 339 | * fired when the input focused 340 | */ 341 | onBlur() { 342 | this.autoBlur.emit(); 343 | } 344 | 345 | /** 346 | * handle document click 347 | * @param event 348 | */ 349 | @HostListener('document:click', ['$event']) 350 | private documentClickHandler(event) { 351 | if ((this.searchbarElem 352 | && !this.searchbarElem._elementRef.nativeElement.contains(event.target)) 353 | || 354 | (!this.inputElem && this.inputElem._elementRef.nativeElement.contains(event.target)) 355 | ) { 356 | this.hideItemList(); 357 | } 358 | } 359 | 360 | private getFormValue(selection: any): any { 361 | if (selection == null) { 362 | return null; 363 | } 364 | let attr = this.dataProvider.formValueAttribute == null ? this.dataProvider.labelAttribute : this.dataProvider.formValueAttribute; 365 | if (typeof selection === 'object' && attr) { 366 | return selection[attr]; 367 | } 368 | return selection; 369 | } 370 | 371 | private getLabel(selection: any): string { 372 | if (selection == null) { 373 | return ''; 374 | } 375 | let attr = this.dataProvider.labelAttribute; 376 | let value = selection; 377 | if (this.dataProvider.getItemLabel) { 378 | value = this.dataProvider.getItemLabel(value); 379 | } 380 | if (typeof value === 'object' && attr) { 381 | return value[attr] || ''; 382 | } 383 | return value || ''; 384 | } 385 | } 386 | -------------------------------------------------------------------------------- /src/boldprefix.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, PipeTransform, Pipe } from '@angular/core'; 2 | 3 | /** 4 | * bolds the beggining of the matching string in the item 5 | */ 6 | @Pipe({ 7 | name: 'boldprefix' 8 | }) 9 | @Injectable() 10 | export class BoldPrefix implements PipeTransform { 11 | transform(value: string, keyword: string): any { 12 | if (!keyword) return value; 13 | let escaped_keyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 14 | return value.replace(new RegExp(escaped_keyword, 'gi'), function(str) { return str.bold(); }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ModuleWithProviders } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { AutoCompleteComponent } from './autocomplete.component'; 5 | import { BoldPrefix } from './boldprefix.pipe'; 6 | import {IonicModule} from 'ionic-angular'; 7 | 8 | export * from './autocomplete.component'; 9 | export * from './boldprefix.pipe'; 10 | export * from './auto-complete.service'; 11 | 12 | @NgModule({ 13 | imports: [ 14 | CommonModule, 15 | FormsModule, 16 | IonicModule 17 | ], 18 | declarations: [ 19 | AutoCompleteComponent, 20 | BoldPrefix 21 | ], 22 | exports: [ 23 | AutoCompleteComponent, 24 | BoldPrefix 25 | ] 26 | }) 27 | export class AutoCompleteModule { 28 | static forRoot(): ModuleWithProviders { 29 | return { 30 | ngModule: AutoCompleteModule, 31 | providers: [] 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ionic2-auto-complete", 3 | "version": "1.6.2-alpha", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/kadoshms/ionic2-autocomplete" 7 | }, 8 | "author": { 9 | "name": "mor", 10 | "email": "kadoshms@gmail.com" 11 | }, 12 | "keywords": [ 13 | "angular" 14 | ], 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/kadoshms/ionic2-autocomplete/issues" 18 | }, 19 | "module": "ionic2-auto-complete.js", 20 | "typings": "ionic2-auto-complete.d.ts", 21 | "peerDependencies": { 22 | "@angular/core": "^4.0.0", 23 | "rxjs": "^5.1.0", 24 | "zone.js": "^0.8.4" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/tsconfig.es5.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "module": "es2015", 5 | "target": "es5", 6 | "baseUrl": ".", 7 | "stripInternal": true, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "moduleResolution": "node", 11 | "outDir": "../build", 12 | "rootDir": ".", 13 | "lib": [ 14 | "es2015", 15 | "dom" 16 | ], 17 | "skipLibCheck": true, 18 | "types": [] 19 | }, 20 | "angularCompilerOptions": { 21 | "annotateForClosureCompiler": true, 22 | "strictMetadataEmit": true, 23 | "skipTemplateCodegen": true, 24 | "flatModuleOutFile": "ionic2-auto-complete.js", 25 | "flatModuleId": "ionic2-auto-complete" 26 | }, 27 | "files": [ 28 | "./index.ts" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /tools/gulp/inline-resources.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // https://github.com/filipesilva/angular-quickstart-lib/blob/master/inline-resources.js 3 | 'use strict'; 4 | 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const glob = require('glob'); 8 | const sass = require('node-sass'); 9 | 10 | /** 11 | * Simple Promiseify function that takes a Node API and return a version that supports promises. 12 | * We use promises instead of synchronized functions to make the process less I/O bound and 13 | * faster. It also simplifies the code. 14 | */ 15 | function promiseify(fn) { 16 | return function () { 17 | const args = [].slice.call(arguments, 0); 18 | return new Promise((resolve, reject) => { 19 | fn.apply(this, args.concat([function (err, value) { 20 | if (err) { 21 | reject(err); 22 | } else { 23 | resolve(value); 24 | } 25 | }])); 26 | }); 27 | }; 28 | } 29 | 30 | const readFile = promiseify(fs.readFile); 31 | const writeFile = promiseify(fs.writeFile); 32 | 33 | /** 34 | * Inline resources in a tsc/ngc compilation. 35 | * @param projectPath {string} Path to the project. 36 | */ 37 | function inlineResources(projectPath) { 38 | 39 | // Match only TypeScript files in projectPath. 40 | const files = glob.sync('**/*.ts', {cwd: projectPath}); 41 | 42 | // For each file, inline the templates and styles under it and write the new file. 43 | return Promise.all(files.map(filePath => { 44 | const fullFilePath = path.join(projectPath, filePath); 45 | return readFile(fullFilePath, 'utf-8') 46 | .then(content => inlineResourcesFromString(content, url => { 47 | // Resolve the template url. 48 | return path.join(path.dirname(fullFilePath), url); 49 | })) 50 | .then(content => writeFile(fullFilePath, content)) 51 | .catch(err => { 52 | console.error('An error occured: ', err); 53 | }); 54 | })); 55 | } 56 | 57 | /** 58 | * Inline resources from a string content. 59 | * @param content {string} The source file's content. 60 | * @param urlResolver {Function} A resolver that takes a URL and return a path. 61 | * @returns {string} The content with resources inlined. 62 | */ 63 | function inlineResourcesFromString(content, urlResolver) { 64 | // Curry through the inlining functions. 65 | return [ 66 | inlineTemplate, 67 | inlineStyle, 68 | removeModuleId 69 | ].reduce((content, fn) => fn(content, urlResolver), content); 70 | } 71 | 72 | /** 73 | * Inline the templates for a source file. Simply search for instances of `templateUrl: ...` and 74 | * replace with `template: ...` (with the content of the file included). 75 | * @param content {string} The source file's content. 76 | * @param urlResolver {Function} A resolver that takes a URL and return a path. 77 | * @return {string} The content with all templates inlined. 78 | */ 79 | function inlineTemplate(content, urlResolver) { 80 | return content.replace(/templateUrl:\s*'([^']+?\.html)'/g, function (m, templateUrl) { 81 | const templateFile = urlResolver(templateUrl); 82 | const templateContent = fs.readFileSync(templateFile, 'utf-8'); 83 | const shortenedTemplate = templateContent 84 | .replace(/([\n\r]\s*)+/gm, ' ') 85 | .replace(/"/g, '\\"'); 86 | return `template: "${shortenedTemplate}"`; 87 | }); 88 | } 89 | 90 | 91 | /** 92 | * Inline the styles for a source file. Simply search for instances of `styleUrls: [...]` and 93 | * replace with `styles: [...]` (with the content of the file included). 94 | * @param urlResolver {Function} A resolver that takes a URL and return a path. 95 | * @param content {string} The source file's content. 96 | * @return {string} The content with all styles inlined. 97 | */ 98 | function inlineStyle(content, urlResolver) { 99 | return content.replace(/styleUrls:\s*(\[[\s\S]*?\])/gm, function (m, styleUrls) { 100 | const urls = eval(styleUrls); 101 | return 'styles: [' 102 | + urls.map(styleUrl => { 103 | const styleFile = urlResolver(styleUrl); 104 | const originContent = fs.readFileSync(styleFile, 'utf-8'); 105 | const styleContent = styleFile.endsWith('.scss') ? buildSass(originContent, styleFile) : originContent; 106 | const shortenedStyle = styleContent 107 | .replace(/([\n\r]\s*)+/gm, ' ') 108 | .replace(/"/g, '\\"'); 109 | return `"${shortenedStyle}"`; 110 | }) 111 | .join(',\n') 112 | + ']'; 113 | }); 114 | } 115 | 116 | /** 117 | * build sass content to css 118 | * @param content {string} the css content 119 | * @param sourceFile {string} the scss file sourceFile 120 | * @return {string} the generated css, empty string if error occured 121 | */ 122 | function buildSass(content, sourceFile) { 123 | try { 124 | const result = sass.renderSync({data: content}); 125 | return result.css.toString() 126 | } catch (e) { 127 | console.error('\x1b[41m'); 128 | console.error('at ' + sourceFile + ':' + e.line + ":" + e.column); 129 | console.error(e.formatted); 130 | console.error('\x1b[0m'); 131 | return ""; 132 | } 133 | } 134 | 135 | /** 136 | * Remove every mention of `moduleId: module.id`. 137 | * @param content {string} The source file's content. 138 | * @returns {string} The content with all moduleId: mentions removed. 139 | */ 140 | function removeModuleId(content) { 141 | return content.replace(/\s*moduleId:\s*module\.id\s*,?\s*/gm, ''); 142 | } 143 | 144 | module.exports = inlineResources; 145 | module.exports.inlineResourcesFromString = inlineResourcesFromString; 146 | 147 | // Run inlineResources if module is being called directly from the CLI with arguments. 148 | if (require.main === module && process.argv.length > 2) { 149 | console.log('Inlining resources from project:', process.argv[2]); 150 | return inlineResources(process.argv[2]); 151 | } 152 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./src", 4 | "experimentalDecorators": true, 5 | "moduleResolution": "node", 6 | "rootDir": "./src", 7 | "lib": [ 8 | "es2015", 9 | "dom" 10 | ], 11 | "skipLibCheck": true, 12 | "types": [] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "class-name": true, 7 | "comment-format": [ 8 | true, 9 | "check-space" 10 | ], 11 | "curly": true, 12 | "eofline": true, 13 | "forin": true, 14 | "indent": [ 15 | true, 16 | "spaces" 17 | ], 18 | "label-position": true, 19 | "max-line-length": [ 20 | true, 21 | 140 22 | ], 23 | "member-access": false, 24 | "member-ordering": [ 25 | true, 26 | "static-before-instance", 27 | "variables-before-functions" 28 | ], 29 | "no-arg": true, 30 | "no-bitwise": true, 31 | "no-console": [ 32 | true, 33 | "debug", 34 | "info", 35 | "time", 36 | "timeEnd", 37 | "trace" 38 | ], 39 | "no-construct": true, 40 | "no-debugger": true, 41 | "no-duplicate-variable": true, 42 | "no-empty": false, 43 | "no-eval": true, 44 | "no-inferrable-types": true, 45 | "no-shadowed-variable": true, 46 | "no-string-literal": false, 47 | "no-switch-case-fall-through": true, 48 | "no-trailing-whitespace": true, 49 | "no-unused-expression": true, 50 | "no-unused-variable": true, 51 | "no-use-before-declare": true, 52 | "no-var-keyword": true, 53 | "object-literal-sort-keys": false, 54 | "one-line": [ 55 | true, 56 | "check-open-brace", 57 | "check-catch", 58 | "check-else", 59 | "check-whitespace" 60 | ], 61 | "quotemark": [ 62 | true, 63 | "single" 64 | ], 65 | "radix": true, 66 | "semicolon": [ 67 | "always" 68 | ], 69 | "triple-equals": [ 70 | true, 71 | "allow-null-check" 72 | ], 73 | "typedef-whitespace": [ 74 | true, 75 | { 76 | "call-signature": "nospace", 77 | "index-signature": "nospace", 78 | "parameter": "nospace", 79 | "property-declaration": "nospace", 80 | "variable-declaration": "nospace" 81 | } 82 | ], 83 | "variable-name": false, 84 | "whitespace": [ 85 | true, 86 | "check-branch", 87 | "check-decl", 88 | "check-operator", 89 | "check-separator", 90 | "check-type" 91 | ], 92 | "directive-selector": [true, "attribute", "", "camelCase"], 93 | "component-selector": [true, "element", "", "kebab-case"], 94 | "use-input-property-decorator": true, 95 | "use-output-property-decorator": true, 96 | "use-host-property-decorator": true, 97 | "no-input-rename": true, 98 | "no-output-rename": true, 99 | "use-life-cycle-interface": true, 100 | "use-pipe-transform-interface": true, 101 | "component-class-suffix": true, 102 | "directive-class-suffix": true 103 | } 104 | } 105 | --------------------------------------------------------------------------------