├── .editorconfig ├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── base.spec.ts ├── demos ├── angular2-data-table │ ├── 0.1.0-optGroup.gif │ └── 0.1.0.gif └── build.sh ├── gulpfile.ts ├── karma.conf.ts ├── package.json ├── src ├── configs.ts ├── inline-editor.component.html ├── inline-editor.component.ts ├── inline-editor.module.ts ├── inline-editor.service.ts ├── inputs │ ├── index.ts │ ├── input-base.spec.ts │ ├── input-base.ts │ ├── input-checkbox.component.ts │ ├── input-date.component.ts │ ├── input-datetime.component.ts │ ├── input-number.component.ts │ ├── input-password.component.ts │ ├── input-range.component.ts │ ├── input-select.component.ts │ ├── input-text.component.ts │ ├── input-textarea.component.ts │ ├── input-time.component.ts │ └── input.component.css ├── themes │ └── bootstrap.css ├── tsconfig.json └── types │ ├── edit-options.interface.ts │ ├── inline-configs.ts │ ├── inline-editor-error.interface.ts │ ├── inline-editor-events.class.ts │ ├── inline-editor-state.class.ts │ ├── input-configs.ts │ ├── input-type.type.ts │ ├── lifecycles.interface.ts │ ├── select-options.interface.ts │ └── testable-inputs.interface.ts ├── tsconfig.base.json ├── tsconfig.json ├── tsconfig.spec.json ├── tslint.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | 3 | # Node 4 | node_modules/* 5 | npm-debug.log 6 | yarn-error.log 7 | 8 | # TypeScript 9 | src/*.js 10 | src/*.map 11 | src/*.d.ts 12 | typings 13 | typings.json 14 | 15 | # VS Code 16 | .vscode/* 17 | 18 | # JetBrains 19 | .idea 20 | .project 21 | .settings 22 | .idea/* 23 | *.iml 24 | 25 | # Windows 26 | Thumbs.db 27 | Desktop.ini 28 | 29 | # Mac 30 | .DS_Store 31 | **/.DS_Store 32 | 33 | # Ngc generated files 34 | **/*.ngfactory.ts 35 | 36 | # Build files 37 | dist 38 | .tmp 39 | build 40 | 41 | # Debug chrome 42 | out/chrome 43 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Our 2 | demos 3 | typings 4 | 5 | # Node 6 | node_modules/* 7 | npm-debug.log 8 | 9 | # yarn 10 | yarn.lock 11 | 12 | # DO NOT IGNORE TYPESCRIPT FILES FOR NPM 13 | # TypeScript 14 | # *.js 15 | # *.map 16 | # *.d.ts 17 | typings 18 | typings.json 19 | 20 | # JetBrains 21 | .idea 22 | .project 23 | .settings 24 | .idea/* 25 | *.iml 26 | 27 | # VS Code 28 | .vscode/* 29 | 30 | # Windows 31 | Thumbs.db 32 | Desktop.ini 33 | 34 | # Mac 35 | .DS_Store 36 | **/.DS_Store 37 | 38 | # Temp 39 | .tmp 40 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Carlos Caballero González 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Native UI Inline-editor Angular (version 4+) component ([demo](demos)) 2 | 3 | Follow me [![twitter](https://img.shields.io/twitter/follow/carlillo.svg?style=social&label=%20carlillo)](https://twitter.com/carlillo) to be notified about new releases. 4 | 5 | ngx-inline-editor is a library of Angular (version 4+) that allows you to create editable elements. 6 | Such technique is also known as *click-to-edit* or *edit-in-place*. 7 | It is based on ideas of [angular-xeditable](https://github.com/vitalets/angular-xeditable) which is developed in AngularJS. 8 | 9 | 10 | ![Version 0.1.0](https://github.com/qontu/ngx-inline-editor/raw/master/demos/basic/0.1.0.gif) 11 | ## Dependencies 12 | 13 | Basically it does not depend on any libraries except Angular4 itself. 14 | For themes you may need to include Twitter Bootstrap CSS. 15 | 16 | ### Angular 4+ Version 17 | 18 | Angular 4 is now stable. Therefore, if encountering errors using this 19 | lib, ensure your version of Angular is compatible. The current version used to develop this lib is angular4 **^4.0.0**. 20 | 21 | ## Controls & Features 22 | 23 | * [x] text 24 | * [x] textarea 25 | * [x] select 26 | * [ ] checkbox 27 | * [ ] radio 28 | * [ ] date 29 | * [ ] time 30 | * [x] datetime 31 | * [ ] html5 inputs 32 | * [x] pattern 33 | * [x] number 34 | * [x] range 35 | * [ ] typeahead 36 | * [ ] ui-select 37 | * [ ] complex form 38 | * [ ] editable row 39 | * [ ] editable column 40 | * [ ] editable table 41 | * [x] themes 42 | 43 | 44 | ## Quick start 45 | 46 | 1. A recommended way to install ***ngx-inline-editor*** is through [npm](https://www.npmjs.com/search?q=ngx-inline-editor) package manager using the following command: 47 | 48 | `npm i @qontu/ngx-inline-editor --save` 49 | 50 | 2. Include the basic theme or configure your own styles which are in the following path: 51 | 52 | `dist/themes/bootstrap.css` 53 | 54 | 3. Include [Twitter Bootstrap](http://v4-alpha.getbootstrap.com/) and [FontAwesome](http://fontawesome.io/) in your project. 55 | 56 | 57 | Usage 58 | ----- 59 | 60 | ## Angular (4+) and later 61 | 62 | Import `InlineEditorModule` into your app's modules: 63 | 64 | ``` typescript 65 | import {InlineEditorModule} from '@qontu/ngx-inline-editor'; 66 | 67 | @NgModule({ 68 | imports: [ 69 | InlineEditorModule 70 | ] 71 | }) 72 | ``` 73 | 74 | This makes all the `@qontu/ngx-inline-editor` components available for use in your app components. 75 | 76 | 77 | ## Simple Example 78 | 79 | You can find a complete example [here](demos/basic) 80 | ```TypeScript 81 | import {Component} from '@angular/core'; 82 | 83 | @Component({ 84 | selector: 'my-component', 85 | template: ` 86 |
87 | 88 |
89 |
90 | 91 |
92 |
93 | 94 |
95 |
96 | 98 |
` 99 | }) 100 | export class MyComponent { 101 | title = 'My component!'; 102 | 103 | editableText = 'myText'; 104 | editablePassword = 'myPassword'; 105 | editableTextArea = 'Text in text area'; 106 | editableSelect = 2; 107 | editableSelectOptions =[ 108 | {value: 1, text: 'status1'}, 109 | {value: 2, text: 'status2'}, 110 | {value: 3, text: 'status3'}, 111 | {value: 4, text: 'status4'} 112 | ]; 113 | 114 | saveEditable(value) { 115 | //call to http service 116 | console.log('http.service: ' + value); 117 | } 118 | ``` 119 | 120 | 121 | ## API 122 | 123 | ### InlineEditorDirectives 124 | 125 | ##### Text 126 | 127 | 128 | ```HTML 129 | 140 | ``` 141 | 142 | * **`type`** [`string`] Specifies the type `` element to display. 143 | * **`onSave`** [`event handler`] The expression specified will be invoked whenever the form is save via a click on save button. 144 | The `$event` argument will be the value return of the input send. 145 | * **`onError`** [`event handler`] The expression specified will be invoked whenever the form is save via a click on save button and an error is provoked (example: the value is not between min and max). 146 | * **`name`** [`string`] Defines the name of an `` element. Default is `undefined`. 147 | * **`size`** [`number`] Defines the width, in characters, of an `` element. Default is `8`. 148 | * **`disabled`** [`boolean`] If set to `true`, a disabled input element is unusable and un-clickable. Default is `false`. 149 | * **`min`** [`number`] the min attribute specifies the minimum value for an `` element. Default is `1`. 150 | * **`max`** [`number`] the max attribute specifies the maximum value for an `` element. Default is `Infinity`. 151 | 152 | 153 | ##### Password 154 | 155 | ```HTML 156 | 166 | ``` 167 | 168 | * **`type`** [`string`] Specifies the type `` element to display. 169 | * **`onSave`** [`event handler`] The expression specified will be invoked whenever the form is save via a click on save button. 170 | The `$event` argument will be the value return of the input send. 171 | * **`onError`** [`event handler`] The expression specified will be invoked whenever the form is save via a click on save button and an error is provoked (example: the value is not between min and max). 172 | * **`name`** [`string`] Defines the name of an `` element. Default is `undefined`. 173 | * **`size`** [`number`] Defines the width, in characters, of an `` element. Default is `8`. 174 | * **`disabled`** [`boolean`] If set to `true`, a disabled input element is unusable and un-clickable. Default is `false`. 175 | * **`min`** [`number`] the min attribute specifies the minimum value for an `` element. Default is `1`. 176 | * **`max`** [`number`] the max attribute specifies the maximum value for an `` element. Default is `Infinity`. 177 | 178 | 179 | ##### TextArea 180 | 181 | ```HTML 182 | 194 | 195 | ``` 196 | 197 | * **`type`** [`string`] Specifies the type `` element to display. 198 | * **`onSave`** [`event handler`] The expression specified will be invoked whenever the form is save via a click on save button. 199 | The `$event` argument will be the value return of the input send. 200 | * **`onError`** [`event handler`] The expression specified will be invoked whenever the form is save via a click on save button and an error is provoked (example: the value is not between min and max). 201 | * **`name`** [`string`] Defines the name of an `` element. Default is `undefined`. 202 | * **`size`** [`number`] Defines the width, in characters, of an `` element. Default is `8`. 203 | * **`disabled`** [`boolean`] If set to `true`, a disabled input element is unusable and un-clickable. Default is `false`. 204 | * **`cols`** [`number`] Specifies the visible width of a text area. Default is `50`. 205 | * **`rows`** [`number`] Specifies the visible height of a text area. Default is `4`. 206 | * **`min`** [`number`] the min attribute specifies the minimum value for an `` element. Default is `1`. 207 | * **`max`** [`number`] the max attribute specifies the maximum value for an `` element. Default is `Infinity`. 208 | 209 | 210 | ##### Select 211 | 212 | ##### Basic example 213 | 214 | ```HTML 215 | 222 | ``` 223 | 224 | * **`type`** [`string`] Specifies the type `` element to display. 225 | * **`onSave`** [`event handler`] The expression specified will be invoked whenever the form is save via a click on save button. 226 | The `$event` argument will be the value return of the input send. 227 | * **`name`** [`string`] Defines the name of an `` element. Default is `undefined`. 228 | * **`disabled`** [`boolean`] If set to `true`, a disabled input element is unusable and un-clickable. Default is `false`. 229 | * **`options`** [`Array | Object:{ data: Array | Object: { data: Array 322 | 323 | 327 | ``` 328 | 329 | * **`empty`** [`string`] Specifies the default message to display if there are not ngModel for the component. 330 | If the type is `select` then the default selected element is the first element of the `options` array. 331 | 332 | 333 | 334 | # Style/Theme 335 | 336 | The `inline-editor` has the following basic theme which you can find in `dist/themes/bootstrap.css`: 337 | 338 | ```CSS 339 | a.c-inline-editor { 340 | text-decoration: none; 341 | color: #428bca; 342 | border-bottom: dashed 1px #428bca; 343 | cursor: pointer; 344 | line-height: 2; 345 | margin-right: 5px; 346 | margin-left: 5px; 347 | } 348 | .c-inline-editor.editable-empty, 349 | .c-inline-editor.editable-empty:hover, 350 | .c-inline-editor.editable-empty:focus, 351 | .c-inline-editor.a.editable-empty, 352 | .c-inline-editor.a.editable-empty:hover, 353 | .c-inline-editor.a.editable-empty:focus { 354 | font-style: italic; 355 | color: #DD1144; 356 | text-decoration: none; 357 | } 358 | 359 | .c-inline-editor.inlineEditForm { 360 | display: inline-block; 361 | white-space: nowrap; 362 | margin: 0; 363 | } 364 | 365 | #inlineEditWrapper { 366 | display: inline-block; 367 | } 368 | 369 | .c-inline-editor.inlineEditForm input, 370 | .c-inline-editor.select { 371 | width: auto; 372 | display: inline; 373 | } 374 | 375 | .c-inline-editor.inline-editor-button-group { 376 | display: inline-block; 377 | } 378 | 379 | .c-inline-editor.editInvalid { 380 | color: #a94442; 381 | margin-bottom: 0; 382 | } 383 | 384 | .c-inline-editor.error { 385 | border-color: #a94442; 386 | } 387 | 388 | [hidden].c-inline-editor { 389 | display: none; 390 | } 391 | ``` 392 | 393 | 394 | # Integration with other ngx-libraries 395 | 396 | ## ngx-data-table 397 | Example using [angular2-data-table](https://github.com/swimlane/angular2-data-table) ([demo](demos)) 398 | ![Version 0.1.0-angular2-data-table](demos/angular2-data-table/0.1.0.gif) 399 | 400 | 401 | # Troubleshooting 402 | 403 | Please follow this guidelines when reporting bugs and feature requests: 404 | 405 | 1. Use [GitHub Issues](https://github.com/qontu/ngx-inline-editor/issues) board to report bugs and feature requests (not our email address) 406 | 2. Please **always** write steps to reproduce the error. That way we can focus on fixing the bug, not scratching our heads trying to reproduce it. 407 | 408 | Thanks for understanding! 409 | 410 | # Development 411 | 412 | 1. To generate all `*.js`, `*.js.map` and `*.d.ts` files: 413 | 414 | `npm run build` 415 | 416 | 2. To debug : 417 | 418 | `npm run build:watch` 419 | 420 | ## Authors 421 | Carlos Caballero - [https://github.com/caballerog](hhttps://github.com/caballerog) 422 | 423 | Antonio Villena - [https://github.com/xxxtonixxx](https://github.com/xxxtonixxx) 424 | ## License 425 | 426 | The MIT License (See the [LICENSE](https://github.com/qontu/ngx-inline-editor/blob/master/LICENSE.MD) file for the full text) - 427 | 428 | -------------------------------------------------------------------------------- /base.spec.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import "zone.js/dist/zone.js"; 3 | import "zone.js/dist/proxy.js"; 4 | import "zone.js/dist/sync-test.js"; 5 | import "zone.js/dist/jasmine-patch.js"; 6 | import "zone.js/dist/async-test.js"; 7 | import "zone.js/dist/fake-async-test.js"; 8 | import { TestBed } from "@angular/core/testing"; 9 | import { 10 | BrowserDynamicTestingModule, 11 | platformBrowserDynamicTesting, 12 | } from "@angular/platform-browser-dynamic/testing"; 13 | 14 | TestBed.initTestEnvironment( 15 | BrowserDynamicTestingModule, 16 | platformBrowserDynamicTesting(), 17 | ); 18 | -------------------------------------------------------------------------------- /demos/angular2-data-table/0.1.0-optGroup.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qontu/ngx-inline-editor/d53d2f719d1ead2b07e2ff7885017a7541431799/demos/angular2-data-table/0.1.0-optGroup.gif -------------------------------------------------------------------------------- /demos/angular2-data-table/0.1.0.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qontu/ngx-inline-editor/d53d2f719d1ead2b07e2ff7885017a7541431799/demos/angular2-data-table/0.1.0.gif -------------------------------------------------------------------------------- /demos/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | validate_it_is_at_root_dir() { 4 | if [ ! -d ./.git ]; then 5 | echo "To run this script, you need to be at the root directory of the repository" 6 | exit 1 7 | fi 8 | } 9 | 10 | validate_no_uncommited_changes() { 11 | if [[ -n $(git status -s) ]]; then 12 | echo "To run this script, you must not have uncommited changes" 13 | exit 1 14 | fi 15 | } 16 | 17 | setup_examples() { 18 | # copy the `demos` dir from the `master` branch 19 | # but do not track it with git 20 | git checkout master -- demos && git reset 21 | 22 | rm -rf ./examples && mkdir examples 23 | 24 | cd ./demos 25 | 26 | for DEMO_DIR in * ; do if [ -d "$DEMO_DIR" ]; then 27 | cd "$DEMO_DIR" 28 | 29 | # for speed, expect that the node_modules are up to date 30 | # if the directory is present 31 | if [ ! -d node_modules ]; then 32 | echo "Installing npm modules of $DEMO_DIR" 33 | npm i 34 | fi 35 | 36 | # expect by default that it is using angular-cli 37 | # build dev version until minified version 38 | # of demos/angular2-data-table2 is broken 39 | ./node_modules/.bin/ng build --dev 40 | 41 | # update the base href of the repo 42 | sed -i.tmp "s|||" \ 43 | dist/index.html && rm -f dist/index.html.tmp 44 | 45 | cp -r dist ../../examples/"$DEMO_DIR" 46 | cd .. 47 | fi done 48 | 49 | cd .. 50 | } 51 | 52 | create_index_HTML_file() { 53 | rm -f index.html && cat > index.html << EOL 54 | 55 | 56 | 57 | ng2-inline-editor 58 | 59 | 60 | 61 | 62 | 63 |
64 | 67 |

Demos:

68 |
    69 | EOL 70 | 71 | cd examples 72 | for EXAMPLE_DIR in * ; do if [ -d "$EXAMPLE_DIR" ]; then 73 | echo "
  • $EXAMPLE_DIR
  • " >> ../index.html 74 | fi done 75 | cd .. 76 | 77 | cat >> index.html << EOL 78 |
79 |
80 | 81 | 82 | EOL 83 | } 84 | 85 | commit_changes_and_exit() { 86 | if [[ -n $(git status -s) ]]; then 87 | MASTER_COMMIT_START=$(git rev-parse master | head -c 10) 88 | git add -A . && git commit -m "Generate examples of $MASTER_COMMIT_START" 89 | echo "The new examples generated were commited. Please run:" 90 | echo "git push origin gh-pages" 91 | else 92 | echo "The result of the examples is the same as before. Not committing" 93 | fi 94 | 95 | git checkout master 96 | 97 | exit 0 98 | } 99 | 100 | validate_it_is_at_root_dir 101 | 102 | validate_no_uncommited_changes 103 | 104 | git checkout gh-pages 105 | 106 | echo "Generating the examples directory using the demos in the 'gh-pages' branch" 107 | 108 | setup_examples 109 | 110 | create_index_HTML_file 111 | 112 | commit_changes_and_exit 113 | -------------------------------------------------------------------------------- /gulpfile.ts: -------------------------------------------------------------------------------- 1 | import { task, src, dest, watch } from "gulp"; 2 | import * as rename from "gulp-rename"; 3 | import * as changed from "gulp-changed"; 4 | import * as runSequence from "run-sequence"; 5 | import * as path from "path"; 6 | import * as del from "del"; 7 | import * as ts from "gulp-typescript"; 8 | import * as ngc from "gulp-ngc"; 9 | import * as merge from "merge2"; 10 | 11 | const rollup = require("gulp-rollup"); 12 | const inlineNg2Template = require("gulp-inline-ng2-template"); 13 | 14 | const ROLLUP_GLOBALS = { 15 | "@angular/core": "_angular_core", 16 | "@angular/common": "_angular_common", 17 | "@angular/forms": "_angular_forms", 18 | }; 19 | const ROLLUP_EXTERNAL = Object.keys(ROLLUP_GLOBALS); 20 | 21 | const rootFolder = path.join(__dirname); 22 | const srcFolder = path.join(rootFolder, "src"); 23 | const tmpFolder = path.join(rootFolder, ".tmp"); 24 | const tmpBundlesFolder = path.join(tmpFolder, ".bundles"); 25 | const buildFolder = path.join(rootFolder, "build"); 26 | const distFolder = path.join(rootFolder, "dist"); 27 | 28 | const { 29 | name: libName, 30 | main: bandleNameUMD, 31 | module: bundleNameES5, 32 | es2015: bundleNameES2015, 33 | } = require("./package.json") as { [key: string]: string }; 34 | 35 | /** 36 | * 0. Prepare dist to debug. This copies all files from /src to /dist 37 | * and rename inline-editor.module.ts to index.ts, so typescript import works. 38 | */ 39 | task("debug", () => merge( 40 | src( 41 | [ 42 | `!${srcFolder}/inline-editor.module.ts`, 43 | `!${srcFolder}/tsconfig.json`, 44 | `${srcFolder}/**/*`, 45 | ]) 46 | .pipe(changed(distFolder)) 47 | .pipe(dest(distFolder)), 48 | 49 | src([`${srcFolder}/inline-editor.module.ts`]) 50 | .pipe(changed(distFolder, { transformPath: () => `${distFolder}/index.ts` } as any)) 51 | .pipe(rename("index.ts")) 52 | .pipe(dest(distFolder)), 53 | )); 54 | 55 | /** 56 | * 1. Clean /dist 57 | */ 58 | /** 59 | * 1.1 Delete /dist folder 60 | */ 61 | task("clean:dist", () => deleteFolders([distFolder])); 62 | /** 63 | * 1.2 Delete /dist/* files 64 | */ 65 | task("clean:dist:files", () => deleteFolders([`${distFolder}/*`])); 66 | 67 | /** 68 | * 2. Clone the /src folder into /.tmp and Inline template (.html) and style (.css) files 69 | * into the component .ts files. We do this on the /.tmp folder to avoid editing 70 | * the original /src files. If an npm link inside /src has been made, 71 | * then it's likely that a node_modules folder exists. Ignore this folder 72 | * when copying to /.tmp. 73 | */ 74 | task("copy:source", () => src([`${srcFolder}/**/*`]) 75 | .pipe(inlineNg2Template({ 76 | base: srcFolder, 77 | target: "es5", 78 | useRelativePaths: true, 79 | removeLineBreaks: true, 80 | })) 81 | .pipe(dest(tmpFolder)), 82 | ); 83 | 84 | 85 | /** 86 | * 3. Run the Angular compiler, ngc, on the /.tmp folder. This will output all 87 | * compiled modules to the /build folder. 88 | */ 89 | task("ngc", () => ngc(`${tmpFolder}/tsconfig.json`)); 90 | 91 | 92 | /** 93 | * 4. Run rollup inside the /build folder to generate ours Flat ES modules and place the 94 | * generated files into the /.tmp/bundles folder. 95 | * 96 | * rollup:es2015 -> Create bundle using ES2015 javascript. 97 | * rollup:es5 -> Using bundle generated from rollup:es2015, transpile to ES5 using tsc. 98 | * rollup:umd -> Using es5 transpiled code from rollup:es5, re-rollup to bundle as an UMD module. 99 | */ 100 | task("rollup", () => runSequence("rollup:es2015", "rollup:es5", "rollup:umd")); 101 | 102 | /** 103 | * We cached the bundles to use on watch mode. 104 | */ 105 | const rollupES2015Caches = {}; 106 | 107 | task("rollup:es2015", () => src(`${buildFolder}/**/*.js`) 108 | .pipe(rollup({ 109 | entry: `${buildFolder}/inline-editor.module.js`, 110 | external: ROLLUP_EXTERNAL, 111 | format: "es", 112 | separateCaches: rollupES2015Caches, 113 | })) 114 | .on("bundle", (bundle, name) => { 115 | rollupES2015Caches[name] = bundle; 116 | }) 117 | .pipe(rename(bundleNameES2015)) 118 | .pipe(dest(tmpBundlesFolder)), 119 | ); 120 | 121 | /** 122 | * We create a TS Project to use incremental compilation feature of gulp-typescript when 123 | * we use the watch mode. 124 | */ 125 | const tsProject = ts.createProject({ 126 | target: "es5", 127 | module: "es2015", 128 | lib: [ 129 | "es2016", 130 | ], 131 | allowJs: true, 132 | typescript: require("typescript"), 133 | }); 134 | 135 | task("rollup:es5", () => { 136 | const tsResult = src(`${tmpBundlesFolder}/${bundleNameES2015}`) 137 | .pipe(tsProject()); 138 | 139 | return merge([ 140 | tsResult.js.pipe(rename(bundleNameES5)).pipe(dest(tmpBundlesFolder)), 141 | // There are not .d.ts files, it can be removed or used 142 | tsResult.dts.pipe(dest(tmpBundlesFolder)), 143 | ]); 144 | }); 145 | 146 | /** 147 | * We cached the bundles to use on watch mode. 148 | */ 149 | const rollupUMDCaches = {}; 150 | 151 | task("rollup:umd", () => src(`${tmpBundlesFolder}/${bundleNameES5}`) 152 | .pipe(rollup({ 153 | entry: `${tmpBundlesFolder}/${bundleNameES5}`, 154 | globals: ROLLUP_GLOBALS, 155 | external: ROLLUP_EXTERNAL, 156 | format: "umd", 157 | moduleName: libName, 158 | separateCaches: rollupUMDCaches, 159 | onwarn(message) { 160 | if (message.code === "THIS_IS_UNDEFINED") { 161 | return; 162 | } 163 | 164 | console.warn(message); 165 | }, 166 | })) 167 | .on("bundle", (bundle, name) => { 168 | rollupUMDCaches[name] = bundle; 169 | }) 170 | .pipe(rename(bandleNameUMD)) 171 | .pipe(dest(tmpBundlesFolder)), 172 | ); 173 | 174 | 175 | /** 176 | * 5. Copy all the files from /build to /dist, except .js files. We ignore all .js from /build 177 | * because with don't need individual modules anymore, just the modules generated 178 | * on step 4. 179 | * Copy themes from .tmp/themes to /dist 180 | */ 181 | task("copy:build", () => runSequence("copy:buildTS", "copy:buildCSS")); 182 | 183 | task("copy:buildTS", () => src( 184 | [ 185 | `${buildFolder}/**/*`, 186 | `!${buildFolder}/**/*.js`, 187 | ]) 188 | .pipe(dest(distFolder)), 189 | ); 190 | 191 | task("copy:buildCSS", () => src( 192 | [ 193 | `${srcFolder}/themes/**/*`, 194 | ]) 195 | .pipe(dest(`${distFolder}/themes`)), 196 | ); 197 | 198 | /** 199 | * 6. Copy bundle files from /.tmp/bundles to /dist. There must be only three javascript files. 200 | * The files generated on step 4. 201 | */ 202 | task("copy:bundles", () => src(`${tmpBundlesFolder}/*.js`).pipe(dest(distFolder))); 203 | 204 | 205 | /** 206 | * 7. Copy package.json and README.md from /src to /dist 207 | */ 208 | task("copy:manifest", () => src([ 209 | `${rootFolder}/package.json`, 210 | `${rootFolder}/README.md`, 211 | ]).pipe(dest(distFolder))); 212 | 213 | /** 214 | * 8. Delete /.tmp folder 215 | */ 216 | task("clean:tmp", () => deleteFolders([tmpFolder])); 217 | 218 | /** 219 | * 9. Delete /build folder 220 | */ 221 | task("clean:build", () => deleteFolders([buildFolder])); 222 | 223 | task("compile", () => runSequence( 224 | "copy:source", 225 | "ngc", 226 | "rollup", 227 | "clean:dist:files", 228 | "copy:buildTS", 229 | "copy:buildCSS", 230 | "copy:bundles", 231 | "copy:manifest", 232 | "clean:build", 233 | "clean:tmp", 234 | (err?: any) => { 235 | if (err) { 236 | console.log("ERROR:", err.message); 237 | deleteFolders([distFolder, tmpFolder, buildFolder]); 238 | } else { 239 | console.log("Compilation finished succesfully"); 240 | } 241 | }), 242 | ); 243 | 244 | 245 | task("compile:debug", () => runSequence( 246 | "clean:dist:files", 247 | "copy:manifest", 248 | "debug", 249 | (err?: any) => { 250 | if (err) { 251 | console.log("ERROR:", err.message); 252 | deleteFolders([distFolder]); 253 | } else { 254 | console.log("Compilation finished succesfully"); 255 | } 256 | }), 257 | ); 258 | 259 | /** 260 | * Watch for any change in the /src folder and compile files 261 | */ 262 | task("watch", ["compile"], () => watch(`${srcFolder}/**/*`, ["compile"])); 263 | task("watch:debug", ["compile:debug"], () => watch(`${srcFolder}/**/*`, ["debug"])); 264 | 265 | task("clean", ["clean:dist", "clean:tmp", "clean:build"]); 266 | 267 | task("build", ["clean", "compile"]); 268 | task("build:watch", ["build", "watch"]); 269 | task("default", ["build:watch"]); 270 | 271 | /** 272 | * Deletes the specified folder 273 | */ 274 | function deleteFolders(folders: string | string[]) { 275 | return del(folders); 276 | } 277 | -------------------------------------------------------------------------------- /karma.conf.ts: -------------------------------------------------------------------------------- 1 | import { Config, ConfigOptions } from "karma"; 2 | import { KarmaTypescriptConfig } from "karma-typescript/src/api/configuration"; 3 | 4 | const env = process.env.NODE_ENV || "development"; 5 | 6 | console.log(`*** Running karma in ${env} mode`); 7 | 8 | function isDev(): boolean { 9 | return env === "development"; 10 | } 11 | 12 | const reporters = isDev() 13 | ? ["jasmine-diff", "mocha", "kjhtml", "karma-typescript"] 14 | : ["jasmine-diff", "mocha", "karma-typescript"]; 15 | 16 | const browsers = isDev() 17 | ? ["Chrome"] 18 | : ["PhantomJS"]; 19 | 20 | module.exports = function (config: Config) { 21 | config.set({ 22 | reporters, 23 | browsers, 24 | frameworks: ["jasmine", "karma-typescript"], 25 | files: [ 26 | "base.spec.ts", 27 | "src/**/*.*(ts|html)", 28 | ], 29 | preprocessors: { 30 | "**/*.ts": ["karma-typescript"], 31 | }, 32 | browserNoActivityTimeout: 100000, 33 | karmaTypescriptConfig: { 34 | coverageOptions: { 35 | instrumentation: false, 36 | }, 37 | tsconfig: "./tsconfig.spec.json", 38 | bundlerOptions: { 39 | entrypoints: /\.spec\.ts$/, 40 | transforms: [ 41 | require("karma-typescript-angular2-transform"), 42 | ], 43 | }, 44 | compilerDelay: 500, 45 | } as KarmaTypescriptConfig, 46 | client: { 47 | clearContext: false, 48 | }, 49 | } as ConfigOptions); 50 | }; 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@qontu/ngx-inline-editor", 3 | "version": "0.2.0-alpha.11", 4 | "author": { 5 | "name": "Carlos Caballero", 6 | "email": "caballerog.carlos@gmail.com" 7 | }, 8 | "maintainers": [ 9 | { 10 | "name": "Carlos Caballero", 11 | "email": "caballerog.carlos@gmail.com" 12 | }, 13 | { 14 | "name": "Toni Villena", 15 | "email": "tonivj5@gmail.com" 16 | } 17 | ], 18 | "license": "MIT", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/qontu/ngx-inline-editor" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/qontu/ngx-inline-editor/issues" 25 | }, 26 | "keywords": [ 27 | "angular", 28 | "angular4" 29 | ], 30 | "main": "ngx-inline-editor.umd.js", 31 | "module": "ngx-inline-editor.es5.js", 32 | "es2015": "ngx-inline-editor.es2015.js", 33 | "typings": "ngx-inline-editor.d.ts", 34 | "scripts": { 35 | "gulp": "node_modules/.bin/gulp", 36 | "build": "node_modules/.bin/gulp build && echo 'To publish a new version of library, please use npm run publish'", 37 | "build:watch": "node_modules/.bin/gulp", 38 | "debug": "node_modules/.bin/gulp watch:debug", 39 | "clean": "node_modules/.bin/gulp clean", 40 | "commit": "node_modules/.bin/git-cz", 41 | "lint": "tslint --type-check --project ./src/tsconfig.json src/**/*.ts", 42 | "publish": "cd dist && npm publish", 43 | "test": "karma start" 44 | }, 45 | "config": { 46 | "commitizen": { 47 | "path": "node_modules/cz-conventional-changelog" 48 | } 49 | }, 50 | "dependencies": {}, 51 | "peerDependencies": { 52 | "@angular/common": "^4.0.0", 53 | "@angular/core": "^4.0.0", 54 | "@angular/forms": "^4.0.0" 55 | }, 56 | "devDependencies": { 57 | "@angular/common": "^4.3.6", 58 | "@angular/compiler": "^4.3.6", 59 | "@angular/core": "^4.3.6", 60 | "@angular/forms": "^4.3.6", 61 | "@angular/platform-browser": "^4.3.6", 62 | "@angular/platform-browser-dynamic": "^4.3.6", 63 | "@types/del": "^2.2.32", 64 | "@types/gulp": "4.0.3", 65 | "@types/gulp-changed": "^0.0.31", 66 | "@types/gulp-rename": "^0.0.32", 67 | "@types/jasmine": "^2.5.47", 68 | "@types/karma": "^0.13.36", 69 | "@types/merge2": "^0.3.30", 70 | "@types/node": "~7.0.13", 71 | "@types/run-sequence": "^0.0.28", 72 | "codelyzer": "^3.1.2", 73 | "commitizen": "^2.9.6", 74 | "conventional-changelog-lint": "^1.1.9", 75 | "core-js": "^2.5.1", 76 | "del": "^2.2.2", 77 | "gulp": "^3.9.1", 78 | "gulp-changed": "^3.1.0", 79 | "gulp-inline-ng2-template": "^4.0.0", 80 | "gulp-ngc": "^0.3.2", 81 | "gulp-rename": "^1.2.2", 82 | "gulp-rollup": "2.14.0", 83 | "gulp-typescript": "^3.2.2", 84 | "jasmine-core": "^2.6.4", 85 | "karma": "^1.7.0", 86 | "karma-chrome-launcher": "~2.2.0", 87 | "karma-cli": "~1.0.1", 88 | "karma-coverage-istanbul-reporter": "^0.2.0", 89 | "karma-jasmine": "^1.1.0", 90 | "karma-jasmine-diff-reporter": "^1.1.0", 91 | "karma-jasmine-html-reporter": "^0.2.2", 92 | "karma-mocha-reporter": "^2.2.3", 93 | "karma-phantomjs-launcher": "^1.0.4", 94 | "karma-sourcemap-loader": "^0.3.7", 95 | "karma-typescript": "^3.0.4", 96 | "karma-typescript-angular2-transform": "^1.0.0", 97 | "merge2": "^1.0.3", 98 | "node-watch": "^0.5.2", 99 | "phantomjs-prebuilt": "~2.1.14", 100 | "protractor": "~5.1.0", 101 | "reflect-metadata": "~0.1.10", 102 | "rollup": "^0.47.2", 103 | "run-sequence": "^1.2.2", 104 | "rxjs": "^5.4.3", 105 | "ts-node": "~3.3.0", 106 | "tslint": "^5.7.0", 107 | "typescript": "~2.4.2", 108 | "zone.js": "^0.8.17" 109 | }, 110 | "engines": { 111 | "node": ">=6.0.0" 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/configs.ts: -------------------------------------------------------------------------------- 1 | export { 2 | InputConfig, 3 | InputBaseConfig, 4 | InputTextConfig, 5 | InputNumberConfig, 6 | InputSelectConfig, 7 | InputTextareaConfig, 8 | InputCheckboxConfig, 9 | } from "./types/input-configs"; 10 | -------------------------------------------------------------------------------- /src/inline-editor.component.html: -------------------------------------------------------------------------------- 1 |
2 | {{ showText() }} 3 |
4 |
5 |
6 | 7 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /src/inline-editor.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, forwardRef, Input, OnInit, Output, 3 | EventEmitter, ViewChild, 4 | ComponentRef, ComponentFactoryResolver, ViewContainerRef, ReflectiveInjector, OnDestroy, AfterContentInit, ChangeDetectionStrategy, 5 | } from "@angular/core"; 6 | import { NG_VALUE_ACCESSOR, ControlValueAccessor, NG_VALIDATORS, Validator } from "@angular/forms"; 7 | 8 | import { InlineEditorService } from "./inline-editor.service"; 9 | import { InlineConfig } from "./types/inline-configs"; 10 | import { InputNumberComponent } from "./inputs/input-number.component"; 11 | import { InputBase } from "./inputs/input-base"; 12 | import { InputTextComponent } from "./inputs/input-text.component"; 13 | import { InputPasswordComponent } from "./inputs/input-password.component"; 14 | import { InputRangeComponent } from "./inputs/input-range.component"; 15 | import { InputCheckboxComponent } from "./inputs/input-checkbox.component"; 16 | import { InputTextareaComponent } from "./inputs/input-textarea.component"; 17 | import { InputSelectComponent } from "./inputs/input-select.component"; 18 | import { InputDateComponent } from "./inputs/input-date.component"; 19 | import { InputTimeComponent } from "./inputs/input-time.component"; 20 | import { InputDatetimeComponent } from "./inputs/input-datetime.component"; 21 | import { Subscription } from "rxjs/Subscription"; 22 | import { SelectOptions } from "./types/select-options.interface"; 23 | import { InlineEditorError } from "./types/inline-editor-error.interface"; 24 | import { 25 | InlineEditorEvent, 26 | InternalEvent, 27 | Events, 28 | InternalEvents, 29 | ExternalEvents, 30 | ExternalEvent, 31 | } from "./types/inline-editor-events.class"; 32 | import { InlineEditorState, InlineEditorStateOptions } from "./types/inline-editor-state.class"; 33 | import { EditOptions } from "./types/edit-options.interface"; 34 | import { InputType } from "./types/input-type.type"; 35 | import { InputConfig } from "./configs"; 36 | 37 | const defaultConfig: InlineConfig = { 38 | name: "", 39 | required: false, 40 | options: { 41 | data: [], 42 | text: "text", 43 | value: "value", 44 | }, 45 | empty: "empty", 46 | placeholder: "placeholder", 47 | type: "text", 48 | size: 8, 49 | min: 0, 50 | max: Infinity, 51 | cols: 10, 52 | rows: 4, 53 | pattern: "", 54 | disabled: false, 55 | saveOnBlur: false, 56 | saveOnChange: false, 57 | saveOnEnter: true, 58 | editOnClick: true, 59 | cancelOnEscape: true, 60 | hideButtons: false, 61 | onlyValue: true, 62 | checkedText: "Check", 63 | uncheckedText: "Uncheck", 64 | }; 65 | 66 | @Component({ 67 | selector: "inline-editor", 68 | templateUrl: "./inline-editor.component.html", 69 | providers: [ 70 | { 71 | provide: NG_VALUE_ACCESSOR, 72 | useExisting: forwardRef(() => InlineEditorComponent), 73 | multi: true, 74 | }, 75 | { 76 | provide: NG_VALIDATORS, 77 | useExisting: forwardRef(() => InlineEditorComponent), 78 | multi: true, 79 | }, 80 | ], 81 | entryComponents: [ 82 | InputTextComponent, 83 | InputNumberComponent, 84 | InputPasswordComponent, 85 | InputRangeComponent, 86 | InputTextareaComponent, 87 | InputSelectComponent, 88 | InputDateComponent, 89 | InputTimeComponent, 90 | InputDatetimeComponent, 91 | InputCheckboxComponent, 92 | ], 93 | changeDetection: ChangeDetectionStrategy.OnPush, 94 | }) 95 | export class InlineEditorComponent 96 | implements OnInit, AfterContentInit, OnDestroy, ControlValueAccessor, Validator { 97 | 98 | constructor(protected componentFactoryResolver: ComponentFactoryResolver) { } 99 | 100 | public service: InlineEditorService; 101 | public state: InlineEditorState; 102 | public currentComponent: ComponentRef; 103 | 104 | public events: Events = { 105 | internal: new InternalEvents(), 106 | external: new ExternalEvents(), 107 | }; 108 | 109 | @Input() public type?: InputType; 110 | @Input() public config: InlineConfig; 111 | @Output() public onChange: EventEmitter = this.events.external.onChange; 112 | @Output() public onSave: EventEmitter = this.events.external.onSave; 113 | @Output() public onEdit: EventEmitter = this.events.external.onEdit; 114 | @Output() public onCancel: EventEmitter = this.events.external.onCancel; 115 | @Output() public onError: EventEmitter = this.events.external.onError; 116 | @Output() public onEnter: EventEmitter = this.events.external.onEnter; 117 | @Output() public onEscape: EventEmitter = this.events.external.onEscape; 118 | @Output() public onKeyPress: EventEmitter = this.events.external.onKeyPress; 119 | @Output() public onFocus: EventEmitter = this.events.external.onFocus; 120 | @Output() public onBlur: EventEmitter = this.events.external.onBlur; 121 | @Output() public onClick: EventEmitter = this.events.external.onClick; 122 | 123 | 124 | // input's attribute 125 | private _empty?: string; 126 | @Input() public set empty(empty: string | undefined) { 127 | this._empty = empty; 128 | this.updateConfig(undefined, "empty", empty); 129 | } 130 | 131 | public get empty(): string | undefined { 132 | return this._empty; 133 | } 134 | 135 | private _checkedText?: string; 136 | @Input() public set checkedText(checkedText: string | undefined) { 137 | this._checkedText = checkedText; 138 | this.updateConfig(undefined, "checkedText", checkedText); 139 | } 140 | 141 | public get checkedText(): string | undefined { 142 | return this._checkedText; 143 | } 144 | 145 | private _uncheckedText?: string; 146 | @Input() public set uncheckedText(uncheckedText: string | undefined) { 147 | this._uncheckedText = uncheckedText; 148 | this.updateConfig(undefined, "uncheckedText", uncheckedText); 149 | } 150 | 151 | public get uncheckedText(): string | undefined { 152 | return this._uncheckedText; 153 | } 154 | 155 | private _saveOnEnter?: boolean; 156 | @Input() public set saveOnEnter(saveOnEnter: boolean | undefined) { 157 | this._saveOnEnter = saveOnEnter; 158 | this.updateConfig(undefined, "saveOnEnter", saveOnEnter); 159 | } 160 | 161 | public get saveOnEnter(): boolean | undefined { 162 | return this._saveOnEnter; 163 | } 164 | 165 | private _saveOnBlur?: boolean; 166 | @Input() public set saveOnBlur(saveOnBlur: boolean | undefined) { 167 | this._saveOnBlur = saveOnBlur; 168 | this.updateConfig(undefined, "saveOnBlur", saveOnBlur); 169 | } 170 | public get saveOnBlur(): boolean | undefined { 171 | return this._saveOnBlur; 172 | } 173 | private _saveOnChange?: boolean; 174 | @Input() public set saveOnChange(saveOnChange: boolean | undefined) { 175 | this._saveOnChange = saveOnChange; 176 | this.updateConfig(undefined, "saveOnChange", saveOnChange); 177 | } 178 | public get saveOnChange(): boolean | undefined { 179 | return this._saveOnChange; 180 | } 181 | 182 | private _editOnClick?: boolean; 183 | @Input() public set editOnClick(editOnClick: boolean | undefined) { 184 | this._editOnClick = editOnClick; 185 | this.updateConfig(undefined, "editOnClick", editOnClick); 186 | } 187 | 188 | public get editOnClick(): boolean | undefined { 189 | return this._editOnClick; 190 | } 191 | 192 | private _cancelOnEscape?: boolean; 193 | @Input() public set cancelOnEscape(cancelOnEscape: boolean | undefined) { 194 | this._cancelOnEscape = cancelOnEscape; 195 | this.updateConfig(undefined, "cancelOnEscape", cancelOnEscape); 196 | } 197 | 198 | public get cancelOnEscape(): boolean | undefined { 199 | return this._cancelOnEscape; 200 | } 201 | 202 | private _hideButtons?: boolean; 203 | @Input() public set hideButtons(hideButtons: boolean | undefined) { 204 | this._hideButtons = hideButtons; 205 | this.updateConfig(undefined, "hideButtons", hideButtons); 206 | } 207 | 208 | public get hideButtons(): boolean | undefined { 209 | return this._hideButtons; 210 | } 211 | 212 | private _disabled?: boolean; 213 | @Input() public set disabled(disabled: boolean | undefined) { 214 | this._disabled = disabled; 215 | this.updateConfig(undefined, "disabled", disabled); 216 | } 217 | 218 | public get disabled(): boolean | undefined { 219 | return this._disabled; 220 | } 221 | 222 | private _required?: boolean; 223 | @Input() public set required(required: boolean | undefined) { 224 | this._required = required; 225 | this.updateConfig(undefined, "required", required); 226 | } 227 | 228 | public get required(): boolean | undefined { 229 | return this._required; 230 | } 231 | 232 | private _onlyValue?: boolean; 233 | @Input() public set onlyValue(onlyValue: boolean | undefined) { 234 | this._onlyValue = onlyValue; 235 | this.updateConfig(undefined, "onlyValue", onlyValue); 236 | } 237 | 238 | public get onlyValue(): boolean | undefined { 239 | return this._onlyValue; 240 | } 241 | 242 | private _placeholder?: string; 243 | @Input() public set placeholder(placeholder: string | undefined) { 244 | this._placeholder = placeholder; 245 | this.updateConfig(undefined, "placeholder", placeholder); 246 | } 247 | 248 | public get placeholder(): string | undefined { 249 | return this._placeholder; 250 | } 251 | 252 | private _name?: string; 253 | @Input() public set name(name: string | undefined) { 254 | this._name = name; 255 | this.updateConfig(undefined, "name", name); 256 | } 257 | 258 | public get name(): string | undefined { 259 | return this._name; 260 | } 261 | 262 | private _pattern?: string; 263 | @Input() public set pattern(pattern: string | undefined) { 264 | this._pattern = pattern; 265 | this.updateConfig(undefined, "pattern", pattern); 266 | } 267 | 268 | public get pattern(): string | undefined { 269 | return this._pattern; 270 | } 271 | 272 | private _size?: number; 273 | @Input() public set size(size: number | undefined) { 274 | this._size = size; 275 | this.updateConfig(undefined, "size", size); 276 | } 277 | 278 | public get size(): number | undefined { 279 | return this._size; 280 | } 281 | 282 | private _min?: number; 283 | @Input() public set min(min: number | undefined) { 284 | this._min = min; 285 | this.updateConfig(undefined, "min", min); 286 | } 287 | 288 | public get min(): number | undefined { 289 | return this._min; 290 | } 291 | 292 | private _max?: number; 293 | @Input() public set max(max: number | undefined) { 294 | this._max = max; 295 | this.updateConfig(undefined, "max", max); 296 | } 297 | 298 | public get max(): number | undefined { 299 | return this._max; 300 | } 301 | 302 | private _cols?: number; 303 | @Input() public set cols(cols: number | undefined) { 304 | this._cols = cols; 305 | this.updateConfig(undefined, "cols", cols); 306 | } 307 | 308 | public get cols(): number | undefined { 309 | return this._cols; 310 | } 311 | 312 | private _rows?: number; 313 | @Input() public set rows(rows: number | undefined) { 314 | this._rows = rows; 315 | this.updateConfig(undefined, "rows", rows); 316 | } 317 | 318 | public get rows(): number | undefined { 319 | return this._rows; 320 | } 321 | 322 | private _options?: SelectOptions; 323 | @Input() public set options(options: SelectOptions | undefined) { 324 | this._options = options; 325 | this.updateConfig(undefined, "options", options); 326 | } 327 | 328 | public get options(): SelectOptions | undefined { 329 | return this._options; 330 | } 331 | 332 | private subscriptions: { [key: string]: Subscription } = {}; 333 | 334 | private componentRef: ComponentRef; 335 | 336 | @ViewChild("container", { read: ViewContainerRef }) private container: ViewContainerRef; 337 | 338 | private inputInstance: InputBase; 339 | 340 | // Inputs implemented 341 | private components: { [key: string]: any } = { 342 | text: InputTextComponent, 343 | number: InputNumberComponent, 344 | password: InputPasswordComponent, 345 | range: InputRangeComponent, 346 | textarea: InputTextareaComponent, 347 | select: InputSelectComponent, 348 | date: InputDateComponent, 349 | time: InputTimeComponent, 350 | datetime: InputDatetimeComponent, 351 | checkbox: InputCheckboxComponent, 352 | }; 353 | 354 | private refreshNGModel: (_: any) => void; 355 | private isEnterKeyPressed = false; 356 | 357 | ngOnInit() { 358 | this.config = this.generateSafeConfig(); 359 | 360 | this.state = new InlineEditorState({ 361 | disabled: this.config.disabled, 362 | value: "", 363 | }); 364 | 365 | this.service = new InlineEditorService(this.events, { ...this.config }); 366 | 367 | this.subscriptions.onUpdateStateSubcription = this.events.internal.onUpdateStateOfParent.subscribe( 368 | (state: InlineEditorState) => this.state = state, 369 | ); 370 | 371 | this.subscriptions.onSaveSubscription = this.events.internal.onSave.subscribe( 372 | ({ event, state }: InternalEvent) => this.save({ 373 | event, 374 | state: state.getState(), 375 | }), 376 | ); 377 | 378 | this.subscriptions.onCancelSubscription = this.events.internal.onCancel.subscribe( 379 | ({ event, state }: InternalEvent) => this.cancel({ 380 | event, 381 | state: state.getState(), 382 | }), 383 | ); 384 | 385 | 386 | this.subscriptions.onChangeSubcription = this.events.internal.onChange.subscribe( 387 | ({ event, state }: InternalEvent) => { 388 | if (this.config.saveOnChange) { 389 | this.saveAndClose({ 390 | event, 391 | state: state.getState(), 392 | }); 393 | } 394 | this.emit(this.onChange, { 395 | event, 396 | state: state.getState(), 397 | }); 398 | }, 399 | ); 400 | 401 | this.subscriptions.onKeyPressSubcription = this.events.internal.onKeyPress.subscribe( 402 | ({ event, state }: InternalEvent) => this.emit(this.onKeyPress, { 403 | event, 404 | state: state.getState(), 405 | }), 406 | ); 407 | 408 | 409 | this.subscriptions.onBlurSubscription = this.events.internal.onBlur.subscribe( 410 | ({ event, state }: InternalEvent) => { 411 | // TODO (xxxtonixx): Maybe, this approach is not the best, 412 | // because we need to set a class property and it is dangerous. 413 | // We should search for one better. 414 | const isSavedByEnterKey = this.isEnterKeyPressed && this.config.saveOnEnter; 415 | 416 | if (this.config.saveOnBlur && !isSavedByEnterKey) { 417 | this.saveAndClose({ 418 | event, 419 | state: state.getState(), 420 | }); 421 | } 422 | 423 | this.isEnterKeyPressed = false; 424 | 425 | this.emit(this.onBlur, { 426 | event, 427 | state: state.getState(), 428 | }); 429 | }, 430 | ); 431 | 432 | this.subscriptions.onClickSubcription = this.events.internal.onClick.subscribe( 433 | ({ event, state }: InternalEvent) => this.emit(this.onClick, { 434 | event, 435 | state: state.getState(), 436 | }), 437 | ); 438 | 439 | this.subscriptions.onFocusSubcription = this.events.internal.onFocus.subscribe( 440 | ({ event, state }: InternalEvent) => this.emit(this.onFocus, { 441 | event, 442 | state: state.getState(), 443 | }), 444 | ); 445 | 446 | this.subscriptions.onEnterSubscription = this.events.internal.onEnter.subscribe( 447 | ({ event, state }: InternalEvent) => { 448 | this.isEnterKeyPressed = true; 449 | 450 | if (this.config.saveOnEnter) { 451 | this.save({ 452 | event, 453 | state: state.getState(), 454 | }); 455 | 456 | this.edit({ editing: false }); 457 | } 458 | 459 | this.emit(this.onEnter, { 460 | event, 461 | state: state.getState(), 462 | }); 463 | }, 464 | ); 465 | 466 | this.subscriptions.onEscapeSubscription = this.events.internal.onEscape.subscribe( 467 | ({ event, state }: InternalEvent) => { 468 | if (this.config.cancelOnEscape) { 469 | this.cancel({ 470 | event, 471 | state: state.getState(), 472 | }); 473 | } 474 | 475 | this.emit(this.onEscape, { 476 | event, 477 | state: state.getState(), 478 | }); 479 | }, 480 | ); 481 | 482 | } 483 | 484 | ngAfterContentInit() { 485 | this.service.onUpdateStateOfService.emit(this.state.clone()); 486 | this.generateComponent(this.config.type); 487 | } 488 | 489 | ngOnDestroy() { 490 | Object.values(this.subscriptions).forEach(subscription => subscription.unsubscribe()); 491 | this.currentComponent.destroy(); 492 | this.service.destroy(); 493 | } 494 | 495 | validate(): { [key: string]: any; } | null { 496 | const errors = this.inputInstance ? this.inputInstance.checkValue() : []; 497 | return errors.length === 0 ? null : { 498 | InlineEditorError: { 499 | valid: false, 500 | }, 501 | }; 502 | } 503 | 504 | writeValue(value: any) { 505 | this.state = this.state.newState({ 506 | ...this.state.getState(), 507 | value, 508 | }); 509 | 510 | this.events.internal.onUpdateStateOfChild.emit(this.state.clone()); 511 | } 512 | 513 | registerOnChange(refreshNGModel: (_: any) => void) { 514 | this.refreshNGModel = refreshNGModel; 515 | } 516 | 517 | registerOnTouched() { } 518 | 519 | // Method to display the inline editor form and hide the element 520 | public edit({ editing = true, focus = true, select = false, event }: EditOptions = {}) { 521 | this.state = this.state.newState({ 522 | ...this.state.getState(), 523 | editing, 524 | }); 525 | 526 | this.events.internal.onUpdateStateOfChild.emit(this.state.clone()); 527 | 528 | if (editing) { 529 | this.emit(this.onEdit, { 530 | event, 531 | state: this.state.getState(), 532 | }); 533 | } 534 | 535 | if (editing && focus) { 536 | this.inputInstance.focus(); 537 | } 538 | 539 | if (editing && select) { 540 | this.inputInstance.select(); 541 | } 542 | 543 | } 544 | 545 | public save({ event, state: hotState }: ExternalEvent) { 546 | const prevState = this.state.getState(); 547 | 548 | const state = { 549 | ...prevState, 550 | ...hotState, 551 | }; 552 | 553 | const errors = this.inputInstance.checkValue(); 554 | if (errors.length !== 0) { 555 | this.onError.emit(errors); 556 | } else { 557 | this.state = this.state.newState(state); 558 | 559 | this.refreshNGModel(state.value); 560 | 561 | this.emit(this.onSave, { 562 | event, 563 | state, 564 | }); 565 | } 566 | } 567 | 568 | public saveAndClose(outsideEvent: ExternalEvent) { 569 | this.save(outsideEvent); 570 | 571 | this.edit({ editing: false }); 572 | } 573 | 574 | // Method to reset the editable value 575 | public cancel(outsideEvent: ExternalEvent) { 576 | this.edit({ editing: false }); 577 | this.emit(this.onCancel, outsideEvent); 578 | } 579 | 580 | public getHotState(): InlineEditorStateOptions { 581 | return this.inputInstance.state.getState(); 582 | } 583 | 584 | public showText(): string { 585 | return this.inputInstance ? this.inputInstance.showText() : "Loading..."; 586 | } 587 | 588 | private getComponentType(typeName: InputType): string | never { 589 | const type = this.components[typeName]; 590 | 591 | if (!type) { 592 | throw new Error("That type does not exist or it is not implemented yet!"); 593 | } 594 | 595 | return type; 596 | } 597 | 598 | private generateComponent(type: InputType) { 599 | const componentType = this.getComponentType(type); 600 | this.inputInstance = this.createInputInstance(componentType); 601 | } 602 | 603 | private createInputInstance(componentType): InputBase { 604 | const providers = ReflectiveInjector.resolve([{ 605 | provide: InlineEditorService, 606 | useValue: this.service, 607 | }]); 608 | const injector = ReflectiveInjector.fromResolvedProviders(providers, this.container.parentInjector); 609 | 610 | const factory = this.componentFactoryResolver.resolveComponentFactory(componentType); 611 | 612 | this.componentRef = factory.create(injector); 613 | this.container.insert(this.componentRef.hostView); 614 | 615 | if (this.currentComponent) { 616 | this.currentComponent.destroy(); 617 | } 618 | 619 | this.currentComponent = this.componentRef; 620 | 621 | return this.componentRef.instance; 622 | } 623 | 624 | private removeUndefinedProperties(object: Object): T { 625 | return JSON.parse( 626 | JSON.stringify( 627 | typeof object === "object" ? object : {}, 628 | ), 629 | ); 630 | } 631 | 632 | private generateSafeConfig(): InlineConfig { 633 | const configFromAttrs: InlineConfig = { 634 | type: this.type!, 635 | name: this.name!, 636 | size: this.size!, 637 | placeholder: this.placeholder!, 638 | empty: this.empty!, 639 | required: this.required!, 640 | disabled: this.disabled!, 641 | hideButtons: this.hideButtons!, 642 | min: this.min!, 643 | max: this.max!, 644 | cols: this.cols!, 645 | rows: this.rows!, 646 | options: this.options!, 647 | pattern: this.pattern!, 648 | saveOnEnter: this.saveOnEnter!, 649 | saveOnBlur: this.saveOnBlur!, 650 | saveOnChange: this.saveOnChange!, 651 | editOnClick: this.editOnClick!, 652 | cancelOnEscape: this.cancelOnEscape!, 653 | onlyValue: this.onlyValue!, 654 | checkedText: this.checkedText!, 655 | uncheckedText: this.uncheckedText!, 656 | }; 657 | 658 | return { 659 | // First default config 660 | ...defaultConfig, 661 | // Default config is overwritten by [config] attr 662 | ...this.removeUndefinedProperties(this.config), 663 | // Config from attributes have preference over all others 664 | ...this.removeUndefinedProperties(configFromAttrs), 665 | }; 666 | } 667 | 668 | private updateConfig(config?: InlineConfig, property?: string, value?: any) { 669 | if (this.config) { 670 | config = config || this.config; 671 | 672 | if (property) { 673 | config[property] = value; 674 | } 675 | 676 | this.config = { ...config }; 677 | 678 | this.events.internal.onUpdateConfig.emit(this.config); 679 | } 680 | } 681 | 682 | 683 | private emit(event: EventEmitter, data: ExternalEvent) { 684 | if (this.config.onlyValue) { 685 | event.emit(data.state.value); 686 | } else { 687 | (event as EventEmitter) 688 | .emit({ 689 | ...data, 690 | instance: this, 691 | }); 692 | } 693 | } 694 | } 695 | -------------------------------------------------------------------------------- /src/inline-editor.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { CommonModule } from "@angular/common"; 3 | import { FormsModule } from "@angular/forms"; 4 | 5 | import { InlineEditorComponent } from "./inline-editor.component"; 6 | import { 7 | InputTimeComponent, 8 | InputDateComponent, 9 | InputDatetimeComponent, 10 | InputNumberComponent, 11 | InputRangeComponent, 12 | InputPasswordComponent, 13 | InputSelectComponent, 14 | InputTextareaComponent, 15 | InputTextComponent, 16 | InputCheckboxComponent, 17 | } from "./inputs/index"; 18 | import { InputBase } from "./inputs/input-base"; 19 | 20 | export { InlineEditorComponent } from "./inline-editor.component"; 21 | export * from "./inputs/index"; 22 | export * from "./configs"; 23 | export { InputBase } from "./inputs/input-base"; 24 | export { InlineEditorEvent } from "./types/inline-editor-events.class"; 25 | 26 | const EXPORTS = [ 27 | InputBase, 28 | InputTextComponent, 29 | InputNumberComponent, 30 | InputPasswordComponent, 31 | InputRangeComponent, 32 | InputTextareaComponent, 33 | InputSelectComponent, 34 | InputDateComponent, 35 | InputTimeComponent, 36 | InputDatetimeComponent, 37 | InputCheckboxComponent, 38 | InlineEditorComponent, 39 | ]; 40 | 41 | @NgModule({ 42 | imports: [CommonModule, FormsModule], 43 | declarations: EXPORTS, 44 | exports: [InlineEditorComponent], 45 | }) 46 | export class InlineEditorModule { } 47 | -------------------------------------------------------------------------------- /src/inline-editor.service.ts: -------------------------------------------------------------------------------- 1 | import { InlineConfig } from "./types/inline-configs"; 2 | import { Subscription } from "rxjs/Subscription"; 3 | import { Events } from "./types/inline-editor-events.class"; 4 | import { InlineEditorState } from "./types/inline-editor-state.class"; 5 | import { EventEmitter } from "@angular/core"; 6 | 7 | export class InlineEditorService { 8 | constructor( 9 | public events: Events, 10 | public config?: InlineConfig, 11 | ) { 12 | this.subscriptions.onUpdateStateSubscription = this.onUpdateStateOfService.subscribe( 13 | (state: InlineEditorState) => this.state = state, 14 | ); 15 | } 16 | 17 | public onUpdateStateOfService: EventEmitter = new EventEmitter(); 18 | 19 | private state: InlineEditorState; 20 | private subscriptions: { [key: string]: Subscription } = {}; 21 | 22 | 23 | public setConfig(config: InlineConfig) { 24 | this.config = config; 25 | } 26 | 27 | public getConfig(): InlineConfig | undefined { 28 | return this.config; 29 | } 30 | 31 | public getState(): InlineEditorState { 32 | return this.state.clone(); 33 | } 34 | 35 | public destroy() { 36 | Object.values(this.subscriptions).forEach(subscription => subscription.unsubscribe()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/inputs/index.ts: -------------------------------------------------------------------------------- 1 | export { InputTextComponent } from "./input-text.component"; 2 | export { InputDateComponent } from "./input-date.component"; 3 | export { InputDatetimeComponent } from "./input-datetime.component"; 4 | export { InputNumberComponent } from "./input-number.component"; 5 | export { InputRangeComponent } from "./input-range.component"; 6 | export { InputPasswordComponent } from "./input-password.component"; 7 | export { InputSelectComponent } from "./input-select.component"; 8 | export { InputTextareaComponent } from "./input-textarea.component"; 9 | export { InputTimeComponent } from "./input-time.component"; 10 | export { InputCheckboxComponent } from "./input-checkbox.component"; -------------------------------------------------------------------------------- /src/inputs/input-base.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, fakeAsync, tick } from "@angular/core/testing"; 2 | import { Renderer, Injector } from "@angular/core"; 3 | 4 | import { InputBase } from "./input-base"; 5 | import { inject } from "@angular/core/testing"; 6 | import { InlineEditorService } from "../inline-editor.service"; 7 | import { InlineEditorEvent, Events, ExternalEvent } from "../types/inline-editor-events.class"; 8 | import { InlineEditorState } from "../types/inline-editor-state.class"; 9 | 10 | let component: InputBase; 11 | 12 | let inlineEditorService: InlineEditorService; 13 | let renderer: Renderer; 14 | 15 | function inlineEditorSpyFactory(): InlineEditorService { 16 | const createObservableSpy = () => ({ 17 | subscribe: jasmine.createSpy("subscribe") 18 | .and.returnValue(jasmine.createSpyObj("observable", ["unsubscribe"])), 19 | }); 20 | 21 | const spy = { 22 | getConfig: jasmine.createSpy("getConfig").and.returnValue({ disabled: false }), 23 | getState: jasmine.createSpy("getState"), 24 | events: { 25 | internal: { 26 | onUpdateConfig: createObservableSpy(), 27 | onSave: { 28 | emit: jasmine.createSpy("emit"), 29 | }, 30 | onCancel: { 31 | emit: jasmine.createSpy("emit"), 32 | }, 33 | onEnter: { 34 | emit: jasmine.createSpy("emit"), 35 | }, 36 | onEscape: { 37 | emit: jasmine.createSpy("emit"), 38 | }, 39 | onBlur: { 40 | emit: jasmine.createSpy("emit"), 41 | }, 42 | onFocus: { 43 | emit: jasmine.createSpy("emit"), 44 | }, 45 | onClick: { 46 | emit: jasmine.createSpy("emit"), 47 | }, 48 | onUpdateStateOfChild: createObservableSpy(), 49 | }, 50 | }, 51 | onUpdateStateOfService: { 52 | emit: jasmine.createSpy("emit"), 53 | }, 54 | }; 55 | 56 | return spy as any as InlineEditorService; 57 | } 58 | 59 | function rendererSpyFactory(): Renderer { 60 | const spy = jasmine.createSpyObj("Renderer", ["invokeElementMethod"]); 61 | 62 | return spy as Renderer; 63 | } 64 | 65 | describe("InputBaseComponent", () => { 66 | beforeEach(() => { 67 | TestBed.configureTestingModule({ 68 | declarations: [], 69 | providers: [ 70 | { 71 | provide: InlineEditorService, 72 | useFactory: inlineEditorSpyFactory, 73 | }, 74 | { 75 | provide: Renderer, 76 | useFactory: rendererSpyFactory, 77 | }, 78 | ], 79 | }); 80 | }); 81 | 82 | beforeEach(() => { 83 | component = new InputBase(TestBed); 84 | }); 85 | 86 | 87 | beforeEach(inject([InlineEditorService, Renderer], 88 | (_inlineEditorService: InlineEditorService, _renderer: Renderer) => { 89 | inlineEditorService = _inlineEditorService; 90 | renderer = _renderer; 91 | }), 92 | ); 93 | 94 | describe("should capture state when", () => { 95 | 96 | it("Call onSave event", () => { 97 | component.save(); 98 | expect(inlineEditorService.events.internal.onSave.emit).toHaveBeenCalledTimes(1); 99 | expect(inlineEditorService.events.internal.onSave.emit).toHaveBeenCalledWith(jasmine.objectContaining({ 100 | state: jasmine.objectContaining({ 101 | value: "", 102 | editing: false, 103 | disabled: false, 104 | empty: true, 105 | }), 106 | })); 107 | }); 108 | 109 | it("Call onCancel event", () => { 110 | component.cancel(); 111 | 112 | expect(inlineEditorService.events.internal.onCancel.emit).toHaveBeenCalledTimes(1); 113 | expect(inlineEditorService.events.internal.onCancel.emit).toHaveBeenCalledWith(jasmine.objectContaining({ 114 | state: jasmine.objectContaining({ 115 | value: "", 116 | editing: false, 117 | disabled: false, 118 | empty: true, 119 | }), 120 | })); 121 | }); 122 | 123 | it("Call onEnter event", () => { 124 | event = null; 125 | component.onEnter(event); 126 | 127 | expect(inlineEditorService.events.internal.onEnter.emit).toHaveBeenCalledTimes(1); 128 | expect(inlineEditorService.events.internal.onEnter.emit).toHaveBeenCalledWith(jasmine.objectContaining({ 129 | event, 130 | state: jasmine.objectContaining({ 131 | value: "", 132 | editing: false, 133 | disabled: false, 134 | empty: true, 135 | }), 136 | })); 137 | }); 138 | 139 | it("Call onEscape event", () => { 140 | event = null; 141 | component.onEscape(event); 142 | 143 | expect(inlineEditorService.events.internal.onEscape.emit).toHaveBeenCalledTimes(1); 144 | expect(inlineEditorService.events.internal.onEscape.emit).toHaveBeenCalledWith(jasmine.objectContaining({ 145 | event, 146 | state: jasmine.objectContaining({ 147 | value: "", 148 | editing: false, 149 | disabled: false, 150 | empty: true, 151 | }), 152 | })); 153 | }); 154 | 155 | it("Call onBlur event", () => { 156 | event = null; 157 | component.onBlur(event); 158 | 159 | expect(inlineEditorService.events.internal.onBlur.emit).toHaveBeenCalledTimes(1); 160 | expect(inlineEditorService.events.internal.onBlur.emit).toHaveBeenCalledWith(jasmine.objectContaining({ 161 | event, 162 | state: jasmine.objectContaining({ 163 | value: "", 164 | editing: false, 165 | disabled: false, 166 | empty: true, 167 | }), 168 | })); 169 | }); 170 | 171 | it("Call onClick event", () => { 172 | event = null; 173 | component.onClick(event); 174 | 175 | expect(inlineEditorService.events.internal.onClick.emit).toHaveBeenCalledTimes(1); 176 | expect(inlineEditorService.events.internal.onClick.emit).toHaveBeenCalledWith(jasmine.objectContaining({ 177 | event, 178 | state: jasmine.objectContaining({ 179 | value: "", 180 | editing: false, 181 | disabled: false, 182 | empty: true, 183 | }), 184 | })); 185 | }); 186 | 187 | it("Call onFocus event", () => { 188 | event = null; 189 | component.onFocus(event); 190 | 191 | expect(inlineEditorService.events.internal.onFocus.emit).toHaveBeenCalledTimes(1); 192 | expect(inlineEditorService.events.internal.onFocus.emit).toHaveBeenCalledWith(jasmine.objectContaining({ 193 | event, 194 | state: jasmine.objectContaining({ 195 | value: "", 196 | editing: false, 197 | disabled: false, 198 | empty: true, 199 | }), 200 | })); 201 | }); 202 | 203 | }); 204 | 205 | describe("should call funtion invokeElementMethod from renderer object when", () => { 206 | 207 | it("Call focus ", fakeAsync(() => { 208 | component.focus(); 209 | tick(); 210 | 211 | expect(renderer.invokeElementMethod).toHaveBeenCalledTimes(1); 212 | expect(renderer.invokeElementMethod).toHaveBeenCalledWith(component.inputElement, "focus", []); 213 | })); 214 | 215 | it("Call select", fakeAsync(() => { 216 | component.select(); 217 | tick(); 218 | 219 | expect(renderer.invokeElementMethod).toHaveBeenCalledTimes(1); 220 | expect(renderer.invokeElementMethod).toHaveBeenCalledWith(component.inputElement, "select", []); 221 | })); 222 | }); 223 | }); 224 | -------------------------------------------------------------------------------- /src/inputs/input-base.ts: -------------------------------------------------------------------------------- 1 | import { InlineBaseConfig, InlineConfig } from "../types/inline-configs"; 2 | import { 3 | Renderer, Component, ViewChild, ElementRef, OnInit, 4 | Injector, OnChanges, DoCheck, AfterContentInit, 5 | AfterViewInit, AfterViewChecked, AfterContentChecked, 6 | OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, 7 | } from "@angular/core"; 8 | import { Subscription } from "rxjs/Subscription"; 9 | import { InlineEditorError } from "../types/inline-editor-error.interface"; 10 | import { InlineEditorState } from "../types/inline-editor-state.class"; 11 | import { InlineEditorService } from "../inline-editor.service"; 12 | import { OnUpdateConfig } from "../types/lifecycles.interface"; 13 | import { InputRegexTestable, InputLengthTestable } from "../types/testable-inputs.interface"; 14 | @Component({ 15 | template: " ", 16 | changeDetection: ChangeDetectionStrategy.OnPush, 17 | }) 18 | export class InputBase implements OnInit, OnChanges, DoCheck, 19 | AfterContentInit, AfterContentChecked, AfterViewInit, 20 | AfterViewChecked, OnDestroy, 21 | OnUpdateConfig { 22 | 23 | constructor(protected injector: Injector) { 24 | this.renderer = injector.get(Renderer); 25 | this.service = injector.get(InlineEditorService); 26 | this.cd = injector.get(ChangeDetectorRef); 27 | 28 | this.onUpdateConfig(this.service.getConfig()!); 29 | 30 | this.state = this.service.getState().clone(); 31 | 32 | this.subscriptions.onUpdateConfigSubcription = this.service.events.internal.onUpdateConfig.subscribe( 33 | (config: InlineConfig) => this.onUpdateConfig(config), 34 | ); 35 | 36 | this.subscriptions.onUpdateStateSubscription = this.service.events.internal.onUpdateStateOfChild.subscribe( 37 | (state: InlineEditorState) => { 38 | const newState = state.getState(); 39 | this.updateState(this.state.newState({ 40 | ...newState, 41 | empty: this.isEmpty(newState.value), 42 | })); 43 | 44 | this.service.events.internal.onUpdateStateOfParent.emit(this.state.clone()); 45 | }); 46 | } 47 | 48 | public state: InlineEditorState; 49 | public set value(value) { 50 | if (this.value === value) { 51 | return; 52 | } 53 | 54 | this.updateState(this.state.newState({ 55 | ...this.state.getState(), 56 | empty: this.isEmpty(value), 57 | value, 58 | })); 59 | 60 | this.service.events.internal.onChange.emit({ 61 | state: this.state.clone(), 62 | }); 63 | } 64 | 65 | public get value() { 66 | return this.state.getState().value; 67 | } 68 | @ViewChild("inputRef") public inputRef: ElementRef; 69 | 70 | public config: InlineBaseConfig; 71 | public service: InlineEditorService; 72 | public inputElement: HTMLInputElement; 73 | public isNumeric = false; 74 | public isRegexTestable = false; 75 | public isLengthTestable = false; 76 | protected renderer: Renderer; 77 | protected cd: ChangeDetectorRef; 78 | protected subscriptions: { [key: string]: Subscription } = {}; 79 | 80 | 81 | ngOnChanges() { } 82 | 83 | ngOnInit() { 84 | this.inputElement = this.inputRef.nativeElement; 85 | } 86 | 87 | ngDoCheck() { } 88 | 89 | ngAfterContentInit() { } 90 | 91 | ngAfterContentChecked() { } 92 | 93 | ngAfterViewInit() { } 94 | 95 | ngAfterViewChecked() { } 96 | 97 | ngOnDestroy() { 98 | Object.values(this.subscriptions).forEach(subscription => subscription.unsubscribe()); 99 | } 100 | 101 | onUpdateConfig(newConfig: InlineBaseConfig) { 102 | this.config = newConfig; 103 | } 104 | 105 | public save() { 106 | this.service.events.internal.onSave.emit({ 107 | state: this.state.clone(), 108 | }); 109 | } 110 | 111 | public cancel() { 112 | this.service.events.internal.onCancel.emit({ 113 | state: this.state.clone(), 114 | }); 115 | } 116 | 117 | public onEnter(event: Event) { 118 | this.service.events.internal.onEnter.emit({ 119 | event, 120 | state: this.state.clone(), 121 | }); 122 | } 123 | 124 | public onEscape(event: Event) { 125 | this.service.events.internal.onEscape.emit({ 126 | event, 127 | state: this.state.clone(), 128 | }); 129 | } 130 | 131 | public onBlur(event: Event) { 132 | this.service.events.internal.onBlur.emit({ 133 | event, 134 | state: this.state.clone(), 135 | }); 136 | } 137 | 138 | public onClick(event: Event) { 139 | this.service.events.internal.onClick.emit({ 140 | event, 141 | state: this.state.clone(), 142 | }); 143 | } 144 | 145 | public onKeyPress(event: Event) { 146 | this.service.events.internal.onKeyPress.emit({ 147 | event, 148 | state: this.state.clone(), 149 | }); 150 | } 151 | 152 | public onFocus(event: Event) { 153 | this.service.events.internal.onFocus.emit({ 154 | event, 155 | state: this.state.clone(), 156 | }); 157 | } 158 | 159 | public checkValue(): InlineEditorError[] { 160 | const errs: InlineEditorError[] = []; 161 | 162 | const { value } = this.state.getState(); 163 | 164 | if (this.canTestRegex(this.config)) { 165 | if (!new RegExp(this.config.pattern as string).test(value != null && value !== false ? value : '')) { 166 | errs.push({ 167 | type: "PATTERN_ERROR", 168 | message: "Test pattern has failed", 169 | }); 170 | } 171 | } 172 | 173 | if (this.canTestLength(this.config)) { 174 | const { min, max } = this.config; 175 | const length = value ? (this.isNumeric ? Number(value) : value.length) : 0; 176 | 177 | if (length < min || length > max) { 178 | errs.push({ 179 | type: "LENGTH_ERROR", 180 | message: "Test length has failed", 181 | }); 182 | } 183 | } 184 | 185 | return errs; 186 | } 187 | 188 | public showText(): string { 189 | return this.state.isEmpty() ? this.config.empty : this.state.getState().value; 190 | } 191 | 192 | public focus() { 193 | setTimeout(() => this.renderer.invokeElementMethod(this.inputElement, "focus", [])); 194 | } 195 | 196 | public select() { 197 | setTimeout(() => this.renderer.invokeElementMethod(this.inputElement, "select", [])); 198 | } 199 | 200 | protected updateState(newState: InlineEditorState) { 201 | const { empty: wasEmpty, disabled: wasDisabled } = this.state.getState(); 202 | 203 | if (newState.isEmpty() && newState.isEmpty() !== wasEmpty) { 204 | // onEmpty() 205 | } 206 | 207 | if (newState.isDisabled() && newState.isDisabled() !== wasDisabled) { 208 | // onDisabled() 209 | } 210 | 211 | this.state = newState; 212 | 213 | this.cd.markForCheck(); 214 | 215 | this.service.onUpdateStateOfService.emit(this.state.clone()); 216 | } 217 | 218 | protected isEmpty(value: any): boolean { 219 | return value == null || value === ""; 220 | } 221 | 222 | protected canTestRegex(config: any): config is InputRegexTestable { 223 | return this.isRegexTestable && 224 | config.pattern != null && 225 | (config.pattern instanceof RegExp || typeof config.pattern === "string"); 226 | } 227 | 228 | protected canTestLength(config: any): config is InputLengthTestable { 229 | return (this.isNumeric || this.isLengthTestable) && 230 | (config.min != null || config.max != null); 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/inputs/input-checkbox.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Injector, ChangeDetectionStrategy } from "@angular/core"; 2 | import { InputBase } from "./input-base"; 3 | import { InlineConfig } from "../types/inline-configs"; 4 | 5 | @Component({ 6 | selector: "inline-editor-checkbox", 7 | styleUrls: ["./input.component.css"], 8 | template: ``, 11 | changeDetection: ChangeDetectionStrategy.OnPush, 12 | }) 13 | export class InputCheckboxComponent extends InputBase { 14 | 15 | constructor(injector: Injector) { 16 | super(injector); 17 | } 18 | 19 | public config: InlineConfig; 20 | 21 | public showText() { 22 | return this.value ? this.config.checkedText : this.config.uncheckedText; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/inputs/input-date.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Injector, ChangeDetectionStrategy } from "@angular/core"; 2 | import { InputBase } from "./input-base"; 3 | import { InlineConfig } from "../types/inline-configs"; 4 | 5 | @Component({ 6 | selector: "inline-editor-date", 7 | styleUrls: ["./input.component.css"], 8 | template: ``, 13 | changeDetection: ChangeDetectionStrategy.OnPush, 14 | }) 15 | export class InputDateComponent extends InputBase implements OnInit { 16 | 17 | constructor(injector: Injector) { 18 | super(injector); 19 | this.isRegexTestable = true; 20 | } 21 | 22 | public config: InlineConfig; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/inputs/input-datetime.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Injector, ChangeDetectionStrategy } from "@angular/core"; 2 | import { InputBase } from "./input-base"; 3 | import { InlineConfig } from "../types/inline-configs"; 4 | 5 | @Component({ 6 | selector: "inline-editor-datetime", 7 | styleUrls: ["./input.component.css"], 8 | template: ``, 13 | changeDetection: ChangeDetectionStrategy.OnPush, 14 | }) 15 | export class InputDatetimeComponent extends InputBase implements OnInit { 16 | 17 | constructor(injector: Injector) { 18 | super(injector); 19 | this.isRegexTestable = true; 20 | } 21 | public config: InlineConfig; 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/inputs/input-number.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Injector, ChangeDetectionStrategy } from "@angular/core"; 2 | import { InputBase } from "./input-base"; 3 | import { InlineNumberConfig } from "../types/inline-configs"; 4 | 5 | @Component({ 6 | selector: "inline-editor-number", 7 | styleUrls: ["./input.component.css"], 8 | template: ``, 13 | changeDetection: ChangeDetectionStrategy.OnPush, 14 | }) 15 | export class InputNumberComponent extends InputBase implements OnInit { 16 | 17 | constructor(injector: Injector) { 18 | super(injector); 19 | this.isNumeric = true; 20 | } 21 | 22 | public config: InlineNumberConfig; 23 | } 24 | -------------------------------------------------------------------------------- /src/inputs/input-password.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Injector, ChangeDetectionStrategy } from "@angular/core"; 2 | import { InputBase } from "./input-base"; 3 | import { InlineTextConfig } from "../types/inline-configs"; 4 | 5 | @Component({ 6 | selector: "inline-editor-password", 7 | styleUrls: ["./input.component.css"], 8 | template: ``, 13 | changeDetection: ChangeDetectionStrategy.OnPush, 14 | }) 15 | export class InputPasswordComponent extends InputBase implements OnInit { 16 | 17 | constructor(injector: Injector) { 18 | super(injector); 19 | this.isRegexTestable = true; 20 | this.isLengthTestable = true; 21 | } 22 | 23 | public config: InlineTextConfig; 24 | 25 | public showText(): string { 26 | const isEmpty = this.state.isEmpty(); 27 | const value = String(this.state.getState().value); 28 | return isEmpty ? 29 | this.config.empty : 30 | "*".repeat(value.length); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/inputs/input-range.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Injector, ChangeDetectionStrategy } from "@angular/core"; 2 | import { InputBase } from "./input-base"; 3 | import { InlineConfig } from "../types/inline-configs"; 4 | 5 | @Component({ 6 | selector: "inline-editor-range", 7 | styleUrls: ["./input.component.css"], 8 | template: ``, 13 | changeDetection: ChangeDetectionStrategy.OnPush, 14 | }) 15 | export class InputRangeComponent extends InputBase implements OnInit { 16 | 17 | constructor(injector: Injector) { 18 | super(injector); 19 | this.isNumeric = true; 20 | } 21 | 22 | public config: InlineConfig; 23 | } 24 | -------------------------------------------------------------------------------- /src/inputs/input-select.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Injector, ChangeDetectionStrategy } from "@angular/core"; 2 | import { InputBase } from "./input-base"; 3 | import { InlineSelectConfig, InlineConfig } from "../types/inline-configs"; 4 | import { SelectOptionWithChildren, SelectOption } from "../types/select-options.interface"; 5 | import { OnUpdateConfig } from "../types/lifecycles.interface"; 6 | 7 | @Component({ 8 | selector: "inline-editor-select", 9 | styleUrls: ["./input.component.css"], 10 | template: ` 11 | 25 | `, 26 | changeDetection: ChangeDetectionStrategy.OnPush, 27 | }) 28 | export class InputSelectComponent extends InputBase implements OnInit, OnUpdateConfig { 29 | 30 | constructor(injector: Injector) { 31 | super(injector); 32 | 33 | this.subscriptions.onUpdateConfigSubcription.unsubscribe(); 34 | this.subscriptions.onUpdateConfigSubcription = this.service.events.internal.onUpdateConfig.subscribe( 35 | (config: InlineConfig) => this.onUpdateConfig(config), 36 | ); 37 | } 38 | 39 | public config: InlineSelectConfig; 40 | 41 | onUpdateConfig(config: InlineSelectConfig) { 42 | super.onUpdateConfig(config); 43 | 44 | const { options } = this.config; 45 | this.config.options = options instanceof Array ? 46 | { 47 | data: options, 48 | value: "value", 49 | text: "text", 50 | } : options; 51 | 52 | this.config = { ...this.config }; 53 | } 54 | 55 | public showText(): string { 56 | const { text: keyOfText, value: keyOfValue, data: options } = this.config.options; 57 | const currentValue = this.state.getState().value; 58 | const optionSelected = this.getOptionSelected(currentValue, keyOfValue, options); 59 | 60 | return optionSelected ? optionSelected[keyOfText] : this.config.empty; 61 | } 62 | 63 | protected getOptionSelected( 64 | currentValue: any, 65 | keyOfValue: string, 66 | options: (SelectOption | SelectOptionWithChildren)[], 67 | ): SelectOption | undefined { 68 | 69 | let optionSelected: SelectOption | undefined; 70 | 71 | for (const option of options) { 72 | if (this.isAnOptionWithChildren(option)) { 73 | optionSelected = this.getOptionSelected(currentValue, keyOfValue, option.children!); 74 | } else { 75 | const typeOfValue = typeof option[keyOfValue]; 76 | 77 | /** 78 | * If the type is a number, the equal must be soft to match, ex: 79 | * 1 == "1" -> true 80 | * 81 | * If the type is other, the equiality can be hard, because, 82 | * when the currentValue is a string that contains "[object Object]" 83 | * if you test it against an object, it will be true, ex: 84 | * "[object Object]" == {} -> true 85 | * "[object Object]" === {} -> false 86 | * 87 | */ 88 | if (typeOfValue === "string" || typeOfValue === "number") { 89 | // tslint:disable-next-line:triple-equals 90 | optionSelected = option[keyOfValue] == currentValue ? option : undefined; 91 | } else { 92 | optionSelected = option[keyOfValue] === currentValue ? option : undefined; 93 | } 94 | } 95 | 96 | if (optionSelected) { 97 | break; 98 | } 99 | } 100 | 101 | return optionSelected; 102 | } 103 | 104 | protected isEmpty(value: any): boolean { 105 | const { value: keyOfValue, data: options } = this.config.options; 106 | return this.getOptionSelected(value, keyOfValue, options) == null; 107 | } 108 | 109 | protected isAnOptionWithChildren(options: SelectOptionWithChildren): options is SelectOptionWithChildren { 110 | return options.children != null && options.children instanceof Array; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/inputs/input-text.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Injector, ChangeDetectionStrategy } from "@angular/core"; 2 | import { InputBase } from "./input-base"; 3 | import { InlineTextConfig } from "../types/inline-configs"; 4 | 5 | @Component({ 6 | selector: "inline-editor-text", 7 | styleUrls: ["./input.component.css"], 8 | template: ``, 13 | changeDetection: ChangeDetectionStrategy.OnPush, 14 | }) 15 | export class InputTextComponent extends InputBase implements OnInit { 16 | 17 | constructor(injector: Injector) { 18 | super(injector); 19 | this.isRegexTestable = true; 20 | this.isLengthTestable = true; 21 | } 22 | 23 | public config: InlineTextConfig; 24 | } 25 | -------------------------------------------------------------------------------- /src/inputs/input-textarea.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Injector, ChangeDetectionStrategy } from "@angular/core"; 2 | import { InputBase } from "./input-base"; 3 | import { InlineTextareaConfig } from "../types/inline-configs"; 4 | 5 | @Component({ 6 | selector: "inline-editor-textarea", 7 | styleUrls: ["./input.component.css"], 8 | template: ``, 13 | changeDetection: ChangeDetectionStrategy.OnPush, 14 | }) 15 | export class InputTextareaComponent extends InputBase implements OnInit { 16 | 17 | constructor(injector: Injector) { 18 | super(injector); 19 | this.isRegexTestable = true; 20 | this.isLengthTestable = true; 21 | } 22 | 23 | public config: InlineTextareaConfig; 24 | } 25 | -------------------------------------------------------------------------------- /src/inputs/input-time.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Injector, ChangeDetectionStrategy } from "@angular/core"; 2 | import { InputBase } from "./input-base"; 3 | import { InlineConfig } from "../types/inline-configs"; 4 | 5 | @Component({ 6 | selector: "inline-editor-time", 7 | styleUrls: ["./input.component.css"], 8 | template: ``, 13 | changeDetection: ChangeDetectionStrategy.OnPush, 14 | }) 15 | export class InputTimeComponent extends InputBase implements OnInit { 16 | 17 | constructor(injector: Injector) { 18 | super(injector); 19 | this.isRegexTestable = true; 20 | } 21 | 22 | public config: InlineConfig; 23 | } 24 | -------------------------------------------------------------------------------- /src/inputs/input.component.css: -------------------------------------------------------------------------------- 1 | a { 2 | text-decoration: none; 3 | color: #428bca; 4 | border-bottom: dashed 1px #428bca; 5 | cursor: pointer; 6 | line-height: 2; 7 | margin-right: 5px; 8 | margin-left: 5px; 9 | } 10 | 11 | 12 | /* editable-empty */ 13 | 14 | .editable-empty, 15 | .editable-empty:hover, 16 | .editable-empty:focus, 17 | a.editable-empty, 18 | a.editable-empty:hover, 19 | a.editable-empty:focus { 20 | font-style: italic; 21 | color: #DD1144; 22 | text-decoration: none; 23 | } 24 | 25 | .inlineEditForm { 26 | display: inline-block; 27 | white-space: nowrap; 28 | margin: 0; 29 | } 30 | 31 | #inlineEditWrapper { 32 | display: inline-block; 33 | } 34 | 35 | .inlineEditForm input, 36 | select { 37 | width: auto; 38 | display: inline; 39 | } 40 | 41 | .editInvalid { 42 | color: #a94442; 43 | margin-bottom: 0; 44 | } 45 | 46 | .error { 47 | border-color: #a94442; 48 | } 49 | 50 | [hidden] { 51 | display: none; 52 | } 53 | -------------------------------------------------------------------------------- /src/themes/bootstrap.css: -------------------------------------------------------------------------------- 1 | a.c-inline-editor { 2 | text-decoration: none; 3 | color: #428bca; 4 | border-bottom: dashed 1px #428bca; 5 | cursor: pointer; 6 | line-height: 2; 7 | margin-right: 5px; 8 | margin-left: 5px; 9 | } 10 | .c-inline-editor.editable-empty, 11 | .c-inline-editor.editable-empty:hover, 12 | .c-inline-editor.editable-empty:focus, 13 | .c-inline-editor.a.editable-empty, 14 | .c-inline-editor.a.editable-empty:hover, 15 | .c-inline-editor.a.editable-empty:focus { 16 | font-style: italic; 17 | color: #DD1144; 18 | text-decoration: none; 19 | } 20 | 21 | .c-inline-editor.inlineEditForm { 22 | display: inline-block; 23 | white-space: nowrap; 24 | margin: 0; 25 | } 26 | 27 | #inlineEditWrapper { 28 | display: inline-block; 29 | } 30 | 31 | .c-inline-editor.inlineEditForm input, 32 | .c-inline-editor.select { 33 | width: auto; 34 | display: inline; 35 | } 36 | 37 | .c-inline-editor.inline-editor-button-group { 38 | display: inline-block; 39 | } 40 | 41 | .c-inline-editor.editInvalid { 42 | color: #a94442; 43 | margin-bottom: 0; 44 | } 45 | 46 | .c-inline-editor.error { 47 | border-color: #a94442; 48 | } 49 | 50 | [hidden].c-inline-editor { 51 | display: none; 52 | } 53 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "module": "es2015", 6 | "target": "es2015", 7 | "stripInternal": true, 8 | "outDir": "../build", 9 | "rootDir": ".", 10 | "lib": [ 11 | "es2015", 12 | "es2017.object", 13 | "dom" 14 | ], 15 | "strictNullChecks": true, 16 | "skipLibCheck": true, 17 | "types": [] 18 | }, 19 | "angularCompilerOptions": { 20 | "annotateForClosureCompiler": true, 21 | "strictMetadataEmit": true, 22 | "skipTemplateCodegen": true, 23 | "flatModuleOutFile": "ngx-inline-editor.js", 24 | "flatModuleId": "@qontu/ngx-inline-editor" 25 | }, 26 | "files": [ 27 | "./inline-editor.module.ts" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /src/types/edit-options.interface.ts: -------------------------------------------------------------------------------- 1 | export interface EditOptions { 2 | editing?: boolean; 3 | focus?: boolean; 4 | select?: boolean; 5 | event?: Event; 6 | } 7 | -------------------------------------------------------------------------------- /src/types/inline-configs.ts: -------------------------------------------------------------------------------- 1 | import { InputLengthTestable, InputRegexTestable, InputSelectable } from "./testable-inputs.interface"; 2 | import { InputType } from "./input-type.type"; 3 | import { InputBaseConfig, TextareaConfig, CheckboxConfig } from "./input-configs"; 4 | 5 | export interface InlineBaseConfig extends InputBaseConfig { 6 | type: InputType; 7 | name?: string; 8 | size: number; 9 | placeholder: string; 10 | empty: string; 11 | hideButtons?: boolean; 12 | required?: boolean; 13 | disabled?: boolean; 14 | onlyValue?: boolean; 15 | } 16 | 17 | export interface InlineTextConfig extends InlineBaseConfig, InputRegexTestable { } 18 | 19 | export interface InlineSelectConfig extends InlineBaseConfig, InputSelectable { } 20 | 21 | export interface InlineNumberConfig extends InlineBaseConfig, InputLengthTestable { } 22 | 23 | export interface InlineTextareaConfig extends InlineBaseConfig, TextareaConfig, InputRegexTestable { 24 | rows: number; 25 | cols: number; 26 | } 27 | 28 | export interface InlineCheckboxConfig extends InlineBaseConfig, CheckboxConfig { 29 | checkedText: string; 30 | uncheckedText: string; 31 | } 32 | 33 | export interface InlineConfig extends InlineTextConfig, InlineTextareaConfig, 34 | InlineSelectConfig, InlineNumberConfig, InlineCheckboxConfig { 35 | hideButtons: boolean; 36 | required: boolean; 37 | disabled: boolean; 38 | saveOnBlur: boolean; 39 | saveOnChange: boolean; 40 | saveOnEnter: boolean; 41 | cancelOnEscape: boolean; 42 | editOnClick: boolean; 43 | onlyValue: boolean; 44 | } 45 | -------------------------------------------------------------------------------- /src/types/inline-editor-error.interface.ts: -------------------------------------------------------------------------------- 1 | export interface InlineEditorError { 2 | type: string; 3 | message: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/types/inline-editor-events.class.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "@angular/core"; 2 | import { InlineEditorError } from "./inline-editor-error.interface"; 3 | import { InlineConfig } from "../types/inline-configs"; 4 | import { InlineEditorState, InlineEditorStateOptions } from "./inline-editor-state.class"; 5 | import { InlineEditorComponent } from "../inline-editor.component"; 6 | 7 | export interface Events { 8 | internal: InternalEvents; 9 | external: ExternalEvents; 10 | } 11 | 12 | export class InternalEvents { 13 | public onUpdateStateOfParent: EventEmitter = new EventEmitter(); 14 | public onUpdateStateOfChild: EventEmitter = new EventEmitter(); 15 | public onChange: EventEmitter = new EventEmitter(); 16 | public onFocus: EventEmitter = new EventEmitter(); 17 | public onBlur: EventEmitter = new EventEmitter(); 18 | public onKeyPress: EventEmitter = new EventEmitter(); 19 | public onEnter: EventEmitter = new EventEmitter(); 20 | public onEscape: EventEmitter = new EventEmitter(); 21 | public onSave: EventEmitter = new EventEmitter(); 22 | public onEdit: EventEmitter = new EventEmitter(); 23 | public onCancel: EventEmitter = new EventEmitter(); 24 | public onClick: EventEmitter = new EventEmitter(); 25 | public onUpdateConfig: EventEmitter = new EventEmitter(); 26 | } 27 | 28 | export class ExternalEvents { 29 | public onChange: EventEmitter = new EventEmitter(); 30 | public onSave: EventEmitter = new EventEmitter(); 31 | public onKeyPress: EventEmitter = new EventEmitter(); 32 | public onFocus: EventEmitter = new EventEmitter(); 33 | public onBlur: EventEmitter = new EventEmitter(); 34 | public onEnter: EventEmitter = new EventEmitter(); 35 | public onEscape: EventEmitter = new EventEmitter(); 36 | public onEdit: EventEmitter = new EventEmitter(); 37 | public onCancel: EventEmitter = new EventEmitter(); 38 | public onClick: EventEmitter = new EventEmitter(); 39 | public onError: EventEmitter = new EventEmitter(); 40 | } 41 | 42 | export interface InternalEvent { 43 | event?: Event; 44 | state: InlineEditorState; 45 | } 46 | 47 | export interface ExternalEvent { 48 | event?: Event; 49 | state: InlineEditorStateOptions; 50 | } 51 | 52 | export interface InlineEditorEvent extends ExternalEvent { 53 | instance: InlineEditorComponent; 54 | } 55 | -------------------------------------------------------------------------------- /src/types/inline-editor-state.class.ts: -------------------------------------------------------------------------------- 1 | export interface InlineEditorStateOptions { 2 | value: any; 3 | editing?: boolean; 4 | disabled?: boolean; 5 | empty?: boolean; 6 | } 7 | 8 | export class InlineEditorState { 9 | 10 | constructor({ 11 | value, 12 | disabled = false, 13 | editing = false, 14 | empty = false, 15 | }: InlineEditorStateOptions = { value: "" }) { 16 | this.value = value; 17 | this.disabled = disabled; 18 | this.editing = editing; 19 | this.empty = empty; 20 | } 21 | 22 | private empty: boolean; 23 | private value: any; 24 | private disabled: boolean; 25 | private editing: boolean; 26 | 27 | public newState(state: InlineEditorState | InlineEditorStateOptions) { 28 | return new InlineEditorState(state instanceof InlineEditorState ? 29 | state.getState() : state); 30 | } 31 | 32 | public getState(): InlineEditorStateOptions { 33 | const { value, editing, disabled, empty } = this; 34 | 35 | return { 36 | value, 37 | editing, 38 | disabled, 39 | empty, 40 | }; 41 | } 42 | 43 | public clone(): InlineEditorState { 44 | return this.newState(this); 45 | } 46 | 47 | public isEmpty() { 48 | return this.empty; 49 | } 50 | 51 | public isEditing() { 52 | return this.editing; 53 | } 54 | 55 | public isDisabled() { 56 | return this.disabled; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/types/input-configs.ts: -------------------------------------------------------------------------------- 1 | import { InputType } from "./input-type.type"; 2 | import { SelectOptions } from "./select-options.interface"; 3 | 4 | 5 | export interface InlineActionsOnEvents { 6 | saveOnChange?: boolean; 7 | saveOnBlur?: boolean; 8 | saveOnEnter?: boolean; 9 | cancelOnEscape?: boolean; 10 | editOnClick?: boolean; 11 | } 12 | 13 | export interface RegexTestable { 14 | pattern?: string | RegExp; 15 | } 16 | 17 | export interface Selectable { 18 | options?: SelectOptions; 19 | } 20 | 21 | export interface LengthTestable { 22 | min?: number; 23 | max?: number; 24 | } 25 | 26 | export interface InputBaseConfig extends InlineActionsOnEvents { 27 | type: InputType; 28 | name?: string; 29 | size?: number; 30 | placeholder?: string; 31 | empty?: string; 32 | hideButtons?: boolean; 33 | required?: boolean; 34 | disabled?: boolean; 35 | onlyValue?: boolean; 36 | } 37 | 38 | export interface InputTextConfig extends InputBaseConfig, RegexTestable { 39 | type: "text"; 40 | } 41 | 42 | export interface InputSelectConfig extends InputBaseConfig, Selectable { 43 | type: "select"; 44 | } 45 | 46 | export interface InputNumberConfig extends InputBaseConfig, LengthTestable { 47 | type: "number"; 48 | } 49 | 50 | export interface TextareaConfig { 51 | rows?: number; 52 | cols?: number; 53 | } 54 | export interface InputTextareaConfig extends InputBaseConfig, TextareaConfig, RegexTestable { 55 | type: "textarea"; 56 | } 57 | 58 | export interface CheckboxConfig { 59 | checkedText?: string; 60 | uncheckedText?: string; 61 | } 62 | 63 | export interface InputCheckboxConfig extends InputBaseConfig, CheckboxConfig { 64 | type: "checkbox"; 65 | } 66 | 67 | export interface InputConfig extends 68 | InputBaseConfig, 69 | LengthTestable, 70 | LengthTestable, 71 | RegexTestable, 72 | TextareaConfig, 73 | CheckboxConfig { } 74 | -------------------------------------------------------------------------------- /src/types/input-type.type.ts: -------------------------------------------------------------------------------- 1 | export type InputType = "text" | "number" | "select" | "range" 2 | | "textarea" | "date" | "time" | "datetime" | "checkbox"; 3 | -------------------------------------------------------------------------------- /src/types/lifecycles.interface.ts: -------------------------------------------------------------------------------- 1 | import { InlineBaseConfig } from "../types/inline-configs"; 2 | 3 | export interface OnUpdateConfig { 4 | onUpdateConfig(config: InlineBaseConfig); 5 | } 6 | -------------------------------------------------------------------------------- /src/types/select-options.interface.ts: -------------------------------------------------------------------------------- 1 | export interface SelectOption { [key: string]: any; } 2 | export interface SelectOptionWithChildren extends SelectOption { 3 | children?: SelectOption[]; 4 | } 5 | 6 | export interface SelectOptions { 7 | text: string; 8 | value: string; 9 | data: SelectOptionWithChildren[]; 10 | } 11 | -------------------------------------------------------------------------------- /src/types/testable-inputs.interface.ts: -------------------------------------------------------------------------------- 1 | import { SelectOptions } from "./select-options.interface"; 2 | import { RegexTestable, Selectable, LengthTestable } from "./input-configs"; 3 | 4 | export interface InputRegexTestable extends RegexTestable { 5 | pattern: string | RegExp; 6 | } 7 | 8 | export interface InputSelectable extends Selectable { 9 | options: SelectOptions; 10 | } 11 | 12 | export interface InputLengthTestable extends LengthTestable { 13 | min: number; 14 | max: number; 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "emitDecoratorMetadata": true, 4 | "experimentalDecorators": true, 5 | "pretty": true, 6 | "allowUnreachableCode": false, 7 | "allowUnusedLabels": false, 8 | "noUnusedParameters": true, 9 | "noUnusedLocals": true, 10 | "noImplicitReturns": true, 11 | "noImplicitUseStrict": false, 12 | "noFallthroughCasesInSwitch": true, 13 | "moduleResolution": "node" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "target": "es2016", 5 | "module": "commonjs", 6 | "declaration": false, 7 | "removeComments": true, 8 | "noUnusedLocals": false, 9 | "noLib": false, 10 | "lib": [ 11 | "es2016" 12 | ], 13 | "sourceMap": true, 14 | "typeRoots": [ 15 | "./node_modules/@types", 16 | "./node_modules" 17 | ], 18 | "types": [ 19 | "node" 20 | ] 21 | }, 22 | "exclude": [ 23 | "node_modules", 24 | "dist", 25 | "src" 26 | ], 27 | "compileOnSave": false 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "moduleResolution": "node", 5 | "sourceMap": true, 6 | "target": "es5", 7 | "typeRoots": [ 8 | "node_modules/@types" 9 | ], 10 | "lib": [ 11 | "es6", 12 | "dom", 13 | "es2017.object" 14 | ], 15 | "skipLibCheck": true, 16 | "types": [ 17 | "jasmine", 18 | "node" 19 | ] 20 | }, 21 | "include": [ 22 | "base.spec.ts", 23 | "src/**/*" 24 | ], 25 | "exclude": [ 26 | "node_modules/**/*" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "callable-types": true, 7 | "class-name": true, 8 | "comment-format": [ 9 | true, 10 | "check-space" 11 | ], 12 | "curly": true, 13 | "eofline": true, 14 | "forin": true, 15 | "import-blacklist": [true, "rxjs"], 16 | "import-spacing": true, 17 | "indent": [ 18 | true, 19 | "spaces" 20 | ], 21 | "interface-over-type-literal": true, 22 | "label-position": true, 23 | "max-line-length": [ 24 | true, 25 | 140 26 | ], 27 | "member-access": false, 28 | "member-ordering": [ 29 | true, 30 | { 31 | "order": [ 32 | "public-constructor", 33 | "protected-constructor", 34 | "private-constructor", 35 | "public-instance-field", 36 | "protected-instance-field", 37 | "private-instance-field", 38 | "public-static-field", 39 | "protected-static-field", 40 | "public-instance-method", 41 | "protected-instance-method", 42 | "private-instance-method", 43 | "public-static-method", 44 | "private-static-method", 45 | "protected-static-method" 46 | ] 47 | } 48 | ], 49 | "no-arg": true, 50 | "no-bitwise": true, 51 | "templates-use-public": true, 52 | "invoke-injectable": true, 53 | "use-input-property-decorator": true, 54 | "use-output-property-decorator": true, 55 | "use-host-property-decorator": true, 56 | "no-input-rename": true, 57 | "no-output-rename": true, 58 | "radix": true, 59 | "no-console": [ 60 | true, 61 | "debug", 62 | "info", 63 | "time", 64 | "timeEnd", 65 | "trace" 66 | ], 67 | "no-construct": true, 68 | "no-debugger": true, 69 | "no-duplicate-variable": true, 70 | "no-empty": false, 71 | "no-empty-interface": true, 72 | "no-eval": true, 73 | "no-inferrable-types": [true, "ignore-params"], 74 | "no-shadowed-variable": true, 75 | "no-string-literal": false, 76 | "no-string-throw": true, 77 | "no-switch-case-fall-through": true, 78 | "no-trailing-whitespace": true, 79 | "no-unused-expression": true, 80 | "no-var-keyword": true, 81 | "object-literal-sort-keys": false, 82 | "object-literal-shorthand": true, 83 | "object-literal-key-quotes": [ 84 | true, "as-needed" 85 | ], 86 | "typeof-compare": true, 87 | "typedef-whitespace": [ 88 | true, 89 | { 90 | "call-signature": "nospace", 91 | "index-signature": "nospace", 92 | "parameter": "nospace", 93 | "property-declaration": "nospace", 94 | "variable-declaration": "nospace" 95 | } 96 | ], 97 | "trailing-comma": [true, { 98 | "multiline": "always", 99 | "singleline": "never" 100 | }], 101 | "use-life-cycle-interface": true, 102 | "use-pipe-transform-interface": true, 103 | "directive-selector": [true, "attribute", "inline-editor", "camelCase"], 104 | "component-selector": [true, "element", "inline-editor", "kebab-case"], 105 | "prefer-const": true, 106 | "one-line": [ 107 | true, 108 | "check-open-brace", 109 | "check-catch", 110 | "check-else", 111 | "check-whitespace" 112 | ], 113 | "quotemark": [ 114 | true, 115 | "double" 116 | ], 117 | "semicolon": [ 118 | true, 119 | "always" 120 | ], 121 | "triple-equals": [ 122 | true, 123 | "allow-null-check" 124 | ], 125 | "variable-name": false, 126 | "whitespace": [ 127 | true, 128 | "check-branch", 129 | "check-decl", 130 | "check-operator", 131 | "check-separator", 132 | "check-type" 133 | ] 134 | } 135 | } 136 | --------------------------------------------------------------------------------