├── .editorconfig ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc.json ├── .travis.yml ├── LICENSE ├── README.md ├── angular.json ├── config └── environments │ ├── environment.prod.ts │ └── environment.ts ├── example └── src │ ├── app │ ├── app.component.html │ ├── app.component.ts │ └── app.module.ts │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ └── tsconfig.json ├── karma.conf.js ├── package.json ├── scripts └── gh-pages.sh ├── src ├── index.ts ├── input-trim.directive.ts └── input-trim.module.ts ├── test ├── form.reactive.spec.ts ├── form.td.spec.ts └── tests.ts ├── tsconfig.json ├── tsconfig.spec.json ├── tslint.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | *ngfactory.ts 3 | *ngsummary.json 4 | *.log 5 | .idea 6 | dist 7 | /coverage/ 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Node generated files 2 | node_modules 3 | 4 | # OS generated files 5 | Thumbs.db 6 | .DS_Store 7 | .DS_Store? 8 | ._* 9 | .Spotlight-V100 10 | .Trashes 11 | ehthumbs.db 12 | 13 | # Ignored files 14 | *.ts 15 | !*.d.ts 16 | 17 | # Logs 18 | logs 19 | *.log 20 | npm-debug.log* 21 | yarn-debug.log* 22 | 23 | # Runtime data 24 | pids 25 | *.pid 26 | *.seed 27 | *.pid.lock 28 | 29 | # Project files 30 | config 31 | example 32 | src 33 | yarn.lock 34 | .angular-cli.json 35 | .gitignore 36 | .travis.yml 37 | tslint.json 38 | tsconfig.json -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.json 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs" : false, 3 | "printWidth" : 120, 4 | "tabWidth" : 2, 5 | "singleQuote" : false, 6 | "semi" : true, 7 | "bracketSpacing": true 8 | } 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | notifications: 4 | email: false 5 | node_js: 6 | - "10" 7 | install: 8 | - yarn install 9 | cache: 10 | bundler: true 11 | directories: 12 | - node_modules 13 | script: 14 | - yarn run test 15 | - yarn run build 16 | branches: 17 | only: 18 | - master 19 | before_deploy: 20 | - yarn run build 21 | deploy: 22 | - provider: npm 23 | email: al.eine@protonmail.com 24 | skip_cleanup: true 25 | local-dir: dist 26 | api_key: 27 | secure: ph6RuYePLI5a8ogoZvp2rNZgBJxfSfnxaKEHpOTSrjD3IZcSoj1DYerRXa65CxKnnWQ0v1UxLBtK7S5GNG6JdGg9iTtFsVlV4w8zSD1JlbxsAsRoY79vca+knSL39BjrZ72XOtgNeqUo3EodjSZVaUVxUFB8NrGJMpiFOJZBUaVlfsFKOBSGbmQ+NLFQutsUmBJv/llhDjVfa1c78kW7n9qgQTl2J2bcwr614eGwYjDZ24DyMJrkqOc64UQPEFC4DKQ/osN6r8FNmpuC6oABbkuJXgoYCRq1DhrYHerXGZzwKfwZWUSX6UQQt0QzKyNY6awr6YUQs5Bh0r+b6lwdGjXKYaOxseARBcPNOFlvpbqWUwNXL0OauC6MU636O7B/fZGhYLEdxgdAlij6UGk1K7nmOIwC+pnjrpJfDB0P7YDiGcf9Eh2rnWCLWfTZ6WMcuTc8pjqbmHsDCdCK1geWN2GMxLi5G9EkoR97DWLQZn30eSeCv1AbzEWgw1uSkrG6P7xovIHMssTjtBPgZy5kAYJhc/IF+JY8xe6mHYu2du4QQDkpvE8Rf8HtQNyVVNzWn6ECZK6z24zEExAUZKeik6oYSGP2/RFOG9eBjYaRAOM0osceA6/gRgkU5ITe5yYHzVNxNSHb8n/0rrdEKq9HnQ8DA6KbsiNq0gGLlehVRUg= 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Alexander N. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ng2-trim-directive 2 | [![Build Status](https://travis-ci.org/anein/angular2-trim-directive.svg?branch=master)](https://travis-ci.org/anein/angular2-trim-directive) 3 | [![npm](https://img.shields.io/npm/v/ng2-trim-directive.svg)](https://www.npmjs.com/package/ng2-trim-directive) 4 | 5 | >The directive trims whitespaces from the end of an input text value. 6 | 7 | ## Demo 8 | 9 | Play with the directive here [https://anein.github.io/angular2-trim-directive/](https://anein.github.io/angular2-trim-directive/). 10 | 11 | ## Usage 12 | 13 | 1. Install `ng2-trim-directive`. 14 | 15 | ```bash 16 | npm i ng2-trim-directive 17 | ``` 18 | 19 | or using **Yarn** 20 | 21 | ```bash 22 | yarn add ng2-trim-directive 23 | ``` 24 | 25 | 2. Import `InputTrimModule` to your Angular module. 26 | 27 | ```typescript 28 | import { InputTrimModule } from 'ng2-trim-directive'; 29 | @NgModule({ 30 | imports: [ 31 | ... 32 | InputTrimModule, 33 | ... 34 | ], 35 | ... 36 | ``` 37 | 38 | 3. Add the "trim" attribute to a text input or textarea element. 39 | ```html 40 | 41 | 42 | ``` 43 | 44 | or with an option: trim value only on the blur event. 45 | 46 | ```html 47 | 48 | 49 | ``` 50 | **note**: if you use the directive with 185 | 186 | 187 |
188 | 189 |
190 | 191 |
192 |
193 |     

Reactive Form values {{ exampleForm.value | json }}

194 |

Form status {{ exampleForm.status | json }}

195 |
196 | 197 | 198 | -------------------------------------------------------------------------------- /example/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | Component, 4 | Inject, 5 | OnInit, 6 | ViewEncapsulation 7 | } from "@angular/core"; 8 | import { FormBuilder, FormControl, FormGroup, NgForm, Validators } from "@angular/forms"; 9 | 10 | @Component( { 11 | selector : "in-app", 12 | templateUrl : "./app.component.html", 13 | styles : ["pre { background-color: whitesmoke;} small {color: #AAA}"], 14 | encapsulation : ViewEncapsulation.None, 15 | changeDetection: ChangeDetectionStrategy.OnPush 16 | } ) 17 | export class AppComponent implements OnInit { 18 | trigger: FormControl; 19 | 20 | exampleForm: FormGroup; 21 | 22 | exampleFormInfo = {}; 23 | 24 | constructor( @Inject( FormBuilder ) private fb: FormBuilder ) { 25 | this.trigger = this.fb.control( "input" ); 26 | 27 | this.exampleForm = this.fb.group( { 28 | text : ["Booobbb "], 29 | text_undefined: [undefined], 30 | text_disabled : { value: "I'm disabled", disabled: true }, 31 | text_autofill : [undefined], 32 | email : ["", [Validators.email]], 33 | number : ["", []], 34 | url : ["", []], 35 | textarea : ["", [Validators.maxLength( 10 )]] 36 | } ); 37 | 38 | this.updateStates(); 39 | } 40 | 41 | ngOnInit() { 42 | this.exampleForm.controls.text_undefined.setValue( undefined ); 43 | } 44 | 45 | /** 46 | * ngFor Helper 47 | */ 48 | getKeys( obj: Object ): Array { 49 | this.updateStates(); 50 | 51 | return Object.keys( obj ); 52 | } 53 | 54 | /** 55 | * Can be simplified 56 | */ 57 | updateStates() { 58 | const fields = ["status", "dirty", "touched"]; 59 | 60 | for (let item in this.exampleForm.controls) { 61 | if (!this.exampleForm.controls[item]) continue; 62 | this.exampleFormInfo[item] = {}; 63 | for (let field of fields) { 64 | this.exampleFormInfo[item][field] = this.exampleForm.controls[item][field]; 65 | } 66 | } 67 | } 68 | 69 | onTemplateFormSubmit( form: NgForm ): void { 70 | console.dir( form.control.getRawValue() ); 71 | } 72 | 73 | onSubmit(): void { 74 | console.log( this.exampleForm.getRawValue() ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /example/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { AppComponent } from './app.component'; 4 | import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; 5 | import { InputTrimModule } from '../../../src/'; 6 | 7 | @NgModule( { 8 | imports : [ 9 | BrowserModule, 10 | FormsModule, 11 | InputTrimModule, 12 | ReactiveFormsModule, 13 | ], 14 | declarations: [ 15 | AppComponent, 16 | ], 17 | bootstrap : [AppComponent] 18 | } ) 19 | export class AppModule { 20 | 21 | } 22 | 23 | -------------------------------------------------------------------------------- /example/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | Example InputTrimDirective 8 | 9 | 10 | 11 | 12 | 22 | 23 | 48 |
49 | ng2-trim-directive 50 | 51 |
52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /example/src/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | import { AppModule } from './app/app.module'; 3 | 4 | platformBrowserDynamic().bootstrapModule( AppModule ); 5 | -------------------------------------------------------------------------------- /example/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * IE9, IE10 and IE11 requires all of the following polyfills. 23 | */ 24 | // import 'core-js/es6/symbol'; 25 | // import 'core-js/es6/object'; 26 | // import 'core-js/es6/function'; 27 | // import 'core-js/es6/parse-int'; 28 | // import 'core-js/es6/parse-float'; 29 | // import 'core-js/es6/number'; 30 | // import 'core-js/es6/math'; 31 | // import 'core-js/es6/string'; 32 | // import 'core-js/es6/date'; 33 | // import 'core-js/es6/array'; 34 | // import 'core-js/es6/regexp'; 35 | // import 'core-js/es6/map'; 36 | // import 'core-js/es6/weak-map'; 37 | // import 'core-js/es6/set'; 38 | 39 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 40 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 41 | 42 | /** 43 | * Evergreen browsers require these. 44 | */ 45 | import "core-js/es6/reflect"; 46 | 47 | import "core-js/es6/symbol"; 48 | import "core-js/es6/object"; 49 | import "core-js/es6/string"; 50 | import "core-js/es6/array"; 51 | /*************************************************************************************************** 52 | * Zone JS is required by Angular itself. 53 | */ 54 | import "zone.js/dist/zone"; // Included with Angular CLI. 55 | 56 | /*************************************************************************************************** 57 | * APPLICATION IMPORTS 58 | */ 59 | 60 | /** 61 | * Date, currency, decimal and percent pipes. 62 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 63 | */ 64 | // import 'intl'; // Run `npm install --save intl`. 65 | /** 66 | * Need to import at least one locale-data with intl. 67 | */ 68 | // import 'intl/locale-data/jsonp/en'; 69 | -------------------------------------------------------------------------------- /example/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions" : { 3 | "module" : "commonjs", 4 | "target" : "es5", 5 | "noImplicitAny" : false, 6 | "sourceMap" : false, 7 | "experimentalDecorators": true, 8 | "sourceRoot" : ".", 9 | "lib" : [ 10 | "es5", 11 | "es2017", 12 | "dom" 13 | ], 14 | "baseUrl" : "/", 15 | "paths" : { 16 | "@": [ 17 | "node_modules/" 18 | ] 19 | } 20 | }, 21 | "exclude" : [ 22 | "node_modules" 23 | ], 24 | "formatCodeOptions": { 25 | "indentSize" : 2, 26 | "tabSize" : 2, 27 | "insertSpaceAfterCommaDelimiter" : true, 28 | "insertSpaceAfterSemicolonInForStatements" : true, 29 | "insertSpaceBeforeAndAfterBinaryOperators" : true, 30 | "insertSpaceAfterKeywordsInControlFlowStatements" : true, 31 | "insertSpaceAfterFunctionKeywordForAnonymousFunctions" : true, 32 | "insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": true 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | const process = require('process'); 2 | process.env.CHROME_BIN = require('puppeteer').executablePath(); 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath : '', 7 | frameworks : ['jasmine', '@angular-devkit/build-angular'], 8 | plugins : [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client : { 16 | clearContext: false 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, 'coverage'), reports : ['html', 'lcovonly'], 20 | fixWebpackSourcePaths: true 21 | }, 22 | 23 | reporters : ['progress', 'kjhtml'], 24 | port : 9876, 25 | colors : true, 26 | logLevel : config.LOG_INFO, 27 | autoWatch : false, 28 | browsers : ['ChromeHeadlessNoSandbox'], 29 | singleRun : true, 30 | customLaunchers: { 31 | ChromeHeadlessNoSandbox: { 32 | base: 'ChromeHeadless', 33 | flags: ['--no-sandbox'] 34 | } 35 | }, 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "ng2-trim-directive", 3 | "version" : "2.3.4", 4 | "author" : "Alexander Ein (@alexanderein)", 5 | "license" : "MIT", 6 | "repository" : { 7 | "type": "git", 8 | "url" : "git+ssh://github.com/anein/angular2-trim-directive.git" 9 | }, 10 | "bugs" : { 11 | "url": "https://github.com/anein/angular2-trim-directive/issues" 12 | }, 13 | "keywords" : [ 14 | "angular", 15 | "ng2", 16 | "directive", 17 | "typescript", 18 | "trim", 19 | "javascript" 20 | ], 21 | "main" : "dist/index.js", 22 | "types" : "dist/index.d.ts", 23 | "files" : [ 24 | "dist", 25 | "LICENSE" 26 | ], 27 | "scripts" : { 28 | "eg:build" : "ng build", 29 | "eg:serve" : "ng serve", 30 | "build" : "ngc --project ./tsconfig.json", 31 | "test" : "ng test", 32 | "tslint-check": "tslint-config-prettier-check ./tslint.json", 33 | "tslint" : "tslint -c ./tslint.json -p ./tsconfig.json", 34 | "lint" : "ng lint", 35 | "format" : "pretty-quick", 36 | "prepublish" : "npm run build" 37 | }, 38 | "husky" : { 39 | "hooks": { 40 | "pre-commit": "yarn run lint", 41 | "pre-push" : "yarn run lint && yarn run test && yarn run format && yarn run build" 42 | } 43 | }, 44 | "devDependencies": { 45 | "@angular-devkit/build-angular" : "~0.10.6", 46 | "@angular/cli" : "~7.0.6", 47 | "@angular/common" : "~7.0.4", 48 | "@angular/compiler" : "~7.0.4", 49 | "@angular/compiler-cli" : "~7.0.4", 50 | "@angular/core" : "~7.0.4", 51 | "@angular/forms" : "~7.0.4", 52 | "@angular/platform-browser" : "~7.0.4", 53 | "@angular/platform-browser-dynamic": "~7.0.4", 54 | "@ngtools/webpack" : "~7.0.6", 55 | "@types/jasmine" : "^2.8.3", 56 | "@types/node" : "^10.12.9", 57 | "codelyzer" : "^4.5.0", 58 | "husky" : "^1.1.4", 59 | "jasmine-core" : "^3.3.0", 60 | "karma" : "^3.1.1", 61 | "karma-chrome-launcher" : "^2.2.0", 62 | "karma-cli" : "^1.0.1", 63 | "karma-coverage-istanbul-reporter" : "^2.0.4", 64 | "karma-jasmine" : "^2.0.1", 65 | "karma-jasmine-html-reporter" : "^1.4.0", 66 | "prettier" : "^1.15.2", 67 | "pretty-quick" : "^1.8.0", 68 | "puppeteer" : "^1.13.0", 69 | "rxjs" : "^6.3.3", 70 | "travis" : "^0.1.1", 71 | "travis-cli" : "^1.0.9", 72 | "tslint" : "^5.11.0", 73 | "tslint-config-prettier" : "^1.16.0", 74 | "typescript" : "~3.1.6", 75 | "webpack" : "^4.26.0", 76 | "zone.js" : "^0.8.26" 77 | }, 78 | "dependencies" : {} 79 | } 80 | -------------------------------------------------------------------------------- /scripts/gh-pages.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o errexit 3 | 4 | yarn run 'eg:build' 5 | 6 | # pre-deploy 7 | cd example/dist 8 | 9 | # config 10 | git config --global user.email "nobody@nobody.org" 11 | git config --global user.name "Travis CI Bot" 12 | 13 | # 14 | git init 15 | git add . 16 | git commit -m "Deploy to Github Pages" 17 | git push --force --quiet "https://${GITHUB_TOKEN}@$github.com/${GITHUB_REPO}.git" master:gh-pages > /dev/null 2>&1 18 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './input-trim.directive'; 2 | export * from './input-trim.module'; 3 | -------------------------------------------------------------------------------- /src/input-trim.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Directive, 3 | ElementRef, 4 | HostListener, 5 | Inject, 6 | Input, 7 | Optional, 8 | Renderer2 9 | } from "@angular/core"; 10 | import { 11 | COMPOSITION_BUFFER_MODE, 12 | ControlValueAccessor, 13 | NG_VALUE_ACCESSOR 14 | } from "@angular/forms"; 15 | 16 | @Directive({ 17 | selector: "input[trim], textarea[trim]", 18 | providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: InputTrimDirective, multi: true }] 19 | }) 20 | export class InputTrimDirective implements ControlValueAccessor { 21 | 22 | private get _type(): string { 23 | return this._sourceElementRef.nativeElement.type || "text"; 24 | } 25 | 26 | // Get a value of the trim attribute if it was set. 27 | @Input() trim: string; 28 | 29 | /** 30 | * Keep the value of input element in a cache. 31 | * 32 | * @type {string} 33 | * @private 34 | */ 35 | private _value: string; 36 | 37 | // Source services to modify elements. 38 | private _sourceRenderer: Renderer2; 39 | private _sourceElementRef: ElementRef; 40 | 41 | /** 42 | * Updates the value on the blur event. 43 | */ 44 | @HostListener("blur", ["$event.type", "$event.target.value"]) 45 | onBlur(event: string, value: string): void { 46 | this.updateValue(event, value.trim()); 47 | this.onTouched(); 48 | } 49 | 50 | /** 51 | * Updates the value on the input event. 52 | */ 53 | @HostListener("input", ["$event.type", "$event.target.value"]) 54 | onInput(event: string, value: string): void { 55 | this.updateValue(event, value); 56 | } 57 | 58 | onChange = (_: any) => {}; 59 | 60 | onTouched = () => {}; 61 | 62 | constructor( 63 | @Inject(Renderer2) renderer: Renderer2, 64 | @Inject(ElementRef) elementRef: ElementRef, 65 | @Optional() @Inject(COMPOSITION_BUFFER_MODE) compositionMode: boolean 66 | ) { 67 | this._sourceRenderer = renderer; 68 | this._sourceElementRef = elementRef; 69 | } 70 | 71 | registerOnChange(fn: (_: any) => void): void { this.onChange = fn; } 72 | 73 | registerOnTouched(fn: () => void): void { this.onTouched = fn; } 74 | 75 | /** 76 | * Writes a new value to the element based on the type of input element. 77 | * 78 | * @param {any} value - new value 79 | */ 80 | public writeValue(value: any): void { 81 | // 82 | // The Template Driven Form doesn't automatically convert undefined values to null. We will do, 83 | // keeping an empty string as string because the condition `'' || null` returns null what 84 | // could change the initial state of a model. 85 | // The Reactive Form does it automatically during initialization. 86 | // 87 | // SEE: https://github.com/anein/angular2-trim-directive/issues/18 88 | // 89 | this._value = value === "" ? "" : value || null; 90 | 91 | this._sourceRenderer.setProperty(this._sourceElementRef.nativeElement, "value", this._value); 92 | 93 | // a dirty trick (or magic) goes here: 94 | // it updates the element value if `setProperty` doesn't set a new value for some reason. 95 | // 96 | // SEE: https://github.com/anein/angular2-trim-directive/issues/9 97 | // 98 | if (this._type !== "text") { 99 | this._sourceRenderer.setAttribute(this._sourceElementRef.nativeElement, "value", this._value); 100 | } 101 | } 102 | 103 | setDisabledState(isDisabled: boolean): void { 104 | this._sourceRenderer.setProperty(this._sourceElementRef.nativeElement, 'disabled', isDisabled); 105 | } 106 | 107 | /** 108 | * Writes the cursor position in safari 109 | * 110 | * @param cursorPosition - the cursor current position 111 | * @param hasTypedSymbol 112 | */ 113 | private setCursorPointer(cursorPosition: any, hasTypedSymbol: boolean): void { 114 | // move the cursor to the stored position (Safari usually moves the cursor to the end) 115 | // setSelectionRange method apply only to inputs of types text, search, URL, tel and password 116 | if (hasTypedSymbol && ["text", "search", "url", "tel", "password"].indexOf(this._type) >= 0) { 117 | // Ok, for some reason in the tests the type changed is not being catch and because of that 118 | // this line is executed and causes an error of DOMException, it pass the text without problem 119 | // But it should be a better way to validate that type change 120 | this._sourceElementRef.nativeElement.setSelectionRange(cursorPosition, cursorPosition); 121 | } 122 | } 123 | 124 | /** 125 | * Trims an input value, and sets it to the model and element. 126 | * 127 | * @param {string} value - input value 128 | * @param {string} event - input event 129 | */ 130 | private updateValue(event: string, value: string): void { 131 | // check if the user has set an optional attribute, and Trimmmm!!! Uhahahaha! 132 | value = this.trim !== "" && event !== this.trim ? value : value.trim(); 133 | 134 | const previous = this._value; 135 | 136 | // store the cursor position 137 | const cursorPosition = this._sourceElementRef.nativeElement.selectionStart; 138 | 139 | // write value to the element. 140 | this.writeValue(value); 141 | 142 | // Update the model only on getting new value, and prevent firing 143 | // the `dirty` state when click on empty fields. 144 | // 145 | // SEE: 146 | // https://github.com/anein/angular2-trim-directive/issues/17 147 | // https://github.com/anein/angular2-trim-directive/issues/35 148 | // https://github.com/anein/angular2-trim-directive/issues/39 149 | // 150 | if ((this._value || previous) && this._value.trim() !== previous) { 151 | this.onChange(this._value); 152 | } 153 | 154 | // check that non-null value is being changed 155 | const hasTypedSymbol = value && previous && value !== previous; 156 | 157 | // write the cursor position 158 | this.setCursorPointer(cursorPosition, hasTypedSymbol); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/input-trim.module.ts: -------------------------------------------------------------------------------- 1 | import { InputTrimDirective } from './input-trim.directive'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | @NgModule( { 5 | imports : [], 6 | exports : [InputTrimDirective], 7 | declarations: [InputTrimDirective], 8 | providers : [], 9 | } ) 10 | export class InputTrimModule { 11 | } 12 | -------------------------------------------------------------------------------- /test/form.reactive.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; 3 | import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; 4 | import { InputTrimModule } from '../src'; 5 | import { By } from '@angular/platform-browser'; 6 | 7 | @Component( { 8 | template: ` 9 |

ReactiveFormComponent

10 |
11 | 12 | 13 |
14 | ` 15 | } ) 16 | class ReactiveFormComponent { 17 | 18 | readonly myGroup = new FormGroup( { 19 | example : new FormControl( '' ), 20 | example2 : new FormControl( '' ), 21 | nullState : new FormControl( null ), 22 | undefinedState: new FormControl( undefined ) 23 | } ); 24 | 25 | constructor() { 26 | } 27 | 28 | } 29 | 30 | describe( 'Tests: Reactive Form', () => { 31 | 32 | let componentInstance: ReactiveFormComponent; 33 | 34 | let fixture: ComponentFixture; 35 | 36 | let inputElement: HTMLInputElement; 37 | 38 | const value: string = 'Bob'; 39 | const valueWithWhitespaces = 'Bob '; 40 | 41 | beforeEach( () => { 42 | TestBed.configureTestingModule( { 43 | imports : [ReactiveFormsModule, InputTrimModule], 44 | declarations: [ReactiveFormComponent] 45 | } ); 46 | } ); 47 | 48 | describe( 'Initialization', () => { 49 | 50 | beforeEach( () => createComponentHelper() ); 51 | 52 | it( 'should create component', () => expect( componentInstance ).toBeDefined() ); 53 | 54 | it( 'should have title "ReactiveFormComponent" ', () => { 55 | 56 | const el = fixture.debugElement.query( By.css( 'h2' ) ).nativeElement; 57 | 58 | expect( el.textContent ).toContain( 'ReactiveFormComponent' ); 59 | 60 | } ); 61 | 62 | it( 'should have the empty input field and model', () => { 63 | 64 | expect( inputElement.value ).toBe( '' ); 65 | expect( componentInstance.myGroup ).toBeDefined(); 66 | expect( componentInstance.myGroup.value.example ).toBeDefined(); 67 | expect( componentInstance.myGroup.value.example2 ).toBe( '' ); 68 | expect( componentInstance.myGroup.value.nullState ).toBe( null ); 69 | expect( componentInstance.myGroup.value.undefinedState ).toBe( null ); 70 | 71 | } ); 72 | 73 | it( 'should write value to the element when the form control value has been set', () => { 74 | 75 | componentInstance.myGroup.controls.example.setValue( value ); 76 | expect( inputElement.value ).toBe( value ); 77 | 78 | componentInstance.myGroup.controls.example.setValue( valueWithWhitespaces ); 79 | expect( inputElement.value ).toBe( valueWithWhitespaces ); 80 | 81 | } ); 82 | 83 | it( 'should write null to the element and update the model', () => { 84 | 85 | // componentInstance.myGroup.controls.example.setValue( value ); 86 | inputElement.value = componentInstance.myGroup.value.nullState; 87 | 88 | inputElement.dispatchEvent( new Event( 'input' ) ); 89 | 90 | expect( inputElement.value ).toBe( '', "Input element has wrong value" ); 91 | 92 | expect( componentInstance.myGroup.value.example ).toBe( '', "THe model is not updated" ); 93 | 94 | } ); 95 | 96 | it( 'should write "undefined" to the element and set string `undefined` to the model', () => { 97 | 98 | inputElement.value = componentInstance.myGroup.value.undefinedState; 99 | 100 | expect( typeof inputElement.value ).toBe( "string" ); 101 | 102 | inputElement.dispatchEvent( new Event( 'input' ) ); 103 | 104 | expect( componentInstance.myGroup.value.example ).toBe( '', "THe model is not updated" ); 105 | 106 | } ); 107 | 108 | it( 'should reflect the disabled state', () => { 109 | 110 | componentInstance.myGroup.get('example').disable(); 111 | 112 | expect( inputElement.disabled ).toBe( true ); 113 | 114 | componentInstance.myGroup.get('example').enable(); 115 | 116 | expect( inputElement.disabled ).toBe( false ); 117 | 118 | } ); 119 | 120 | } ); 121 | 122 | describe( 'Directive without additional options.', () => { 123 | 124 | beforeEach( () => createComponentHelper() ); 125 | 126 | it( 'should trim whitespaces from the end on the INPUT event', () => { 127 | 128 | componentInstance.myGroup.controls.example.setValue( valueWithWhitespaces ); 129 | 130 | expect( componentInstance.myGroup.controls.example.pristine ).toBeTruthy(); 131 | expect( componentInstance.myGroup.pristine ).toBeTruthy(); 132 | 133 | inputElement.dispatchEvent( new Event( 'input' ) ); 134 | 135 | fixture.detectChanges(); 136 | 137 | expect( inputElement.value ).toBe( value, 'Input value is not trimmed' ); 138 | expect( componentInstance.myGroup.value.example ).toBe( value, 'Model is not trimmed' ); 139 | expect( componentInstance.myGroup.value.example ).toBe( inputElement.value ); 140 | expect( componentInstance.myGroup.value.example2 ).toBe( '' ); 141 | expect( componentInstance.myGroup.value.nullState ).toBe( null ); 142 | expect( componentInstance.myGroup.value.undefinedState ).toBe( null ); 143 | 144 | expect( componentInstance.myGroup.controls.example.pristine ).toBeFalsy(); 145 | expect( componentInstance.myGroup.pristine ).toBeFalsy(); 146 | 147 | } ); 148 | 149 | it( 'should keep pristine from the end on the BLUR event w/o changes', () => { 150 | 151 | componentInstance.myGroup.controls.example.setValue( value ); 152 | 153 | expect( componentInstance.myGroup.controls.example.touched ).toBeFalsy(); 154 | expect( componentInstance.myGroup.touched ).toBeFalsy(); 155 | expect( componentInstance.myGroup.controls.example.pristine ).toBeTruthy(); 156 | expect( componentInstance.myGroup.pristine ).toBeTruthy(); 157 | 158 | inputElement.dispatchEvent( new Event( 'blur' ) ); 159 | 160 | fixture.detectChanges(); 161 | 162 | expect( inputElement.value ).toBe( value ); 163 | expect( componentInstance.myGroup.value.example ).toBe( value ); 164 | expect( componentInstance.myGroup.value.example ).toBe( inputElement.value ); 165 | 166 | expect( componentInstance.myGroup.controls.example.touched ).toBeTruthy(); 167 | expect( componentInstance.myGroup.touched ).toBeTruthy(); 168 | expect( componentInstance.myGroup.controls.example.dirty ).toBeFalsy( "The same values shouldn't be marked as dirty" ); 169 | 170 | } ); 171 | 172 | it( 'should trim whitespaces from the end on the BLUR event', () => { 173 | 174 | componentInstance.myGroup.controls.example.setValue( valueWithWhitespaces ); 175 | 176 | expect( componentInstance.myGroup.controls.example.touched ).toBeFalsy(); 177 | expect( componentInstance.myGroup.touched ).toBeFalsy(); 178 | expect( componentInstance.myGroup.controls.example.pristine ).toBeTruthy(); 179 | expect( componentInstance.myGroup.pristine ).toBeTruthy(); 180 | 181 | inputElement.dispatchEvent( new Event( 'blur' ) ); 182 | 183 | fixture.detectChanges(); 184 | 185 | expect( inputElement.value ).toBe( value, 'Input value is not trimmed' ); 186 | expect( componentInstance.myGroup.value.example ).toBe( value, 'Model is not trimmed' ); 187 | expect( componentInstance.myGroup.value.example ).toBe( inputElement.value ); 188 | 189 | expect( componentInstance.myGroup.controls.example.touched ).toBeTruthy(); 190 | expect( componentInstance.myGroup.touched ).toBeTruthy(); 191 | expect( componentInstance.myGroup.controls.example.pristine ).toBeFalsy(); 192 | expect( componentInstance.myGroup.pristine ).toBeFalsy(); 193 | 194 | } ); 195 | 196 | it( 'should trim whitespaces of value of `url` input', () => { 197 | 198 | componentInstance.myGroup.controls.example.setValue( valueWithWhitespaces ); 199 | inputElement.type = 'url'; 200 | 201 | inputElement.dispatchEvent( new Event( 'input' ) ); 202 | 203 | fixture.detectChanges(); 204 | 205 | expect( inputElement.value ).toBe( value, 'Input value is not trimmed' ); 206 | expect( componentInstance.myGroup.value.example ).toBe( value, 'Model is not trimmed' ); 207 | expect( componentInstance.myGroup.value.example ).toBe( inputElement.value ); 208 | 209 | } ); 210 | 211 | it( 'should trim whitespaces of value of `email` input', () => { 212 | const emailWithWhitespaces = 'joe@gmail.com '; 213 | const emailWithoutWhitespaces = emailWithWhitespaces.trim(); 214 | 215 | componentInstance.myGroup.controls.example.setValue( emailWithWhitespaces ); 216 | inputElement.type = 'email'; 217 | 218 | inputElement.dispatchEvent( new Event( 'input' ) ); 219 | 220 | fixture.detectChanges(); 221 | 222 | expect( inputElement.value ).toBe( emailWithoutWhitespaces, 'Input value is not trimmed' ); 223 | expect( componentInstance.myGroup.value.example ).toBe( emailWithoutWhitespaces, 'Model is not trimmed' ); 224 | expect( componentInstance.myGroup.value.example ).toBe( inputElement.value ); 225 | 226 | } ); 227 | 228 | it( 'should trim a value w/ whitespaces on two-way binding.', fakeAsync( () => { 229 | 230 | componentInstance.myGroup.controls.example.setValue( valueWithWhitespaces ); 231 | 232 | fixture.detectChanges(); 233 | tick(); 234 | 235 | expect( componentInstance.myGroup.value.example ) 236 | .toBe( inputElement.value, 'Value of model and input is the same' ); 237 | 238 | inputElement.dispatchEvent( new Event( 'input' ) ); 239 | 240 | expect( inputElement.value ).toBe( value, 'Input value is not trimmed' ); 241 | expect( componentInstance.myGroup.value.example ).toBe( value, 'Model is not trimmed' ); 242 | 243 | } ) ); 244 | 245 | } ); 246 | 247 | describe( 'Directive with the blur option', () => { 248 | 249 | const template = `

ReactiveFormComponent

250 |
251 | 252 | 253 | 254 |
`; 255 | 256 | beforeEach( () => { 257 | TestBed.overrideTemplate( ReactiveFormComponent, template ); 258 | createComponentHelper(); 259 | } ); 260 | 261 | it( 'should not trim whitespaces from the end on the INPUT event ', () => { 262 | 263 | componentInstance.myGroup.controls.example.setValue( valueWithWhitespaces ); 264 | 265 | inputElement.dispatchEvent( new Event( 'input' ) ); 266 | 267 | fixture.detectChanges(); 268 | 269 | expect( inputElement.value ).not.toBe( value, 'Input value is trimmed' ); 270 | // tslint:disable-next-line: max-line-length 271 | expect( componentInstance.myGroup.value.example ).toBe( valueWithWhitespaces, 'Model is trimmed' ); 272 | 273 | expect( componentInstance.myGroup.value.example ).toBe( inputElement.value ); 274 | 275 | } ); 276 | 277 | it( 'should keep pristine from the end on the BLUR event w/o changes', () => { 278 | 279 | componentInstance.myGroup.controls.example.setValue( value ); 280 | 281 | expect( componentInstance.myGroup.controls.example.touched ).toBeFalsy(); 282 | expect( componentInstance.myGroup.touched ).toBeFalsy(); 283 | expect( componentInstance.myGroup.controls.example.pristine ).toBeTruthy(); 284 | expect( componentInstance.myGroup.pristine ).toBeTruthy(); 285 | 286 | inputElement.dispatchEvent( new Event( 'blur' ) ); 287 | 288 | fixture.detectChanges(); 289 | 290 | expect( inputElement.value ).toBe( value ); 291 | expect( componentInstance.myGroup.value.example ).toBe( value ); 292 | expect( componentInstance.myGroup.value.example ).toBe( inputElement.value ); 293 | 294 | expect( componentInstance.myGroup.controls.example.touched ).toBeTruthy(); 295 | expect( componentInstance.myGroup.touched ).toBeTruthy(); 296 | expect( componentInstance.myGroup.controls.example.pristine ).toBeTruthy(); 297 | expect( componentInstance.myGroup.pristine ).toBeTruthy(); 298 | 299 | } ); 300 | 301 | it( 'should trim whitespaces from the end on the BLUR event', () => { 302 | 303 | componentInstance.myGroup.controls.example.setValue( valueWithWhitespaces ); 304 | 305 | expect( componentInstance.myGroup.controls.example.touched ).toBeFalsy(); 306 | expect( componentInstance.myGroup.touched ).toBeFalsy(); 307 | expect( componentInstance.myGroup.controls.example.pristine ).toBeTruthy(); 308 | expect( componentInstance.myGroup.pristine ).toBeTruthy(); 309 | 310 | inputElement.dispatchEvent( new Event( 'blur' ) ); 311 | 312 | fixture.detectChanges(); 313 | 314 | expect( inputElement.value ).toBe( value, 'Input value is not trimmed' ); 315 | expect( componentInstance.myGroup.value.example ).toBe( value, 'Model is not trimmed' ); 316 | expect( componentInstance.myGroup.value.example ).toBe( inputElement.value ); 317 | 318 | expect( componentInstance.myGroup.controls.example.touched ).toBeTruthy(); 319 | expect( componentInstance.myGroup.touched ).toBeTruthy(); 320 | expect( componentInstance.myGroup.controls.example.pristine ).toBeFalsy(); 321 | expect( componentInstance.myGroup.pristine ).toBeFalsy(); 322 | 323 | } ); 324 | 325 | it( 'should trim whitespaces from the end of Example2 on the INPUT event', () => { 326 | 327 | const inputElement2 = fixture.debugElement 328 | .query( By.css( 'input[name="example2"]' ) ).nativeElement; 329 | 330 | componentInstance.myGroup.controls.example2.setValue( valueWithWhitespaces ); 331 | 332 | inputElement2.dispatchEvent( new Event( 'input' ) ); 333 | 334 | fixture.detectChanges(); 335 | 336 | expect( inputElement2.value ).toBe( value, 'Example2:Input is trimmed' ); 337 | expect( componentInstance.myGroup.value.example2 ).toBe( value, 'Example2:Model is trimmed' ); 338 | 339 | expect( inputElement.value ).not.toBe( value, 'Input value is trimmed' ); 340 | expect( componentInstance.myGroup.value.example ).toBe( inputElement.value ); 341 | 342 | } ); 343 | 344 | } ); 345 | 346 | function createComponentHelper(): void { 347 | 348 | fixture = TestBed.createComponent( ReactiveFormComponent ); 349 | 350 | // get the instance 351 | componentInstance = fixture.componentInstance; 352 | 353 | // get the element 354 | inputElement = fixture.debugElement.query( By.css( 'input' ) ).nativeElement; 355 | 356 | fixture.detectChanges(); 357 | 358 | } 359 | 360 | } ); 361 | -------------------------------------------------------------------------------- /test/form.td.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing"; 2 | import { Component, ViewChild } from "@angular/core"; 3 | import { InputTrimModule } from "../src"; 4 | import { By } from "@angular/platform-browser"; 5 | import { FormsModule, NgModel } from "@angular/forms"; 6 | 7 | /** 8 | * Defines the artificial test component 9 | */ 10 | @Component({ 11 | template: ` 12 |

TemplateDrivenFormComponent

13 | 14 | 15 | Example Not Touched 16 | Example Touched Example Pristine 17 | Example Dirty 18 | ` 19 | }) 20 | class TemplateDrivenFormComponent { 21 | @ViewChild("exampleModel") exampleModel: NgModel; 22 | 23 | example: string | undefined; 24 | } 25 | 26 | describe("Tests: Template-Driven Form", () => { 27 | let componentInstance: TemplateDrivenFormComponent; 28 | 29 | let fixture: ComponentFixture; 30 | 31 | let inputElement: HTMLInputElement; 32 | 33 | let value: string; 34 | let valueWithWhitespaces; 35 | 36 | beforeEach(() => { 37 | 38 | value = "Bob"; 39 | valueWithWhitespaces = "Bob "; 40 | 41 | // create a component fixture 42 | TestBed.configureTestingModule({ 43 | imports: [FormsModule, InputTrimModule], 44 | declarations: [TemplateDrivenFormComponent] 45 | }); 46 | }); 47 | 48 | describe("Initialization", () => { 49 | beforeEach(() => createComponentHelper()); 50 | 51 | it("should create component", () => expect(componentInstance).toBeDefined()); 52 | 53 | it('should have title "TemplateDrivenFormComponent" ', () => { 54 | const el = fixture.debugElement.query(By.css("h2")).nativeElement; 55 | 56 | expect(el.textContent).toContain("TemplateDrivenFormComponent"); 57 | }); 58 | 59 | it("should have the empty input fields and models", () => { 60 | expect(inputElement.value).toBe(""); 61 | expect(componentInstance.example).toBe(undefined); 62 | }); 63 | 64 | it("should write value to the element when the NgModel's form control value is set", () => { 65 | componentInstance.exampleModel.control.setValue(value); 66 | expect(inputElement.value).toBe(value); 67 | 68 | componentInstance.exampleModel.control.setValue(valueWithWhitespaces); 69 | expect(inputElement.value).toBe(valueWithWhitespaces); 70 | }); 71 | 72 | /** 73 | * SEE: 74 | * https://github.com/anein/angular2-trim-directive/issues/39 75 | */ 76 | it( "should allow to set empty string in a field", () => { 77 | 78 | let expectedValue = value; 79 | 80 | inputElement.value = value; 81 | 82 | inputElement.dispatchEvent( new Event( "input" ) ); 83 | 84 | fixture.detectChanges(); 85 | 86 | expect( componentInstance.example ).toBe( expectedValue, "The model is not updated" ); 87 | 88 | value = ""; 89 | expectedValue = ""; 90 | 91 | inputElement.value = value; 92 | 93 | inputElement.dispatchEvent( new Event( "input" ) ); 94 | 95 | fixture.detectChanges(); 96 | 97 | expect( componentInstance.example ).toBe( expectedValue, "Model is not empty" ) 98 | 99 | } ); 100 | 101 | it(`should NOT change initial state of the "undefined" model if input value is empty`, () => { 102 | componentInstance.exampleModel.control.setValue(undefined); 103 | 104 | inputElement.dispatchEvent(new Event("input")); 105 | 106 | fixture.detectChanges(); 107 | 108 | expect(typeof inputElement.value).toBe("string"); 109 | expect(inputElement.value).toBe(""); 110 | 111 | expect(componentInstance.example).toBe(undefined); 112 | }); 113 | 114 | it(`should return an empty string if value is null `, () => { 115 | componentInstance.exampleModel.control.setValue(null); 116 | 117 | inputElement.dispatchEvent(new Event("input")); 118 | 119 | fixture.detectChanges(); 120 | 121 | expect(inputElement.value).toBe(""); 122 | }); 123 | }); 124 | 125 | describe("Directive without additional options.", () => { 126 | beforeEach(() => createComponentHelper()); 127 | 128 | it("should trim whitespaces on the INPUT event", () => { 129 | componentInstance.exampleModel.control.setValue(valueWithWhitespaces); 130 | 131 | expect(componentInstance.exampleModel.pristine).toBeTruthy(); 132 | 133 | inputElement.dispatchEvent(new Event("input")); 134 | 135 | // fixture.detectChanges(); 136 | 137 | expect(inputElement.value).toBe(value, "Input value is not trimmed"); 138 | expect(componentInstance.example).toBe(value, "Model is not trimmed"); 139 | expect(componentInstance.example).toBe(inputElement.value); 140 | 141 | expect(componentInstance.exampleModel.dirty).toBeTruthy(); 142 | }); 143 | 144 | it("should keep pristine from the end on the BLUR event w/o changes", () => { 145 | componentInstance.exampleModel.control.setValue(value); 146 | 147 | fixture.detectChanges(); 148 | 149 | expect(componentInstance.exampleModel.touched).toBeFalsy("Example Touched!"); 150 | expect(componentInstance.exampleModel.pristine).toBeTruthy("Example Pristined"); 151 | 152 | inputElement.dispatchEvent(new Event("blur")); 153 | 154 | fixture.detectChanges(); 155 | 156 | expect(inputElement.value).toBe(value); 157 | expect(componentInstance.example).toBe(value); 158 | expect(componentInstance.example).toBe(inputElement.value); 159 | 160 | expect(componentInstance.exampleModel.touched).toBeTruthy("Example NOT Touched!"); 161 | expect(componentInstance.exampleModel.pristine).toBeTruthy("Example Pristined"); 162 | }); 163 | 164 | it("should trim whitespaces from the end on the BLUR event", () => { 165 | componentInstance.exampleModel.control.setValue(valueWithWhitespaces); 166 | 167 | fixture.detectChanges(); 168 | expect(componentInstance.exampleModel.touched).toBeFalsy("Example Touched"); 169 | expect(componentInstance.exampleModel.dirty).toBeFalsy("Example model changed!"); 170 | expect(componentInstance.exampleModel.pristine).toBeTruthy("'Example NOT pristine"); 171 | 172 | inputElement.dispatchEvent(new Event("blur")); 173 | 174 | fixture.detectChanges(); 175 | 176 | expect(inputElement.value).toBe(value, "Input value is not trimmed"); 177 | expect(componentInstance.example).toBe(value, "Model is not trimmed"); 178 | expect(componentInstance.example).toBe(inputElement.value); 179 | 180 | expect(componentInstance.exampleModel.touched).toBeTruthy("Example NOT Touched"); 181 | expect(componentInstance.exampleModel.dirty).toBeTruthy("Example model NOT changed!"); 182 | }); 183 | 184 | it("should trim whitespaces of value of `email` input", () => { 185 | componentInstance.exampleModel.control.setValue(valueWithWhitespaces); 186 | inputElement.type = "email"; 187 | 188 | inputElement.dispatchEvent(new Event("input")); 189 | 190 | fixture.detectChanges(); 191 | 192 | expect(inputElement.value).toBe(value, "Input value is not trimmed"); 193 | expect(componentInstance.example).toBe(value, "Model is not trimmed"); 194 | expect(componentInstance.example).toBe(inputElement.value); 195 | }); 196 | 197 | it("should trim a value w/ whitespaces on two-way binding.", fakeAsync(() => { 198 | componentInstance.example = valueWithWhitespaces; 199 | 200 | fixture.detectChanges(); 201 | tick(); 202 | 203 | expect(componentInstance.example).toBe(inputElement.value, "Value of model and input is the same"); 204 | 205 | inputElement.dispatchEvent(new Event("input")); 206 | 207 | expect(inputElement.value).toBe(value, "Input value is not trimmed"); 208 | expect(componentInstance.example).toBe(value, "Model is not trimmed"); 209 | })); 210 | }); 211 | 212 | describe("Directive with the blur option", () => { 213 | const template = ` 214 | 215 | Example Not Touched 216 | Example Touched 217 | Example Pristine 218 | Example Dirty 219 | `; 220 | 221 | beforeEach(() => { 222 | TestBed.overrideTemplate(TemplateDrivenFormComponent, template); 223 | createComponentHelper(); 224 | }); 225 | 226 | it("should not trim whitespaces from the end on the INPUT event ", () => { 227 | componentInstance.example = inputElement.value = valueWithWhitespaces; 228 | 229 | inputElement.dispatchEvent(new Event("input")); 230 | 231 | fixture.detectChanges(); 232 | 233 | expect(inputElement.value).not.toBe(value, "Input value is trimmed"); 234 | expect(componentInstance.example).toBe(valueWithWhitespaces, "Model is trimmed"); 235 | 236 | expect(componentInstance.example).toBe(inputElement.value); 237 | }); 238 | 239 | 240 | it("should trim whitespaces from the end on the blur event and update the model", () => { 241 | inputElement.value = "a"; 242 | 243 | inputElement.dispatchEvent(new Event("blur")); 244 | 245 | fixture.detectChanges(); 246 | 247 | expect(componentInstance.example).toBe("a", "The model is not updated"); 248 | }); 249 | 250 | it("should NOT change the model on the BLUR event", () => { 251 | componentInstance.exampleModel.control.setValue(value); 252 | 253 | fixture.detectChanges(); 254 | 255 | expect(componentInstance.exampleModel.touched).toBeFalsy("Example Touched"); 256 | expect(componentInstance.exampleModel.dirty).toBeFalsy("Example model CHANGED"); 257 | 258 | inputElement.dispatchEvent(new Event("blur")); 259 | 260 | fixture.detectChanges(); 261 | 262 | expect(inputElement.value).toBe(value); 263 | expect(componentInstance.example).toBe(value); 264 | expect(componentInstance.example).toBe(inputElement.value); 265 | 266 | expect(componentInstance.exampleModel.touched).toBeTruthy("Example NOT Touched"); 267 | expect(componentInstance.exampleModel.dirty).toBeFalsy("Example model CHANGED"); 268 | }); 269 | 270 | it("should trim whitespaces from the end on the BLUR event", () => { 271 | componentInstance.exampleModel.control.setValue(valueWithWhitespaces); 272 | 273 | expect(componentInstance.exampleModel.touched).toBeFalsy("Example Touched"); 274 | expect(componentInstance.exampleModel.dirty).toBeFalsy("Example model CHANGED"); 275 | 276 | inputElement.dispatchEvent(new Event("blur")); 277 | 278 | fixture.detectChanges(); 279 | 280 | expect(inputElement.value).toBe(value, "Input value is not trimmed"); 281 | expect(componentInstance.example).toBe(value, "Model is not trimmed"); 282 | expect(componentInstance.example).toBe(inputElement.value); 283 | 284 | expect(componentInstance.exampleModel.touched).toBeTruthy("Example NOT Touched"); 285 | expect(componentInstance.exampleModel.dirty).toBeTruthy("Example model NOT CHANGED"); 286 | }); 287 | }); 288 | 289 | function createComponentHelper(): void { 290 | fixture = TestBed.createComponent(TemplateDrivenFormComponent); 291 | 292 | // get the instance 293 | componentInstance = fixture.componentInstance; 294 | 295 | // get the element 296 | inputElement = fixture.debugElement.query(By.css("input")).nativeElement; 297 | 298 | fixture.detectChanges(); 299 | } 300 | }); 301 | -------------------------------------------------------------------------------- /test/tests.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | import { getTestBed } from '@angular/core/testing'; 10 | import { 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting 13 | } from '@angular/platform-browser-dynamic/testing'; 14 | 15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 16 | declare const __karma__: any; 17 | declare const require: any; 18 | 19 | // Prevent Karma from running prematurely. 20 | __karma__.loaded = function () { 21 | }; 22 | 23 | // First, initialize the Angular testing environment. 24 | getTestBed().initTestEnvironment( 25 | BrowserDynamicTestingModule, 26 | platformBrowserDynamicTesting() 27 | ); 28 | // Then we find all the tests. 29 | const context = require.context( './', true, /\.spec\.ts$/ ); 30 | // And load the modules. 31 | context.keys().map( context ); 32 | // Finally, start Karma to run the tests. 33 | __karma__.start(); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions" : { 3 | "declaration" : true, 4 | "experimentalDecorators": true, 5 | "module" : "commonjs", 6 | "noImplicitAny" : false, 7 | "rootDir" : "src", 8 | "outDir" : "dist", 9 | "sourceMap" : false, 10 | "target" : "es5", 11 | "pretty" : true, 12 | "removeComments" : true, 13 | "lib" : [ 14 | "es5", 15 | "es2017", 16 | "dom" 17 | ], 18 | "baseUrl" : "./", 19 | "paths" : { 20 | "@": [ 21 | "node_modules/" 22 | ] 23 | } 24 | }, 25 | "include" : [ 26 | "src/**/*" 27 | ], 28 | "exclude" : [ 29 | "node_modules", 30 | "**/*.spec.ts" 31 | ], 32 | "angularCompilerOptions": { 33 | "strictMetadataEmit": true 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends" : "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir" : "./out-tsc/spec", 5 | "baseUrl" : "./", 6 | "module" : "commonjs", 7 | "target" : "es5", 8 | "experimentalDecorators": true, 9 | "types" : [ 10 | "jasmine", 11 | "node" 12 | ] 13 | }, 14 | "files" : [ 15 | "test/tests.ts", 16 | "example/src/polyfills.ts" 17 | ], 18 | "include" : [ 19 | "**/*.spec.ts", 20 | "**/*.d.ts" 21 | ], 22 | "exclude" : [ 23 | "**/*.js", 24 | "node_modules", 25 | "src/" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer", 4 | "tslint-config-prettier" 5 | ], 6 | "rules" : { 7 | "member-access" : false, 8 | "member-ordering" : [ 9 | true, 10 | "public-before-private", 11 | "static-before-instance", 12 | "variables-before-functions" 13 | ], 14 | "no-any" : false, 15 | "no-inferrable-types" : false, 16 | "no-internal-module" : true, 17 | "no-var-requires" : false, 18 | "typedef" : false, 19 | "ban" : false, 20 | "curly" : false, 21 | "forin" : true, 22 | "label-position" : true, 23 | "no-arg" : true, 24 | "no-bitwise" : true, 25 | "no-conditional-assignment" : true, 26 | "no-console" : [ 27 | true, 28 | "debug", 29 | "info", 30 | "time", 31 | "timeEnd", 32 | "trace" 33 | ], 34 | "no-construct" : true, 35 | "no-debugger" : true, 36 | "no-duplicate-variable" : true, 37 | "no-empty" : false, 38 | "no-eval" : true, 39 | "no-null-keyword" : false, 40 | "no-shadowed-variable" : true, 41 | "no-string-literal" : false, 42 | "no-switch-case-fall-through" : true, 43 | "no-unused-expression" : true, 44 | "no-use-before-declare" : true, 45 | "no-var-keyword" : true, 46 | "radix" : true, 47 | "switch-default" : true, 48 | "triple-equals" : [ 49 | true, 50 | "allow-null-check" 51 | ], 52 | "no-require-imports" : false, 53 | "object-literal-sort-keys" : false, 54 | "align" : false, 55 | "class-name" : true, 56 | "comment-format" : [ 57 | true, 58 | "check-space" 59 | ], 60 | "interface-name" : false, 61 | "jsdoc-format" : true, 62 | "no-consecutive-blank-lines" : false, 63 | "variable-name" : [ 64 | true, 65 | "check-format", 66 | "allow-leading-underscore", 67 | "ban-keywords" 68 | ], 69 | "directive-selector" : [ 70 | true, 71 | "attribute", 72 | [], 73 | "camelCase" 74 | ], 75 | "component-selector" : [ 76 | true, 77 | "element", 78 | [], 79 | "kebab-case" 80 | ], 81 | "use-input-property-decorator" : true, 82 | "use-output-property-decorator" : true, 83 | "use-host-property-decorator" : true, 84 | "no-attribute-parameter-decorator": true, 85 | "no-input-rename" : true, 86 | "no-output-rename" : true, 87 | "no-forward-ref" : false, 88 | "use-life-cycle-interface" : true, 89 | "use-pipe-transform-interface" : true, 90 | "directive-class-suffix" : true 91 | } 92 | } 93 | --------------------------------------------------------------------------------