├── .editorconfig ├── .gitignore ├── README.md ├── angular.json ├── browserslist ├── changelog.md ├── demo ├── e2e │ ├── protractor.conf.js │ ├── src │ │ ├── app.e2e-spec.ts │ │ └── app.po.ts │ └── tsconfig.json └── src │ ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.ts │ ├── app.module.ts │ └── examples │ │ ├── 01-example-one │ │ ├── example-one.component.html │ │ ├── example-one.component.scss │ │ └── example-one.component.ts │ │ ├── 02-example-two │ │ ├── example-two.component.html │ │ ├── example-two.component.scss │ │ └── example-two.component.ts │ │ ├── 03-example-three │ │ ├── example-three.component.html │ │ ├── example-three.component.scss │ │ └── example-three.component.ts │ │ ├── 03.5-example-three-five │ │ ├── example-three-five.component.html │ │ ├── example-three-five.component.scss │ │ └── example-three-five.component.ts │ │ ├── 04-example-four │ │ ├── example-four.component.html │ │ ├── example-four.component.scss │ │ └── example-four.component.ts │ │ ├── 05-example-five │ │ ├── example-five.component.html │ │ ├── example-five.component.scss │ │ ├── example-five.component.ts │ │ └── my-control.directive.ts │ │ ├── 06-example-six │ │ ├── example-six.component.html │ │ ├── example-six.component.scss │ │ └── example-six.component.ts │ │ ├── 07-example-seven │ │ ├── example-seven.component.html │ │ ├── example-seven.component.scss │ │ └── example-seven.component.ts │ │ ├── 08-example-eight │ │ ├── example-eight.component.html │ │ ├── example-eight.component.scss │ │ └── example-eight.component.ts │ │ └── 09-example-nine │ │ ├── example-nine.component.html │ │ ├── example-nine.component.scss │ │ └── example-nine.component.ts │ ├── assets │ └── .gitkeep │ ├── environments │ ├── environment.prod.ts │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── styles.scss │ └── test.ts ├── jest.config.js ├── karma.conf.js ├── libs └── reactive-forms-module2-proposal │ ├── README.md │ ├── compat │ ├── package.json │ └── src │ │ ├── lib │ │ └── directives │ │ │ ├── form.module.ts │ │ │ ├── index.ts │ │ │ ├── ng_compat_form_control.ts │ │ │ ├── ng_compat_form_control_directive.ts │ │ │ └── ng_compat_form_control_name_directive.ts │ │ └── public_api.ts │ ├── ng-package.json │ ├── package.json │ ├── src │ ├── lib │ │ ├── accessors │ │ │ ├── accessors.module.ts │ │ │ ├── default_value_accessor.ts │ │ │ ├── index.ts │ │ │ ├── interface.ts │ │ │ └── util.ts │ │ ├── directives │ │ │ ├── base.directive.ts │ │ │ ├── control-name.directive.ts │ │ │ ├── control.directive.ts │ │ │ ├── form-array-name.directive.ts │ │ │ ├── form-array.directive.ts │ │ │ ├── form-control-name.directive.ts │ │ │ ├── form-control.directive.ts │ │ │ ├── form-group-name.directive.ts │ │ │ ├── form-group.directive.ts │ │ │ ├── form.module.ts │ │ │ ├── index.ts │ │ │ ├── interface.ts │ │ │ └── util.ts │ │ └── models │ │ │ ├── abstract-control.ts │ │ │ ├── control-base.ts │ │ │ ├── control-container-base.ts │ │ │ ├── control-container.ts │ │ │ ├── form-array.ts │ │ │ ├── form-control.spec.ts │ │ │ ├── form-control.ts │ │ │ ├── form-group.spec.ts │ │ │ ├── form-group.ts │ │ │ ├── index.ts │ │ │ ├── util.spec.ts │ │ │ └── util.ts │ └── public-api.ts │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── tslint.json ├── package.json ├── test.setup.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json ├── tslint.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "libs", 5 | "projects": { 6 | "demo": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss", 11 | "skipTests": true 12 | }, 13 | "@schematics/angular:class": { 14 | "skipTests": true 15 | }, 16 | "@schematics/angular:directive": { 17 | "skipTests": true 18 | }, 19 | "@schematics/angular:guard": { 20 | "skipTests": true 21 | }, 22 | "@schematics/angular:module": { 23 | "skipTests": true 24 | }, 25 | "@schematics/angular:pipe": { 26 | "skipTests": true 27 | }, 28 | "@schematics/angular:service": { 29 | "skipTests": true 30 | } 31 | }, 32 | "root": "", 33 | "sourceRoot": "demo/src", 34 | "prefix": "app", 35 | "architect": { 36 | "build": { 37 | "builder": "@angular-devkit/build-angular:browser", 38 | "options": { 39 | "outputPath": "dist/demo", 40 | "index": "demo/src/index.html", 41 | "main": "demo/src/main.ts", 42 | "polyfills": "demo/src/polyfills.ts", 43 | "tsConfig": "tsconfig.app.json", 44 | "aot": false, 45 | "assets": ["demo/src/favicon.ico", "demo/src/assets"], 46 | "styles": ["demo/src/styles.scss"], 47 | "scripts": [] 48 | }, 49 | "configurations": { 50 | "production": { 51 | "fileReplacements": [ 52 | { 53 | "replace": "demo/src/environments/environment.ts", 54 | "with": "demo/src/environments/environment.prod.ts" 55 | } 56 | ], 57 | "optimization": true, 58 | "outputHashing": "all", 59 | "sourceMap": false, 60 | "extractCss": true, 61 | "namedChunks": false, 62 | "aot": true, 63 | "extractLicenses": true, 64 | "vendorChunk": false, 65 | "buildOptimizer": true, 66 | "budgets": [ 67 | { 68 | "type": "initial", 69 | "maximumWarning": "2mb", 70 | "maximumError": "5mb" 71 | }, 72 | { 73 | "type": "anyComponentStyle", 74 | "maximumWarning": "6kb", 75 | "maximumError": "10kb" 76 | } 77 | ] 78 | } 79 | } 80 | }, 81 | "serve": { 82 | "builder": "@angular-devkit/build-angular:dev-server", 83 | "options": { 84 | "browserTarget": "demo:build" 85 | }, 86 | "configurations": { 87 | "production": { 88 | "browserTarget": "demo:build:production" 89 | } 90 | } 91 | }, 92 | "extract-i18n": { 93 | "builder": "@angular-devkit/build-angular:extract-i18n", 94 | "options": { 95 | "browserTarget": "demo:build" 96 | } 97 | }, 98 | "test": { 99 | "builder": "@angular-builders/jest:run", 100 | "options": { 101 | } 102 | }, 103 | "lint": { 104 | "builder": "@angular-devkit/build-angular:tslint", 105 | "options": { 106 | "tsConfig": [ 107 | "tsconfig.app.json", 108 | "tsconfig.spec.json", 109 | "e2e/tsconfig.json" 110 | ], 111 | "exclude": ["**/node_modules/**"] 112 | } 113 | }, 114 | "e2e": { 115 | "builder": "@angular-devkit/build-angular:protractor", 116 | "options": { 117 | "protractorConfig": "demo/e2e/protractor.conf.js", 118 | "devServerTarget": "demo:serve" 119 | }, 120 | "configurations": { 121 | "production": { 122 | "devServerTarget": "demo:serve:production" 123 | } 124 | } 125 | } 126 | } 127 | }, 128 | "rf2": { 129 | "projectType": "library", 130 | "root": "libs/reactive-forms-module2-proposal", 131 | "sourceRoot": "libs/reactive-forms-module2-proposal/src", 132 | "prefix": "lib", 133 | "architect": { 134 | "build": { 135 | "builder": "@angular-devkit/build-ng-packagr:build", 136 | "options": { 137 | "tsConfig": "libs/reactive-forms-module2-proposal/tsconfig.lib.json", 138 | "project": "libs/reactive-forms-module2-proposal/ng-package.json" 139 | } 140 | }, 141 | "test": { 142 | "builder": "@angular-builders/jest:run", 143 | "options": {} 144 | }, 145 | "lint": { 146 | "builder": "@angular-devkit/build-angular:tslint", 147 | "options": { 148 | "tsConfig": [ 149 | "libs/reactive-forms-module2-proposal/tsconfig.lib.json", 150 | "libs/reactive-forms-module2-proposal/tsconfig.spec.json" 151 | ], 152 | "exclude": ["**/node_modules/**"] 153 | } 154 | } 155 | } 156 | } 157 | }, 158 | "defaultProject": "demo" 159 | } 160 | -------------------------------------------------------------------------------- /browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2019/11/3 4 | 5 | - Fnished FormGroup and FormArray implementation. This also resulted in some tweaks to the ControlEvent API. 6 | - Added some form directives such as `FormGroupNameDirective` and cleaned up the directive implementations. 7 | - Fixed potential validation race conditions. 8 | 9 | ## 2019/10/15 10 | 11 | 1. Added support for `data` state change event 12 | 2. Fixed `replayState()` so the returned events have `stateChange: true` 13 | 3. Updated `events` type so that state change is an optional property. Also updated `processEvent()` so that it never adds `stateChange: false` to an event, only `stateChange: true`. This allows the user to pass in a custom state change event. Rreviously, the system would have overwridden the custom `stateChange: true` property with `stateChange: false`, because the custom event wouldn't be recognized as a state change. 14 | 15 | ## 2019/10/14 16 | 17 | - Initial publishing of the prototype `reactive-forms-module2-proposal`. 18 | -------------------------------------------------------------------------------- /demo/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | 'browserName': 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 31 | } 32 | }; -------------------------------------------------------------------------------- /demo/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('Welcome to demo!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /demo/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText() { 9 | return element(by.css('app-root h1')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /demo/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /demo/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | 5 | const routes: Routes = []; 6 | 7 | @NgModule({ 8 | imports: [RouterModule.forRoot(routes)], 9 | exports: [RouterModule] 10 | }) 11 | export class AppRoutingModule { } 12 | -------------------------------------------------------------------------------- /demo/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | ReactiveForms2Module proposal demo 4 | 5 | 6 | 7 |
8 |

9 | This is a demo for a proposal to improve @angular/forms. The associated code 10 | can be found on github here: 11 | https://github.com/thefliik/reactive-forms-2-proposal. 14 |

15 |
16 | 17 |
18 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
32 | -------------------------------------------------------------------------------- /demo/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | app-root { 2 | display: block; 3 | font-size: 1.1rem; 4 | background: lavender; 5 | } 6 | 7 | .description, 8 | .examples { 9 | padding: 2rem; 10 | } 11 | 12 | .example { 13 | display: block; 14 | margin-bottom: 2rem; 15 | } 16 | 17 | .error { 18 | margin-top: 1rem; 19 | color: red; 20 | } 21 | 22 | code { 23 | background-color: lightgray; 24 | } 25 | -------------------------------------------------------------------------------- /demo/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewEncapsulation } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'], 7 | host: { 8 | class: 'mat-typography', 9 | }, 10 | encapsulation: ViewEncapsulation.None, 11 | }) 12 | export class AppComponent { 13 | title = 'demo'; 14 | } 15 | -------------------------------------------------------------------------------- /demo/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 4 | 5 | import { AppRoutingModule } from './app-routing.module'; 6 | import { AppComponent } from './app.component'; 7 | 8 | import { 9 | MatCardModule, 10 | MatInputModule, 11 | MatProgressSpinnerModule, 12 | MatToolbarModule, 13 | MatFormFieldModule, 14 | MatDatepickerModule, 15 | MatNativeDateModule, 16 | } from '@angular/material'; 17 | import { ExampleOneComponent } from './examples/01-example-one/example-one.component'; 18 | import { ExampleTwoComponent } from './examples/02-example-two/example-two.component'; 19 | import { ExampleThreeComponent } from './examples/03-example-three/example-three.component'; 20 | import { ExampleThreeFiveComponent } from './examples/03.5-example-three-five/example-three-five.component'; 21 | import { ExampleFourComponent } from './examples/04-example-four/example-four.component'; 22 | import { 23 | ExampleFiveComponent, 24 | ValidationWrapperComponent, 25 | } from './examples/05-example-five/example-five.component'; 26 | import { MyControlDirective } from './examples/05-example-five/my-control.directive'; 27 | import { ExampleSixComponent } from './examples/06-example-six/example-six.component'; 28 | import { ExampleSevenComponent } from './examples/07-example-seven/example-seven.component'; 29 | import { ReactiveFormsModule } from '@angular/forms'; 30 | import { ExampleEightComponent } from './examples/08-example-eight/example-eight.component'; 31 | import { ExampleNineComponent } from './examples/09-example-nine/example-nine.component'; 32 | import { ReactiveFormsModule2 } from 'reactive-forms-module2-proposal'; 33 | import { ReactiveFormsModule2Compat } from 'reactive-forms-module2-proposal/compat'; 34 | 35 | @NgModule({ 36 | declarations: [ 37 | AppComponent, 38 | ExampleOneComponent, 39 | ExampleTwoComponent, 40 | ExampleThreeComponent, 41 | ExampleThreeFiveComponent, 42 | ExampleFourComponent, 43 | ExampleFiveComponent, 44 | MyControlDirective, 45 | ValidationWrapperComponent, 46 | ExampleSixComponent, 47 | ExampleSevenComponent, 48 | ExampleEightComponent, 49 | ExampleNineComponent, 50 | ], 51 | imports: [ 52 | BrowserAnimationsModule, 53 | BrowserModule, 54 | AppRoutingModule, 55 | ReactiveFormsModule, 56 | ReactiveFormsModule2, 57 | ReactiveFormsModule2Compat, 58 | MatCardModule, 59 | MatInputModule, 60 | MatProgressSpinnerModule, 61 | MatToolbarModule, 62 | MatFormFieldModule, 63 | MatDatepickerModule, 64 | MatNativeDateModule, 65 | ], 66 | providers: [], 67 | bootstrap: [AppComponent], 68 | }) 69 | export class AppModule {} 70 | -------------------------------------------------------------------------------- /demo/src/app/examples/01-example-one/example-one.component.html: -------------------------------------------------------------------------------- 1 | 2 | Example 1 3 | 4 | 5 | linking one FormControl to another FormControl 6 | 7 | 8 | 9 |

10 | Here, by subscribing the source of controlB to the changes of 11 | controlA, controlB will reflect all changes to 12 | controlA. 13 |

14 |

15 | The opposite is not true however. Changes to controlB do not 16 | effect controlA. 17 |

18 |
19 | 20 | 21 |
22 | 23 | 24 |
25 | 26 |
27 | 28 | 29 |
30 |
31 |
32 | -------------------------------------------------------------------------------- /demo/src/app/examples/01-example-one/example-one.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorroll/reactive-forms-2-proposal/0d9b0775aa793ad472c8b3f59fcd8e69269c2036/demo/src/app/examples/01-example-one/example-one.component.scss -------------------------------------------------------------------------------- /demo/src/app/examples/01-example-one/example-one.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormControl, FormGroup } from 'reactive-forms-module2-proposal'; 3 | import { filter } from 'rxjs/operators'; 4 | 5 | @Component({ 6 | selector: 'app-example-one', 7 | templateUrl: './example-one.component.html', 8 | styleUrls: ['./example-one.component.scss'], 9 | }) 10 | export class ExampleOneComponent implements OnInit { 11 | controlA = new FormControl(''); 12 | controlB = new FormControl(''); 13 | 14 | constructor() {} 15 | 16 | ngOnInit() { 17 | this.controlA.events.subscribe(this.controlB.source); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /demo/src/app/examples/02-example-two/example-two.component.html: -------------------------------------------------------------------------------- 1 | 2 | Example 2 3 | 4 | 5 | linking two FormControls to each other 6 | 7 | 8 | 9 |

10 | Here, by subscribing the source of controlB to the changes of 11 | controlA, as well as the source of controlA to 12 | the changes of controlB, any changes effecting either form 13 | control will be mirrored on the other form control (this doesn't cause an 14 | infinite loop). 15 |

16 |
17 | 18 | 19 |
20 | 21 | 22 |
23 | 24 |
25 |

controlA Errors:

26 |

{{ controlA.errors | json }}

27 |
28 | 29 |
30 | 31 | 32 |
33 | 34 |
35 |

controlB Errors:

36 |

{{ controlB.errors | json }}

37 |
38 |
39 |
40 | -------------------------------------------------------------------------------- /demo/src/app/examples/02-example-two/example-two.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorroll/reactive-forms-2-proposal/0d9b0775aa793ad472c8b3f59fcd8e69269c2036/demo/src/app/examples/02-example-two/example-two.component.scss -------------------------------------------------------------------------------- /demo/src/app/examples/02-example-two/example-two.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormControl, ValidatorFn } from 'reactive-forms-module2-proposal'; 3 | 4 | @Component({ 5 | selector: 'app-example-two', 6 | templateUrl: './example-two.component.html', 7 | styleUrls: ['./example-two.component.scss'], 8 | }) 9 | export class ExampleTwoComponent implements OnInit { 10 | controlA = new FormControl(''); 11 | controlB = new FormControl(''); 12 | 13 | constructor() {} 14 | 15 | ngOnInit() { 16 | this.controlA.events.subscribe(this.controlB.source); 17 | this.controlB.events.subscribe(this.controlA.source); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /demo/src/app/examples/03-example-three/example-three.component.html: -------------------------------------------------------------------------------- 1 | 2 | Example 3 3 | 4 | 5 | dynamically parse a control's text input 6 | 7 | 8 | 9 |

10 | Here, a user is providing text date values and we want a control with 11 | javascript `Date` objects. 12 |

13 | 14 |

15 | To accomplish this, you can simply provide a value mapper object 16 | containing `fromControl` and `toControl` mapper functions. Optionally, you 17 | can also provide a validator for the user text values. 18 |

19 |
20 | 21 | 22 |
23 | 24 | 32 | 33 |
34 |

controlA Errors:

35 |

{{ controlA.errors | json }}

36 |
37 | 38 |
39 |

controlA mapped value:

40 |

{{ controlA.value && controlA.value.toString() }}

41 |
42 |
43 |
44 |
45 | -------------------------------------------------------------------------------- /demo/src/app/examples/03-example-three/example-three.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorroll/reactive-forms-2-proposal/0d9b0775aa793ad472c8b3f59fcd8e69269c2036/demo/src/app/examples/03-example-three/example-three.component.scss -------------------------------------------------------------------------------- /demo/src/app/examples/03-example-three/example-three.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ValidatorFn, FormControl } from 'reactive-forms-module2-proposal'; 3 | 4 | // regex from https://www.regextester.com/96683 5 | const dateRegex = /^([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))$/; 6 | 7 | const dateValidatorFn: ValidatorFn = control => { 8 | if (!dateRegex.test(control.value)) { 9 | return { 10 | invalidDate: 'Invalid date format! Try YYYY-MM-DD', 11 | }; 12 | } 13 | 14 | return null; 15 | }; 16 | 17 | function dateToString(date: Date | null) { 18 | if (!date) return ''; 19 | 20 | const year = date.getFullYear(); 21 | const month = date.getMonth() + 1; 22 | const day = date.getDate(); 23 | 24 | return `${year}-${padString(month)}-${padString(day)}`; 25 | } 26 | 27 | function padString(int: number) { 28 | return int < 10 ? `0${int}` : `${int}`; 29 | } 30 | 31 | function stringToDate(text: string) { 32 | if (!dateRegex.test(text)) return null; 33 | 34 | const parts = text.split('-'); 35 | 36 | const date = new Date(2010, 0, 1); 37 | 38 | date.setFullYear(parseInt(parts[0], 10)); 39 | date.setMonth(parseInt(parts[1], 10) - 1); 40 | date.setDate(parseInt(parts[2], 10)); 41 | 42 | return date; 43 | } 44 | 45 | @Component({ 46 | selector: 'app-example-three', 47 | templateUrl: './example-three.component.html', 48 | styleUrls: ['./example-three.component.scss'], 49 | }) 50 | export class ExampleThreeComponent { 51 | controlA = new FormControl(null); 52 | 53 | stringToDate = stringToDate; 54 | dateToString = dateToString; 55 | dateValidatorFn = dateValidatorFn; 56 | } 57 | -------------------------------------------------------------------------------- /demo/src/app/examples/03.5-example-three-five/example-three-five.component.html: -------------------------------------------------------------------------------- 1 | 2 | Example 3.5 3 | 4 | 5 | dynamically parse a control's text input (another way) 6 | 7 | 8 | 9 |

10 | This example is similar to example 3, in that it also shows how to 11 | dynamically transform text date values into javascript `Date` objects. 12 |

13 | 14 |

15 | While the last example made use of the value mapper option provided by the 16 | `FormControlDirective`, this example shows manually linking two controls 17 | together and transforming the values between them. 18 |

19 | 20 |

21 | Note: 22 |

23 | 24 |
    25 |
  1. 26 | Only the changes of controls are being synced. Any state the 27 | controls acquire before being linked to one another is not shared. In 28 | this example, the inputControl begins with an error because it's initial 29 | value is invalid. The dateControl does not. 30 |
  2. 31 |
32 |
33 | 34 | 35 |
36 | 37 | 38 | 39 |
40 |

inputControl Errors:

41 |

{{ inputControl.errors | json }}

42 |
43 | 44 |
45 |

inputControl value:

46 |

{{ inputControl.value }}

47 |
48 |
49 | 50 |
51 | 52 | 53 | 54 |
55 |

dateControl Errors:

56 |

{{ dateControl.errors | json }}

57 |
58 | 59 |
60 |

dateControl value:

61 |

{{ dateControl.value | json }}

62 |
63 |
64 |
65 | 66 |
67 | 68 | 69 |

70 | All aspects of the controls are in sync. Note though, that while the 71 | values of both controls are in sync, the values are different. A change to 72 | the inputControl value effects the 73 | dateControl value and vice-versa. 74 |

75 | 76 |
77 |

inputControl touched: {{ inputControl.touched }}

78 |

dateControl touched: {{ dateControl.touched }}

79 | 80 | 83 | 84 | 87 |
88 |
89 |
90 | -------------------------------------------------------------------------------- /demo/src/app/examples/03.5-example-three-five/example-three-five.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorroll/reactive-forms-2-proposal/0d9b0775aa793ad472c8b3f59fcd8e69269c2036/demo/src/app/examples/03.5-example-three-five/example-three-five.component.scss -------------------------------------------------------------------------------- /demo/src/app/examples/03.5-example-three-five/example-three-five.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ValidatorFn, FormControl } from 'reactive-forms-module2-proposal'; 3 | import { map } from 'rxjs/operators'; 4 | 5 | // regex from https://www.regextester.com/96683 6 | const dateRegex = /^([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))$/; 7 | 8 | const stringValidatorFn: ValidatorFn = control => { 9 | if (dateRegex.test(control.value)) { 10 | return null; 11 | } 12 | 13 | return { 14 | invalidDate: 'Invalid date format! Try YYYY-MM-DD', 15 | }; 16 | }; 17 | 18 | function dateToString(date: Date | null) { 19 | if (!date) return ''; 20 | 21 | const year = date.getFullYear(); 22 | const month = date.getMonth() + 1; 23 | const day = date.getDate(); 24 | 25 | return `${year}-${padString(month)}-${padString(day)}`; 26 | } 27 | 28 | function padString(int: number) { 29 | return int < 10 ? `0${int}` : `${int}`; 30 | } 31 | 32 | function stringToDate(text: string) { 33 | if (!dateRegex.test(text)) return null; 34 | 35 | const parts = text.split('-'); 36 | 37 | const date = new Date(2010, 0, 1); 38 | 39 | date.setFullYear(parseInt(parts[0], 10)); 40 | date.setMonth(parseInt(parts[1], 10) - 1); 41 | date.setDate(parseInt(parts[2], 10)); 42 | 43 | return date; 44 | } 45 | 46 | const dateValidatorFn: ValidatorFn = control => { 47 | if (control.value) { 48 | return null; 49 | } 50 | 51 | return { 52 | invalidDate: 'Invalid date format!', 53 | }; 54 | }; 55 | 56 | @Component({ 57 | selector: 'app-example-three-five', 58 | templateUrl: './example-three-five.component.html', 59 | styleUrls: ['./example-three-five.component.scss'], 60 | }) 61 | export class ExampleThreeFiveComponent implements OnInit { 62 | inputControl = new FormControl('', { 63 | validators: stringValidatorFn, 64 | }); 65 | 66 | dateControl = new FormControl(null); 67 | 68 | stringToDate = stringToDate; 69 | dateToString = dateToString; 70 | 71 | constructor() {} 72 | 73 | ngOnInit() { 74 | // To understand why this works, 75 | // see the github README on the ControlEvent API. 76 | // As a reminder, any ControlEvent originating from the `inputControl` 77 | // will not be re-processed by the inputControl (even if the `dateControl` 78 | // modifies the inputControl's ControlEvent before applying it). 79 | 80 | // example flow: 81 | // - inputControl is changed in the UI 82 | // - inputControl processes the value change ControlEvent and emits it from `inputControl#events` 83 | // - The dateControl's subscription to `inputControl#events` turns the string value 84 | // into a `Date` value before processing the ControlEvent and re-emitting it from 85 | // `dateControl#events`. 86 | // - The inputControl's subscription to `dateControl#events` turns the `Date` into a string 87 | // before then filtering out the `ControlEvent` because the `inputControl` has already 88 | // processed it. 89 | 90 | // Important not to sync all the inputControl's state as the `dateControl` 91 | // will not play nice with the inputControl's validatorFn (which expects 92 | // strings) 93 | this.inputControl.events 94 | .pipe( 95 | map(event => { 96 | if (event.type === 'StateChange' && event.changes.has('value')) { 97 | const changes = new Map(event.changes); 98 | 99 | changes.set('value', stringToDate(event.changes.get('value'))); 100 | 101 | return { 102 | ...event, 103 | changes, 104 | }; 105 | } 106 | 107 | return event; 108 | }), 109 | ) 110 | .subscribe(this.dateControl.source); 111 | 112 | this.dateControl.events 113 | .pipe( 114 | map(event => { 115 | if (event.type === 'StateChange' && event.changes.has('value')) { 116 | const changes = new Map(event.changes); 117 | 118 | changes.set('value', dateToString(event.changes.get('value'))); 119 | 120 | return { 121 | ...event, 122 | changes, 123 | }; 124 | } 125 | 126 | return event; 127 | }), 128 | ) 129 | .subscribe(this.inputControl.source); 130 | } 131 | 132 | setDate() { 133 | this.dateControl.setValue(new Date()); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /demo/src/app/examples/04-example-four/example-four.component.html: -------------------------------------------------------------------------------- 1 | 2 | Example 4 3 | 4 | 5 | validating the value of an AbstractControl via a service 6 | 7 | 8 | 9 |

10 | Here, a usernameControl is receiving text value from a user 11 | and we want to validate that with an external service (e.g. "does the 12 | username already exist?"). 13 |

14 |
15 | 16 | 17 |
18 | 19 | 20 | 21 |
22 |

usernameControl Errors:

23 |

{{ usernameControl.errors | json }}

24 |
25 |
26 | 27 |
28 |

control pending

29 | 30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /demo/src/app/examples/04-example-four/example-four.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorroll/reactive-forms-2-proposal/0d9b0775aa793ad472c8b3f59fcd8e69269c2036/demo/src/app/examples/04-example-four/example-four.component.scss -------------------------------------------------------------------------------- /demo/src/app/examples/04-example-four/example-four.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Injectable, Inject } from '@angular/core'; 2 | import { FormControl } from 'reactive-forms-module2-proposal'; 3 | import { interval, of, Observable, NEVER } from 'rxjs'; 4 | import { 5 | take, 6 | tap, 7 | debounceTime, 8 | switchMap, 9 | map, 10 | filter, 11 | pairwise, 12 | } from 'rxjs/operators'; 13 | 14 | @Injectable({ 15 | providedIn: 'root', 16 | }) 17 | export class UserService { 18 | start = Math.random() > 0.5; 19 | 20 | doesNameExist(_: string) { 21 | this.start = !this.start; 22 | const payload = this.start; 23 | 24 | return interval(500).pipe( 25 | map(() => ({ payload })), 26 | take(1), 27 | ); 28 | } 29 | } 30 | 31 | @Component({ 32 | selector: 'app-example-four', 33 | templateUrl: './example-four.component.html', 34 | styleUrls: ['./example-four.component.scss'], 35 | }) 36 | export class ExampleFourComponent implements OnInit { 37 | usernameControl = new FormControl(''); 38 | 39 | constructor( 40 | // using Inject here for stackblitz compatibility 41 | @Inject(UserService) 42 | private userService: UserService, 43 | ) {} 44 | 45 | /** 46 | * # Overview 47 | * 48 | * So the easy way of doing async validation is to simply observe 49 | * a control's `value` changes and then begin async validation. 50 | * 51 | * For example: 52 | * 53 | * ```ts 54 | * this.usernameControl 55 | * .observeChanges('value', { ignoreNoEmit: true }) 56 | * .pipe( 57 | * tap(() => { 58 | * this.usernameControl.markPending(true, { 59 | * source: 'userService', 60 | * }); 61 | * }), 62 | * debounceTime(500), 63 | * switchMap(value => this.userService.doesNameExist(value)), 64 | * tap(() => 65 | * this.usernameControl.markPending(false, { source: 'userService' }), 66 | * ), 67 | * ) 68 | * .subscribe(response => { 69 | * const errors = response.payload ? { userNameExists: true } : null; 70 | * this.usernameControl.setErrors(errors, { 71 | * source: 'userService', 72 | * }); 73 | * }); 74 | * ``` 75 | * 76 | * While this approach may be fine for most use cases, it has some 77 | * limitations: 78 | * 79 | * 1. Async validation is being performed even if the synchronous 80 | * validators have found the value to be invalid. 81 | * 2. The control's `value` state change event is being emitted before 82 | * any async validation services have had a chance to react. 83 | * 84 | * This means that it's possible something subscribed to `value` 85 | * state changes could: 86 | * 87 | * 1. Receive the state change event. 88 | * 2. Check if the control is valid and see that it is. 89 | * 3. Check if the control is pending and see that it isn't. 90 | * 91 | * All before any async services have had a chance to mark the control 92 | * as pending or invalid. 93 | * 94 | * If you want to ensure that any `value` state change events 95 | * are only emitted after async services have had a chance to react 96 | * (usually by synchronously marking the control as `pending`), then we need 97 | * to subscribe to the "validation end" lifecycle event. 98 | */ 99 | 100 | ngOnInit() { 101 | this.usernameControl.validationEvents 102 | .pipe( 103 | // Wait for the control to complete its synchronous validation. 104 | filter(event => event.label === 'End'), 105 | tap(() => { 106 | // Discard any existing errors set by the userService as they are 107 | // no longer applicable. 108 | this.usernameControl.setErrors(null, { 109 | source: `userService`, 110 | }); 111 | 112 | // If the control is already marked invalid, we're going to skip the async 113 | // validation check so don't bother to mark pending. 114 | this.usernameControl.markPending( 115 | this.usernameControl.value !== '' && this.usernameControl.valid, 116 | { 117 | source: `userService`, 118 | }, 119 | ); 120 | }), 121 | // By running validation inside a `switchMap` + `interval()` (instead 122 | // of `debounceTime()`), we ensure that an in-progress async validation 123 | // check is discarded if the user starts typing again. 124 | switchMap(() => { 125 | // If the control is already invalid we don't need to do anything. 126 | if ( 127 | this.usernameControl.value === '' || 128 | this.usernameControl.invalid 129 | ) { 130 | return NEVER; 131 | } 132 | 133 | // Else run validation. 134 | return interval(500).pipe( 135 | take(1), 136 | switchMap(() => 137 | this.userService.doesNameExist(this.usernameControl.value), 138 | ), 139 | ); 140 | }), 141 | ) 142 | .subscribe(response => { 143 | this.usernameControl.markPending(false, { 144 | source: `userService`, 145 | }); 146 | 147 | const errors = response.payload ? { userNameExists: true } : null; 148 | 149 | this.usernameControl.setErrors(errors, { 150 | source: `userService`, 151 | }); 152 | }); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /demo/src/app/examples/05-example-five/example-five.component.html: -------------------------------------------------------------------------------- 1 | 2 | Example 5 3 | 4 | 5 | using dependency injection to dynamically add new validator functions to a 6 | control 7 | 8 | 9 | 10 |

11 | Here, we have two form controls (controlA and 12 | controlB) which both do not have any validator functions. 13 | Separately, we have a custom directive, [myControlDirective], 14 | which receives a FormControl and dynamically adds injected validator 15 | functions. We also have a custom component, 16 | app-validation-wrapper, which provides a custom validator 17 | function. This function is picked up by the custom directive and added to 18 | controlB. It will throw an error if the text is too long. 19 |

20 |
21 | 22 | 23 |

24 | Why this is cool: 25 |

26 | 27 |

28 | In the existing ReactiveFormsModule, when you pass a control 29 | to a FormControlDirective via [formControl], 30 | that directive may dynamically add validator functions to the control. It 31 | does this by creating a new validator function which combines the 32 | control's existing validator function(s) with any additional validator 33 | functions the FormControlDirective has had injected. 34 |

35 | 36 |

37 | It then replaces the control's existing validator function with the new 38 | one. This process is complex and can lead to bugs. For example, after this 39 | process is complete there isn't any way to determine which validator 40 | functions were added by the user vs which ones were added dynamically. 41 |

42 | 43 |

44 | Here, validators are internally stored keyed to a source id (similar to 45 | errors). If a FormControl is passed to a directive which dymanically 46 | injects additional validator functions, those functions will be stored 47 | separately from the FormControl's other functions (and are deleted 48 | separately). This leads to more consistent, predictable behavior that an 49 | unknowledgable user cannot mess with. 50 |

51 | 52 |

53 | 54 | Admittedly this is bordering on an implementation detail, but it's now 55 | so much easier to dynamically add validator functions to a control! You 56 | don't need to worry about messing stuff up! 57 | 58 |

59 |
60 | 61 | 62 |
63 | 64 | 65 | 66 |
67 |

controlA Errors:

68 |

{{ controlA.errors | json }}

69 |
70 |
71 | 72 | 73 | 74 | 75 | 76 |
77 |

controlB Errors:

78 |

{{ controlB.errors | json }}

79 |
80 |
81 |
82 |
83 | -------------------------------------------------------------------------------- /demo/src/app/examples/05-example-five/example-five.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorroll/reactive-forms-2-proposal/0d9b0775aa793ad472c8b3f59fcd8e69269c2036/demo/src/app/examples/05-example-five/example-five.component.scss -------------------------------------------------------------------------------- /demo/src/app/examples/05-example-five/example-five.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ValidatorFn, FormControl } from 'reactive-forms-module2-proposal'; 3 | import { NG_VALIDATORS_2 } from './my-control.directive'; 4 | 5 | export const lengthValidator: ValidatorFn = control => { 6 | if (control.value && control.value.length > 5) { 7 | return { 8 | tooLong: 'Text is too long!', 9 | }; 10 | } 11 | 12 | return null; 13 | }; 14 | 15 | @Component({ 16 | selector: 'app-validation-wrapper', 17 | template: ` 18 | 19 | `, 20 | providers: [ 21 | { 22 | provide: NG_VALIDATORS_2, 23 | useValue: lengthValidator, 24 | multi: true, 25 | }, 26 | ], 27 | }) 28 | export class ValidationWrapperComponent {} 29 | 30 | @Component({ 31 | selector: 'app-example-five', 32 | templateUrl: './example-five.component.html', 33 | styleUrls: ['./example-five.component.scss'], 34 | }) 35 | export class ExampleFiveComponent implements OnInit { 36 | controlA = new FormControl(''); 37 | controlB = new FormControl(''); 38 | 39 | constructor() {} 40 | 41 | ngOnInit() {} 42 | } 43 | -------------------------------------------------------------------------------- /demo/src/app/examples/05-example-five/my-control.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | OnInit, 4 | Directive, 5 | Input, 6 | Optional, 7 | Self, 8 | Inject, 9 | InjectionToken, 10 | SimpleChanges, 11 | SimpleChange, 12 | OnChanges, 13 | } from '@angular/core'; 14 | import { AbstractControl, ValidatorFn } from 'reactive-forms-module2-proposal'; 15 | import { filter } from 'rxjs/operators'; 16 | import { Subscription } from 'rxjs'; 17 | 18 | export const NG_VALIDATORS_2 = new InjectionToken( 19 | 'NG_VALIDATORS_2', 20 | ); 21 | 22 | @Directive({ 23 | selector: '[myControlDirective]', 24 | }) 25 | export class MyControlDirective implements OnChanges { 26 | static id = 0; 27 | 28 | @Input('myControlDirective') control!: AbstractControl; 29 | 30 | private id = Symbol(`myControlDirective ${MyControlDirective.id}`); 31 | private subscriptions: Subscription[] = []; 32 | 33 | constructor( 34 | @Optional() 35 | @Self() 36 | @Inject(NG_VALIDATORS_2) 37 | private validators: ValidatorFn[] | null, 38 | ) { 39 | MyControlDirective.id++; 40 | } 41 | 42 | ngOnChanges(changes: { control: SimpleChange }) { 43 | if (changes.control.previousValue) { 44 | this.clearSubscriptions(); 45 | 46 | // clear injected validators from the old control 47 | const oldControl = changes.control.previousValue; 48 | 49 | oldControl.setValidators(null, { 50 | source: this.id, 51 | }); 52 | } 53 | 54 | // add injected validators to the new control 55 | this.control.setValidators(this.validators, { 56 | source: this.id, 57 | }); 58 | 59 | // If the `validatorStore` of the control is ever reset, 60 | // re-add these validators 61 | this.subscriptions.push( 62 | this.control.events 63 | .pipe( 64 | filter( 65 | ({ source, type, changes }) => 66 | source !== this.id && 67 | type === 'StateChange' && 68 | changes.has('validatorStore'), 69 | ), 70 | ) 71 | .subscribe(() => { 72 | this.control.setValidators(this.validators, { 73 | source: this.id, 74 | }); 75 | }), 76 | ); 77 | } 78 | 79 | ngOnDestroy() { 80 | this.clearSubscriptions(); 81 | } 82 | 83 | private clearSubscriptions() { 84 | this.subscriptions.forEach(sub => sub.unsubscribe()); 85 | this.subscriptions = []; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /demo/src/app/examples/06-example-six/example-six.component.html: -------------------------------------------------------------------------------- 1 | 2 | Example 6 3 | 4 | 5 | saving a snapshot of a form control's state 6 | 7 | 8 | 9 |

10 | Here, we save a snapshot of the original "default" state of the form 11 | control. We can use this snapshot to restore the control to that state. 12 | The snapshot is simply an observable of the state change events which 13 | define the form control. 14 |

15 |

16 | Because the snapshot is an observable, we can use rxjs operators like 17 | `filter()` to prevent some of the state from being synced to a form 18 | control. 19 |

20 |
21 | 22 | 23 |
24 | 25 | 26 |
27 | 28 |
29 |

30 | control touched: {{ inputControl.touched }}. control changed: 31 | {{ inputControl.changed }}. 32 |

33 | 34 |
35 |
36 |
37 | -------------------------------------------------------------------------------- /demo/src/app/examples/06-example-six/example-six.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorroll/reactive-forms-2-proposal/0d9b0775aa793ad472c8b3f59fcd8e69269c2036/demo/src/app/examples/06-example-six/example-six.component.scss -------------------------------------------------------------------------------- /demo/src/app/examples/06-example-six/example-six.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormControl, StateChange } from 'reactive-forms-module2-proposal'; 3 | import { Observable } from 'rxjs'; 4 | 5 | @Component({ 6 | selector: 'app-example-six', 7 | templateUrl: './example-six.component.html', 8 | styleUrls: ['./example-six.component.scss'], 9 | }) 10 | export class ExampleSixComponent implements OnInit { 11 | inputControl = new FormControl('start typing!'); 12 | 13 | inputControlDefaults!: Observable; 14 | 15 | constructor() {} 16 | 17 | ngOnInit() { 18 | this.inputControlDefaults = this.inputControl.replayState(); 19 | } 20 | 21 | reset() { 22 | // this.inputControlDefaults.subscribe(event => { 23 | // console.log('reset event', event); 24 | // }); 25 | this.inputControlDefaults.subscribe(this.inputControl.source); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /demo/src/app/examples/07-example-seven/example-seven.component.html: -------------------------------------------------------------------------------- 1 | 2 | Example 7 3 | 4 | 5 | compatibility: using the new API with components made for the old API 6 | 7 | 8 | 9 |

10 | To ease the transition from the old ReactiveFormsModule API to the new 11 | API, a compatibility layer should be included. 12 |

13 |

14 | This example demonstrates using the new FormControl with existing angular 15 | material components (which expect an old FormControl). I haven't spent 16 | *that* much time on this compatibility directive, so you can probably 17 | break it, but hopefully you get the idea. 18 |

19 |
20 | 21 | 22 |
23 | 24 | Try selecting a date! 25 | 26 | 32 | 33 | 37 | 38 | 39 | 40 | Errors! 41 | 42 |
43 | 44 |
45 |

controlA value: {{ controlA.value | json }}

46 |
47 |
48 |
49 | -------------------------------------------------------------------------------- /demo/src/app/examples/07-example-seven/example-seven.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorroll/reactive-forms-2-proposal/0d9b0775aa793ad472c8b3f59fcd8e69269c2036/demo/src/app/examples/07-example-seven/example-seven.component.scss -------------------------------------------------------------------------------- /demo/src/app/examples/07-example-seven/example-seven.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormControl } from 'reactive-forms-module2-proposal'; 3 | 4 | @Component({ 5 | selector: 'app-example-seven', 6 | templateUrl: './example-seven.component.html', 7 | styleUrls: ['./example-seven.component.scss'], 8 | }) 9 | export class ExampleSevenComponent implements OnInit { 10 | controlA = new FormControl(null); 11 | 12 | constructor() {} 13 | 14 | ngOnInit() {} 15 | } 16 | -------------------------------------------------------------------------------- /demo/src/app/examples/08-example-eight/example-eight.component.html: -------------------------------------------------------------------------------- 1 | 2 | Example 8 3 | 4 | 5 | compatibility: using the new API, you can actually do more things with the 6 | old API 7 | 8 | 9 | 10 |

11 | Building off of example 7, you can actually still link one 12 | "ReactiveFormsModule2" FormControl to multiple "ReactiveFormsModule" 13 | inputs using the compatibility directive. If you aren't familiar, using 14 | the existing "ReactiveFormsModule" API you can't link a single FormControl 15 | to multiple inputs. 16 |

17 |
18 | 19 | 20 |
21 | 22 | Try selecting a date! 23 | 24 | 30 | 31 | 35 | 36 | 37 | 38 | Errors! 39 | 40 |
41 | 42 |
43 | 44 | Or use this input! 45 | 46 | 52 | 53 | 57 | 58 | 59 | 60 | Errors! 61 | 62 |
63 | 64 |
65 |

controlA value: {{ controlA.value | json }}

66 |
67 |
68 |
69 | -------------------------------------------------------------------------------- /demo/src/app/examples/08-example-eight/example-eight.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorroll/reactive-forms-2-proposal/0d9b0775aa793ad472c8b3f59fcd8e69269c2036/demo/src/app/examples/08-example-eight/example-eight.component.scss -------------------------------------------------------------------------------- /demo/src/app/examples/08-example-eight/example-eight.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormControl } from 'reactive-forms-module2-proposal'; 3 | 4 | @Component({ 5 | selector: 'app-example-eight', 6 | templateUrl: './example-eight.component.html', 7 | styleUrls: ['./example-eight.component.scss'], 8 | }) 9 | export class ExampleEightComponent implements OnInit { 10 | controlA = new FormControl(null); 11 | 12 | constructor() {} 13 | 14 | ngOnInit() {} 15 | } 16 | -------------------------------------------------------------------------------- /demo/src/app/examples/09-example-nine/example-nine.component.html: -------------------------------------------------------------------------------- 1 | 2 | Example 9 3 | 4 | 5 | adding arbitrary meta-data to a "value" emission 6 | 7 | 8 | 9 |

10 | This example demonstrates adding arbitrary metadata to a new value. You 11 | can add metadata to any emission (e.g. touched, dirty, setValidator, etc). 12 |

13 |
14 | 15 | 16 |
17 | 18 | 19 |
20 | 21 |
22 | 25 | 26 | 27 | 36 |
37 |
38 | 39 | 40 |
41 |

The resulting state change emission: {{ values$ | async | json }}

42 | 43 |

44 | The resulting value emission (it doesn't include metadata): 45 | {{ controlA.value }} 46 |

47 |
48 |
49 |
50 | -------------------------------------------------------------------------------- /demo/src/app/examples/09-example-nine/example-nine.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorroll/reactive-forms-2-proposal/0d9b0775aa793ad472c8b3f59fcd8e69269c2036/demo/src/app/examples/09-example-nine/example-nine.component.scss -------------------------------------------------------------------------------- /demo/src/app/examples/09-example-nine/example-nine.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormControl } from 'reactive-forms-module2-proposal'; 3 | import { filter, map } from 'rxjs/operators'; 4 | 5 | @Component({ 6 | selector: 'app-example-nine', 7 | templateUrl: './example-nine.component.html', 8 | styleUrls: ['./example-nine.component.scss'], 9 | }) 10 | export class ExampleNineComponent implements OnInit { 11 | controlA = new FormControl(''); 12 | values$ = this.controlA.events.pipe( 13 | filter(e => e.type === 'StateChange' && e.changes.has('value')), 14 | map(event => { 15 | return { 16 | ...event, 17 | processed: event.processed.map(i => 18 | typeof i === 'symbol' ? i.toString() : i, 19 | ), 20 | changes: Object.fromEntries(event.changes), 21 | }; 22 | }), 23 | ); 24 | 25 | constructor() {} 26 | 27 | ngOnInit() {} 28 | } 29 | -------------------------------------------------------------------------------- /demo/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorroll/reactive-forms-2-proposal/0d9b0775aa793ad472c8b3f59fcd8e69269c2036/demo/src/assets/.gitkeep -------------------------------------------------------------------------------- /demo/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /demo/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /demo/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorroll/reactive-forms-2-proposal/0d9b0775aa793ad472c8b3f59fcd8e69269c2036/demo/src/favicon.ico -------------------------------------------------------------------------------- /demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Demo 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /demo/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /demo/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/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | /*************************************************************************************************** 61 | * APPLICATION IMPORTS 62 | */ 63 | 64 | import 'core-js/features/reflect'; 65 | -------------------------------------------------------------------------------- /demo/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | @import '~@angular/material/prebuilt-themes/indigo-pink.css'; 4 | 5 | body { 6 | padding: 0; 7 | margin: 0; 8 | } 9 | -------------------------------------------------------------------------------- /demo/src/test.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/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // testRegex: '(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$', 3 | // moduleDirectories: ['node_modules'], 4 | // moduleFileExtensions: ['js', 'ts', 'tsx'], 5 | // modulePathIgnorePatterns: ['tmp'], 6 | // coveragePathIgnorePatterns: ['/node_modules/', '/tests/'], 7 | // coverageThreshold: { 8 | // global: { 9 | // branches: 60, 10 | // functions: 70, 11 | // lines: 70, 12 | // statements: 70, 13 | // }, 14 | // }, 15 | // collectCoverage: true, 16 | // verbose: true, 17 | setupFiles: ['/test.setup.ts'], 18 | // testURL: 'http://localhost/', 19 | // globals: { 20 | // 'ts-jest': { 21 | // tsConfig: 'tsconfig.jest.json', 22 | // }, 23 | // }, 24 | // preset: 'ts-jest', 25 | // testMatch: null, 26 | }; 27 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 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 // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/demo'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/README.md: -------------------------------------------------------------------------------- 1 | # ReactiveFormsModule2Proposal 2 | 3 | Entry points: 4 | 5 | - `reactive-forms-module2-proposal` 6 | - Main entry point 7 | - `reactive-forms-module2-proposal/compat` 8 | - Compatibility module for `@angular/forms` 9 | - This adds two new dependencies (`@angular/forms` and `lodash-es`) 10 | 11 | See the main readme on GitHub for more information https://github.com/thefliik/reactive-forms-2-proposal 12 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/compat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "ngPackage": {} 3 | } 4 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/compat/src/lib/directives/form.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { NgCompatFormControlDirective } from './ng_compat_form_control_directive'; 3 | import { NgCompatFormControlNameDirective } from './ng_compat_form_control_name_directive'; 4 | 5 | @NgModule({ 6 | imports: [], 7 | providers: [], 8 | declarations: [ 9 | NgCompatFormControlDirective, 10 | NgCompatFormControlNameDirective, 11 | ], 12 | exports: [NgCompatFormControlDirective, NgCompatFormControlNameDirective], 13 | }) 14 | export class ReactiveFormsModule2Compat {} 15 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/compat/src/lib/directives/index.ts: -------------------------------------------------------------------------------- 1 | // export * from './form_control_directive'; 2 | export * from './form.module'; 3 | export * from './ng_compat_form_control_directive'; 4 | export * from './ng_compat_form_control_name_directive'; 5 | export * from './ng_compat_form_control'; 6 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/compat/src/lib/directives/ng_compat_form_control.ts: -------------------------------------------------------------------------------- 1 | import { FormControl as OriginalFormControl } from '@angular/forms'; 2 | import { 3 | FormControl, 4 | StateChange, 5 | ValidationErrors, 6 | } from 'reactive-forms-module2-proposal'; 7 | import { filter } from 'rxjs/operators'; 8 | 9 | export class NgCompatFormControl extends OriginalFormControl { 10 | get id() { 11 | return this.swControl.id; 12 | } 13 | 14 | constructor(readonly swControl: FormControl) { 15 | super(); 16 | 17 | this.swControl.events 18 | .pipe( 19 | filter( 20 | ({ type, source }) => type === 'StateChange' && source !== this.id, 21 | ), 22 | ) 23 | .subscribe(event => { 24 | (event as StateChange).changes.forEach((value, prop) => { 25 | switch (prop) { 26 | case 'value': { 27 | this.setValue(value, { 28 | swSource: event.source, 29 | processed: event.processed, 30 | }); 31 | break; 32 | } 33 | case 'touched': { 34 | if (value) { 35 | this.markAsTouched({ 36 | swSource: event.source, 37 | processed: event.processed, 38 | }); 39 | } else { 40 | this.markAsUntouched({ 41 | swSource: event.source, 42 | processed: event.processed, 43 | }); 44 | } 45 | break; 46 | } 47 | case 'changed': { 48 | if (value) { 49 | this.markAsDirty({ 50 | swSource: event.source, 51 | processed: event.processed, 52 | }); 53 | } else { 54 | this.markAsPristine({ 55 | swSource: event.source, 56 | processed: event.processed, 57 | }); 58 | } 59 | break; 60 | } 61 | case 'disabled': { 62 | if (value) { 63 | this.disable({ 64 | swSource: event.source, 65 | processed: event.processed, 66 | }); 67 | } else { 68 | this.enable({ 69 | swSource: event.source, 70 | processed: event.processed, 71 | }); 72 | } 73 | break; 74 | } 75 | case 'errorsStore': { 76 | const errors = Array.from(value as Map< 77 | string, 78 | ValidationErrors | null 79 | >).reduce( 80 | (prev, [, curr]) => { 81 | return { 82 | ...prev, 83 | ...curr, 84 | }; 85 | }, 86 | {} as ValidationErrors, 87 | ); 88 | 89 | this.setErrors(Object.keys(errors).length === 0 ? null : errors, { 90 | swSource: event.source, 91 | processed: event.processed, 92 | }); 93 | break; 94 | } 95 | } 96 | }); 97 | 98 | this.updateValueAndValidity(); 99 | }); 100 | } 101 | 102 | private options(op: any) { 103 | return { source: op.swSource || this.id, processed: op.processed }; 104 | } 105 | 106 | markAsTouched(options: any = {}) { 107 | super.markAsTouched(options); 108 | if (!this.swControl || options.source === this.id) { 109 | return; 110 | } 111 | this.swControl.markTouched(true, this.options(options)); 112 | } 113 | 114 | markAsUntouched(options: any = {}) { 115 | super.markAsUntouched(options); 116 | if (!this.swControl || options.source === this.id) { 117 | return; 118 | } 119 | this.swControl.markTouched(false, this.options(options)); 120 | } 121 | 122 | markAsDirty(options: any = {}) { 123 | super.markAsDirty(options); 124 | if (!this.swControl || options.source === this.id) { 125 | return; 126 | } 127 | this.swControl.markChanged(true, this.options(options)); 128 | } 129 | 130 | markAsPristine(options: any = {}) { 131 | super.markAsPristine(options); 132 | if (!this.swControl || options.source === this.id) { 133 | return; 134 | } 135 | this.swControl.markChanged(false, this.options(options)); 136 | } 137 | 138 | disable(options: any = {}) { 139 | super.disable(options); 140 | if (!this.swControl || options.source === this.id) { 141 | return; 142 | } 143 | this.swControl.markDisabled(true, this.options(options)); 144 | } 145 | 146 | enable(options: any = {}) { 147 | super.enable(options); 148 | if (!this.swControl || options.source === this.id) { 149 | return; 150 | } 151 | this.swControl.markDisabled(true, this.options(options)); 152 | } 153 | 154 | setValue(value: any, options: any = {}) { 155 | super.setValue(value, options); 156 | if (!this.swControl || options.source === this.id) { 157 | return; 158 | } 159 | this.swControl.setValue(value, this.options(options)); 160 | this.swControl.setErrors(this.errors, this.options(options)); 161 | this.swControl.markPending(false, this.options(options)); 162 | } 163 | 164 | patchValue(value: any, options: any = {}) { 165 | super.patchValue(value, options); 166 | if (!this.swControl || options.source === this.id) { 167 | return; 168 | } 169 | this.swControl.patchValue(value, this.options(options)); 170 | this.swControl.setErrors(this.errors, this.options(options)); 171 | this.swControl.markPending(false, this.options(options)); 172 | } 173 | 174 | setErrors(value: any, options: any = {}) { 175 | super.setErrors(value, options); 176 | if (!this.swControl || options.source === this.id) { 177 | return; 178 | } 179 | this.swControl.setErrors(value, this.options(options)); 180 | } 181 | 182 | markAsPending(options: any = {}) { 183 | super.markAsPending(options); 184 | if (!this.swControl || options.source === this.id) { 185 | return; 186 | } 187 | this.swControl.markPending(true, this.options(options)); 188 | } 189 | 190 | updateValueAndValidity(options: any = {}) { 191 | const errors = this.errors ? { ...this.errors } : {}; 192 | super.updateValueAndValidity(options); 193 | 194 | if (!this.swControl || options.source === this.id) { 195 | return; 196 | } 197 | 198 | const newErrors = this.errors ? { ...this.errors } : {}; 199 | 200 | if (objectsEqual(errors, newErrors)) return; 201 | 202 | this.swControl.setErrors(this.errors, this.options(options)); 203 | } 204 | 205 | get invalid() { 206 | return this.swControl.invalid; 207 | } 208 | 209 | get valid() { 210 | return this.swControl.valid; 211 | } 212 | } 213 | 214 | function objectsEqual(a: { [key: string]: any }, b: { [key: string]: any }) { 215 | if (Object.keys(a).length !== Object.keys(b).length) return false; 216 | 217 | for (const prop in a) { 218 | if (!a.hasOwnProperty(prop)) continue; 219 | 220 | if (a[prop] !== b[prop]) return false; 221 | } 222 | 223 | return true; 224 | } 225 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/compat/src/lib/directives/ng_compat_form_control_directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Input, 3 | OnDestroy, 4 | OnChanges, 5 | Directive, 6 | ElementRef, 7 | Inject, 8 | Self, 9 | SimpleChange, 10 | SimpleChanges, 11 | forwardRef, 12 | Renderer2, 13 | } from '@angular/core'; 14 | import { 15 | FormControl, 16 | ControlAccessor, 17 | NG_CONTROL_DIRECTIVE, 18 | ɵNgControlDirective, 19 | } from 'reactive-forms-module2-proposal'; 20 | 21 | import { 22 | FormControlDirective, 23 | NgControl, 24 | ControlValueAccessor, 25 | } from '@angular/forms'; 26 | 27 | import { NgCompatFormControl } from './ng_compat_form_control'; 28 | 29 | @Directive({ 30 | selector: '[ngFormControl][formControl]', 31 | exportAs: 'ngCompatForm', 32 | providers: [ 33 | { 34 | provide: NG_CONTROL_DIRECTIVE, 35 | useExisting: forwardRef(() => NgCompatFormControlDirective), 36 | }, 37 | ], 38 | }) 39 | export class NgCompatFormControlDirective 40 | extends ɵNgControlDirective 41 | implements ControlAccessor, OnChanges, OnDestroy { 42 | static id = 0; 43 | @Input('ngFormControl') providedControl!: FormControl; 44 | 45 | protected ngControl = new NgCompatFormControl( 46 | new FormControl(undefined, { 47 | id: Symbol( 48 | `NgCompatFormControlDirective-${NgCompatFormControlDirective.id++}`, 49 | ), 50 | }), 51 | ); 52 | 53 | control = this.ngControl.swControl; 54 | 55 | protected valueAccessor: ControlValueAccessor | null; 56 | 57 | constructor( 58 | @Self() 59 | @Inject(NgControl) 60 | protected ngDirective: FormControlDirective, 61 | renderer: Renderer2, 62 | el: ElementRef, 63 | ) { 64 | super(renderer, el); 65 | 66 | const self = this; 67 | 68 | const orig = this.ngDirective.ngOnChanges.bind(this.ngDirective); 69 | 70 | let index = 0; 71 | 72 | this.ngDirective.ngOnChanges = (changes: SimpleChanges) => { 73 | const old = self.ngDirective.form; 74 | self.ngDirective.form = self.ngControl; 75 | orig({ 76 | ...changes, 77 | form: new SimpleChange(old, self.ngControl, index === 0), 78 | }); 79 | index++; 80 | }; 81 | 82 | this.valueAccessor = this.ngDirective.valueAccessor; 83 | } 84 | 85 | ngOnChanges(changes: { providedControl?: SimpleChange }) { 86 | if (!this.providedControl) { 87 | throw new Error( 88 | `NgCompatFormControlDirective must be passed a FormControl`, 89 | ); 90 | } 91 | 92 | if (!this.valueAccessor) { 93 | throw new Error( 94 | `NgCompatFormControlDirective could not find valueAccessor`, 95 | ); 96 | } 97 | 98 | super.ngOnChanges(changes); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/compat/src/lib/directives/ng_compat_form_control_name_directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Input, 3 | OnDestroy, 4 | OnChanges, 5 | Directive, 6 | ElementRef, 7 | Inject, 8 | Self, 9 | SimpleChange, 10 | SimpleChanges, 11 | SkipSelf, 12 | forwardRef, 13 | Renderer2, 14 | } from '@angular/core'; 15 | import { 16 | FormControl, 17 | NG_CONTROL_CONTAINER_ACCESSOR, 18 | ControlContainerAccessor, 19 | ControlAccessor, 20 | NG_CONTROL_DIRECTIVE, 21 | ɵNgControlNameDirective, 22 | } from 'reactive-forms-module2-proposal'; 23 | 24 | import { 25 | FormControlDirective, 26 | NgControl, 27 | ControlValueAccessor, 28 | } from '@angular/forms'; 29 | 30 | import { NgCompatFormControl } from './ng_compat_form_control'; 31 | 32 | @Directive({ 33 | selector: '[ngFormControlName][formControl]', 34 | exportAs: 'ngCompatForm', 35 | providers: [ 36 | { 37 | provide: NG_CONTROL_DIRECTIVE, 38 | useExisting: forwardRef(() => NgCompatFormControlNameDirective), 39 | }, 40 | ], 41 | }) 42 | export class NgCompatFormControlNameDirective 43 | extends ɵNgControlNameDirective 44 | implements ControlAccessor, OnChanges, OnDestroy { 45 | static id = 0; 46 | 47 | @Input('ngFormControlName') controlName!: string; 48 | 49 | protected ngControl = new NgCompatFormControl( 50 | new FormControl(undefined, { 51 | id: Symbol( 52 | `NgCompatFormControlNameDirective-${NgCompatFormControlNameDirective.id++}`, 53 | ), 54 | }), 55 | ); 56 | 57 | control = this.ngControl.swControl; 58 | 59 | protected valueAccessor: ControlValueAccessor | null; 60 | 61 | constructor( 62 | @Self() 63 | @Inject(NgControl) 64 | protected ngDirective: FormControlDirective, 65 | @SkipSelf() 66 | @Inject(NG_CONTROL_CONTAINER_ACCESSOR) 67 | protected containerAccessor: ControlContainerAccessor, 68 | renderer: Renderer2, 69 | el: ElementRef, 70 | ) { 71 | super(renderer, el); 72 | 73 | const self = this; 74 | 75 | const orig = this.ngDirective.ngOnChanges.bind(this.ngDirective); 76 | 77 | let index = 0; 78 | 79 | this.ngDirective.ngOnChanges = (changes: SimpleChanges) => { 80 | const old = self.ngDirective.form; 81 | self.ngDirective.form = self.ngControl; 82 | orig({ 83 | ...changes, 84 | form: new SimpleChange(old, self.ngControl, index === 0), 85 | }); 86 | index++; 87 | }; 88 | 89 | this.valueAccessor = this.ngDirective.valueAccessor; 90 | } 91 | 92 | ngOnChanges(_: { controlName?: SimpleChange }) { 93 | if (!this.controlName) { 94 | throw new Error( 95 | `NgCompatFormControlNameDirective must be passed a ngFormControlName`, 96 | ); 97 | } 98 | 99 | super.ngOnChanges(_); 100 | } 101 | 102 | protected validateProvidedControl(control: any): control is FormControl { 103 | if (!(control instanceof FormControl)) { 104 | throw new Error( 105 | 'NgCompatFormControlNameDirective must link to an instance of FormControl', 106 | ); 107 | } 108 | 109 | return true; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/compat/src/public_api.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/directives'; 2 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/reactive-forms-module2-proposal", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | }, 7 | "whitelistedNonPeerDependencies": ["@angular/forms", "lodash-es"] 8 | } 9 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactive-forms-module2-proposal", 3 | "version": "0.2.2", 4 | "peerDependencies": { 5 | "@angular/common": "^8.2.0", 6 | "@angular/core": "^8.2.0" 7 | }, 8 | "optionalDependencies": { 9 | "@angular/forms": "^8.2.0", 10 | "lodash-es": "^4.17.15" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/src/lib/accessors/accessors.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { DefaultValueAccessor } from './default_value_accessor'; 3 | 4 | @NgModule({ 5 | declarations: [DefaultValueAccessor], 6 | exports: [DefaultValueAccessor], 7 | providers: [], 8 | }) 9 | export class AccessorsModule {} 10 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/src/lib/accessors/default_value_accessor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | import { 10 | ElementRef, 11 | InjectionToken, 12 | Renderer2, 13 | forwardRef, 14 | Inject, 15 | Optional, 16 | Directive, 17 | } from '@angular/core'; 18 | import { ɵgetDOM as getDOM } from '@angular/platform-browser'; 19 | import { distinctUntilChanged, filter, map } from 'rxjs/operators'; 20 | import { setupListeners } from './util'; 21 | import { FormControl } from '../models'; 22 | import { NG_CONTROL_ACCESSOR, ControlAccessor } from './interface'; 23 | 24 | /** 25 | * We must check whether the agent is Android because composition events 26 | * behave differently between iOS and Android. 27 | */ 28 | function _isAndroid(): boolean { 29 | const userAgent = getDOM() ? getDOM().getUserAgent() : ''; 30 | return /android (\d+)/.test(userAgent.toLowerCase()); 31 | } 32 | 33 | /** 34 | * @description 35 | * Provide this token to control if form directives buffer IME input until 36 | * the "compositionend" event occurs. 37 | * @publicApi 38 | */ 39 | export const COMPOSITION_BUFFER_MODE = new InjectionToken( 40 | 'CompositionEventMode', 41 | ); 42 | 43 | /** 44 | * @description 45 | * The default `ControlValueAccessor` for writing a value and listening to changes on input 46 | * elements. The accessor is used by the `FormControlDirective`, `FormControlName`, and 47 | * `NgModel` directives. 48 | * 49 | * @usageNotes 50 | * 51 | * ### Using the default value accessor 52 | * 53 | * The following example shows how to use an input element that activates the default value accessor 54 | * (in this case, a text field). 55 | * 56 | * ```ts 57 | * const firstNameControl = new FormControl(); 58 | * ``` 59 | * 60 | * ``` 61 | * 62 | * ``` 63 | * 64 | * @ngModule ReactiveFormsModule 65 | * @ngModule FormsModule 66 | * @publicApi 67 | */ 68 | @Directive({ 69 | selector: 'input:not([type=checkbox]),textarea,[ngDefaultControl]', 70 | providers: [ 71 | { 72 | provide: NG_CONTROL_ACCESSOR, 73 | useExisting: forwardRef(() => DefaultValueAccessor), 74 | multi: true, 75 | }, 76 | ], 77 | }) 78 | export class DefaultValueAccessor implements ControlAccessor { 79 | readonly control = new FormControl(); 80 | 81 | constructor( 82 | protected renderer: Renderer2, 83 | protected el: ElementRef, 84 | @Optional() 85 | @Inject(COMPOSITION_BUFFER_MODE) 86 | protected _compositionMode: boolean, 87 | ) { 88 | setupListeners(this, 'blur', 'onTouched'); 89 | setupListeners(this, 'input', '_handleInput'); 90 | setupListeners(this, 'compositionstart', '_compositionStart'); 91 | setupListeners(this, 'compositionend', '_compositionEnd'); 92 | 93 | if (this._compositionMode == null) { 94 | this._compositionMode = !_isAndroid(); 95 | } 96 | 97 | this.control.events 98 | .pipe( 99 | filter( 100 | event => 101 | event.type === 'StateChange' && 102 | event.changes.has('value') && 103 | event.source !== this.control.id, 104 | ), 105 | map(() => this.control.value), 106 | ) 107 | .subscribe(() => { 108 | const normalizedValue = 109 | this.control.value == null ? '' : this.control.value; 110 | this.renderer.setProperty( 111 | this.el.nativeElement, 112 | 'value', 113 | normalizedValue, 114 | ); 115 | }); 116 | 117 | this.control.events 118 | .pipe( 119 | filter( 120 | event => 121 | event.type === 'StateChange' && 122 | event.changes.has('disabled') && 123 | event.source !== this.control.id, 124 | ), 125 | map(() => this.control.value), 126 | distinctUntilChanged(), 127 | ) 128 | .subscribe(isDisabled => { 129 | this.renderer.setProperty( 130 | this.el.nativeElement, 131 | 'disabled', 132 | isDisabled, 133 | ); 134 | }); 135 | } 136 | 137 | /** Whether the user is creating a composition string (IME events). */ 138 | private _composing = false; 139 | /** 140 | * @description 141 | * The registered callback function called when an input event occurs on the input element. 142 | */ 143 | onChange = (_: any) => { 144 | this.control.markChanged(true); 145 | this.control.setValue(_); 146 | } 147 | 148 | /** 149 | * @description 150 | * The registered callback function called when a blur event occurs on the input element. 151 | */ 152 | onTouched = () => { 153 | this.control.markTouched(true); 154 | } 155 | 156 | /** @internal */ 157 | _handleInput(event: any): void { 158 | const value = event.target.value; 159 | 160 | if (!this._compositionMode || (this._compositionMode && !this._composing)) { 161 | this.onChange(value); 162 | } 163 | } 164 | 165 | /** @internal */ 166 | _compositionStart(): void { 167 | this._composing = true; 168 | } 169 | 170 | /** @internal */ 171 | _compositionEnd(event: any): void { 172 | const value = event.target.value; 173 | 174 | this._composing = false; 175 | 176 | if (this._compositionMode) { 177 | this.onChange(value); 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/src/lib/accessors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './accessors.module'; 2 | export * from './interface'; 3 | export * from './default_value_accessor'; 4 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/src/lib/accessors/interface.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | import { AbstractControl, ControlContainer } from '../models'; 3 | 4 | export interface ControlAccessor { 5 | readonly control: T; 6 | } 7 | 8 | export const NG_CONTROL_ACCESSOR = new InjectionToken( 9 | 'NG_CONTROL_ACCESSOR', 10 | ); 11 | 12 | export interface ControlContainerAccessor< 13 | T extends ControlContainer = ControlContainer 14 | > extends ControlAccessor {} 15 | 16 | export const NG_CONTROL_CONTAINER_ACCESSOR = new InjectionToken< 17 | ControlContainerAccessor 18 | >('NG_CONTROL_CONTAINER_ACCESSOR'); 19 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/src/lib/accessors/util.ts: -------------------------------------------------------------------------------- 1 | import { AbstractControl } from '../models'; 2 | import { filter, startWith, map } from 'rxjs/operators'; 3 | 4 | export function looseIdentical(a: any, b: any): boolean { 5 | return ( 6 | a === b || 7 | (typeof a === 'number' && typeof b === 'number' && isNaN(a) && isNaN(b)) 8 | ); 9 | } 10 | 11 | export function setupListeners(dir: any, event: string, fn: string) { 12 | dir.renderer.listen(dir.el.nativeElement, event, dir[fn].bind(dir)); 13 | } 14 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/src/lib/directives/base.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | OnDestroy, 3 | OnChanges, 4 | Renderer2, 5 | ElementRef, 6 | InjectionToken, 7 | } from '@angular/core'; 8 | import { Subscription } from 'rxjs'; 9 | import { AbstractControl, ControlEvent } from '../models'; 10 | import { ControlValueMapper } from './interface'; 11 | import { ControlAccessor } from '../accessors'; 12 | import { isValueStateChange } from './util'; 13 | 14 | export const NG_CONTROL_DIRECTIVE = new InjectionToken< 15 | NgBaseDirective 16 | >('NG_CONTROL_DIRECTIVE'); 17 | 18 | export abstract class NgBaseDirective 19 | implements ControlAccessor, OnChanges, OnDestroy { 20 | static id = 0; 21 | abstract readonly control: T; 22 | 23 | valueMapper?: ControlValueMapper; 24 | 25 | protected accessorValidatorId = Symbol( 26 | `NgDirectiveAccessorValidator-${NgBaseDirective.id++}`, 27 | ); 28 | 29 | protected onChangesSubscriptions: Subscription[] = []; 30 | protected subscriptions: Subscription[] = []; 31 | 32 | constructor(protected renderer: Renderer2, protected el: ElementRef) {} 33 | 34 | abstract ngOnChanges(...args: any[]): void; 35 | 36 | ngOnInit() { 37 | // The nativeElement will be a comment if a directive is place on 38 | // an `` element. 39 | if (!(this.el.nativeElement instanceof HTMLElement)) return; 40 | 41 | this.subscriptions.push( 42 | this.control 43 | .observe('touched', { ignoreNoEmit: true }) 44 | .subscribe(touched => { 45 | if (touched) { 46 | this.renderer.addClass(this.el.nativeElement, 'sw-touched'); 47 | this.renderer.removeClass(this.el.nativeElement, 'sw-untouched'); 48 | } else { 49 | this.renderer.addClass(this.el.nativeElement, 'sw-untouched'); 50 | this.renderer.removeClass(this.el.nativeElement, 'sw-touched'); 51 | } 52 | }), 53 | this.control 54 | .observe('submitted', { ignoreNoEmit: true }) 55 | .subscribe(submitted => { 56 | if (submitted) { 57 | this.renderer.addClass(this.el.nativeElement, 'sw-submitted'); 58 | this.renderer.removeClass(this.el.nativeElement, 'sw-unsubmitted'); 59 | } else { 60 | this.renderer.addClass(this.el.nativeElement, 'sw-unsubmitted'); 61 | this.renderer.removeClass(this.el.nativeElement, 'sw-submitted'); 62 | } 63 | }), 64 | this.control 65 | .observe('changed', { ignoreNoEmit: true }) 66 | .subscribe(changed => { 67 | if (changed) { 68 | this.renderer.addClass(this.el.nativeElement, 'sw-changed'); 69 | this.renderer.removeClass(this.el.nativeElement, 'sw-unchanged'); 70 | } else { 71 | this.renderer.addClass(this.el.nativeElement, 'sw-unchanged'); 72 | this.renderer.removeClass(this.el.nativeElement, 'sw-changed'); 73 | } 74 | }), 75 | ); 76 | } 77 | 78 | ngOnDestroy() { 79 | this.onChangesSubscriptions.forEach(sub => sub.unsubscribe()); 80 | this.subscriptions.forEach(sub => sub.unsubscribe()); 81 | } 82 | 83 | protected assertValidValueMapper(name: string, mapper?: ControlValueMapper) { 84 | if (!mapper) return; 85 | 86 | if (typeof mapper !== 'object') { 87 | throw new Error(`${name} expected an object`); 88 | } 89 | 90 | if (typeof mapper.toControl !== 'function') { 91 | throw new Error(`${name} expected to have a "toControl" mapper function`); 92 | } 93 | 94 | if (typeof mapper.toAccessor !== 'function') { 95 | throw new Error( 96 | `${name} expected to have a "toAccessor" mapper function`, 97 | ); 98 | } 99 | 100 | if ( 101 | mapper.accessorValidator && 102 | typeof mapper.accessorValidator !== 'function' 103 | ) { 104 | throw new Error( 105 | `${name} optional "accessorValidator" expected to be a function`, 106 | ); 107 | } 108 | } 109 | 110 | protected toProvidedControlMapFn() { 111 | if (this.valueMapper) { 112 | const valueMapper = this.valueMapper; 113 | 114 | return (event: ControlEvent) => { 115 | if (isValueStateChange(event)) { 116 | const changes = new Map(event.changes); 117 | 118 | changes.set( 119 | 'value', 120 | valueMapper.toControl(event.changes.get('value')), 121 | ); 122 | 123 | return { 124 | ...event, 125 | changes, 126 | }; 127 | } 128 | 129 | return event; 130 | }; 131 | } 132 | 133 | return (event: ControlEvent) => event; 134 | } 135 | 136 | protected toAccessorControlMapFn() { 137 | if (this.valueMapper) { 138 | const valueMapper = this.valueMapper; 139 | 140 | return (event: ControlEvent) => { 141 | if (isValueStateChange(event)) { 142 | const changes = new Map(event.changes); 143 | 144 | changes.set( 145 | 'value', 146 | valueMapper.toAccessor(event.changes.get('value')), 147 | ); 148 | 149 | return { 150 | ...event, 151 | changes, 152 | }; 153 | } 154 | 155 | return event; 156 | }; 157 | } 158 | 159 | return (event: ControlEvent) => event; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/src/lib/directives/control-name.directive.ts: -------------------------------------------------------------------------------- 1 | import { Input, OnDestroy, OnChanges, SimpleChange } from '@angular/core'; 2 | import { AbstractControl } from '../models'; 3 | import { ControlValueMapper, ControlAccessorEvent } from './interface'; 4 | import { map, filter } from 'rxjs/operators'; 5 | import { NgBaseDirective } from './base.directive'; 6 | import { ControlAccessor, ControlContainerAccessor } from '../accessors'; 7 | import { concat, Subscription } from 'rxjs'; 8 | 9 | export abstract class NgControlNameDirective 10 | extends NgBaseDirective 11 | implements ControlAccessor, OnChanges, OnDestroy { 12 | abstract controlName: string; 13 | 14 | valueMapper?: ControlValueMapper; 15 | 16 | abstract readonly control: T; 17 | protected abstract containerAccessor: ControlContainerAccessor; 18 | 19 | protected innerSubscriptions: Subscription[] = []; 20 | 21 | ngOnChanges(_: { controlName?: SimpleChange; valueMapper?: SimpleChange }) { 22 | if (!this.controlName) { 23 | throw new Error( 24 | `NgFormControlNameDirective must be passed a ngFormControlName`, 25 | ); 26 | } 27 | 28 | this.cleanupInnerSubs(); 29 | this.onChangesSubscriptions.forEach(sub => sub.unsubscribe()); 30 | this.onChangesSubscriptions = []; 31 | 32 | this.onChangesSubscriptions.push( 33 | this.containerAccessor.control 34 | .observe('controls', this.controlName, { ignoreNoEmit: true }) 35 | .subscribe((providedControl: T) => { 36 | this.cleanupInnerSubs(); 37 | 38 | if (providedControl) { 39 | this.validateProvidedControl(providedControl); 40 | 41 | this.control.emitEvent({ 42 | type: 'ControlAccessor', 43 | label: 'PreInit', 44 | }); 45 | 46 | this.innerSubscriptions.push( 47 | concat(providedControl.replayState(), providedControl.events) 48 | .pipe(map(this.toAccessorControlMapFn())) 49 | .subscribe(this.control.source), 50 | ); 51 | 52 | if (this.valueMapper && this.valueMapper.accessorValidator) { 53 | const validator = this.valueMapper.accessorValidator; 54 | 55 | this.control.setErrors(validator(this.control), { 56 | source: this.accessorValidatorId, 57 | }); 58 | 59 | // validate the control via a service to avoid the possibility 60 | // of the user somehow deleting our validator function. 61 | this.onChangesSubscriptions.push( 62 | this.control.validationEvents 63 | .pipe(filter(({ label }) => label === 'InternalComplete')) 64 | .subscribe(() => { 65 | this.control.setErrors(validator(this.control), { 66 | source: this.accessorValidatorId, 67 | }); 68 | }), 69 | ); 70 | } else { 71 | this.control.setErrors(null, { 72 | source: this.accessorValidatorId, 73 | }); 74 | } 75 | 76 | this.innerSubscriptions.push( 77 | this.control.events 78 | .pipe(map(this.toProvidedControlMapFn())) 79 | .subscribe(providedControl.source), 80 | ); 81 | 82 | this.control.emitEvent({ 83 | type: 'ControlAccessor', 84 | label: 'PostInit', 85 | }); 86 | } 87 | }), 88 | ); 89 | } 90 | 91 | ngOnDestroy() { 92 | super.ngOnDestroy(); 93 | 94 | this.cleanupInnerSubs(); 95 | } 96 | 97 | protected abstract validateProvidedControl(control: any): control is T; 98 | 99 | protected cleanupInnerSubs() { 100 | this.innerSubscriptions.forEach(sub => sub.unsubscribe()); 101 | 102 | this.control.emitEvent({ 103 | type: 'ControlAccessor', 104 | label: 'Cleanup', 105 | }); 106 | 107 | this.innerSubscriptions = []; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/src/lib/directives/control.directive.ts: -------------------------------------------------------------------------------- 1 | import { OnDestroy, OnChanges, SimpleChange } from '@angular/core'; 2 | import { AbstractControl } from '../models'; 3 | import { ControlValueMapper, ControlAccessorEvent } from './interface'; 4 | import { map, filter } from 'rxjs/operators'; 5 | import { NgBaseDirective } from './base.directive'; 6 | import { ControlAccessor } from '../accessors'; 7 | import { concat } from 'rxjs'; 8 | 9 | export abstract class NgControlDirective 10 | extends NgBaseDirective 11 | implements ControlAccessor, OnChanges, OnDestroy { 12 | abstract providedControl: T; 13 | 14 | valueMapper?: ControlValueMapper; 15 | 16 | abstract readonly control: T; 17 | 18 | ngOnChanges(_: { 19 | providedControl?: SimpleChange; 20 | valueMapper?: SimpleChange; 21 | }) { 22 | this.onChangesSubscriptions.forEach(sub => sub.unsubscribe()); 23 | this.onChangesSubscriptions = []; 24 | 25 | this.control.emitEvent({ 26 | type: 'ControlAccessor', 27 | label: 'Cleanup', 28 | }); 29 | 30 | this.control.emitEvent({ 31 | type: 'ControlAccessor', 32 | label: 'PreInit', 33 | }); 34 | 35 | this.onChangesSubscriptions.push( 36 | concat(this.providedControl.replayState(), this.providedControl.events) 37 | .pipe(map(this.toAccessorControlMapFn())) 38 | .subscribe(this.control.source), 39 | ); 40 | 41 | if (this.valueMapper && this.valueMapper.accessorValidator) { 42 | const validator = this.valueMapper.accessorValidator; 43 | 44 | this.control.setErrors(validator(this.control), { 45 | source: this.accessorValidatorId, 46 | }); 47 | 48 | // validate the control via a service to avoid the possibility 49 | // of the user somehow deleting our validator function. 50 | this.onChangesSubscriptions.push( 51 | this.control.validationEvents 52 | .pipe(filter(({ label }) => label === 'InternalComplete')) 53 | .subscribe(() => { 54 | this.control.setErrors(validator(this.control), { 55 | source: this.accessorValidatorId, 56 | }); 57 | }), 58 | ); 59 | } else { 60 | this.control.setErrors(null, { 61 | source: this.accessorValidatorId, 62 | }); 63 | } 64 | 65 | this.onChangesSubscriptions.push( 66 | this.control.events 67 | .pipe(map(this.toProvidedControlMapFn())) 68 | .subscribe(this.providedControl.source), 69 | ); 70 | 71 | this.control.emitEvent({ 72 | type: 'ControlAccessor', 73 | label: 'PostInit', 74 | }); 75 | } 76 | 77 | ngOnDestroy() { 78 | super.ngOnDestroy(); 79 | 80 | this.control.emitEvent({ 81 | type: 'ControlAccessor', 82 | label: 'Cleanup', 83 | }); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/src/lib/directives/form-array-name.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Input, 3 | OnDestroy, 4 | OnChanges, 5 | Directive, 6 | Inject, 7 | SimpleChange, 8 | SkipSelf, 9 | Renderer2, 10 | ElementRef, 11 | forwardRef, 12 | } from '@angular/core'; 13 | import { FormArray } from '../models'; 14 | import { ControlValueMapper } from './interface'; 15 | import { NG_CONTROL_DIRECTIVE } from './base.directive'; 16 | import { 17 | ControlAccessor, 18 | NG_CONTROL_CONTAINER_ACCESSOR, 19 | ControlContainerAccessor, 20 | } from '../accessors'; 21 | import { NgControlNameDirective } from './control-name.directive'; 22 | 23 | @Directive({ 24 | selector: '[ngFormArrayName]', 25 | exportAs: 'ngForm', 26 | providers: [ 27 | { 28 | provide: NG_CONTROL_DIRECTIVE, 29 | useExisting: forwardRef(() => NgFormArrayNameDirective), 30 | }, 31 | { 32 | provide: NG_CONTROL_CONTAINER_ACCESSOR, 33 | useExisting: forwardRef(() => NgFormArrayNameDirective), 34 | }, 35 | ], 36 | }) 37 | export class NgFormArrayNameDirective extends NgControlNameDirective 38 | implements ControlAccessor, OnChanges, OnDestroy { 39 | static id = 0; 40 | 41 | @Input('ngFormArrayName') controlName!: string; 42 | @Input('ngFormArrayValueMapper') 43 | valueMapper: ControlValueMapper | undefined; 44 | 45 | readonly control = new FormArray( 46 | {}, 47 | { 48 | id: Symbol(`NgFormArrayNameDirective-${NgFormArrayNameDirective.id++}`), 49 | }, 50 | ); 51 | 52 | constructor( 53 | @SkipSelf() 54 | @Inject(NG_CONTROL_CONTAINER_ACCESSOR) 55 | protected containerAccessor: ControlContainerAccessor, 56 | renderer: Renderer2, 57 | el: ElementRef, 58 | ) { 59 | super(renderer, el); 60 | } 61 | 62 | ngOnChanges(_: { controlName?: SimpleChange; valueMapper?: SimpleChange }) { 63 | if (!this.controlName) { 64 | throw new Error( 65 | `NgFormArrayNameDirective must be passed a ngFormControlName`, 66 | ); 67 | } 68 | 69 | this.assertValidValueMapper( 70 | 'NgFormArrayNameDirective#ngFormControlValueMapper', 71 | this.valueMapper, 72 | ); 73 | 74 | super.ngOnChanges(_); 75 | } 76 | 77 | protected validateProvidedControl(control: any): control is FormArray { 78 | if (!(control instanceof FormArray)) { 79 | throw new Error( 80 | 'NgFormArrayNameDirective must link to an instance of FormArray', 81 | ); 82 | } 83 | 84 | return true; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/src/lib/directives/form-array.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | OnChanges, 3 | Directive, 4 | SimpleChange, 5 | forwardRef, 6 | Self, 7 | Inject, 8 | Renderer2, 9 | ElementRef, 10 | Optional, 11 | Input, 12 | } from '@angular/core'; 13 | import { FormArray } from '../models'; 14 | import { NG_CONTROL_DIRECTIVE } from './base.directive'; 15 | import { resolveControlContainerAccessor } from './util'; 16 | import { 17 | NG_CONTROL_CONTAINER_ACCESSOR, 18 | ControlContainerAccessor, 19 | NG_CONTROL_ACCESSOR, 20 | ControlAccessor, 21 | } from '../accessors'; 22 | import { NgControlDirective } from './control.directive'; 23 | import { ControlValueMapper } from './interface'; 24 | import { concat } from 'rxjs'; 25 | 26 | @Directive({ 27 | selector: '[ngFormArray]', 28 | exportAs: 'ngForm', 29 | providers: [ 30 | { 31 | provide: NG_CONTROL_DIRECTIVE, 32 | useExisting: forwardRef(() => NgFormArrayDirective), 33 | }, 34 | { 35 | provide: NG_CONTROL_CONTAINER_ACCESSOR, 36 | useExisting: forwardRef(() => NgFormArrayDirective), 37 | }, 38 | ], 39 | }) 40 | export class NgFormArrayDirective extends NgControlDirective 41 | implements OnChanges { 42 | static id = 0; 43 | @Input('ngFormArray') providedControl!: FormArray; 44 | @Input('ngFormArrayValueMapper') 45 | valueMapper: ControlValueMapper | undefined; 46 | 47 | readonly control = new FormArray( 48 | {}, 49 | { 50 | id: Symbol(`NgFormArrayDirective-${NgFormArrayDirective.id++}`), 51 | }, 52 | ); 53 | 54 | readonly accessor: ControlContainerAccessor | null; 55 | 56 | constructor( 57 | @Optional() 58 | @Self() 59 | @Inject(NG_CONTROL_ACCESSOR) 60 | accessors: ControlAccessor[] | null, 61 | renderer: Renderer2, 62 | el: ElementRef, 63 | ) { 64 | super(renderer, el); 65 | 66 | this.accessor = accessors && resolveControlContainerAccessor(accessors); 67 | 68 | if (this.accessor) { 69 | this.subscriptions.push( 70 | concat( 71 | this.accessor.control.replayState(), 72 | this.accessor.control.events, 73 | ).subscribe(this.control.source), 74 | this.control.events.subscribe(this.accessor.control.source), 75 | ); 76 | } 77 | } 78 | 79 | ngOnChanges(_: { 80 | providedControl?: SimpleChange; 81 | valueMapper?: SimpleChange; 82 | }) { 83 | if (!this.providedControl) { 84 | throw new Error(`NgFormArrayDirective must be passed a ngFormArray`); 85 | } 86 | 87 | this.assertValidValueMapper( 88 | 'NgFormArrayDirective#ngFormArrayValueMapper', 89 | this.valueMapper, 90 | ); 91 | 92 | super.ngOnChanges(_); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/src/lib/directives/form-control-name.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Input, 3 | OnDestroy, 4 | OnChanges, 5 | Directive, 6 | Inject, 7 | Self, 8 | SimpleChange, 9 | SkipSelf, 10 | Renderer2, 11 | ElementRef, 12 | forwardRef, 13 | } from '@angular/core'; 14 | import { concat } from 'rxjs'; 15 | import { FormControl } from '../models'; 16 | import { ControlValueMapper } from './interface'; 17 | import { NG_CONTROL_DIRECTIVE } from './base.directive'; 18 | import { 19 | ControlAccessor, 20 | NG_CONTROL_ACCESSOR, 21 | NG_CONTROL_CONTAINER_ACCESSOR, 22 | ControlContainerAccessor, 23 | } from '../accessors'; 24 | import { resolveControlAccessor } from './util'; 25 | import { NgControlNameDirective } from './control-name.directive'; 26 | 27 | @Directive({ 28 | selector: '[ngFormControlName]:not([formControl])', 29 | exportAs: 'ngForm', 30 | providers: [ 31 | { 32 | provide: NG_CONTROL_DIRECTIVE, 33 | useExisting: forwardRef(() => NgFormControlNameDirective), 34 | }, 35 | ], 36 | }) 37 | export class NgFormControlNameDirective 38 | extends NgControlNameDirective 39 | implements ControlAccessor, OnChanges, OnDestroy { 40 | static id = 0; 41 | 42 | @Input('ngFormControlName') controlName!: string; 43 | @Input('ngFormControlValueMapper') 44 | valueMapper: ControlValueMapper | undefined; 45 | 46 | readonly control = new FormControl({ 47 | id: Symbol(`NgFormControlNameDirective-${NgFormControlNameDirective.id++}`), 48 | }); 49 | 50 | readonly accessor: ControlAccessor; 51 | 52 | constructor( 53 | @Self() 54 | @Inject(NG_CONTROL_ACCESSOR) 55 | accessors: ControlAccessor[], 56 | @SkipSelf() 57 | @Inject(NG_CONTROL_CONTAINER_ACCESSOR) 58 | protected containerAccessor: ControlContainerAccessor, 59 | renderer: Renderer2, 60 | el: ElementRef, 61 | ) { 62 | super(renderer, el); 63 | 64 | this.accessor = resolveControlAccessor(accessors); 65 | 66 | this.subscriptions.push( 67 | concat( 68 | this.accessor.control.replayState(), 69 | this.accessor.control.events, 70 | ).subscribe(this.control.source), 71 | this.control.events.subscribe(this.accessor.control.source), 72 | ); 73 | } 74 | 75 | ngOnChanges(_: { controlName?: SimpleChange; valueMapper?: SimpleChange }) { 76 | if (!this.controlName) { 77 | throw new Error( 78 | `NgFormControlNameDirective must be passed a ngFormControlName`, 79 | ); 80 | } 81 | 82 | this.assertValidValueMapper( 83 | 'NgFormControlNameDirective#ngFormControlValueMapper', 84 | this.valueMapper, 85 | ); 86 | 87 | super.ngOnChanges(_); 88 | } 89 | 90 | protected validateProvidedControl(control: any): control is FormControl { 91 | if (!(control instanceof FormControl)) { 92 | throw new Error( 93 | 'NgFormControlNameDirective must link to an instance of FormControl', 94 | ); 95 | } 96 | 97 | return true; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/src/lib/directives/form-control.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | OnDestroy, 3 | OnChanges, 4 | Directive, 5 | Inject, 6 | Self, 7 | SimpleChange, 8 | Renderer2, 9 | ElementRef, 10 | forwardRef, 11 | Input, 12 | } from '@angular/core'; 13 | import { FormControl } from '../models'; 14 | import { NG_CONTROL_DIRECTIVE } from './base.directive'; 15 | import { resolveControlAccessor } from './util'; 16 | import { ControlAccessor, NG_CONTROL_ACCESSOR } from '../accessors'; 17 | import { NgControlDirective } from './control.directive'; 18 | import { ControlValueMapper } from './interface'; 19 | import { concat } from 'rxjs'; 20 | 21 | @Directive({ 22 | selector: '[ngFormControl]:not([formControl])', 23 | exportAs: 'ngForm', 24 | providers: [ 25 | { 26 | provide: NG_CONTROL_DIRECTIVE, 27 | useExisting: forwardRef(() => NgFormControlDirective), 28 | }, 29 | ], 30 | }) 31 | export class NgFormControlDirective extends NgControlDirective 32 | implements ControlAccessor, OnChanges, OnDestroy { 33 | static id = 0; 34 | @Input('ngFormControl') providedControl!: FormControl; 35 | @Input('ngFormControlValueMapper') 36 | valueMapper: ControlValueMapper | undefined; 37 | 38 | readonly control = new FormControl(null, { 39 | id: Symbol(`NgFormControlDirective-${NgFormControlDirective.id++}`), 40 | }); 41 | 42 | readonly accessor: ControlAccessor; 43 | 44 | constructor( 45 | @Self() 46 | @Inject(NG_CONTROL_ACCESSOR) 47 | accessors: ControlAccessor[], 48 | renderer: Renderer2, 49 | el: ElementRef, 50 | ) { 51 | super(renderer, el); 52 | 53 | this.accessor = resolveControlAccessor(accessors); 54 | 55 | this.subscriptions.push( 56 | concat( 57 | this.accessor.control.replayState(), 58 | this.accessor.control.events, 59 | ).subscribe(this.control.source), 60 | this.control.events.subscribe(this.accessor.control.source), 61 | ); 62 | } 63 | 64 | ngOnChanges(_: { 65 | providedControl?: SimpleChange; 66 | valueMapper?: SimpleChange; 67 | }) { 68 | if (!this.providedControl) { 69 | throw new Error(`NgFormControlDirective must be passed a ngFormControl`); 70 | } 71 | 72 | this.assertValidValueMapper( 73 | 'NgFormControlDirective#ngFormControlValueMapper', 74 | this.valueMapper, 75 | ); 76 | 77 | super.ngOnChanges(_); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/src/lib/directives/form-group-name.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Input, 3 | OnDestroy, 4 | OnChanges, 5 | Directive, 6 | Inject, 7 | SimpleChange, 8 | SkipSelf, 9 | Renderer2, 10 | ElementRef, 11 | forwardRef, 12 | } from '@angular/core'; 13 | import { FormGroup } from '../models'; 14 | import { ControlValueMapper } from './interface'; 15 | import { NG_CONTROL_DIRECTIVE } from './base.directive'; 16 | import { 17 | ControlAccessor, 18 | NG_CONTROL_CONTAINER_ACCESSOR, 19 | ControlContainerAccessor, 20 | } from '../accessors'; 21 | import { NgControlNameDirective } from './control-name.directive'; 22 | 23 | @Directive({ 24 | selector: '[ngFormGroupName]', 25 | exportAs: 'ngForm', 26 | providers: [ 27 | { 28 | provide: NG_CONTROL_DIRECTIVE, 29 | useExisting: forwardRef(() => NgFormGroupNameDirective), 30 | }, 31 | { 32 | provide: NG_CONTROL_CONTAINER_ACCESSOR, 33 | useExisting: forwardRef(() => NgFormGroupNameDirective), 34 | }, 35 | ], 36 | }) 37 | export class NgFormGroupNameDirective extends NgControlNameDirective 38 | implements ControlAccessor, OnChanges, OnDestroy { 39 | static id = 0; 40 | 41 | @Input('ngFormGroupName') controlName!: string; 42 | @Input('ngFormGroupValueMapper') 43 | valueMapper: ControlValueMapper | undefined; 44 | 45 | readonly control = new FormGroup( 46 | {}, 47 | { 48 | id: Symbol(`NgFormGroupNameDirective-${NgFormGroupNameDirective.id++}`), 49 | }, 50 | ); 51 | 52 | constructor( 53 | @SkipSelf() 54 | @Inject(NG_CONTROL_CONTAINER_ACCESSOR) 55 | protected containerAccessor: ControlContainerAccessor, 56 | renderer: Renderer2, 57 | el: ElementRef, 58 | ) { 59 | super(renderer, el); 60 | } 61 | 62 | ngOnChanges(_: { controlName?: SimpleChange; valueMapper?: SimpleChange }) { 63 | if (!this.controlName) { 64 | throw new Error( 65 | `NgFormGroupNameDirective must be passed a ngFormControlName`, 66 | ); 67 | } 68 | 69 | this.assertValidValueMapper( 70 | 'NgFormGroupNameDirective#ngFormControlValueMapper', 71 | this.valueMapper, 72 | ); 73 | 74 | super.ngOnChanges(_); 75 | } 76 | 77 | protected validateProvidedControl(control: any): control is FormGroup { 78 | if (!(control instanceof FormGroup)) { 79 | throw new Error( 80 | 'NgFormGroupNameDirective must link to an instance of FormGroup', 81 | ); 82 | } 83 | 84 | return true; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/src/lib/directives/form-group.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | OnChanges, 3 | Directive, 4 | SimpleChange, 5 | forwardRef, 6 | Self, 7 | Inject, 8 | Renderer2, 9 | ElementRef, 10 | Optional, 11 | Input, 12 | } from '@angular/core'; 13 | import { FormGroup } from '../models'; 14 | import { NG_CONTROL_DIRECTIVE } from './base.directive'; 15 | import { resolveControlContainerAccessor } from './util'; 16 | import { 17 | NG_CONTROL_CONTAINER_ACCESSOR, 18 | ControlContainerAccessor, 19 | NG_CONTROL_ACCESSOR, 20 | ControlAccessor, 21 | } from '../accessors'; 22 | import { NgControlDirective } from './control.directive'; 23 | import { ControlValueMapper } from './interface'; 24 | import { concat } from 'rxjs'; 25 | 26 | @Directive({ 27 | selector: '[ngFormGroup]', 28 | exportAs: 'ngForm', 29 | providers: [ 30 | { 31 | provide: NG_CONTROL_DIRECTIVE, 32 | useExisting: forwardRef(() => NgFormGroupDirective), 33 | }, 34 | { 35 | provide: NG_CONTROL_CONTAINER_ACCESSOR, 36 | useExisting: forwardRef(() => NgFormGroupDirective), 37 | }, 38 | ], 39 | }) 40 | export class NgFormGroupDirective extends NgControlDirective 41 | implements OnChanges { 42 | static id = 0; 43 | @Input('ngFormGroup') providedControl!: FormGroup; 44 | @Input('ngFormGroupValueMapper') 45 | valueMapper: ControlValueMapper | undefined; 46 | 47 | readonly control = new FormGroup( 48 | {}, 49 | { 50 | id: Symbol(`NgFormGroupDirective-${NgFormGroupDirective.id++}`), 51 | }, 52 | ); 53 | 54 | readonly accessor: ControlContainerAccessor | null; 55 | 56 | constructor( 57 | @Optional() 58 | @Self() 59 | @Inject(NG_CONTROL_ACCESSOR) 60 | accessors: ControlAccessor[] | null, 61 | renderer: Renderer2, 62 | el: ElementRef, 63 | ) { 64 | super(renderer, el); 65 | 66 | this.accessor = accessors && resolveControlContainerAccessor(accessors); 67 | 68 | if (this.accessor) { 69 | this.subscriptions.push( 70 | concat( 71 | this.accessor.control.replayState(), 72 | this.accessor.control.events, 73 | ).subscribe(this.control.source), 74 | this.control.events.subscribe(this.accessor.control.source), 75 | ); 76 | } 77 | } 78 | 79 | ngOnChanges(_: { 80 | providedControl?: SimpleChange; 81 | valueMapper?: SimpleChange; 82 | }) { 83 | if (!this.providedControl) { 84 | throw new Error(`NgFormGroupDirective must be passed a ngFormGroup`); 85 | } 86 | 87 | this.assertValidValueMapper( 88 | 'NgFormGroupDirective#ngFormGroupValueMapper', 89 | this.valueMapper, 90 | ); 91 | 92 | super.ngOnChanges(_); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/src/lib/directives/form.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { NgFormControlNameDirective } from './form-control-name.directive'; 3 | import { NgFormGroupDirective } from './form-group.directive'; 4 | import { NgFormControlDirective } from './form-control.directive'; 5 | import { NgFormGroupNameDirective } from './form-group-name.directive'; 6 | import { AccessorsModule } from '../accessors'; 7 | import { NgFormArrayDirective } from './form-array.directive'; 8 | import { NgFormArrayNameDirective } from './form-array-name.directive'; 9 | 10 | @NgModule({ 11 | imports: [AccessorsModule], 12 | providers: [], 13 | declarations: [ 14 | NgFormControlDirective, 15 | NgFormControlNameDirective, 16 | NgFormGroupDirective, 17 | NgFormGroupNameDirective, 18 | NgFormArrayDirective, 19 | NgFormArrayNameDirective, 20 | ], 21 | exports: [ 22 | AccessorsModule, 23 | NgFormControlDirective, 24 | NgFormControlNameDirective, 25 | NgFormGroupDirective, 26 | NgFormGroupNameDirective, 27 | NgFormArrayDirective, 28 | NgFormArrayNameDirective, 29 | ], 30 | }) 31 | export class ReactiveFormsModule2 {} 32 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/src/lib/directives/index.ts: -------------------------------------------------------------------------------- 1 | export * from './form.module'; 2 | export * from './interface'; 3 | export * from './form-control-name.directive'; 4 | export * from './form-control.directive'; 5 | export * from './form-group.directive'; 6 | export * from './form-group-name.directive'; 7 | export * from './form-array.directive'; 8 | export * from './form-array-name.directive'; 9 | export { 10 | NgControlNameDirective as ɵNgControlNameDirective, 11 | } from './control-name.directive'; 12 | export { NgControlDirective as ɵNgControlDirective } from './control.directive'; 13 | export { NG_CONTROL_DIRECTIVE } from './base.directive'; 14 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/src/lib/directives/interface.ts: -------------------------------------------------------------------------------- 1 | import { ControlEvent, ValidatorFn } from '../models'; 2 | 3 | export interface ControlValueMapper { 4 | toControl: (value: AccessorValue) => ControlValue; 5 | toAccessor: (value: ControlValue) => AccessorValue; 6 | accessorValidator?: ValidatorFn; 7 | } 8 | 9 | export interface ControlAccessorEvent extends ControlEvent { 10 | type: 'ControlAccessor'; 11 | label: 'Cleanup' | 'PreInit' | 'PostInit'; 12 | } 13 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/src/lib/directives/util.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ControlAccessor, 3 | DefaultValueAccessor, 4 | ControlContainerAccessor, 5 | } from '../accessors'; 6 | import { ControlContainer, PartialControlEvent, StateChange } from '../models'; 7 | 8 | const STD_ACCESSORS: ControlAccessor[] = []; 9 | 10 | export function resolveControlAccessor(accessors: ControlAccessor[]) { 11 | if (accessors.length > 3) { 12 | throw new Error( 13 | 'Too many accessors found. Can only resolve a single custom accessor', 14 | ); 15 | } else if (accessors.length === 3) { 16 | const customAccessor = accessors.find( 17 | acc => 18 | !STD_ACCESSORS.includes(acc) && !(acc instanceof DefaultValueAccessor), 19 | ); 20 | 21 | if (!customAccessor) { 22 | throw new Error( 23 | 'Error resolving accessor. Expected to find custom accessor', 24 | ); 25 | } 26 | 27 | return customAccessor; 28 | } else if (accessors.length === 2) { 29 | const customAccessor = accessors.find( 30 | acc => 31 | !STD_ACCESSORS.includes(acc) && !(acc instanceof DefaultValueAccessor), 32 | ); 33 | 34 | if (customAccessor) { 35 | return customAccessor; 36 | } 37 | 38 | const stdAccessor = accessors.find(acc => STD_ACCESSORS.includes(acc)); 39 | 40 | if (!stdAccessor) { 41 | throw new Error( 42 | 'Error resolving accessor. Expected to find std accessor', 43 | ); 44 | } 45 | 46 | return stdAccessor; 47 | } else if (accessors.length === 1) { 48 | return accessors[0]; 49 | } else { 50 | throw new Error('Could not find control accessor'); 51 | } 52 | } 53 | 54 | export function resolveControlContainerAccessor( 55 | accessors: ControlAccessor[], 56 | ): ControlContainerAccessor { 57 | const containerAccessors = accessors.filter(acc => 58 | ControlContainer.isControlContainer(acc), 59 | ); 60 | 61 | if (containerAccessors.length > 1) { 62 | console.error('containerAccessors', containerAccessors); 63 | throw new Error( 64 | `Error resolving container accessor. Expected ` + 65 | `to find 0 or 1 but found ${containerAccessors.length}`, 66 | ); 67 | } else if (containerAccessors.length === 1) { 68 | return accessors[0] as ControlContainerAccessor; 69 | } else { 70 | throw new Error('Could not find control container accessor'); 71 | } 72 | } 73 | 74 | export function isStateChange( 75 | event: PartialControlEvent, 76 | ): event is StateChange { 77 | return event.type === 'StateChange'; 78 | } 79 | 80 | export function isValueStateChange( 81 | event: PartialControlEvent, 82 | ): event is StateChange { 83 | return ( 84 | event.type === 'StateChange' && (event as StateChange).changes.has('value') 85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/src/lib/models/abstract-control.ts: -------------------------------------------------------------------------------- 1 | import { Observable, Subject } from 'rxjs'; 2 | 3 | export interface ValidationErrors { 4 | [key: string]: any; 5 | } 6 | 7 | export type ValidatorFn = ( 8 | control: AbstractControl, 9 | ) => ValidationErrors | null; 10 | 11 | export type AbstractControlValue = T extends AbstractControl 12 | ? V 13 | : any; 14 | export type AbstractControlData = T extends AbstractControl 15 | ? D 16 | : any; 17 | 18 | export type ControlId = string | symbol; 19 | 20 | export interface PartialControlEvent { 21 | id?: string; 22 | source: ControlId; 23 | readonly processed: ControlId[]; 24 | type: string; 25 | meta?: { [key: string]: any }; 26 | noEmit?: boolean; 27 | } 28 | 29 | export interface ControlEvent extends PartialControlEvent { 30 | id: string; 31 | meta: { [key: string]: any }; 32 | } 33 | 34 | export interface ValidationEvent extends ControlEvent { 35 | type: 'Validation'; 36 | label: string; 37 | } 38 | 39 | export interface ControlEventOptions { 40 | noEmit?: boolean; 41 | meta?: { [key: string]: any }; 42 | eventId?: string; 43 | source?: ControlId; 44 | processed?: ControlId[]; 45 | } 46 | 47 | export type DeepReadonly = T extends Array 48 | ? DeepReadonlyArray 49 | : T extends Function 50 | ? T 51 | : T extends object 52 | ? DeepReadonlyObject 53 | : T; 54 | 55 | interface DeepReadonlyArray extends ReadonlyArray> {} 56 | 57 | type DeepReadonlyObject = { 58 | readonly [P in keyof T]: DeepReadonly; 59 | }; 60 | 61 | /** 62 | * ControlSource is a special rxjs Subject which never 63 | * completes. 64 | */ 65 | export class ControlSource extends Subject { 66 | /** NOOP: Complete does nothing */ 67 | complete() {} 68 | } 69 | 70 | let _eventId = 0; 71 | 72 | export namespace AbstractControl { 73 | export const ABSTRACT_CONTROL_INTERFACE = Symbol( 74 | '@@AbstractControlInterface', 75 | ); 76 | export function eventId() { 77 | return (_eventId++).toString(); 78 | } 79 | export function isAbstractControl(object?: any): object is AbstractControl { 80 | return ( 81 | typeof object === 'object' && 82 | typeof object[AbstractControl.ABSTRACT_CONTROL_INTERFACE] === 83 | 'function' && 84 | object[AbstractControl.ABSTRACT_CONTROL_INTERFACE]() === object 85 | ); 86 | } 87 | } 88 | 89 | export interface AbstractControl { 90 | /** 91 | * The ID is used to determine where StateChanges originated, 92 | * and to ensure that a given AbstractControl only processes 93 | * values one time. 94 | */ 95 | readonly id: ControlId; 96 | 97 | data: Data; 98 | 99 | /** 100 | * **Warning!** Do not use this property unless you know what you are doing. 101 | * 102 | * A control's `source` is the source of truth for the control. Events emitted 103 | * by the source are used to update the control's values. By passing events to 104 | * this control's source, you can programmatically control every aspect of 105 | * of this control. 106 | * 107 | * Never subscribe to the source directly. If you want to receive events for 108 | * this control, subscribe to the `events` observable. 109 | */ 110 | source: ControlSource; 111 | 112 | /** An observable of all events for this AbstractControl */ 113 | events: Observable; 114 | 115 | readonly value: DeepReadonly; 116 | 117 | readonly errors: ValidationErrors | null; 118 | 119 | /** 120 | * A map of validation errors keyed to the source which added them. 121 | */ 122 | readonly errorsStore: ReadonlyMap; 123 | 124 | readonly disabled: boolean; 125 | readonly enabled: boolean; 126 | readonly valid: boolean; 127 | readonly invalid: boolean; 128 | readonly pending: boolean; 129 | 130 | /** 131 | * A map of pending states keyed to the source which added them. 132 | * So long as there are any `true` boolean values, this control's 133 | * `pending` property will be `true`. 134 | */ 135 | readonly pendingStore: ReadonlyMap; 136 | 137 | readonly status: 'DISABLED' | 'PENDING' | 'VALID' | 'INVALID'; 138 | 139 | /** 140 | * focusChanges allows consumers to be notified when this 141 | * form control should be focused or blurred. 142 | */ 143 | focusChanges: Observable; 144 | 145 | /** 146 | * These are special, internal events which signal when this control is 147 | * starting or finishing validation. 148 | * 149 | * These events are not emitted from the `events` observable. 150 | */ 151 | validationEvents: Observable; 152 | 153 | readonly readonly: boolean; 154 | readonly submitted: boolean; 155 | readonly touched: boolean; 156 | readonly changed: boolean; 157 | readonly dirty: boolean; 158 | 159 | /** 160 | * A map of ValidatorFn keyed to the source which added them. 161 | * 162 | * In general, users won't need to access this. But it is exposed for 163 | * advanced usage. 164 | */ 165 | readonly validatorStore: ReadonlyMap; 166 | 167 | /** 168 | * ***Advanced API*** 169 | * 170 | * The "atomic" map is used by controls + parent ControlContainers to ensure 171 | * that parent/child state changes happen atomically before any events are 172 | * emitted. 173 | */ 174 | readonly atomic: Map (() => void) | null>; 175 | 176 | [AbstractControl.ABSTRACT_CONTROL_INTERFACE](): this; 177 | 178 | observeChanges< 179 | A extends keyof this, 180 | B extends keyof this[A], 181 | C extends keyof this[A][B], 182 | D extends keyof this[A][B][C], 183 | E extends keyof this[A][B][C][D], 184 | F extends keyof this[A][B][C][D][E], 185 | G extends keyof this[A][B][C][D][E][F], 186 | H extends keyof this[A][B][C][D][E][F][G], 187 | I extends keyof this[A][B][C][D][E][F][G][H], 188 | J extends keyof this[A][B][C][D][E][F][G][H][I], 189 | K extends keyof this[A][B][C][D][E][F][G][H][I][J] 190 | >( 191 | a: A, 192 | b: B, 193 | c: C, 194 | d: D, 195 | e: E, 196 | f: F, 197 | g: G, 198 | h: H, 199 | i: I, 200 | j: J, 201 | k: K, 202 | options?: { ignoreNoEmit?: boolean }, 203 | ): Observable; 204 | observeChanges< 205 | A extends keyof this, 206 | B extends keyof this[A], 207 | C extends keyof this[A][B], 208 | D extends keyof this[A][B][C], 209 | E extends keyof this[A][B][C][D], 210 | F extends keyof this[A][B][C][D][E], 211 | G extends keyof this[A][B][C][D][E][F], 212 | H extends keyof this[A][B][C][D][E][F][G], 213 | I extends keyof this[A][B][C][D][E][F][G][H], 214 | J extends keyof this[A][B][C][D][E][F][G][H][I] 215 | >( 216 | a: A, 217 | b: B, 218 | c: C, 219 | d: D, 220 | e: E, 221 | f: F, 222 | g: G, 223 | h: H, 224 | i: I, 225 | j: J, 226 | options?: { ignoreNoEmit?: boolean }, 227 | ): Observable; 228 | observeChanges< 229 | A extends keyof this, 230 | B extends keyof this[A], 231 | C extends keyof this[A][B], 232 | D extends keyof this[A][B][C], 233 | E extends keyof this[A][B][C][D], 234 | F extends keyof this[A][B][C][D][E], 235 | G extends keyof this[A][B][C][D][E][F], 236 | H extends keyof this[A][B][C][D][E][F][G], 237 | I extends keyof this[A][B][C][D][E][F][G][H] 238 | >( 239 | a: A, 240 | b: B, 241 | c: C, 242 | d: D, 243 | e: E, 244 | f: F, 245 | g: G, 246 | h: H, 247 | i: I, 248 | options?: { ignoreNoEmit?: boolean }, 249 | ): Observable; 250 | observeChanges< 251 | A extends keyof this, 252 | B extends keyof this[A], 253 | C extends keyof this[A][B], 254 | D extends keyof this[A][B][C], 255 | E extends keyof this[A][B][C][D], 256 | F extends keyof this[A][B][C][D][E], 257 | G extends keyof this[A][B][C][D][E][F], 258 | H extends keyof this[A][B][C][D][E][F][G] 259 | >( 260 | a: A, 261 | b: B, 262 | c: C, 263 | d: D, 264 | e: E, 265 | f: F, 266 | g: G, 267 | h: H, 268 | options?: { ignoreNoEmit?: boolean }, 269 | ): Observable; 270 | observeChanges< 271 | A extends keyof this, 272 | B extends keyof this[A], 273 | C extends keyof this[A][B], 274 | D extends keyof this[A][B][C], 275 | E extends keyof this[A][B][C][D], 276 | F extends keyof this[A][B][C][D][E], 277 | G extends keyof this[A][B][C][D][E][F] 278 | >( 279 | a: A, 280 | b: B, 281 | c: C, 282 | d: D, 283 | e: E, 284 | f: F, 285 | g: G, 286 | options?: { ignoreNoEmit?: boolean }, 287 | ): Observable; 288 | observeChanges< 289 | A extends keyof this, 290 | B extends keyof this[A], 291 | C extends keyof this[A][B], 292 | D extends keyof this[A][B][C], 293 | E extends keyof this[A][B][C][D], 294 | F extends keyof this[A][B][C][D][E] 295 | >( 296 | a: A, 297 | b: B, 298 | c: C, 299 | d: D, 300 | e: E, 301 | f: F, 302 | options?: { ignoreNoEmit?: boolean }, 303 | ): Observable; 304 | observeChanges< 305 | A extends keyof this, 306 | B extends keyof this[A], 307 | C extends keyof this[A][B], 308 | D extends keyof this[A][B][C], 309 | E extends keyof this[A][B][C][D] 310 | >( 311 | a: A, 312 | b: B, 313 | c: C, 314 | d: D, 315 | e: E, 316 | options?: { ignoreNoEmit?: boolean }, 317 | ): Observable; 318 | observeChanges< 319 | A extends keyof this, 320 | B extends keyof this[A], 321 | C extends keyof this[A][B], 322 | D extends keyof this[A][B][C] 323 | >( 324 | a: A, 325 | b: B, 326 | c: C, 327 | d: D, 328 | options?: { ignoreNoEmit?: boolean }, 329 | ): Observable; 330 | observeChanges< 331 | A extends keyof this, 332 | B extends keyof this[A], 333 | C extends keyof this[A][B] 334 | >( 335 | a: A, 336 | b: B, 337 | c: C, 338 | options?: { ignoreNoEmit?: boolean }, 339 | ): Observable; 340 | observeChanges( 341 | a: A, 342 | b: B, 343 | options?: { ignoreNoEmit?: boolean }, 344 | ): Observable; 345 | observeChanges( 346 | a: A, 347 | options?: { ignoreNoEmit?: boolean }, 348 | ): Observable; 349 | observeChanges( 350 | props: string[], 351 | options?: { ignoreNoEmit?: boolean }, 352 | ): Observable; 353 | 354 | observe< 355 | A extends keyof this, 356 | B extends keyof this[A], 357 | C extends keyof this[A][B], 358 | D extends keyof this[A][B][C], 359 | E extends keyof this[A][B][C][D], 360 | F extends keyof this[A][B][C][D][E], 361 | G extends keyof this[A][B][C][D][E][F], 362 | H extends keyof this[A][B][C][D][E][F][G], 363 | I extends keyof this[A][B][C][D][E][F][G][H], 364 | J extends keyof this[A][B][C][D][E][F][G][H][I], 365 | K extends keyof this[A][B][C][D][E][F][G][H][I][J] 366 | >( 367 | a: A, 368 | b: B, 369 | c: C, 370 | d: D, 371 | e: E, 372 | f: F, 373 | g: G, 374 | h: H, 375 | i: I, 376 | j: J, 377 | k: K, 378 | options?: { ignoreNoEmit?: boolean }, 379 | ): Observable; 380 | observe< 381 | A extends keyof this, 382 | B extends keyof this[A], 383 | C extends keyof this[A][B], 384 | D extends keyof this[A][B][C], 385 | E extends keyof this[A][B][C][D], 386 | F extends keyof this[A][B][C][D][E], 387 | G extends keyof this[A][B][C][D][E][F], 388 | H extends keyof this[A][B][C][D][E][F][G], 389 | I extends keyof this[A][B][C][D][E][F][G][H], 390 | J extends keyof this[A][B][C][D][E][F][G][H][I] 391 | >( 392 | a: A, 393 | b: B, 394 | c: C, 395 | d: D, 396 | e: E, 397 | f: F, 398 | g: G, 399 | h: H, 400 | i: I, 401 | j: J, 402 | options?: { ignoreNoEmit?: boolean }, 403 | ): Observable; 404 | observe< 405 | A extends keyof this, 406 | B extends keyof this[A], 407 | C extends keyof this[A][B], 408 | D extends keyof this[A][B][C], 409 | E extends keyof this[A][B][C][D], 410 | F extends keyof this[A][B][C][D][E], 411 | G extends keyof this[A][B][C][D][E][F], 412 | H extends keyof this[A][B][C][D][E][F][G], 413 | I extends keyof this[A][B][C][D][E][F][G][H] 414 | >( 415 | a: A, 416 | b: B, 417 | c: C, 418 | d: D, 419 | e: E, 420 | f: F, 421 | g: G, 422 | h: H, 423 | i: I, 424 | options?: { ignoreNoEmit?: boolean }, 425 | ): Observable; 426 | observe< 427 | A extends keyof this, 428 | B extends keyof this[A], 429 | C extends keyof this[A][B], 430 | D extends keyof this[A][B][C], 431 | E extends keyof this[A][B][C][D], 432 | F extends keyof this[A][B][C][D][E], 433 | G extends keyof this[A][B][C][D][E][F], 434 | H extends keyof this[A][B][C][D][E][F][G] 435 | >( 436 | a: A, 437 | b: B, 438 | c: C, 439 | d: D, 440 | e: E, 441 | f: F, 442 | g: G, 443 | h: H, 444 | options?: { ignoreNoEmit?: boolean }, 445 | ): Observable; 446 | observe< 447 | A extends keyof this, 448 | B extends keyof this[A], 449 | C extends keyof this[A][B], 450 | D extends keyof this[A][B][C], 451 | E extends keyof this[A][B][C][D], 452 | F extends keyof this[A][B][C][D][E], 453 | G extends keyof this[A][B][C][D][E][F] 454 | >( 455 | a: A, 456 | b: B, 457 | c: C, 458 | d: D, 459 | e: E, 460 | f: F, 461 | g: G, 462 | options?: { ignoreNoEmit?: boolean }, 463 | ): Observable; 464 | observe< 465 | A extends keyof this, 466 | B extends keyof this[A], 467 | C extends keyof this[A][B], 468 | D extends keyof this[A][B][C], 469 | E extends keyof this[A][B][C][D], 470 | F extends keyof this[A][B][C][D][E] 471 | >( 472 | a: A, 473 | b: B, 474 | c: C, 475 | d: D, 476 | e: E, 477 | f: F, 478 | options?: { ignoreNoEmit?: boolean }, 479 | ): Observable; 480 | observe< 481 | A extends keyof this, 482 | B extends keyof this[A], 483 | C extends keyof this[A][B], 484 | D extends keyof this[A][B][C], 485 | E extends keyof this[A][B][C][D] 486 | >( 487 | a: A, 488 | b: B, 489 | c: C, 490 | d: D, 491 | e: E, 492 | options?: { ignoreNoEmit?: boolean }, 493 | ): Observable; 494 | 495 | observe< 496 | A extends keyof this, 497 | B extends keyof this[A], 498 | C extends keyof this[A][B], 499 | D extends keyof this[A][B][C] 500 | >( 501 | a: A, 502 | b: B, 503 | c: C, 504 | d: D, 505 | options?: { ignoreNoEmit?: boolean }, 506 | ): Observable; 507 | observe< 508 | A extends keyof this, 509 | B extends keyof this[A], 510 | C extends keyof this[A][B] 511 | >( 512 | a: A, 513 | b: B, 514 | c: C, 515 | options?: { ignoreNoEmit?: boolean }, 516 | ): Observable; 517 | observe( 518 | a: A, 519 | b: B, 520 | options?: { ignoreNoEmit?: boolean }, 521 | ): Observable; 522 | observe( 523 | a: A, 524 | options?: { ignoreNoEmit?: boolean }, 525 | ): Observable; 526 | observe( 527 | props: string[], 528 | options?: { ignoreNoEmit?: boolean }, 529 | ): Observable; 530 | 531 | equalValue(value: Value): value is Value; 532 | 533 | setValue(value: Value, options?: ControlEventOptions): void; 534 | 535 | patchValue(value: any, options?: ControlEventOptions): void; 536 | 537 | /** 538 | * If provided a `ValidationErrors` object or `null`, replaces the errors 539 | * associated with the source ID. 540 | * 541 | * If provided a `Map` object containing `ValidationErrors` keyed to source IDs, 542 | * uses it to replace the `errorsStore` associated with this control. 543 | */ 544 | setErrors( 545 | value: ValidationErrors | null | ReadonlyMap, 546 | options?: ControlEventOptions, 547 | ): void; 548 | 549 | /** 550 | * If provided a `ValidationErrors` object, that object is merged with the 551 | * existing errors associated with the source ID. If the error object has 552 | * properties containing `null`, errors associated with those keys are deleted 553 | * from the `errorsStore`. 554 | * 555 | * If provided a `Map` object containing `ValidationErrors` keyed to source IDs, 556 | * that object is merged with the existing `errorsStore`. 557 | */ 558 | patchErrors( 559 | value: ValidationErrors | ReadonlyMap, 560 | options?: ControlEventOptions, 561 | ): void; 562 | 563 | markTouched(value: boolean, options?: ControlEventOptions): void; 564 | 565 | markChanged(value: boolean, options?: ControlEventOptions): void; 566 | 567 | markReadonly(value: boolean, options?: ControlEventOptions): void; 568 | 569 | markSubmitted(value: boolean, options?: ControlEventOptions): void; 570 | 571 | markPending( 572 | value: boolean | ReadonlyMap, 573 | options?: ControlEventOptions, 574 | ): void; 575 | 576 | markDisabled(value: boolean, options?: ControlEventOptions): void; 577 | 578 | focus(value?: boolean, options?: ControlEventOptions): void; 579 | 580 | setValidators( 581 | value: 582 | | ValidatorFn 583 | | ValidatorFn[] 584 | | null 585 | | ReadonlyMap, 586 | options?: ControlEventOptions, 587 | ): void; 588 | 589 | /** 590 | * Returns an observable of this control's state in the form of 591 | * StateChange objects which can be used to make another control 592 | * identical to this one. This observable will complete upon 593 | * replaying the necessary state changes. 594 | */ 595 | replayState(options?: ControlEventOptions): Observable; 596 | 597 | /** 598 | * A convenience method for emitting an arbitrary control event. 599 | */ 600 | emitEvent< 601 | T extends PartialControlEvent = PartialControlEvent & { [key: string]: any } 602 | >( 603 | event: Partial< 604 | Pick 605 | > & 606 | Omit & { 607 | type: string; 608 | }, 609 | ): void; 610 | } 611 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/src/lib/models/control-container-base.ts: -------------------------------------------------------------------------------- 1 | import { Subscription, concat, Observable, of } from 'rxjs'; 2 | import { map, filter } from 'rxjs/operators'; 3 | import { 4 | AbstractControl, 5 | ControlEventOptions, 6 | DeepReadonly, 7 | ControlEvent, 8 | ControlId, 9 | } from './abstract-control'; 10 | import { ControlContainer } from './control-container'; 11 | import { ControlBase, StateChange } from './control-base'; 12 | import { isTruthy, capitalize } from './util'; 13 | 14 | export abstract class ControlContainerBase 15 | extends ControlBase 16 | implements ControlContainer { 17 | abstract readonly controlsStore: ReadonlyMap; 18 | 19 | protected abstract _controls: Controls; 20 | get controls() { 21 | return this._controls; 22 | } 23 | 24 | protected _size!: number; 25 | get size() { 26 | return this._size; 27 | } 28 | 29 | protected _value!: Value; 30 | get value() { 31 | return this._value as DeepReadonly; 32 | } 33 | 34 | protected _enabledValue!: EnabledValue; 35 | get enabledValue() { 36 | return this._enabledValue as DeepReadonly; 37 | } 38 | 39 | // VALID / INVALID 40 | 41 | get invalid() { 42 | return this.containerInvalid || this.childInvalid; 43 | } 44 | get valid() { 45 | return !this.invalid; 46 | } 47 | 48 | get containerInvalid() { 49 | return !!this.errors; 50 | } 51 | get containerValid() { 52 | return !this.errors; 53 | } 54 | 55 | protected _childInvalid = false; 56 | get childInvalid() { 57 | return this._childInvalid; 58 | } 59 | 60 | protected _childrenInvalid = false; 61 | get childrenInvalid() { 62 | return this._childrenInvalid; 63 | } 64 | 65 | get childValid() { 66 | return !this.childrenInvalid; 67 | } 68 | 69 | get childrenValid() { 70 | return !this.childInvalid; 71 | } 72 | 73 | // DISABLED 74 | 75 | get disabled() { 76 | return this.containerDisabled || this.childrenDisabled; 77 | } 78 | 79 | get containerDisabled() { 80 | return this._disabled; 81 | } 82 | 83 | protected _childDisabled = false; 84 | get childDisabled() { 85 | return this._childDisabled; 86 | } 87 | 88 | protected _childrenDisabled = false; 89 | get childrenDisabled() { 90 | return this._childrenDisabled; 91 | } 92 | 93 | // READONLY 94 | 95 | get readonly() { 96 | return this.containerReadonly || this.childrenReadonly; 97 | } 98 | 99 | get containerReadonly() { 100 | return this._readonly; 101 | } 102 | 103 | protected _childReadonly = false; 104 | get childReadonly() { 105 | return this._childReadonly; 106 | } 107 | 108 | protected _childrenReadonly = false; 109 | get childrenReadonly() { 110 | return this._childrenReadonly; 111 | } 112 | 113 | // SUBMITTED 114 | 115 | get submitted() { 116 | return this.containerSubmitted || this.childrenSubmitted; 117 | } 118 | 119 | get containerSubmitted() { 120 | return this._submitted; 121 | } 122 | 123 | protected _childSubmitted = false; 124 | get childSubmitted() { 125 | return this._childSubmitted; 126 | } 127 | 128 | protected _childrenSubmitted = false; 129 | get childrenSubmitted() { 130 | return this._childrenSubmitted; 131 | } 132 | 133 | // TOUCHED 134 | 135 | get touched() { 136 | return this.containerTouched || this.childTouched; 137 | } 138 | 139 | get containerTouched() { 140 | return this._touched; 141 | } 142 | 143 | protected _childTouched = false; 144 | get childTouched() { 145 | return this._childTouched; 146 | } 147 | 148 | protected _childrenTouched = false; 149 | get childrenTouched() { 150 | return this._childrenTouched; 151 | } 152 | 153 | // CHANGED 154 | 155 | get changed() { 156 | return this.containerChanged || this.childChanged; 157 | } 158 | 159 | get containerChanged() { 160 | return this._changed; 161 | } 162 | 163 | protected _childChanged = false; 164 | get childChanged() { 165 | return this._childChanged; 166 | } 167 | 168 | protected _childrenChanged = false; 169 | get childrenChanged() { 170 | return this._childrenChanged; 171 | } 172 | 173 | // PENDING 174 | 175 | get pending() { 176 | return this.containerPending || this.childPending; 177 | } 178 | 179 | get containerPending() { 180 | return this._pending; 181 | } 182 | 183 | protected _childPending = false; 184 | get childPending() { 185 | return this._childPending; 186 | } 187 | 188 | protected _childrenPending = false; 189 | get childrenPending() { 190 | return this._childrenPending; 191 | } 192 | 193 | // DIRTY 194 | 195 | get dirty() { 196 | return this.containerDirty || this.childDirty; 197 | } 198 | 199 | get containerDirty() { 200 | return this.containerTouched || this.containerChanged; 201 | } 202 | 203 | get childDirty() { 204 | return this._childTouched || this._childChanged; 205 | } 206 | 207 | get childrenDirty() { 208 | return this._childrenTouched || this._childrenChanged; 209 | } 210 | 211 | protected _controlsSubscriptions = new Map(); 212 | 213 | [ControlContainer.CONTROL_CONTAINER_INTERFACE]() { 214 | return this; 215 | } 216 | 217 | get(...args: any[]): A | null { 218 | if (args.length === 0) return null; 219 | else if (args.length === 1) return (this.controls as any)[args[0]]; 220 | 221 | return args.reduce( 222 | (prev: AbstractControl | null, curr) => { 223 | if (ControlContainer.isControlContainer(prev)) { 224 | return prev.get(curr); 225 | } 226 | 227 | return null; 228 | }, 229 | this as AbstractControl | null, 230 | ); 231 | } 232 | 233 | abstract equalValue( 234 | value: any, 235 | options?: { assertShape?: boolean }, 236 | ): value is Value; 237 | 238 | markChildrenDisabled(value: boolean, options?: ControlEventOptions) { 239 | this.controlsStore.forEach(control => { 240 | control.markDisabled(value, options); 241 | }); 242 | } 243 | 244 | markChildrenTouched(value: boolean, options?: ControlEventOptions) { 245 | this.controlsStore.forEach(control => { 246 | control.markTouched(value, options); 247 | }); 248 | } 249 | 250 | markChildrenChanged(value: boolean, options?: ControlEventOptions) { 251 | this.controlsStore.forEach(control => { 252 | control.markChanged(value, options); 253 | }); 254 | } 255 | 256 | markChildrenReadonly(value: boolean, options?: ControlEventOptions) { 257 | this.controlsStore.forEach(control => { 258 | control.markReadonly(value, options); 259 | }); 260 | } 261 | 262 | markChildrenSubmitted(value: boolean, options?: ControlEventOptions) { 263 | this.controlsStore.forEach(control => { 264 | control.markSubmitted(value, options); 265 | }); 266 | } 267 | 268 | markChildrenPending(value: boolean, options?: ControlEventOptions) { 269 | this.controlsStore.forEach(control => { 270 | control.markPending(value, options); 271 | }); 272 | } 273 | 274 | abstract setControls(...args: any[]): void; 275 | 276 | abstract setControl(...args: any[]): void; 277 | 278 | abstract addControl(...args: any[]): void; 279 | 280 | abstract removeControl(...args: any[]): void; 281 | 282 | replayState(options: ControlEventOptions = {}): Observable { 283 | this.controlsStore.forEach(() => {}); 284 | return concat( 285 | of({ 286 | id: '', 287 | source: options.source || this.id, 288 | processed: [this.id] as [ControlId], 289 | type: 'StateChange', 290 | changes: new Map([ 291 | ['controlsStore', new Map(this.controlsStore)], 292 | ['childDisabled', this.childDisabled], 293 | ['childrenDisabled', this.childrenDisabled], 294 | ['childTouched', this.childTouched], 295 | ['childrenTouched', this.childrenTouched], 296 | ['childChanged', this.childChanged], 297 | ['childrenChanged', this.childrenChanged], 298 | ['childReadonly', this.childReadonly], 299 | ['childrenReadonly', this.childrenReadonly], 300 | ['childInvalid', this.childInvalid], 301 | ['childrenInvalid', this.childrenInvalid], 302 | ['childPending', this.childPending], 303 | ['childrenPending', this.childrenPending], 304 | ]), 305 | noEmit: options.noEmit, 306 | meta: options.meta || {}, 307 | }).pipe( 308 | map(event => { 309 | // we reset the applied array so that this saved 310 | // state change can be applied to the same control 311 | // multiple times 312 | event.id = AbstractControl.eventId(); 313 | (event as any).processed = []; 314 | return event as StateChange; 315 | }), 316 | ), 317 | super.replayState(options), 318 | ); 319 | } 320 | 321 | protected registerControls() { 322 | this.controlsStore.forEach((control, key) => { 323 | const fn = (event: ControlEvent) => { 324 | if (event.processed.includes(this.id)) return null; 325 | 326 | event.processed.push(this.id); 327 | 328 | const newEvent = this.processChildEvent({ control, key, event }); 329 | 330 | if (!newEvent) return null; 331 | 332 | const callbacks: Array<() => void> = []; 333 | 334 | this.atomic.forEach(transaction => { 335 | const cfn = transaction(newEvent); 336 | 337 | if (cfn) callbacks.push(cfn); 338 | }); 339 | 340 | return () => { 341 | this._events.next(newEvent); 342 | 343 | callbacks.forEach(cfn => cfn()); 344 | }; 345 | }; 346 | 347 | control.atomic.set(this.id, fn.bind(this)); 348 | }); 349 | } 350 | 351 | protected deregisterControls() { 352 | this.controlsStore.forEach(control => { 353 | control.atomic.delete(this.id); 354 | }); 355 | } 356 | 357 | protected setupControls(changes: Map) { 358 | let asArray = Array.from(this.controlsStore); 359 | 360 | calcChildrenProps(this as any, 'disabled', asArray, changes); 361 | 362 | asArray = asArray.filter(([, c]) => c.enabled); 363 | 364 | calcChildrenProps(this as any, 'touched', asArray, changes); 365 | calcChildrenProps(this as any, 'readonly', asArray, changes); 366 | calcChildrenProps(this as any, 'changed', asArray, changes); 367 | calcChildrenProps(this as any, 'submitted', asArray, changes); 368 | calcChildrenProps(this as any, 'pending', asArray, changes); 369 | calcChildrenProps(this as any, 'invalid', asArray, changes); 370 | } 371 | 372 | protected processChildEvent(args: { 373 | control: AbstractControl; 374 | key: any; 375 | event: ControlEvent; 376 | }): ControlEvent | null { 377 | const { control, key, event } = args; 378 | 379 | switch (event.type) { 380 | case 'StateChange': { 381 | const changes = new Map(); 382 | 383 | // here, we flatten changes which will result in redundant processing 384 | // e.g. we only need to process "disabled", "childDisabled", 385 | // "childrenDisabled" changes once per event. 386 | new Map( 387 | Array.from((event as StateChange).changes).map(([prop, value]) => { 388 | if (['childDisabled', 'childrenDisabled'].includes(prop)) { 389 | return ['disabled', undefined]; 390 | } 391 | 392 | if (['childTouched', 'childrenTouched'].includes(prop)) { 393 | return ['touched', undefined]; 394 | } 395 | 396 | if (['childPending', 'childrenPending'].includes(prop)) { 397 | return ['pending', undefined]; 398 | } 399 | 400 | if (['childChanged', 'childrenChanged'].includes(prop)) { 401 | return ['changed', undefined]; 402 | } 403 | 404 | if (['childReadonly', 'childrenReadonly'].includes(prop)) { 405 | return ['readonly', undefined]; 406 | } 407 | 408 | if ( 409 | ['childInvalid', 'childrenInvalid', 'errorsStore'].includes(prop) 410 | ) { 411 | return ['invalid', undefined]; 412 | } 413 | 414 | return [prop, value]; 415 | }), 416 | ).forEach((value, prop) => { 417 | const success = this.processChildStateChange({ 418 | control, 419 | key, 420 | event: event as StateChange, 421 | prop, 422 | value, 423 | changes, 424 | }); 425 | 426 | if (!success) { 427 | // we want to emit a state change from the parent 428 | // whenever the child emits a state change, to ensure 429 | // that `observe()` calls trigger properly 430 | changes.set('otherChildStateChange', undefined); 431 | } 432 | }); 433 | 434 | if (changes.size === 0) return null; 435 | 436 | return { 437 | ...event, 438 | changes, 439 | } as StateChange; 440 | } 441 | } 442 | 443 | return null; 444 | } 445 | 446 | protected processChildStateChange(args: { 447 | control: AbstractControl; 448 | key: any; 449 | event: StateChange; 450 | prop: string; 451 | value: any; 452 | changes: Map; 453 | }): boolean { 454 | const { control, prop, changes } = args; 455 | 456 | switch (prop) { 457 | case 'disabled': { 458 | let asArray = Array.from(this.controlsStore); 459 | 460 | calcChildrenProps(this as any, 'disabled', asArray, changes); 461 | 462 | asArray = asArray.filter(([, c]) => c.enabled); 463 | 464 | calcChildrenProps(this as any, 'touched', asArray, changes); 465 | calcChildrenProps(this as any, 'readonly', asArray, changes); 466 | calcChildrenProps(this as any, 'changed', asArray, changes); 467 | calcChildrenProps(this as any, 'submitted', asArray, changes); 468 | calcChildrenProps(this as any, 'pending', asArray, changes); 469 | calcChildrenProps(this as any, 'invalid', asArray, changes); 470 | 471 | return true; 472 | } 473 | case 'touched': { 474 | if (control.disabled) return true; 475 | 476 | const asArray = Array.from(this.controlsStore).filter( 477 | ([, c]) => c.enabled, 478 | ); 479 | 480 | calcChildrenProps(this, 'touched', asArray, changes); 481 | 482 | return true; 483 | } 484 | case 'changed': { 485 | if (control.disabled) return true; 486 | 487 | const asArray = Array.from(this.controlsStore).filter( 488 | ([, c]) => c.enabled, 489 | ); 490 | 491 | calcChildrenProps(this, 'changed', asArray, changes); 492 | 493 | return true; 494 | } 495 | case 'readonly': { 496 | if (control.disabled) return true; 497 | 498 | const asArray = Array.from(this.controlsStore).filter( 499 | ([, c]) => c.enabled, 500 | ); 501 | 502 | calcChildrenProps(this, 'readonly', asArray, changes); 503 | 504 | return true; 505 | } 506 | case 'invalid': { 507 | if (control.disabled) return true; 508 | 509 | const asArray = Array.from(this.controlsStore).filter( 510 | ([, c]) => c.enabled, 511 | ); 512 | 513 | calcChildrenProps(this, 'invalid', asArray, changes); 514 | 515 | return true; 516 | } 517 | case 'pending': { 518 | if (control.disabled) return true; 519 | 520 | const asArray = Array.from(this.controlsStore).filter( 521 | ([, c]) => c.enabled, 522 | ); 523 | 524 | calcChildrenProps(this, 'pending', asArray, changes); 525 | 526 | return true; 527 | } 528 | } 529 | 530 | return false; 531 | } 532 | } 533 | 534 | // const asArray = Array.from(this.controlsStore).filter( 535 | // ([, c]) => c.enabled, 536 | // ); 537 | 538 | // this._childPending = asArray.some(([, c]) => { 539 | // if (ControlContainer.isControlContainer(c)) { 540 | // return c.childPending; 541 | // } else { 542 | // return c.changed; 543 | // } 544 | // }); 545 | 546 | // this._childrenPending = 547 | // this.controlsStore.size > 0 && 548 | // asArray.every(([, c]) => { 549 | // if (ControlContainer.isControlContainer(c)) { 550 | // return c.childrenPending; 551 | // } else { 552 | // return c.changed; 553 | // } 554 | // }); 555 | 556 | export function calcChildrenProps( 557 | parent: ControlContainer, 558 | prop: 559 | | 'pending' 560 | | 'disabled' 561 | | 'touched' 562 | | 'submitted' 563 | | 'changed' 564 | | 'invalid' 565 | | 'readonly', 566 | controls: [any, AbstractControl][], 567 | changes: Map, 568 | ) { 569 | const cprop = capitalize(prop); 570 | const childProp: string & keyof typeof parent = `child${cprop}` as any; 571 | const childrenProp: string & keyof typeof parent = `children${cprop}` as any; 572 | 573 | const child = parent[childProp]; 574 | const children = parent[childrenProp]; 575 | 576 | (parent as any)[`_${childProp}`] = controls.some(([, c]) => { 577 | if (ControlContainer.isControlContainer(c)) { 578 | return (c as any)[childProp]; 579 | } else { 580 | return (c as any)[prop]; 581 | } 582 | }); 583 | 584 | (parent as any)[`_${childrenProp}`] = 585 | controls.length > 0 && 586 | controls.every(([, c]) => { 587 | if (ControlContainer.isControlContainer(c)) { 588 | return (c as any)[childrenProp]; 589 | } else { 590 | return (c as any)[prop]; 591 | } 592 | }); 593 | 594 | if (child !== parent[childProp]) { 595 | changes.set(childProp, parent[childProp]); 596 | } 597 | 598 | if (children !== parent[childrenProp]) { 599 | changes.set(childrenProp, parent[childrenProp]); 600 | } 601 | } 602 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/src/lib/models/control-container.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AbstractControl, 3 | ControlEventOptions, 4 | DeepReadonly, 5 | } from './abstract-control'; 6 | 7 | export type ControlContainerControls = T extends ControlContainer 8 | ? C 9 | : never; 10 | 11 | export namespace ControlContainer { 12 | export const CONTROL_CONTAINER_INTERFACE = Symbol( 13 | '@@ControlContainerInterface', 14 | ); 15 | export function isControlContainer(object?: any): object is ControlContainer { 16 | return ( 17 | AbstractControl.isAbstractControl(object) && 18 | typeof (object as any)[ControlContainer.CONTROL_CONTAINER_INTERFACE] === 19 | 'function' && 20 | (object as any)[ControlContainer.CONTROL_CONTAINER_INTERFACE]() === object 21 | ); 22 | } 23 | } 24 | 25 | export interface ControlContainer< 26 | Controls = any, 27 | Value = any, 28 | EnabledValue = any, 29 | Data = any 30 | > extends AbstractControl { 31 | readonly controls: Controls; 32 | readonly controlsStore: ReadonlyMap; 33 | 34 | readonly size: number; 35 | 36 | readonly value: DeepReadonly; 37 | /** 38 | * Only returns values for `enabled` child controls. If a 39 | * child control is itself a `ControlContainer`, it will return 40 | * the `enabledValue` for that child. 41 | */ 42 | readonly enabledValue: DeepReadonly; 43 | 44 | /** Will return true if `containerValid` and `childrenValid` */ 45 | readonly valid: boolean; 46 | /** Will return true if the `ControlContainer` has no errors. */ 47 | readonly containerValid: boolean; 48 | /** Will return true if *any* enabled child control is valid */ 49 | readonly childValid: boolean; 50 | /** Will return true if *all* enabled child control's are valid */ 51 | readonly childrenValid: boolean; 52 | 53 | /** Will return true if `containerInvalid` or `childInvalid` */ 54 | readonly invalid: boolean; 55 | /** Will return true if the `ControlContainer` has any errors. */ 56 | readonly containerInvalid: boolean; 57 | /** Will return true if *any* enabled child control is invalid */ 58 | readonly childInvalid: boolean; 59 | /** Will return true if *all* enabled child control's are invalid */ 60 | readonly childrenInvalid: boolean; 61 | 62 | /** Will return true if `containerDisabled` or `childrenDisabled` */ 63 | readonly disabled: boolean; 64 | /** Will return true if the `ControlContainer` is disabled. */ 65 | readonly containerDisabled: boolean; 66 | /** Will return true if *any* child control is disabled */ 67 | readonly childDisabled: boolean; 68 | /** Will return true if *all* child control's are disabled */ 69 | readonly childrenDisabled: boolean; 70 | 71 | /** Will return true if `containerReadonly` or `childrenReadonly` */ 72 | readonly readonly: boolean; 73 | /** Will return true if the `ControlContainer` is readonly. */ 74 | readonly containerReadonly: boolean; 75 | /** Will return true if *any* enabled child control is readonly */ 76 | readonly childReadonly: boolean; 77 | /** Will return true if *all* enabled child control's are readonly */ 78 | readonly childrenReadonly: boolean; 79 | 80 | /** Will return true if `containerPending` or `childPending` */ 81 | readonly pending: boolean; 82 | /** Will return true if the `ControlContainer` is pending. */ 83 | readonly containerPending: boolean; 84 | /** Will return true if *any* enabled child control is pending */ 85 | readonly childPending: boolean; 86 | /** Will return true if *all* enabled child control's are pending */ 87 | readonly childrenPending: boolean; 88 | 89 | /** Will return true if `containerTouched` or `childTouched` */ 90 | readonly touched: boolean; 91 | /** Will return true if the `ControlContainer` is touched. */ 92 | readonly containerTouched: boolean; 93 | /** Will return true if *any* enabled child control is touched */ 94 | readonly childTouched: boolean; 95 | /** Will return true if *all* enabled child control's are touched */ 96 | readonly childrenTouched: boolean; 97 | 98 | /** Will return true if `containerChanged` or `childChanged` */ 99 | readonly changed: boolean; 100 | /** Will return true if the `ControlContainer` is changed. */ 101 | readonly containerChanged: boolean; 102 | /** Will return true if *any* enabled child control is changed */ 103 | readonly childChanged: boolean; 104 | /** Will return true if *all* enabled child control's are changed */ 105 | readonly childrenChanged: boolean; 106 | 107 | /** Will return true if `containerSubmitted` or `childrenSubmitted` */ 108 | readonly submitted: boolean; 109 | /** Will return true if the `ControlContainer` is submitted. */ 110 | readonly containerSubmitted: boolean; 111 | /** Will return true if *any* enabled child control is submitted */ 112 | readonly childSubmitted: boolean; 113 | /** Will return true if *all* enabled child control's are submitted */ 114 | readonly childrenSubmitted: boolean; 115 | 116 | /** Will return true if `containerDirty` or `childDirty` */ 117 | readonly dirty: boolean; 118 | /** Will return true if `containerTouched` or `containerChanged`. */ 119 | readonly containerDirty: boolean; 120 | /** Will return true if *any* enabled child control is dirty */ 121 | readonly childDirty: boolean; 122 | /** Will return true if *all* enabled child control's are dirty */ 123 | readonly childrenDirty: boolean; 124 | 125 | [ControlContainer.CONTROL_CONTAINER_INTERFACE](): this; 126 | 127 | equalValue(value: any, options?: { assertShape?: boolean }): value is Value; 128 | 129 | get(...args: any[]): A | null; 130 | 131 | setControls(...args: any[]): void; 132 | 133 | setControl(...args: any[]): void; 134 | 135 | addControl(...args: any[]): void; 136 | 137 | removeControl(...args: any[]): void; 138 | 139 | markChildrenDisabled(value: boolean, options?: ControlEventOptions): void; 140 | markChildrenTouched(value: boolean, options?: ControlEventOptions): void; 141 | markChildrenChanged(value: boolean, options?: ControlEventOptions): void; 142 | markChildrenReadonly(value: boolean, options?: ControlEventOptions): void; 143 | markChildrenSubmitted(value: boolean, options?: ControlEventOptions): void; 144 | markChildrenPending(value: boolean, options?: ControlEventOptions): void; 145 | } 146 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/src/lib/models/form-array.ts: -------------------------------------------------------------------------------- 1 | import { merge, concat } from 'rxjs'; 2 | import { AbstractControl, ControlEventOptions } from './abstract-control'; 3 | import { filter, map, tap } from 'rxjs/operators'; 4 | import { IControlBaseArgs, StateChange } from './control-base'; 5 | import { ControlContainerBase } from './control-container-base'; 6 | import { ControlContainer } from './control-container'; 7 | import { pluckOptions, isMapEqual } from './util'; 8 | 9 | export type IFormArrayArgs = IControlBaseArgs; 10 | 11 | export type FormArrayValue< 12 | T extends readonly AbstractControl[] 13 | > = T[number]['value'][]; 14 | 15 | export type FormArrayEnabledValue< 16 | T extends readonly AbstractControl[] 17 | > = T[number] extends ControlContainer 18 | ? T[number]['enabledValue'][] 19 | : T[number]['value'][]; 20 | 21 | // export type FormArrayCloneValue = 22 | // T[number] extends ControlContainer ? ReturnType[] : T[number]['value'][]; 23 | 24 | // export type FormArrayCloneRawValue = 25 | // T[number] extends ControlContainer ? ReturnType[] : T[number]['value'][]; 26 | 27 | type ArrayElement = T extends Array ? R : any; 28 | 29 | // Typescript currently cannot easily get indices of a tuple 30 | // see https://github.com/microsoft/TypeScript/issues/32917 31 | // This work-around taken from 32 | // https://github.com/microsoft/TypeScript/issues/27995#issuecomment-441157546 33 | // and https://stackoverflow.com/a/57510063/5490505 34 | type ArrayKeys = keyof any[]; 35 | type StringIndices = Exclude; 36 | interface IndexMap { 37 | '0': 0; 38 | '1': 1; 39 | '2': 2; 40 | '3': 3; 41 | '4': 4; 42 | '5': 5; 43 | '6': 6; 44 | '7': 7; 45 | '8': 8; 46 | '9': 9; 47 | '10': 10; 48 | '11': 11; 49 | '12': 12; 50 | } 51 | type CastToNumber = [T] extends [never] 52 | ? number 53 | : T extends keyof IndexMap 54 | ? IndexMap[T] 55 | : number; 56 | type Indices = CastToNumber>; 57 | 58 | // type Indices = Exclude["length"], T['length']>; 59 | 60 | export class FormArray< 61 | T extends ReadonlyArray = Array, 62 | D = any 63 | > extends ControlContainerBase< 64 | T, 65 | FormArrayValue, 66 | FormArrayEnabledValue, 67 | D 68 | > { 69 | static id = 0; 70 | 71 | protected _controlsStore: ReadonlyMap, T[Indices]> = new Map(); 72 | get controlsStore() { 73 | return this._controlsStore; 74 | } 75 | 76 | protected _controls: T; 77 | 78 | constructor( 79 | controls: T = ([] as unknown) as T, 80 | options: IFormArrayArgs = {}, 81 | ) { 82 | super( 83 | options.id || Symbol(`FormArray-${FormArray.id++}`), 84 | extractValue(controls), 85 | options, 86 | ); 87 | 88 | this._controls = (controls.slice() as unknown) as T; 89 | this._controlsStore = new Map, T[Indices]>(this._controls.map( 90 | (c, i) => [i, c], 91 | ) as any); 92 | this._enabledValue = extractEnabledValue(controls); 93 | 94 | this.setupControls(new Map()); 95 | this.registerControls(); 96 | } 97 | 98 | get>(a: A): T[A]; 99 | get(...args: any[]): A | null; 100 | get(...args: any[]): A | null { 101 | return super.get(...args); 102 | } 103 | 104 | equalValue( 105 | value: FormArrayValue, 106 | options: { assertShape?: boolean } = {}, 107 | ): value is FormArrayValue { 108 | const error = () => { 109 | console.error( 110 | `FormArray`, 111 | `incoming value:`, 112 | value, 113 | 'current controls:', 114 | this.controls, 115 | ); 116 | 117 | throw new Error( 118 | `FormArray "value" must have the ` + 119 | `same shape (indices) as the FormArray's controls`, 120 | ); 121 | }; 122 | 123 | if (this.controlsStore.size !== Object.keys(value).length) { 124 | if (options.assertShape) error(); 125 | return false; 126 | } 127 | 128 | return Array.from(this.controlsStore).every(([key, control]) => { 129 | if (!value.hasOwnProperty(key)) { 130 | if (options.assertShape) error(); 131 | return false; 132 | } 133 | 134 | return ControlContainer.isControlContainer(control) 135 | ? control.equalValue(value[key], options) 136 | : control.equalValue(value[key]); 137 | }); 138 | } 139 | 140 | setValue(value: FormArrayValue, options: ControlEventOptions = {}) { 141 | this.emitEvent({ 142 | type: 'StateChange', 143 | changes: new Map([['value', value]]), 144 | ...pluckOptions(options), 145 | }); 146 | } 147 | 148 | patchValue( 149 | value: Partial>, 150 | options: ControlEventOptions = {}, 151 | ) { 152 | if (!Array.isArray(value)) { 153 | throw new Error( 154 | 'FormArray#patchValue() must be provided with an array value', 155 | ); 156 | } 157 | 158 | value.forEach((v, i) => { 159 | const c = this.controls[i]; 160 | 161 | if (!c) { 162 | throw new Error(`FormArray: Invalid patchValue index "${i}".`); 163 | } 164 | 165 | c.patchValue(v, options); 166 | }); 167 | } 168 | 169 | setControls(controls: T, options: ControlEventOptions = {}) { 170 | if (!Array.isArray(controls)) { 171 | throw new Error( 172 | 'FormArray#setControls expects an array of AbstractControls.', 173 | ); 174 | } 175 | 176 | this.emitEvent({ 177 | type: 'StateChange', 178 | changes: new Map([ 179 | ['controlsStore', new Map(controls.map((c, i) => [i, c]))], 180 | ]), 181 | ...pluckOptions(options), 182 | }); 183 | } 184 | 185 | setControl>( 186 | index: N, 187 | control: T[N] | null, 188 | options: ControlEventOptions = {}, 189 | ) { 190 | if (index > this._controls.length) { 191 | throw new Error( 192 | 'Invalid FormArray#setControl index value. ' + 193 | 'Provided index cannot be greater than FormArray#controls.length', 194 | ); 195 | } 196 | 197 | const controls = this.controls.slice(); 198 | 199 | if (control) { 200 | controls[index] = control; 201 | } else { 202 | controls.splice(index, 1); 203 | } 204 | 205 | this.emitEvent({ 206 | type: 'StateChange', 207 | changes: new Map([ 208 | ['controlsStore', new Map(controls.map((c, i) => [i, c]))], 209 | ]), 210 | ...pluckOptions(options), 211 | }); 212 | } 213 | 214 | addControl( 215 | control: ArrayElement>, 216 | options: ControlEventOptions = {}, 217 | ) { 218 | const controls = new Map(this.controlsStore); 219 | 220 | controls.set(controls.size as Indices, control); 221 | 222 | this.emitEvent({ 223 | type: 'StateChange', 224 | changes: new Map([['controlsStore', controls]]), 225 | ...pluckOptions(options), 226 | }); 227 | } 228 | 229 | removeControl>( 230 | control: N | ArrayElement>, 231 | options: ControlEventOptions = {}, 232 | ) { 233 | const index = 234 | typeof control === 'object' 235 | ? this.controls.findIndex(c => c === control) 236 | : control; 237 | 238 | if (!this.controls[index]) return; 239 | 240 | const controls = this.controls.slice(); 241 | 242 | controls.splice(index, 1); 243 | 244 | this.emitEvent({ 245 | type: 'StateChange', 246 | changes: new Map([ 247 | ['controlsStore', new Map(controls.map((c, i) => [i, c]))], 248 | ]), 249 | ...pluckOptions(options), 250 | }); 251 | } 252 | 253 | protected setupControls( 254 | changes: Map, 255 | options?: ControlEventOptions, 256 | ) { 257 | super.setupControls(changes); 258 | 259 | const newValue = [...this._value]; 260 | const newEnabledValue = [...this._enabledValue] as FormArrayEnabledValue; 261 | 262 | this.controlsStore.forEach((control, key) => { 263 | newValue[key] = control.value; 264 | 265 | if (control.enabled) { 266 | newEnabledValue[key] = ControlContainer.isControlContainer(control) 267 | ? control.enabledValue 268 | : control.value; 269 | } 270 | }); 271 | 272 | this._value = newValue; 273 | this._enabledValue = newEnabledValue; 274 | 275 | // updateValidation must come before "value" change 276 | // is set 277 | this.updateValidation(changes, options); 278 | changes.set('value', newValue); 279 | } 280 | 281 | protected processStateChange(args: { 282 | event: StateChange; 283 | value: any; 284 | prop: string; 285 | changes: Map; 286 | }): boolean { 287 | const { value, prop, changes, event } = args; 288 | 289 | switch (prop) { 290 | case 'value': { 291 | if (this.equalValue(value, { assertShape: true })) { 292 | return true; 293 | } 294 | 295 | this.controls.forEach((control, index) => { 296 | control.patchValue(value[index], event); 297 | }); 298 | 299 | const newValue = [...this._value]; 300 | const newEnabledValue = [ 301 | ...this._enabledValue, 302 | ] as FormArrayEnabledValue; 303 | 304 | (value as FormArrayValue).forEach((_, i) => { 305 | const c = this.controls[i]; 306 | newValue[i] = c.value; 307 | newEnabledValue[i] = ControlContainer.isControlContainer(c) 308 | ? c.enabledValue 309 | : c.value; 310 | }); 311 | 312 | this._value = newValue; 313 | this._enabledValue = newEnabledValue; 314 | 315 | // As with the ControlBase "value" change, I think "updateValidation" 316 | // needs to come before the "value" change is set. See the ControlBase 317 | // "value" StateChange for more info. 318 | this.updateValidation(changes, event); 319 | changes.set('value', newValue); 320 | return true; 321 | } 322 | case 'controlsStore': { 323 | if (isMapEqual(this.controlsStore, value)) return true; 324 | 325 | this.deregisterControls(); 326 | this._controlsStore = new Map(value); 327 | this._controls = (Array.from(value.values()) as any) as T; 328 | changes.set('controlsStore', new Map(value)); 329 | this.setupControls(changes, event); // <- will setup value 330 | this.registerControls(); 331 | return true; 332 | } 333 | default: { 334 | return super.processStateChange(args); 335 | } 336 | } 337 | } 338 | 339 | protected processChildStateChange(args: { 340 | control: AbstractControl; 341 | key: Indices; 342 | event: StateChange; 343 | prop: string; 344 | value: any; 345 | changes: Map; 346 | }): boolean { 347 | const { control, key, prop, value, event, changes } = args; 348 | 349 | switch (prop) { 350 | case 'value': { 351 | const newValue = [...this._value]; 352 | const newEnabledValue = [ 353 | ...this._enabledValue, 354 | ] as FormArrayEnabledValue; 355 | 356 | newValue[key] = control.value; 357 | newEnabledValue[key] = ControlContainer.isControlContainer(control) 358 | ? control.enabledValue 359 | : control.value; 360 | 361 | this._value = newValue; 362 | this._enabledValue = newEnabledValue; 363 | 364 | // As with the ControlBase "value" change, I think "updateValidation" 365 | // needs to come before the "value" change is set. See the ControlBase 366 | // "value" StateChange for more info. 367 | this.updateValidation(changes, event); 368 | changes.set('value', newValue); 369 | return true; 370 | } 371 | case 'disabled': { 372 | super.processChildStateChange(args); 373 | 374 | const newEnabledValue = [ 375 | ...this._enabledValue, 376 | ] as FormArrayEnabledValue; 377 | 378 | if (control.enabled) { 379 | newEnabledValue[key] = ControlContainer.isControlContainer(control) 380 | ? control.enabledValue 381 | : control.value; 382 | } else { 383 | delete newEnabledValue[key]; 384 | } 385 | 386 | this._enabledValue = newEnabledValue; 387 | 388 | return true; 389 | } 390 | } 391 | 392 | return super.processChildStateChange(args); 393 | } 394 | } 395 | 396 | function extractEnabledValue>( 397 | controls: T, 398 | ) { 399 | return (controls 400 | .filter(ctrl => ctrl.enabled) 401 | .map(ctrl => 402 | ControlContainer.isControlContainer(ctrl) 403 | ? ctrl.enabledValue 404 | : ctrl.value, 405 | ) as any) as FormArrayEnabledValue; 406 | } 407 | 408 | function extractValue>(controls: T) { 409 | return (controls.map(ctrl => ctrl.value) as any) as FormArrayValue; 410 | } 411 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/src/lib/models/form-control.spec.ts: -------------------------------------------------------------------------------- 1 | import { FormControl } from './form-control'; 2 | 3 | describe('FormControl', () => { 4 | describe('new', () => { 5 | test('', () => { 6 | expect(new FormControl()).toBeTruthy(); 7 | }); 8 | 9 | test('with args', () => { 10 | const control = new FormControl('', { 11 | id: 'my-id', 12 | data: 'myData', 13 | changed: true, 14 | disabled: true, 15 | pending: true, 16 | readonly: true, 17 | submitted: true, 18 | touched: true, 19 | validators: () => null, 20 | }); 21 | expect(control).toBeTruthy(); 22 | expect(control.value).toBe(''); 23 | expect(control.changed).toBe(true); 24 | expect(control.disabled).toBe(true); 25 | expect(control.pending).toBe(true); 26 | expect(control.readonly).toBe(true); 27 | expect(control.submitted).toBe(true); 28 | expect(control.touched).toBe(true); 29 | expect(control.id).toBe('my-id'); 30 | expect(control.data).toBe('myData'); 31 | expect(typeof control.validatorStore.get(control.id)).toBe('function'); 32 | }); 33 | }); 34 | 35 | describe('methods', () => { 36 | let control: FormControl; 37 | 38 | beforeEach(() => { 39 | control = new FormControl(''); 40 | }); 41 | 42 | describe('markChanged', () => { 43 | test('', () => { 44 | expect(control.changed).toBe(false); 45 | expect(control.dirty).toBe(false); 46 | control.markChanged(true); 47 | expect(control.changed).toBe(true); 48 | expect(control.dirty).toBe(true); 49 | control.markChanged(false); 50 | expect(control.changed).toBe(false); 51 | expect(control.dirty).toBe(false); 52 | }); 53 | }); 54 | 55 | describe('markTouched', () => { 56 | test('', () => { 57 | expect(control.touched).toBe(false); 58 | expect(control.dirty).toBe(false); 59 | control.markTouched(true); 60 | expect(control.touched).toBe(true); 61 | expect(control.dirty).toBe(true); 62 | control.markTouched(false); 63 | expect(control.touched).toBe(false); 64 | expect(control.dirty).toBe(false); 65 | }); 66 | }); 67 | 68 | describe('markSubmitted', () => { 69 | test('', () => { 70 | expect(control.submitted).toBe(false); 71 | control.markSubmitted(true); 72 | expect(control.submitted).toBe(true); 73 | control.markSubmitted(false); 74 | expect(control.submitted).toBe(false); 75 | }); 76 | }); 77 | 78 | describe('markReadonly', () => { 79 | test('', () => { 80 | expect(control.readonly).toBe(false); 81 | control.markReadonly(true); 82 | expect(control.readonly).toBe(true); 83 | control.markReadonly(false); 84 | expect(control.readonly).toBe(false); 85 | }); 86 | }); 87 | 88 | describe('markDisabled', () => { 89 | test('', () => { 90 | expect(control.disabled).toBe(false); 91 | expect(control.status).toBe('VALID'); 92 | control.markDisabled(true); 93 | expect(control.disabled).toBe(true); 94 | expect(control.status).toBe('DISABLED'); 95 | control.markDisabled(false); 96 | expect(control.disabled).toBe(false); 97 | expect(control.status).toBe('VALID'); 98 | }); 99 | }); 100 | 101 | describe('setValue', () => { 102 | test('', () => { 103 | expect(control.value).toBe(''); 104 | control.setValue(1); 105 | expect(control.value).toBe(1); 106 | control.setValue('two'); 107 | expect(control.value).toBe('two'); 108 | }); 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/src/lib/models/form-control.ts: -------------------------------------------------------------------------------- 1 | import { ControlBase, IControlBaseArgs } from './control-base'; 2 | 3 | export type IFormControlArgs = IControlBaseArgs; 4 | 5 | export class FormControl extends ControlBase { 6 | static id = 0; 7 | 8 | constructor(value?: V, options: IFormControlArgs = {}) { 9 | super( 10 | options.id || Symbol(`FormControl-${FormControl.id++}`), 11 | value, 12 | options, 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/src/lib/models/form-group.spec.ts: -------------------------------------------------------------------------------- 1 | import { FormControl } from './form-control'; 2 | import { FormGroup } from './form-group'; 3 | import { AbstractControl } from './abstract-control'; 4 | 5 | describe('FormGroup', () => { 6 | describe('no controls', () => { 7 | describe('new', () => { 8 | test('', () => { 9 | const control = new FormGroup(); 10 | expect(control).toBeTruthy(); 11 | expect(control.value).toEqual({}); 12 | }); 13 | 14 | test('with args', () => { 15 | const control = new FormGroup<{ [key: string]: AbstractControl }>( 16 | {}, 17 | { 18 | id: 'my-id', 19 | data: 'myData', 20 | changed: true, 21 | disabled: true, 22 | pending: true, 23 | readonly: true, 24 | submitted: true, 25 | touched: true, 26 | validators: () => null, 27 | }, 28 | ); 29 | expect(control).toBeTruthy(); 30 | expect(control.value).toEqual({}); 31 | expect(control.changed).toBe(true); 32 | expect(control.containerChanged).toBe(true); 33 | expect(control.childrenChanged).toBe(false); 34 | expect(control.disabled).toBe(true); 35 | expect(control.containerDisabled).toBe(true); 36 | expect(control.childrenDisabled).toBe(false); 37 | expect(control.pending).toBe(true); 38 | expect(control.containerPending).toBe(true); 39 | expect(control.childrenPending).toBe(false); 40 | expect(control.readonly).toBe(true); 41 | expect(control.containerReadonly).toBe(true); 42 | expect(control.childrenReadonly).toBe(false); 43 | expect(control.submitted).toBe(true); 44 | expect(control.containerSubmitted).toBe(true); 45 | expect(control.childrenSubmitted).toBe(false); 46 | expect(control.touched).toBe(true); 47 | expect(control.containerTouched).toBe(true); 48 | expect(control.childrenTouched).toBe(false); 49 | expect(control.id).toBe('my-id'); 50 | expect(control.data).toBe('myData'); 51 | expect(typeof control.validatorStore.get(control.id)).toBe('function'); 52 | }); 53 | }); 54 | 55 | describe('methods', () => { 56 | let control: FormGroup; 57 | 58 | beforeEach(() => { 59 | control = new FormGroup(); 60 | }); 61 | 62 | describe('markChanged', () => { 63 | test('', () => { 64 | expect(control.changed).toBe(false); 65 | expect(control.dirty).toBe(false); 66 | control.markChanged(true); 67 | expect(control.changed).toBe(true); 68 | expect(control.dirty).toBe(true); 69 | control.markChanged(false); 70 | expect(control.changed).toBe(false); 71 | expect(control.dirty).toBe(false); 72 | }); 73 | }); 74 | 75 | describe('markTouched', () => { 76 | test('', () => { 77 | expect(control.touched).toBe(false); 78 | expect(control.dirty).toBe(false); 79 | control.markTouched(true); 80 | expect(control.touched).toBe(true); 81 | expect(control.dirty).toBe(true); 82 | control.markTouched(false); 83 | expect(control.touched).toBe(false); 84 | expect(control.dirty).toBe(false); 85 | }); 86 | }); 87 | 88 | describe('markSubmitted', () => { 89 | test('', () => { 90 | expect(control.submitted).toBe(false); 91 | control.markSubmitted(true); 92 | expect(control.submitted).toBe(true); 93 | control.markSubmitted(false); 94 | expect(control.submitted).toBe(false); 95 | }); 96 | }); 97 | 98 | describe('markReadonly', () => { 99 | test('', () => { 100 | expect(control.readonly).toBe(false); 101 | control.markReadonly(true); 102 | expect(control.readonly).toBe(true); 103 | control.markReadonly(false); 104 | expect(control.readonly).toBe(false); 105 | }); 106 | }); 107 | 108 | describe('markDisabled', () => { 109 | test('', () => { 110 | expect(control.disabled).toBe(false); 111 | expect(control.status).toBe('VALID'); 112 | control.markDisabled(true); 113 | expect(control.disabled).toBe(true); 114 | expect(control.status).toBe('DISABLED'); 115 | control.markDisabled(false); 116 | expect(control.disabled).toBe(false); 117 | expect(control.status).toBe('VALID'); 118 | }); 119 | }); 120 | }); 121 | }); 122 | 123 | describe('controls', () => { 124 | describe('new', () => { 125 | test('with args', () => { 126 | const control = new FormGroup<{ [key: string]: AbstractControl }>({ 127 | one: new FormControl('one', { 128 | id: 'my-id', 129 | data: 'myData', 130 | changed: true, 131 | disabled: true, 132 | pending: true, 133 | readonly: true, 134 | submitted: true, 135 | touched: true, 136 | validators: () => null, 137 | }), 138 | }); 139 | expect(control).toBeTruthy(); 140 | expect(control.value).toEqual({ one: 'one' }); 141 | expect(control.containerChanged).toBe(false); 142 | expect(control.childrenChanged).toBe(false); 143 | expect(control.changed).toBe(false); 144 | expect(control.containerDisabled).toBe(false); 145 | expect(control.childrenDisabled).toBe(true); 146 | expect(control.disabled).toBe(true); 147 | expect(control.containerPending).toBe(false); 148 | expect(control.childrenPending).toBe(false); 149 | expect(control.pending).toBe(false); 150 | expect(control.containerReadonly).toBe(false); 151 | expect(control.childrenReadonly).toBe(false); 152 | expect(control.readonly).toBe(false); 153 | expect(control.containerSubmitted).toBe(false); 154 | expect(control.childrenSubmitted).toBe(false); 155 | expect(control.submitted).toBe(false); 156 | expect(control.containerTouched).toBe(false); 157 | expect(control.childrenTouched).toBe(false); 158 | expect(control.touched).toBe(false); 159 | expect(control.data).toBeFalsy(); 160 | expect(control.validatorStore.get(control.id)).toBe(undefined); 161 | }); 162 | 163 | test('with args & disabled', () => { 164 | const control = new FormGroup<{ [key: string]: AbstractControl }>({ 165 | one: new FormControl('one', { 166 | id: 'my-id', 167 | data: 'myData', 168 | changed: true, 169 | disabled: false, 170 | pending: true, 171 | readonly: true, 172 | submitted: true, 173 | touched: true, 174 | validators: () => null, 175 | }), 176 | }); 177 | expect(control).toBeTruthy(); 178 | expect(control.value).toEqual({ one: 'one' }); 179 | expect(control.containerChanged).toBe(false); 180 | expect(control.childrenChanged).toBe(true); 181 | expect(control.changed).toBe(true); 182 | expect(control.containerDisabled).toBe(false); 183 | expect(control.childrenDisabled).toBe(false); 184 | expect(control.disabled).toBe(false); 185 | expect(control.containerPending).toBe(false); 186 | expect(control.childrenPending).toBe(true); 187 | expect(control.pending).toBe(true); 188 | expect(control.containerReadonly).toBe(false); 189 | expect(control.childrenReadonly).toBe(true); 190 | expect(control.readonly).toBe(true); 191 | expect(control.containerSubmitted).toBe(false); 192 | expect(control.childrenSubmitted).toBe(true); 193 | expect(control.submitted).toBe(true); 194 | expect(control.containerTouched).toBe(false); 195 | expect(control.childrenTouched).toBe(true); 196 | expect(control.touched).toBe(true); 197 | expect(control.data).toBeFalsy(); 198 | expect(control.validatorStore.get(control.id)).toBe(undefined); 199 | }); 200 | }); 201 | 202 | describe('methods', () => { 203 | let control: FormGroup; 204 | 205 | beforeEach(() => { 206 | control = new FormGroup<{ [key: string]: AbstractControl }>({ 207 | one: new FormControl('one'), 208 | }); 209 | }); 210 | 211 | describe('markChanged', () => { 212 | test('', () => { 213 | expect(control.changed).toBe(false); 214 | expect(control.dirty).toBe(false); 215 | control.markChanged(true); 216 | expect(control.changed).toBe(true); 217 | expect(control.dirty).toBe(true); 218 | control.markChanged(false); 219 | expect(control.changed).toBe(false); 220 | expect(control.dirty).toBe(false); 221 | }); 222 | }); 223 | 224 | describe('markTouched', () => { 225 | test('', () => { 226 | expect(control.touched).toBe(false); 227 | expect(control.dirty).toBe(false); 228 | control.markTouched(true); 229 | expect(control.touched).toBe(true); 230 | expect(control.dirty).toBe(true); 231 | control.markTouched(false); 232 | expect(control.touched).toBe(false); 233 | expect(control.dirty).toBe(false); 234 | }); 235 | }); 236 | 237 | describe('markSubmitted', () => { 238 | test('', () => { 239 | expect(control.submitted).toBe(false); 240 | control.markSubmitted(true); 241 | expect(control.submitted).toBe(true); 242 | control.markSubmitted(false); 243 | expect(control.submitted).toBe(false); 244 | }); 245 | }); 246 | 247 | describe('markReadonly', () => { 248 | test('', () => { 249 | expect(control.readonly).toBe(false); 250 | control.markReadonly(true); 251 | expect(control.readonly).toBe(true); 252 | control.markReadonly(false); 253 | expect(control.readonly).toBe(false); 254 | }); 255 | }); 256 | 257 | describe('markDisabled', () => { 258 | test('', () => { 259 | expect(control.disabled).toBe(false); 260 | expect(control.status).toBe('VALID'); 261 | control.markDisabled(true); 262 | expect(control.disabled).toBe(true); 263 | expect(control.status).toBe('DISABLED'); 264 | control.markDisabled(false); 265 | expect(control.disabled).toBe(false); 266 | expect(control.status).toBe('VALID'); 267 | }); 268 | }); 269 | }); 270 | }); 271 | }); 272 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/src/lib/models/form-group.ts: -------------------------------------------------------------------------------- 1 | import { merge, concat } from 'rxjs'; 2 | import { map, filter, tap } from 'rxjs/operators'; 3 | import { AbstractControl, ControlEventOptions } from './abstract-control'; 4 | import { IControlBaseArgs, StateChange } from './control-base'; 5 | import { ControlContainerBase } from './control-container-base'; 6 | import { ControlContainer } from './control-container'; 7 | import { isMapEqual, pluckOptions } from './util'; 8 | 9 | export type IFormGroupArgs = IControlBaseArgs; 10 | 11 | export type FormGroupValue = { 12 | [P in keyof T]: T[P]['value']; 13 | }; 14 | 15 | export type FormGroupEnabledValue< 16 | T extends { [key: string]: AbstractControl } 17 | > = Partial< 18 | { 19 | [P in keyof T]: T[P] extends ControlContainer 20 | ? T[P]['enabledValue'] 21 | : T[P]['value']; 22 | } 23 | >; 24 | 25 | export class FormGroup< 26 | T extends { readonly [key: string]: AbstractControl } = { 27 | [key: string]: AbstractControl; 28 | }, 29 | D = any 30 | > extends ControlContainerBase< 31 | T, 32 | FormGroupValue, 33 | FormGroupEnabledValue, 34 | D 35 | > { 36 | static id = 0; 37 | 38 | protected _controlsStore: ReadonlyMap = new Map(); 39 | get controlsStore() { 40 | return this._controlsStore; 41 | } 42 | 43 | protected _controls: T; 44 | 45 | constructor(controls: T = {} as T, options: IFormGroupArgs = {}) { 46 | super( 47 | options.id || Symbol(`FormGroup-${FormGroup.id++}`), 48 | extractValue(controls), 49 | options, 50 | ); 51 | 52 | this._controls = { ...controls }; 53 | this._controlsStore = new Map(Object.entries( 54 | this._controls, 55 | ) as any); 56 | this._enabledValue = extractEnabledValue(controls); 57 | 58 | this.setupControls(new Map()); 59 | this.registerControls(); 60 | } 61 | 62 | get(a: A): T[A]; 63 | get(...args: any[]): A | null; 64 | get(...args: any[]): A | null { 65 | return super.get(...args); 66 | } 67 | 68 | equalValue( 69 | value: FormGroupValue, 70 | options: { assertShape?: boolean } = {}, 71 | ): value is FormGroupValue { 72 | const error = () => { 73 | console.error( 74 | `FormGroup`, 75 | `incoming value:`, 76 | value, 77 | 'current controls:', 78 | this.controls, 79 | ); 80 | 81 | throw new Error( 82 | `FormGroup "value" must have the ` + 83 | `same shape (keys) as the FormGroup's controls`, 84 | ); 85 | }; 86 | 87 | if (this.controlsStore.size !== Object.keys(value).length) { 88 | if (options.assertShape) error(); 89 | return false; 90 | } 91 | 92 | return Array.from(this.controlsStore).every(([key, control]) => { 93 | if (!value.hasOwnProperty(key)) { 94 | if (options.assertShape) error(); 95 | return false; 96 | } 97 | 98 | return ControlContainer.isControlContainer(control) 99 | ? control.equalValue(value[key], options) 100 | : control.equalValue(value[key]); 101 | }); 102 | } 103 | 104 | setValue(value: FormGroupValue, options: ControlEventOptions = {}) { 105 | this.emitEvent({ 106 | type: 'StateChange', 107 | changes: new Map([['value', value]]), 108 | ...pluckOptions(options), 109 | }); 110 | } 111 | 112 | patchValue( 113 | value: Partial>, 114 | options: ControlEventOptions = {}, 115 | ) { 116 | Object.entries(value).forEach(([key, val]) => { 117 | const c = this.controls[key]; 118 | 119 | if (!c) { 120 | throw new Error(`FormGroup: Invalid patchValue key "${key}".`); 121 | } 122 | 123 | c.patchValue(val, options); 124 | }); 125 | } 126 | 127 | setControls(controls: T, options?: ControlEventOptions) { 128 | this.emitEvent({ 129 | type: 'StateChange', 130 | changes: new Map([ 131 | ['controlsStore', new Map(Object.entries(controls))], 132 | ]), 133 | ...pluckOptions(options), 134 | }); 135 | } 136 | 137 | setControl( 138 | name: N, 139 | control: T[N] | null, 140 | options?: ControlEventOptions, 141 | ) { 142 | const controls = new Map(this.controlsStore); 143 | 144 | if (control) { 145 | controls.set(name, control); 146 | } else { 147 | controls.delete(name); 148 | } 149 | 150 | this.emitEvent({ 151 | type: 'StateChange', 152 | changes: new Map([['controlsStore', controls]]), 153 | ...pluckOptions(options), 154 | }); 155 | } 156 | 157 | addControl( 158 | name: N, 159 | control: T[N], 160 | options?: ControlEventOptions, 161 | ) { 162 | if (this.controlsStore.has(name)) return; 163 | 164 | this.setControl(name, control, options); 165 | } 166 | 167 | removeControl(name: keyof T, options?: ControlEventOptions) { 168 | if (!this.controlsStore.has(name)) return; 169 | 170 | this.setControl(name, null, options); 171 | } 172 | 173 | protected setupControls( 174 | changes: Map, 175 | options?: ControlEventOptions, 176 | ) { 177 | super.setupControls(changes); 178 | 179 | const newValue = { ...this._value }; 180 | const newEnabledValue = { ...this._enabledValue }; 181 | 182 | this.controlsStore.forEach((control, key) => { 183 | newValue[key as keyof FormGroupValue] = control.value; 184 | 185 | if (control.enabled) { 186 | newEnabledValue[ 187 | key as keyof FormGroupValue 188 | ] = ControlContainer.isControlContainer(control) 189 | ? control.enabledValue 190 | : control.value; 191 | } 192 | }); 193 | 194 | this._value = newValue; 195 | this._enabledValue = newEnabledValue; 196 | 197 | // updateValidation must come before "value" change 198 | // is set 199 | this.updateValidation(changes, options); 200 | changes.set('value', newValue); 201 | } 202 | 203 | protected processStateChange(args: { 204 | event: StateChange; 205 | value: any; 206 | prop: string; 207 | changes: Map; 208 | }): boolean { 209 | const { value, prop, changes, event } = args; 210 | 211 | switch (prop) { 212 | case 'value': { 213 | if (this.equalValue(value, { assertShape: true })) { 214 | return true; 215 | } 216 | 217 | this.controlsStore.forEach((control, key) => { 218 | control.patchValue(value[key], event); 219 | }); 220 | 221 | const newValue = { ...this._value }; 222 | const newEnabledValue = { ...this._enabledValue }; 223 | 224 | Object.keys(value).forEach(k => { 225 | const c = this.controlsStore.get(k)!; 226 | newValue[k as keyof FormGroupValue] = c.value; 227 | newEnabledValue[ 228 | k as keyof FormGroupValue 229 | ] = ControlContainer.isControlContainer(c) ? c.enabledValue : c.value; 230 | }); 231 | 232 | this._value = newValue; 233 | this._enabledValue = newEnabledValue; 234 | 235 | // As with the ControlBase "value" change, I think "updateValidation" 236 | // needs to come before the "value" change is set. See the ControlBase 237 | // "value" StateChange for more info. 238 | this.updateValidation(changes, event); 239 | changes.set('value', newValue); 240 | return true; 241 | } 242 | case 'controlsStore': { 243 | if (isMapEqual(this.controlsStore, value)) return true; 244 | 245 | this.deregisterControls(); 246 | this._controlsStore = new Map(value); 247 | this._controls = Object.fromEntries(value) as T; 248 | changes.set('controlsStore', new Map(value)); 249 | this.setupControls(changes, event); // <- will setup value 250 | this.registerControls(); 251 | return true; 252 | } 253 | default: { 254 | return super.processStateChange(args); 255 | } 256 | } 257 | } 258 | 259 | protected processChildStateChange(args: { 260 | control: AbstractControl; 261 | key: keyof FormGroupValue; 262 | event: StateChange; 263 | prop: string; 264 | value: any; 265 | changes: Map; 266 | }): boolean { 267 | const { control, key, prop, value, event, changes } = args; 268 | 269 | switch (prop) { 270 | case 'value': { 271 | const newValue = { ...this._value }; 272 | const newEnabledValue = { ...this._enabledValue }; 273 | 274 | newValue[key] = control.value; 275 | newEnabledValue[key] = ControlContainer.isControlContainer(control) 276 | ? control.enabledValue 277 | : control.value; 278 | 279 | this._value = newValue; 280 | this._enabledValue = newEnabledValue; 281 | 282 | // As with the "value" change, I think "updateValidation" 283 | // needs to come before the "value" change is set 284 | this.updateValidation(changes, event); 285 | changes.set('value', newValue); 286 | return true; 287 | } 288 | case 'disabled': { 289 | super.processChildStateChange(args); 290 | 291 | const newEnabledValue = { ...this._enabledValue }; 292 | 293 | if (control.enabled) { 294 | newEnabledValue[key] = ControlContainer.isControlContainer(control) 295 | ? control.enabledValue 296 | : control.value; 297 | } else { 298 | delete newEnabledValue[key]; 299 | } 300 | 301 | this._enabledValue = newEnabledValue; 302 | 303 | return true; 304 | } 305 | } 306 | 307 | return super.processChildStateChange(args); 308 | } 309 | } 310 | 311 | function extractEnabledValue( 312 | obj: T, 313 | ) { 314 | return Object.fromEntries( 315 | Object.entries(obj) 316 | .filter(([_, ctrl]) => ctrl.enabled) 317 | .map(([key, ctrl]) => [ 318 | key, 319 | ControlContainer.isControlContainer(ctrl) 320 | ? ctrl.enabledValue 321 | : ctrl.value, 322 | ]), 323 | ) as FormGroupEnabledValue; 324 | } 325 | 326 | function extractValue(obj: T) { 327 | return Object.fromEntries( 328 | Object.entries(obj).map(([key, ctrl]) => [key, ctrl.value]), 329 | ) as FormGroupValue; 330 | } 331 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/src/lib/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './abstract-control'; 2 | 3 | export { 4 | StateChange, 5 | FocusEvent, 6 | ValidationStartEvent, 7 | ValidationInternalEvent, 8 | ValidationEndEvent, 9 | } from './control-base'; 10 | 11 | export * from './control-container'; 12 | export * from './form-control'; 13 | export * from './form-array'; 14 | export * from './form-group'; 15 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/src/lib/models/util.spec.ts: -------------------------------------------------------------------------------- 1 | import { isMapEqual, mapIsProperty } from './util'; 2 | 3 | describe('isMapEqual', () => { 4 | test('true', () => { 5 | const one = new Map([['one', 1]]); 6 | const two = new Map([['one', 1]]); 7 | 8 | expect(isMapEqual(one, two)).toBe(true); 9 | }); 10 | 11 | test('false', () => { 12 | const one = new Map([['one', 1]]); 13 | const two = new Map([['one', 2]]); 14 | const three = new Map([['three', 2]]); 15 | 16 | expect(isMapEqual(one, two)).toBe(false); 17 | expect(isMapEqual(one, three)).toBe(false); 18 | }); 19 | }); 20 | 21 | describe('mapIsProperty', () => { 22 | test('true', () => { 23 | const one = new Map([['one', true]]); 24 | const two = new Map(); 25 | 26 | expect(mapIsProperty(one)).toBe(true); 27 | expect(mapIsProperty(two)).toBe(true); 28 | }); 29 | 30 | test('false', () => { 31 | const one = new Map([['one', new Map()]]); 32 | 33 | expect(mapIsProperty(one)).toBe(false); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/src/lib/models/util.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PartialControlEvent, 3 | ControlEventOptions, 4 | ControlId, 5 | } from './abstract-control'; 6 | import { StateChange } from './control-base'; 7 | 8 | export function cloneJSON(item: T): T { 9 | return JSON.parse(JSON.stringify(item)); 10 | } 11 | 12 | export function capitalize(t: string) { 13 | return t[0].toUpperCase() + t.slice(1); 14 | } 15 | 16 | export function isMapEqual( 17 | cononical: ReadonlyMap, 18 | other: ReadonlyMap, 19 | ): boolean { 20 | if (cononical.size !== other.size) return false; 21 | 22 | for (const [k, v] of cononical) { 23 | const vv = other.get(k); 24 | 25 | if (v !== vv) return false; 26 | } 27 | 28 | return true; 29 | } 30 | 31 | export function isTruthy(value: T): value is NonNullable { 32 | return !!value; 33 | } 34 | 35 | export function isProcessed( 36 | id: symbol | string, 37 | options?: { processed?: Array }, 38 | ) { 39 | return !!(options && options.processed && options.processed.includes(id)); 40 | } 41 | 42 | export function isStateChange( 43 | event: PartialControlEvent, 44 | ): event is StateChange { 45 | return event.type === 'StateChange'; 46 | } 47 | 48 | export function pluckOptions({ 49 | source, 50 | processed, 51 | eventId, 52 | meta, 53 | noEmit, 54 | }: ControlEventOptions = {}) { 55 | const options = {} as { 56 | id?: string; 57 | source?: ControlId; 58 | processed?: ControlId[]; 59 | meta?: { [key: string]: any }; 60 | noEmit?: boolean; 61 | }; 62 | 63 | if (eventId) options.id = eventId; 64 | if (source) options.source = source; 65 | if (processed) options.processed = processed.slice(); 66 | if (meta) options.meta = meta; 67 | if (noEmit) options.noEmit = noEmit; 68 | 69 | return options; 70 | } 71 | 72 | export function mapIsProperty(a: Map) { 73 | if (a.size === 0) return true; 74 | 75 | for (const [, v] of a) { 76 | return !(v instanceof Map); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of reactive-forms-module2-proposal 3 | */ 4 | 5 | export * from './lib/directives'; 6 | export * from './lib/models'; 7 | export * from './lib/accessors'; 8 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "target": "es2015", 6 | "declaration": true, 7 | "inlineSources": true, 8 | "types": [] 9 | }, 10 | "angularCompilerOptions": { 11 | "annotateForClosureCompiler": true, 12 | "skipTemplateCodegen": true, 13 | "strictMetadataEmit": true, 14 | "fullTemplateTypeCheck": true, 15 | "strictInjectionParameters": true, 16 | "enableResourceInlining": true 17 | }, 18 | "exclude": ["**/*.spec.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": ["jest", "node"] 6 | }, 7 | "files": [], 8 | "include": ["**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /libs/reactive-forms-module2-proposal/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [true, "attribute", "lib", "camelCase"], 5 | "component-selector": [true, "element", "lib", "kebab-case"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "pub:rf2": "ng test rf2 && yarn --cwd=./libs/reactive-forms-module2-proposal version && ng build rf2 && yarn --cwd=./dist/reactive-forms-module2-proposal publish", 11 | "e2e": "ng e2e" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/animations": "~8.2.0", 16 | "@angular/cdk": "^8.1.2", 17 | "@angular/common": "~8.2.0", 18 | "@angular/compiler": "~8.2.0", 19 | "@angular/core": "~8.2.0", 20 | "@angular/forms": "~8.2.0", 21 | "@angular/material": "^8.1.2", 22 | "@angular/platform-browser": "~8.2.0", 23 | "@angular/platform-browser-dynamic": "~8.2.0", 24 | "@angular/router": "~8.2.0", 25 | "classlist.js": "^1.1.20150312", 26 | "core-js": "^3.1.4", 27 | "lodash-es": "^4.17.15", 28 | "rxjs": "~6.4.0", 29 | "specificity": "^0.4.1", 30 | "tslib": "^1.10.0", 31 | "web-animations-js": "^2.3.2", 32 | "zone.js": "~0.9.1" 33 | }, 34 | "devDependencies": { 35 | "@angular-builders/jest": "^8.2.0", 36 | "@angular-devkit/build-angular": "~0.802.0", 37 | "@angular-devkit/build-ng-packagr": "~0.802.0", 38 | "@angular/cli": "~8.2.0", 39 | "@angular/compiler-cli": "~8.2.0", 40 | "@angular/language-service": "~8.2.0", 41 | "@types/jasmine": "~3.3.8", 42 | "@types/jasminewd2": "~2.0.3", 43 | "@types/jest": "^24.0.21", 44 | "@types/lodash-es": "^4.14.136", 45 | "@types/node": "~8.9.4", 46 | "codelyzer": "^5.0.0", 47 | "jasmine-core": "~3.4.0", 48 | "jasmine-spec-reporter": "~4.2.1", 49 | "jest": "^24.9.0", 50 | "ng-packagr": "^5.3.0", 51 | "protractor": "~5.4.0", 52 | "ts-node": "~7.0.0", 53 | "tsickle": "^0.36.0", 54 | "tslint": "~5.15.0", 55 | "typescript": "~3.5.3" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test.setup.ts: -------------------------------------------------------------------------------- 1 | import 'core-js/features/object'; 2 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": ["demo/src/main.ts", "demo/src/polyfills.ts"], 8 | "include": ["demo/src/**/*.ts"], 9 | "exclude": ["demo/src/test.ts", "demo/src/**/*.spec.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "strict": true, 15 | "typeRoots": ["node_modules/@types"], 16 | "lib": ["esnext", "dom"], 17 | "paths": { 18 | "lodash-es/*": ["node_modules/@types/lodash-es/*"], 19 | "reactive-forms-module2-proposal": [ 20 | "libs/reactive-forms-module2-proposal/src/public-api" 21 | ], 22 | "reactive-forms-module2-proposal/compat": [ 23 | "libs/reactive-forms-module2-proposal/compat/src/public_api" 24 | ] 25 | } 26 | }, 27 | "angularCompilerOptions": { 28 | "fullTemplateTypeCheck": true, 29 | "strictInjectionParameters": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "esModuleInterop": true, 6 | "types": ["jest", "node"] 7 | }, 8 | "files": ["demo/src/polyfills.ts"], 9 | "include": ["demo/src/**/*.spec.ts", "demo/src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "variable-name": false, 5 | "array-type": false, 6 | "arrow-parens": false, 7 | "curly": [true, "ignore-same-line"], 8 | "deprecation": { 9 | "severity": "warning" 10 | }, 11 | "component-class-suffix": true, 12 | "contextual-lifecycle": true, 13 | "directive-class-suffix": true, 14 | "directive-selector": [true, "attribute", "app", "camelCase"], 15 | "component-selector": [true, "element", "app", "kebab-case"], 16 | "import-blacklist": [true, "rxjs/Rx"], 17 | "interface-name": false, 18 | "max-classes-per-file": false, 19 | "max-line-length": [true, 140], 20 | "member-access": false, 21 | "member-ordering": [ 22 | true, 23 | { 24 | "order": [ 25 | "static-field", 26 | "static-method", 27 | "instance-field", 28 | "instance-method" 29 | ] 30 | } 31 | ], 32 | "no-consecutive-blank-lines": false, 33 | "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], 34 | "no-empty": false, 35 | "no-inferrable-types": [true, "ignore-params"], 36 | "no-non-null-assertion": false, 37 | "no-redundant-jsdoc": true, 38 | "no-switch-case-fall-through": true, 39 | "no-use-before-declare": true, 40 | "no-var-requires": false, 41 | "object-literal-key-quotes": [true, "as-needed"], 42 | "object-literal-sort-keys": false, 43 | "ordered-imports": false, 44 | "quotemark": [true, "single"], 45 | "trailing-comma": false, 46 | "no-conflicting-lifecycle": true, 47 | "no-host-metadata-property": true, 48 | "no-input-rename": false, 49 | "no-inputs-metadata-property": true, 50 | "no-output-native": true, 51 | "no-output-on-prefix": true, 52 | "no-output-rename": true, 53 | "no-outputs-metadata-property": true, 54 | "template-banana-in-box": true, 55 | "template-no-negated-async": true, 56 | "use-lifecycle-interface": true, 57 | "use-pipe-transform-interface": true, 58 | "no-namespace": false 59 | }, 60 | "rulesDirectory": ["codelyzer"] 61 | } 62 | --------------------------------------------------------------------------------