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
-
+
+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
}