diff --git a/angular-cli.json b/.angular-cli.json similarity index 59% rename from angular-cli.json rename to .angular-cli.json index 37c8c91..3cc3d2d 100644 --- a/angular-cli.json +++ b/.angular-cli.json @@ -1,7 +1,7 @@ { + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "project": { - "version": "1.0.0-beta.20-4", - "name": "form-example" + "name": "forms-example" }, "apps": [ { @@ -13,28 +13,38 @@ ], "index": "index.html", "main": "main.ts", + "polyfills": "polyfills.ts", "test": "test.ts", - "tsconfig": "tsconfig.json", + "tsconfig": "tsconfig.app.json", + "testTsconfig": "tsconfig.spec.json", "prefix": "app", - "mobile": false, "styles": [ "styles.css" ], "scripts": [], + "environmentSource": "environments/environment.ts", "environments": { - "source": "environments/environment.ts", "dev": "environments/environment.ts", "prod": "environments/environment.prod.ts" } } ], - "addons": [], - "packages": [], "e2e": { "protractor": { "config": "./protractor.conf.js" } }, + "lint": [ + { + "project": "src/tsconfig.app.json" + }, + { + "project": "src/tsconfig.spec.json" + }, + { + "project": "e2e/tsconfig.e2e.json" + } + ], "test": { "karma": { "config": "./karma.conf.js" @@ -42,18 +52,6 @@ }, "defaults": { "styleExt": "css", - "prefixInterfaces": false, - "inline": { - "style": false, - "template": false - }, - "spec": { - "class": false, - "component": true, - "directive": true, - "module": false, - "pipe": true, - "service": true - } + "component": {} } } diff --git a/.editorconfig b/.editorconfig index 06dde11..6e87a00 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,5 +9,5 @@ insert_final_newline = true trim_trailing_whitespace = true [*.md] -max_line_length = 0 +max_line_length = off trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore index ce200cb..54bfd20 100644 --- a/.gitignore +++ b/.gitignore @@ -3,24 +3,31 @@ # compiled output /dist /tmp +/out-tsc # dependencies /node_modules -/bower_components # IDEs and editors /.idea -/.vscode .project .classpath .c9/ *.launch .settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json # misc /.sass-cache /connect.lock -/coverage/* +/coverage /libpeerconnection.log npm-debug.log testem.log @@ -30,6 +37,6 @@ testem.log /e2e/*.js /e2e/*.map -#System Files +# System Files .DS_Store Thumbs.db diff --git a/README.md b/README.md index 3677e0d..c8a61bd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # FormExample -This project was generated with [angular-cli](https://github.com/angular/angular-cli) version 1.0.0-beta.20-4. +This project was generated with [angular-cli](https://github.com/angular/angular-cli) version 1.2.0. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. diff --git a/e2e/app.e2e-spec.ts b/e2e/app.e2e-spec.ts index e3db564..60e2716 100644 --- a/e2e/app.e2e-spec.ts +++ b/e2e/app.e2e-spec.ts @@ -1,14 +1,14 @@ -import {FormExamplePage} from './app.po'; +import { FormsExamplePage } from './app.po'; -describe('form-example App', function() { - let page: FormExamplePage; +describe('forms-example App', () => { + let page: FormsExamplePage; beforeEach(() => { - page = new FormExamplePage(); + page = new FormsExamplePage(); }); - it('should display message saying app works', () => { + it('should display welcome message', () => { page.navigateTo(); - expect(page.getParagraphText()).toEqual('app works!'); + expect(page.getParagraphText()).toEqual('My form!'); }); }); diff --git a/e2e/app.po.ts b/e2e/app.po.ts index 3350967..d893e30 100644 --- a/e2e/app.po.ts +++ b/e2e/app.po.ts @@ -1,6 +1,6 @@ -import {browser, element, by} from 'protractor'; +import { browser, by, element } from 'protractor'; -export class FormExamplePage { +export class FormsExamplePage { navigateTo() { return browser.get('/'); } diff --git a/e2e/tsconfig.e2e.json b/e2e/tsconfig.e2e.json new file mode 100644 index 0000000..39b800f --- /dev/null +++ b/e2e/tsconfig.e2e.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/e2e", + "module": "commonjs", + "target": "es5", + "types": [ + "jasmine", + "jasminewd2", + "node" + ] + } +} diff --git a/karma.conf.js b/karma.conf.js index 1f2613a..4d9ab9d 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -4,35 +4,25 @@ module.exports = function (config) { config.set({ basePath: '', - frameworks: ['jasmine', 'angular-cli'], + frameworks: ['jasmine', '@angular/cli'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), - require('karma-remap-istanbul'), - require('angular-cli/plugins/karma') + require('karma-jasmine-html-reporter'), + require('karma-coverage-istanbul-reporter'), + require('@angular/cli/plugins/karma') ], - files: [ - { pattern: './src/test.ts', watched: false } - ], - preprocessors: { - './src/test.ts': ['angular-cli'] - }, - mime: { - 'text/x-typescript': ['ts','tsx'] + client:{ + clearContext: false // leave Jasmine Spec Runner output visible in browser }, - remapIstanbulReporter: { - reports: { - html: 'coverage', - lcovonly: './coverage/coverage.lcov' - } + coverageIstanbulReporter: { + reports: [ 'html', 'lcovonly' ], + fixWebpackSourcePaths: true }, angularCli: { - config: './angular-cli.json', environment: 'dev' }, - reporters: config.angularCli && config.angularCli.codeCoverage - ? ['progress', 'karma-remap-istanbul'] - : ['progress'], + reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, diff --git a/package.json b/package.json index a13da15..417cc95 100644 --- a/package.json +++ b/package.json @@ -1,47 +1,49 @@ { - "name": "form-example", + "name": "forms-example", "version": "0.0.0", "license": "MIT", - "angular-cli": {}, "scripts": { + "ng": "ng", "start": "ng serve", - "lint": "tslint \"src/**/*.ts\"", + "build": "ng build", "test": "ng test", - "pree2e": "webdriver-manager update", - "e2e": "protractor" + "lint": "ng lint", + "e2e": "ng e2e" }, "private": true, "dependencies": { - "@angular/common": "^2.1.0", - "@angular/compiler": "^2.1.0", - "@angular/core": "^2.1.0", - "@angular/forms": "^2.1.0", - "@angular/http": "^2.1.0", - "@angular/platform-browser": "^2.1.0", - "@angular/platform-browser-dynamic": "^2.1.0", - "@angular/router": "^3.1.0", + "@angular/animations": "^4.0.0", + "@angular/common": "^4.0.0", + "@angular/compiler": "^4.0.0", + "@angular/core": "^4.0.0", + "@angular/forms": "^4.0.0", + "@angular/http": "^4.0.0", + "@angular/platform-browser": "^4.0.0", + "@angular/platform-browser-dynamic": "^4.0.0", + "@angular/router": "^4.0.0", "core-js": "^2.4.1", - "rxjs": "5.0.0-beta.12", - "ts-helpers": "^1.1.1", - "zone.js": "^0.6.23" + "rxjs": "^5.1.0", + "zone.js": "^0.8.4" }, "devDependencies": { - "@angular/compiler-cli": "^2.1.0", - "@types/jasmine": "^2.2.30", - "@types/node": "^6.0.42", - "angular-cli": "1.0.0-beta.20-4", - "codelyzer": "~1.0.0-beta.3", - "jasmine-core": "2.4.1", - "jasmine-spec-reporter": "2.5.0", - "karma": "1.2.0", - "karma-chrome-launcher": "^2.0.0", - "karma-cli": "^1.0.1", - "karma-jasmine": "^1.0.2", - "karma-remap-istanbul": "^0.2.1", - "protractor": "4.0.9", - "ts-node": "1.2.1", - "tslint": "3.13.0", - "typescript": "~2.0.3", - "webdriver-manager": "10.2.5" + "@angular/cli": "1.2.0", + "@angular/compiler-cli": "^4.0.0", + "@angular/language-service": "^4.0.0", + "@types/jasmine": "~2.5.53", + "@types/jasminewd2": "~2.0.2", + "@types/node": "~6.0.60", + "codelyzer": "~3.0.1", + "jasmine-core": "~2.6.2", + "jasmine-spec-reporter": "~4.1.0", + "karma": "~1.7.0", + "karma-chrome-launcher": "~2.1.1", + "karma-cli": "~1.0.1", + "karma-coverage-istanbul-reporter": "^1.2.1", + "karma-jasmine": "~1.1.0", + "karma-jasmine-html-reporter": "^0.2.2", + "protractor": "~5.1.2", + "ts-node": "~3.0.4", + "tslint": "~5.3.2", + "typescript": "~2.3.3" } } diff --git a/protractor.conf.js b/protractor.conf.js index 169743b..7ee3b5e 100644 --- a/protractor.conf.js +++ b/protractor.conf.js @@ -1,8 +1,7 @@ // Protractor configuration file, see link for more information -// https://github.com/angular/protractor/blob/master/docs/referenceConf.js +// https://github.com/angular/protractor/blob/master/lib/config.ts -/*global jasmine */ -var SpecReporter = require('jasmine-spec-reporter'); +const { SpecReporter } = require('jasmine-spec-reporter'); exports.config = { allScriptsTimeout: 11000, @@ -20,13 +19,10 @@ exports.config = { defaultTimeoutInterval: 30000, print: function() {} }, - useAllAngular2AppRoots: true, - beforeLaunch: function() { + onPrepare() { require('ts-node').register({ - project: 'e2e' + project: 'e2e/tsconfig.e2e.json' }); - }, - onPrepare: function() { - jasmine.getEnv().addReporter(new SpecReporter()); + jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); } }; diff --git a/src/app/app.component.html b/src/app/app.component.html index 8eb33f6..4c62d14 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,11 +1,14 @@

My form!

-

Enter a hexadecimal value, like 34d7f03

+

Enter a hexadecimal value, like 34d7f03

-
+

NgModel Ex

+ + Enter a hexadecimal value, like 34d7f03 + + +

Reactive Forms Ex

+ +
+ + + + + + + + + + + +
\ No newline at end of file diff --git a/src/app/app.component.scss b/src/app/app.component.scss index 7e31d79..7bcd0af 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -18,6 +18,11 @@ button { } } +form { + overflow: hidden; + margin: 25px; +} + form > * > div { display: flex; margin: 6%; @@ -59,7 +64,7 @@ form > * > div { border: 1px solid red; } - &:not(.invalid) { + &:not(.invalid):not([disabled]) { background: url(data:image/svg+xml;base64,PHN2ZyBpZD0iM2YxOWFjZGEtYTE1ZC00Njk0LTg5NTUtZjIxMmMxZjNmMjJjIiBkYXRhLW5hbWU9IkxheWVyIDEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDY3LjUgNjcuNSI+PHRpdGxlPmNoZWNrPC90aXRsZT48cGF0aCBkPSJNMzUuNCw0NWwtOCw4TDkuNTcsMzUuMWw4LTgsOS45Miw5LjkyTDUwLDE0LjUzbDgsOFpNMzMuNzUsMEEzMy43NSwzMy43NSwwLDEsMCw2Ny41LDMzLjc1LDMzLjc1LDMzLjc1LDAsMCwwLDMzLjc1LDBaIiBmaWxsPSIjNDFkODczIi8+PC9zdmc+); background-size: 18px; background-repeat: no-repeat; diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 0b957a2..34e1e00 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,4 +1,7 @@ -import {Component, ViewEncapsulation} from '@angular/core'; +import { Component, ViewEncapsulation } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +import { validateHexadecimal } from './validators/hexadecimal-validator'; @Component({ selector: 'app-root', @@ -7,11 +10,21 @@ import {Component, ViewEncapsulation} from '@angular/core'; encapsulation: ViewEncapsulation.None, }) export class AppComponent { - public hexadecimalValue: string; + public hexadecimalValue: string = ''; public dropdownValue: string = ''; + reactiveForm: FormGroup; + + constructor(fb: FormBuilder) { + this.reactiveForm = fb.group({ + 'hexadecimalValue': [this.hexadecimalValue, [Validators.required, Validators.maxLength(10), validateHexadecimal()]], + 'dropdownValue': [this.dropdownValue, [Validators.required]] + }); + } + onSubmit(value) { alert(`Submit: ${JSON.stringify(value)}`); } + } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 2cf14a2..9b0cfb4 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,30 +1,34 @@ -import {BrowserModule} from '@angular/platform-browser'; -import {NgModule} from '@angular/core'; -import {FormsModule} from '@angular/forms'; -import {HttpModule} from '@angular/http'; - -import {AppComponent} from './app.component'; - -import {HexadecimalValueValidator} from './validators'; +import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { HttpModule } from '@angular/http'; +import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { AppComponent } from './app.component'; import * as components from './components'; - -const allComponents = Object.keys(components).map(k => components[k]); +import { HexadecimalValueValidator } from './validators'; @NgModule({ declarations: [ AppComponent, - ...allComponents, + components.FormSelectComponent, + components.FormTextComponent, + components.ValidationComponent, + HexadecimalValueValidator ], imports: [ BrowserModule, FormsModule, - HttpModule + ReactiveFormsModule, + HttpModule, + BrowserAnimationsModule ], + bootstrap: [AppComponent], exports: [ - ...allComponents, - ], - providers: [], - bootstrap: [AppComponent] + components.FormSelectComponent, + components.FormTextComponent, + components.ValidationComponent, + HexadecimalValueValidator + ] }) export class AppModule { } diff --git a/src/app/components/input.spec.ts b/src/app/components/input.spec.ts index e748cfd..08c166c 100644 --- a/src/app/components/input.spec.ts +++ b/src/app/components/input.spec.ts @@ -1,10 +1,10 @@ -import {TestBed, async, tick} from '@angular/core/testing'; -import {Component, ViewChild} from '@angular/core'; -import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import { Component, ViewChild } from '@angular/core'; +import { async, TestBed } from '@angular/core/testing'; +import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; -import {FormTextComponent} from './input'; - -import {AppModule} from '../app.module'; +import { AppModule } from '../app.module'; +import { validateHexadecimal } from '../validators'; +import { FormTextComponent } from './input'; describe('FormInput component', () => { beforeEach(done => { @@ -15,22 +15,23 @@ describe('FormInput component', () => { ReactiveFormsModule, ], declarations: [ - FormInputHarness, + FormInputHarnessNgModel, + FormInputHarnessReactive ], }); TestBed.compileComponents().then(done); }); - it('should apply an "invalid" class when validation fails and print a validation failure message', + it('should apply an "invalid" class when validation fails on ngModel and print a validation failure message', async(() => { - const fixture = TestBed.createComponent(FormInputHarness); + const fixture = TestBed.createComponent(FormInputHarnessNgModel); fixture.detectChanges(); fixture.whenStable().then(() => { const element = fixture.debugElement.nativeElement; - Object.assign(fixture.componentInstance.formInput.model.control, {_touched: true}); + Object.assign(fixture.componentInstance.formInput.model.control, { _touched: true }); fixture.detectChanges(); const input = element.querySelector('input'); @@ -41,6 +42,52 @@ describe('FormInput component', () => { expect(failureMessage.textContent.trim()).toBe('Please enter a value'); }); })); + + it('should apply an "invalid" class when validaion failed with a reactive form', + async(() => { + const fixture = TestBed.createComponent(FormInputHarnessReactive); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + const element = fixture.debugElement.nativeElement; + const form = fixture.componentInstance.form; + + form.get('hexadecimalValue').setValue('@#$'); + fixture.detectChanges(); + + const input = element.querySelector('input'); + expect(Array.prototype.indexOf.call(input.classList, 'invalid')).not.toBe(-1); + + const failureMessage = element.querySelector('.validation'); + expect(failureMessage).not.toBeNull(); + expect(failureMessage.textContent.trim()).toBe('Please enter a hexadecimal value (alphanumeric, 0-9 and A-F)'); + }); + })); + + it('should apply an "invalid" class and show multiple errors when multiple validations fail', + async(() => { + const fixture = TestBed.createComponent(FormInputHarnessReactive); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + const element = fixture.debugElement.nativeElement; + const form = fixture.componentInstance.form; + + form.get('hexadecimalValue').setValue('@123123123123'); + fixture.detectChanges(); + + const input = element.querySelector('input'); + expect(Array.prototype.indexOf.call(input.classList, 'invalid')).not.toBe(-1); + + const failureMessage = element.querySelector('.validation'); + expect(failureMessage).not.toBeNull(); + const errorMessages = failureMessage.textContent.trim(); + expect(errorMessages).toContain('Please enter a hexadecimal value (alphanumeric, 0-9 and A-F)'); + expect(errorMessages).toContain('Value must be a maximum of 10 characters'); + }); + })); + + }); @Component({ @@ -49,6 +96,23 @@ describe('FormInput component', () => { ` }) -export class FormInputHarness { +export class FormInputHarnessNgModel { @ViewChild(FormTextComponent) formInput; +} + +@Component({ + selector: 'form-input-container', + template: ` +
+ +
+ ` +}) +export class FormInputHarnessReactive { + form: FormGroup; + constructor(fb: FormBuilder) { + this.form = fb.group({ + 'hexadecimalValue': ['', [validateHexadecimal(), Validators.maxLength(10)]] + }); + } } \ No newline at end of file diff --git a/src/app/components/input.ts b/src/app/components/input.ts index 3bb7542..62bf25c 100644 --- a/src/app/components/input.ts +++ b/src/app/components/input.ts @@ -1,19 +1,7 @@ -import { - Component, - Optional, - Inject, - Input, - ViewChild, -} from '@angular/core'; - -import { - NgModel, - NG_VALUE_ACCESSOR, - NG_VALIDATORS, - NG_ASYNC_VALIDATORS, -} from '@angular/forms'; - -import {ElementBase, animations} from '../form'; +import { Component, Inject, Injector, Input, Optional, ViewChild } from '@angular/core'; +import { NG_ASYNC_VALIDATORS, NG_VALIDATORS, NG_VALUE_ACCESSOR, NgModel } from '@angular/forms'; + +import { animations, ElementBase } from '../form'; @Component({ selector: 'form-text', @@ -21,15 +9,16 @@ import {ElementBase, animations} from '../form';
@@ -40,10 +29,21 @@ import {ElementBase, animations} from '../form'; useExisting: FormTextComponent, multi: true, }], + host: { + '[attr.disabled]': 'disabled' + } }) export class FormTextComponent extends ElementBase { + private _disabled: boolean; + @Input() public label: string; @Input() public placeholder: string; + @Input() get disabled() { + return this.control ? this.control.disabled : this._disabled; + } + set disabled(value: boolean) { + this._disabled = value; + } @ViewChild(NgModel) model: NgModel; @@ -52,9 +52,11 @@ export class FormTextComponent extends ElementBase { constructor( @Optional() @Inject(NG_VALIDATORS) validators: Array, @Optional() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array, + injector: Injector ) { - super(validators, asyncValidators); + super(validators, asyncValidators, injector); } + } let identifier = 0; \ No newline at end of file diff --git a/src/app/components/select.ts b/src/app/components/select.ts index 6173d10..11478e6 100644 --- a/src/app/components/select.ts +++ b/src/app/components/select.ts @@ -1,19 +1,7 @@ -import { - Component, - Optional, - Inject, - Input, - ViewChild, -} from '@angular/core'; - -import { - NgModel, - NG_VALUE_ACCESSOR, - NG_VALIDATORS, - NG_ASYNC_VALIDATORS, -} from '@angular/forms'; - -import {ElementBase, animations} from '../form'; +import { Component, Inject, Injector, Input, Optional, ViewChild } from '@angular/core'; +import { NG_ASYNC_VALIDATORS, NG_VALIDATORS, NG_VALUE_ACCESSOR, NgModel } from '@angular/forms'; + +import { animations, ElementBase } from '../form'; @Component({ selector: 'form-select', @@ -23,13 +11,14 @@ import {ElementBase, animations} from '../form'; @@ -40,11 +29,25 @@ import {ElementBase, animations} from '../form'; useExisting: FormSelectComponent, multi: true, }], + host: { + '[attr.disabled]': 'disabled' + } }) export class FormSelectComponent extends ElementBase { + private _disabled: boolean; + @Input() public label: string; + @Input() public placeholder: string; + @Input() get disabled() { + return this.control ? this.control.disabled : this._disabled; + } + + set disabled(value: boolean) { + this._disabled = value; + } + @ViewChild(NgModel) model: NgModel; public identifier = `form-select-${identifier++}`; @@ -52,8 +55,9 @@ export class FormSelectComponent extends ElementBase { constructor( @Optional() @Inject(NG_VALIDATORS) validators: Array, @Optional() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array, + injector: Injector ) { - super(validators, asyncValidators); + super(validators, asyncValidators, injector); } } diff --git a/src/app/components/validation.ts b/src/app/components/validation.ts index db0bc76..a5ee7b9 100644 --- a/src/app/components/validation.ts +++ b/src/app/components/validation.ts @@ -1,4 +1,4 @@ -import {Component, Input} from '@angular/core'; +import { Component, Input } from '@angular/core'; @Component({ selector: 'validation', diff --git a/src/app/form/animations.ts b/src/app/form/animations.ts index 86262af..ac7d5e8 100644 --- a/src/app/form/animations.ts +++ b/src/app/form/animations.ts @@ -1,22 +1,15 @@ -import { - AnimationEntryMetadata, - animate, - state, - style, - transition, - trigger, -} from '@angular/core'; +import { animate, AnimationEntryMetadata, state, style, transition, trigger } from '@angular/core'; export const animations: Array = [ trigger('flyInOut', [ - state('in', style({transform: 'translateY(0)'})), + state('in', style({ transform: 'translateY(0)' })), transition('void => *', [ - style({transform: 'translateY(-100%)'}), + style({ transform: 'translateY(-100%)' }), animate(100) ]), - state('out', style({transform: 'translateY(100%)'})), + state('out', style({ transform: 'translateY(100%)' })), transition('* => void', [ - animate(100, style({transform: 'translateY(100%)'})) + animate(100, style({ transform: 'translateY(100%)' })) ]) ]) ]; \ No newline at end of file diff --git a/src/app/form/element-base.ts b/src/app/form/element-base.ts index e96f8eb..362eaaf 100644 --- a/src/app/form/element-base.ts +++ b/src/app/form/element-base.ts @@ -1,38 +1,47 @@ -import {NgModel} from '@angular/forms'; +import { Injector } from '@angular/core'; +import { NgModel } from '@angular/forms'; -import {Observable} from 'rxjs'; +import { Observable } from 'rxjs/Observable'; -import {ValueAccessorBase} from './value-accessor'; - -import { - AsyncValidatorArray, - ValidatorArray, - ValidationResult, - message, - validate, -} from './validate'; +import { AsyncValidatorArray, message, validate, ValidationResult, ValidatorArray } from './validate'; +import { ValueAccessorBase } from './value-accessor'; export abstract class ElementBase extends ValueAccessorBase { protected abstract model: NgModel; - constructor( private validators: ValidatorArray, private asyncValidators: AsyncValidatorArray, + private injector: Injector ) { - super(); + super(injector); } - protected validate(): Observable { - return validate - (this.validators, this.asyncValidators) - (this.model.control); + protected validateInnerModel(): Observable { + return validate(this.validators, this.asyncValidators)(this.model.control); } protected get invalid(): Observable { - return this.validate().map(v => Object.keys(v || {}).length > 0); + return Observable.combineLatest(this.validateInnerModel(), this.getErrorsFromOuterModel()) + .map(v => { + let errors = Object.assign(v[0] || {}, v[1] || {}); + return Object.keys(errors || {}).length > 0; + }); } protected get failures(): Observable> { - return this.validate().map(v => Object.keys(v).map(k => message(v, k))); + return Observable.combineLatest(this.validateInnerModel(), this.getErrorsFromOuterModel()) + .map(v => { + let errors = Object.assign(v[0] || {}, v[1] || {}); + return Object.keys(errors || {}).map(k => message(errors, k)); + }); } -} \ No newline at end of file + + private getErrorsFromOuterModel(): Observable { + if (this.control == null || this.control.errors == null) { + return Observable.of(null); + } + + return Observable.of(this.control.errors); + } + +} diff --git a/src/app/form/validate.ts b/src/app/form/validate.ts index 4aadfa8..4740051 100644 --- a/src/app/form/validate.ts +++ b/src/app/form/validate.ts @@ -1,60 +1,54 @@ -import { - AbstractControl, - AsyncValidatorFn, - Validator, - Validators, - ValidatorFn, -} from '@angular/forms'; +import { AbstractControl, AsyncValidatorFn, Validator, ValidatorFn, Validators } from '@angular/forms'; -import {Observable} from 'rxjs'; +import { Observable } from 'rxjs/Rx'; -export type ValidationResult = {[validator: string]: string | boolean}; +export type ValidationResult = { [validator: string]: string | boolean }; export type AsyncValidatorArray = Array; export type ValidatorArray = Array; const normalizeValidator = - (validator: Validator | ValidatorFn): ValidatorFn | AsyncValidatorFn => { - const func = (validator as Validator).validate.bind(validator); - if (typeof func === 'function') { - return (c: AbstractControl) => func(c); - } else { - return validator; - } -}; + (validator: Validator | ValidatorFn): ValidatorFn | AsyncValidatorFn => { + const func = (validator as Validator).validate.bind(validator); + if (typeof func === 'function') { + return (c: AbstractControl) => func(c); + } else { + return validator; + } + }; export const composeValidators = - (validators: ValidatorArray): AsyncValidatorFn | ValidatorFn => { - if (validators == null || validators.length === 0) { - return null; - } - return Validators.compose(validators.map(normalizeValidator)); -}; + (validators: ValidatorArray): AsyncValidatorFn | ValidatorFn => { + if (validators == null || validators.length === 0) { + return null; + } + return Validators.compose(validators.map(normalizeValidator)); + }; export const validate = - (validators: ValidatorArray, asyncValidators: AsyncValidatorArray) => { - return (control: AbstractControl) => { - const synchronousValid = () => composeValidators(validators)(control); + (validators: ValidatorArray, asyncValidators: AsyncValidatorArray) => { + return (control: AbstractControl) => { + const synchronousValid = () => composeValidators(validators)(control); - if (asyncValidators) { - const asyncValidator = composeValidators(asyncValidators); + if (asyncValidators) { + const asyncValidator = composeValidators(asyncValidators); - return asyncValidator(control).map(v => { - const secondary = synchronousValid(); - if (secondary || v) { // compose async and sync validator results - return Object.assign({}, secondary, v); - } - }); - } + return asyncValidator(control).map(v => { + const secondary = synchronousValid(); + if (secondary || v) { // compose async and sync validator results + return Object.assign({}, secondary, v); + } + }); + } - if (validators) { - return Observable.of(synchronousValid()); - } + if (validators) { + return Observable.of(synchronousValid()); + } - return Observable.of(null); + return Observable.of(null); + }; }; -}; export const message = (validator: ValidationResult, key: string): string => { switch (key) { @@ -63,14 +57,14 @@ export const message = (validator: ValidationResult, key: string): string => { case 'pattern': return 'Value does not match required pattern'; case 'minlength': - return 'Value must be N characters'; + return `Value must be ${(validator).minlength.requiredLength} characters`; case 'maxlength': - return 'Value must be a maximum of N characters'; + return `Value must be a maximum of ${(validator).maxlength.requiredLength} characters`; } switch (typeof validator[key]) { case 'string': - return validator[key]; + return validator[key]; default: return `Validation failed: ${key}`; } diff --git a/src/app/form/value-accessor.ts b/src/app/form/value-accessor.ts index 1019e92..ecc15e8 100644 --- a/src/app/form/value-accessor.ts +++ b/src/app/form/value-accessor.ts @@ -1,4 +1,5 @@ -import {ControlValueAccessor} from '@angular/forms'; +import { Injector, Component } from '@angular/core'; +import { ControlValueAccessor, NgControl, NgModel, FormControl } from '@angular/forms'; export abstract class ValueAccessorBase implements ControlValueAccessor { private innerValue: T; @@ -6,6 +7,25 @@ export abstract class ValueAccessorBase implements ControlValueAccessor { private changed = new Array<(value: T) => void>(); private touched = new Array<() => void>(); + private _control: NgControl; + + /** + * The control (NgModel or FormControl) that exists on our custom component. + * Lazy loaded. + * @readonly + * @protected + * @type {NgControl} + * @memberof ValueAccessorBase + */ + protected get control(): NgControl { + if (this._control != null) { + return this._control; + } + + this._control = this._injector.get(NgControl, null).control; + return this._control; + } + get value(): T { return this.innerValue; } @@ -17,7 +37,10 @@ export abstract class ValueAccessorBase implements ControlValueAccessor { } } + constructor(private _injector: Injector) { } + writeValue(value: T) { + //this.parentControl = this._injector.get(NgControl, null); this.innerValue = value; } diff --git a/src/app/validators/hexadecimal-validator.ts b/src/app/validators/hexadecimal-validator.ts index 7fea089..08dde00 100644 --- a/src/app/validators/hexadecimal-validator.ts +++ b/src/app/validators/hexadecimal-validator.ts @@ -1,18 +1,11 @@ -import {Directive} from '@angular/core'; +import { Directive, forwardRef } from '@angular/core'; +import { AbstractControl, NG_VALIDATORS } from '@angular/forms'; -import { - NG_VALIDATORS, - AbstractControl, -} from '@angular/forms'; - -@Directive({ - selector: '[hexadecimal][ngModel]', - providers: [ - { provide: NG_VALIDATORS, useExisting: HexadecimalValueValidator, multi: true } - ] -}) -export class HexadecimalValueValidator { - validate(control: AbstractControl): {[validator: string]: string} { +/** + * Returns a funcation that validates a hexidecimal value + */ +export function validateHexadecimal() { + return (control: AbstractControl) => { const expression = /^([0-9a-fA-F]+)$/i; if (!control.value) { // the [required] validator will check presence, not us return null; @@ -23,6 +16,23 @@ export class HexadecimalValueValidator { return null; } - return {hexadecimal: 'Please enter a hexadecimal value (alphanumeric, 0-9 and A-F)'}; + return { hexadecimal: 'Please enter a hexadecimal value (alphanumeric, 0-9 and A-F)' }; + }; +} + +@Directive({ + selector: '[hexadecimal][ngModel],[hexadecimal][formControl],[hexadecimal][formControlName]', + providers: [ + { provide: NG_VALIDATORS, useExisting: forwardRef(() => HexadecimalValueValidator), multi: true } + ] +}) +export class HexadecimalValueValidator { + validator: Function; + constructor() { + this.validator = validateHexadecimal(); + } + + validate(control: AbstractControl): { [validator: string]: string } { + return this.validator(control); } } diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 00313f1..b7f639a 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -1,7 +1,7 @@ // The file contents for the current environment will overwrite these during build. // The build system defaults to the dev environment which uses `environment.ts`, but if you do // `ng build --env=prod` then `environment.prod.ts` will be used instead. -// The list of which env maps to which file can be found in `angular-cli.json`. +// The list of which env maps to which file can be found in `.angular-cli.json`. export const environment = { production: false diff --git a/src/index.html b/src/index.html index 7378490..4a2b37c 100644 --- a/src/index.html +++ b/src/index.html @@ -1,14 +1,14 @@ - + - FormExample + FormsExample - Loading... + diff --git a/src/main.ts b/src/main.ts index fc4e727..a9ca1ca 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,9 +1,8 @@ -import './polyfills.ts'; +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; -import {enableProdMode} from '@angular/core'; -import {environment} from './environments/environment'; -import {AppModule} from './app/'; +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); diff --git a/src/polyfills.ts b/src/polyfills.ts index 3b4c55b..7831e97 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -1,19 +1,72 @@ -// This file includes polyfills needed by Angular 2 and is loaded before -// the app. You can add your own extra polyfills to this file. -import 'core-js/es6/symbol'; -import 'core-js/es6/object'; -import 'core-js/es6/function'; -import 'core-js/es6/parse-int'; -import 'core-js/es6/parse-float'; -import 'core-js/es6/number'; -import 'core-js/es6/math'; -import 'core-js/es6/string'; -import 'core-js/es6/date'; -import 'core-js/es6/array'; -import 'core-js/es6/regexp'; -import 'core-js/es6/map'; -import 'core-js/es6/set'; -import 'core-js/es6/reflect'; +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** IE9, IE10 and IE11 requires all of the following polyfills. **/ +// import 'core-js/es6/symbol'; +// import 'core-js/es6/object'; +// import 'core-js/es6/function'; +// import 'core-js/es6/parse-int'; +// import 'core-js/es6/parse-float'; +// import 'core-js/es6/number'; +// import 'core-js/es6/math'; +// import 'core-js/es6/string'; +// import 'core-js/es6/date'; +// import 'core-js/es6/array'; +// import 'core-js/es6/regexp'; +// import 'core-js/es6/map'; +// import 'core-js/es6/weak-map'; +// import 'core-js/es6/set'; + +/** IE10 and IE11 requires the following for NgClass support on SVG elements */ +// import 'classlist.js'; // Run `npm install --save classlist.js`. +/** Evergreen browsers require these. **/ +import 'core-js/es6/reflect'; import 'core-js/es7/reflect'; -import 'zone.js/dist/zone'; + + +/** + * Required to support Web Animations `@angular/animation`. + * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation + **/ +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. + + + +/*************************************************************************************************** + * Zone JS is required by Angular itself. + */ +import 'zone.js/dist/zone'; // Included with Angular CLI. + + + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ + +/** + * Date, currency, decimal and percent pipes. + * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 + */ +// import 'intl'; // Run `npm install --save intl`. +/** + * Need to import at least one locale-data with intl. + */ +// import 'intl/locale-data/jsonp/en'; diff --git a/src/test.ts b/src/test.ts index 9b518b3..cd612ee 100644 --- a/src/test.ts +++ b/src/test.ts @@ -1,4 +1,4 @@ -import './polyfills.ts'; +// This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/dist/long-stack-trace-zone'; import 'zone.js/dist/proxy.js'; @@ -6,15 +6,15 @@ import 'zone.js/dist/sync-test'; import 'zone.js/dist/jasmine-patch'; import 'zone.js/dist/async-test'; import 'zone.js/dist/fake-async-test'; -import {getTestBed} from '@angular/core/testing'; +import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. -declare var __karma__: any; -declare var require: any; +declare const __karma__: any; +declare const require: any; // Prevent Karma from running prematurely. __karma__.loaded = function () {}; @@ -25,7 +25,7 @@ getTestBed().initTestEnvironment( platformBrowserDynamicTesting() ); // Then we find all the tests. -let context = require.context('./', true, /\.spec\.ts/); +const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().map(context); // Finally, start Karma to run the tests. diff --git a/src/tsconfig.app.json b/src/tsconfig.app.json new file mode 100644 index 0000000..5e2507d --- /dev/null +++ b/src/tsconfig.app.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/app", + "module": "es2015", + "baseUrl": "", + "types": [] + }, + "exclude": [ + "test.ts", + "**/*.spec.ts" + ] +} diff --git a/src/tsconfig.spec.json b/src/tsconfig.spec.json new file mode 100644 index 0000000..510e3f1 --- /dev/null +++ b/src/tsconfig.spec.json @@ -0,0 +1,20 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/spec", + "module": "commonjs", + "target": "es5", + "baseUrl": "", + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + "test.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/src/typings.d.ts b/src/typings.d.ts index ea52695..ef5c7bd 100644 --- a/src/typings.d.ts +++ b/src/typings.d.ts @@ -1,2 +1,5 @@ -// Typings reference file, you can add your own global typings here -// https://www.typescriptlang.org/docs/handbook/writing-declaration-files.html +/* SystemJS module definition */ +declare var module: NodeModule; +interface NodeModule { + id: string; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a35a8ee --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "baseUrl": "src", + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es5", + "typeRoots": [ + "node_modules/@types" + ], + "lib": [ + "es2016", + "dom" + ] + } +} diff --git a/tslint.json b/tslint.json index ad0093e..dd117b3 100644 --- a/tslint.json +++ b/tslint.json @@ -3,6 +3,8 @@ "node_modules/codelyzer" ], "rules": { + "arrow-return-shorthand": true, + "callable-types": true, "class-name": true, "comment-format": [ true, @@ -11,12 +13,17 @@ "curly": true, "eofline": true, "forin": true, + "import-blacklist": [ + true, + "rxjs" + ], + "import-spacing": true, "indent": [ true, "spaces" ], + "interface-over-type-literal": true, "label-position": true, - "label-undefined": true, "max-line-length": [ true, 140 @@ -39,18 +46,23 @@ ], "no-construct": true, "no-debugger": true, - "no-duplicate-key": true, - "no-duplicate-variable": true, + "no-duplicate-super": true, "no-empty": false, + "no-empty-interface": true, "no-eval": true, - "no-inferrable-types": true, + "no-inferrable-types": [ + true, + "ignore-params" + ], + "no-misused-new": true, + "no-non-null-assertion": true, "no-shadowed-variable": true, "no-string-literal": false, + "no-string-throw": true, "no-switch-case-fall-through": true, "no-trailing-whitespace": true, + "no-unnecessary-initializer": true, "no-unused-expression": true, - "no-unused-variable": true, - "no-unreachable": true, "no-use-before-declare": true, "no-var-keyword": true, "object-literal-sort-keys": false, @@ -61,6 +73,7 @@ "check-else", "check-whitespace" ], + "prefer-const": true, "quotemark": [ true, "single" @@ -83,6 +96,8 @@ "variable-declaration": "nospace" } ], + "typeof-compare": true, + "unified-signatures": true, "variable-name": false, "whitespace": [ true, @@ -92,13 +107,18 @@ "check-separator", "check-type" ], - - "directive-selector-prefix": [true, "app"], - "component-selector-prefix": [true, "app"], - "directive-selector-name": [true, "camelCase"], - "component-selector-name": [true, "kebab-case"], - "directive-selector-type": [true, "attribute"], - "component-selector-type": [true, "element"], + "directive-selector": [ + true, + "attribute", + "app", + "camelCase" + ], + "component-selector": [ + true, + "element", + "app", + "kebab-case" + ], "use-input-property-decorator": true, "use-output-property-decorator": true, "use-host-property-decorator": true, @@ -108,6 +128,7 @@ "use-pipe-transform-interface": true, "component-class-suffix": true, "directive-class-suffix": true, + "no-access-missing-member": true, "templates-use-public": true, "invoke-injectable": true }