├── src ├── assets │ └── .gitkeep ├── app │ ├── app.component.scss │ ├── app.component.ts │ ├── app-routing.module.ts │ ├── app.module.ts │ └── app.component.spec.ts ├── favicon.ico ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── styles.scss ├── index.html ├── main.ts ├── test.ts └── polyfills.ts ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── projects ├── contenteditable │ ├── src │ │ ├── public-api.ts │ │ ├── lib │ │ │ ├── ngs-contenteditable.module.ts │ │ │ └── editable.directive.ts │ │ └── test.ts │ ├── ng-package.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.spec.json │ ├── tsconfig.lib.json │ ├── .browserslistrc │ ├── package.json │ ├── karma.conf.js │ ├── CHANGELOG.md │ └── README.md ├── forms │ ├── ng-package.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.spec.json │ ├── tsconfig.lib.json │ ├── src │ │ ├── lib │ │ │ ├── ngs-forms.module.ts │ │ │ ├── assert.ts │ │ │ ├── form-array.spec.ts │ │ │ ├── input-file.directive.ts │ │ │ ├── assert.spec.ts │ │ │ ├── form-builder.spec.ts │ │ │ ├── form-builder.ts │ │ │ ├── validators.spec.ts │ │ │ ├── form-control.ts │ │ │ ├── types.spec.ts │ │ │ ├── types.ts │ │ │ ├── form-control.spec.ts │ │ │ ├── validators.ts │ │ │ ├── form-array.ts │ │ │ └── form-group.ts │ │ ├── public-api.ts │ │ └── test.ts │ ├── .browserslistrc │ ├── package.json │ ├── karma.conf.js │ ├── CHANGELOG.md │ └── README.md └── api-mock │ ├── ng-package.json │ ├── package.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.spec.json │ ├── tsconfig.lib.json │ ├── src │ ├── public-api.ts │ ├── test.ts │ └── lib │ │ ├── api-mock.module.ts │ │ ├── pick-properties.ts │ │ ├── pick-properties.spec.ts │ │ └── types.ts │ ├── .browserslistrc │ ├── CHANGELOG.md │ ├── karma.conf.js │ └── README.md ├── .editorconfig ├── tsconfig.app.json ├── tsconfig.spec.json ├── .browserslistrc ├── README.md ├── .github └── FUNDING.yml ├── .gitignore ├── package.json ├── tsconfig.json ├── karma.conf.js └── angular.json /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KostyaTretyak/ng-stack/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /projects/contenteditable/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of contenteditable 3 | */ 4 | 5 | export * from './lib/editable.directive'; 6 | export * from './lib/ngs-contenteditable.module'; 7 | -------------------------------------------------------------------------------- /projects/forms/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/forms", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } -------------------------------------------------------------------------------- /projects/api-mock/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/api-mock", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } -------------------------------------------------------------------------------- /projects/contenteditable/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/contenteditable", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'] 7 | }) 8 | export class AppComponent { 9 | title = 'ng-stack-13'; 10 | } 11 | -------------------------------------------------------------------------------- /projects/api-mock/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ng-stack/api-mock", 3 | "version": "1.4.1", 4 | "peerDependencies": { 5 | "@angular/common": ">=4.3.6 <14.0.0", 6 | "@angular/core": ">=4.3.6 <14.0.0", 7 | "rxjs": ">=6.0.0" 8 | }, 9 | "dependencies": { 10 | "tslib": "^2.3.0" 11 | } 12 | } -------------------------------------------------------------------------------- /projects/contenteditable/src/lib/ngs-contenteditable.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { EditableDirective } from './editable.directive'; 3 | 4 | @NgModule({ 5 | declarations: [EditableDirective], 6 | exports: [EditableDirective], 7 | }) 8 | export class NgsContenteditableModule {} 9 | -------------------------------------------------------------------------------- /projects/forms/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | const routes: Routes = []; 5 | 6 | @NgModule({ 7 | imports: [RouterModule.forRoot(routes)], 8 | exports: [RouterModule] 9 | }) 10 | export class AppRoutingModule { } 11 | -------------------------------------------------------------------------------- /projects/api-mock/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /projects/contenteditable/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.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 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NgStack13 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /projects/forms/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /projects/api-mock/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /projects/contenteditable/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /projects/forms/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/lib", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [] 10 | }, 11 | "exclude": [ 12 | "src/test.ts", 13 | "**/*.spec.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /projects/api-mock/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/lib", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [] 10 | }, 11 | "exclude": [ 12 | "src/test.ts", 13 | "**/*.spec.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /projects/contenteditable/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/lib", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [] 10 | }, 11 | "exclude": [ 12 | "src/test.ts", 13 | "**/*.spec.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /projects/forms/src/lib/ngs-forms.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { ReactiveFormsModule } from '@angular/forms'; 3 | 4 | import { FormBuilder } from './form-builder'; 5 | import { InputFileDirective } from './input-file.directive'; 6 | 7 | @NgModule({ 8 | declarations: [InputFileDirective], 9 | exports: [ReactiveFormsModule, InputFileDirective], 10 | providers: [FormBuilder], 11 | }) 12 | export class NgsFormsModule {} 13 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | 4 | import { AppRoutingModule } from './app-routing.module'; 5 | import { AppComponent } from './app.component'; 6 | 7 | @NgModule({ 8 | declarations: [ 9 | AppComponent 10 | ], 11 | imports: [ 12 | BrowserModule, 13 | AppRoutingModule 14 | ], 15 | providers: [], 16 | bootstrap: [AppComponent] 17 | }) 18 | export class AppModule { } 19 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "pwa-chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /projects/api-mock/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of api-mock 3 | */ 4 | 5 | export * from './lib/api-mock.module'; 6 | export * from './lib/http-status-codes'; 7 | export * from './lib/http-backend.service'; 8 | export * from './lib/pick-properties'; 9 | export { 10 | ApiMockService, 11 | ApiMockConfig, 12 | ApiMockRootRoute, 13 | ApiMockRoute, 14 | ApiMockDataCallback, 15 | ApiMockResponseCallback, 16 | ObjectAny, 17 | CallbackAny, 18 | HttpMethod, 19 | ApiMockDataCallbackOptions, 20 | ApiMockResponseCallbackOptions, 21 | } from './lib/types'; 22 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 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 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | -------------------------------------------------------------------------------- /projects/forms/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of forms 3 | */ 4 | export { NgsFormsModule } from './lib/ngs-forms.module'; 5 | export { FormArray } from './lib/form-array'; 6 | export { FormBuilder } from './lib/form-builder'; 7 | export { FormControl } from './lib/form-control'; 8 | export { FormGroup } from './lib/form-group'; 9 | export { Validators } from './lib/validators'; 10 | export { InputFileDirective } from './lib/input-file.directive'; 11 | export { 12 | Status, 13 | ValidatorFn, 14 | AsyncValidatorFn, 15 | ValidationErrors, 16 | AbstractControlOptions, 17 | ValidatorsModel, 18 | Control, 19 | ExtractModelValue, 20 | } from './lib/types'; 21 | -------------------------------------------------------------------------------- /projects/forms/.browserslistrc: -------------------------------------------------------------------------------- 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 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | -------------------------------------------------------------------------------- /projects/api-mock/.browserslistrc: -------------------------------------------------------------------------------- 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 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | -------------------------------------------------------------------------------- /projects/contenteditable/.browserslistrc: -------------------------------------------------------------------------------- 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 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @ng-stack 2 | 3 | This Angular library contains two projects: 4 | - [@ng-stack/forms](./projects/forms) - provides wrapped Angular's Reactive Forms to write its more strongly typed. 5 | - [@ng-stack/contenteditable](./projects/contenteditable) - a micro Angular v4+ contenteditable directive for integration with Angular forms. It just implements [ControlValueAccessor](https://angular.io/api/forms/ControlValueAccessor) for this purpose. 6 | 7 | ## Related resources 8 | 9 | If you love the architectural concepts of Angular and are interested in a backend framework that is very similar to Angular, you can also check out the [Ditsmod](https://ditsmod.github.io/en/) - new Node.js framework, written in TypeScript. 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: ng-stack 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /projects/contenteditable/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ng-stack/contenteditable", 3 | "version": "2.0.1", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/KostyaTretyak/ng-stack/tree/master/projects/contenteditable" 7 | }, 8 | "author": { 9 | "name": "Костя Третяк", 10 | "email": "ktretiak.in.ua@gmail.com" 11 | }, 12 | "keywords": [ 13 | "angular", 14 | "contenteditable" 15 | ], 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/KostyaTretyak/ng-stack/issues?q=label%3A%22project%3A+contenteditable%22" 19 | }, 20 | "peerDependencies": { 21 | "@angular/core": ">=4.0.0" 22 | }, 23 | "dependencies": { 24 | "tslib": "^2.3.0" 25 | } 26 | } -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build` 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/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /projects/forms/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ng-stack/forms", 3 | "version": "3.1.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/KostyaTretyak/ng-stack/tree/master/projects/forms" 7 | }, 8 | "author": { 9 | "name": "Костя Третяк", 10 | "email": "ktretiak.in.ua@gmail.com" 11 | }, 12 | "keywords": [ 13 | "angular", 14 | "forms" 15 | ], 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/KostyaTretyak/ng-stack/issues?q=label%3A%22project%3A+forms%22" 19 | }, 20 | "peerDependencies": { 21 | "@angular/core": ">=13.0.0 < 15.0.0", 22 | "@angular/forms": ">=13.0.0 < 15.0.0", 23 | "typescript": "^4.5.2" 24 | }, 25 | "dependencies": { 26 | "tslib": "^2.3.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.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 | 16 | # IDEs and editors 17 | /.idea 18 | .project 19 | .classpath 20 | .c9/ 21 | *.launch 22 | .settings/ 23 | *.sublime-workspace 24 | 25 | # IDE - VSCode 26 | .vscode/* 27 | !.vscode/settings.json 28 | !.vscode/tasks.json 29 | !.vscode/launch.json 30 | !.vscode/extensions.json 31 | .history/* 32 | 33 | # misc 34 | /.angular/cache 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 | -------------------------------------------------------------------------------- /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/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: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | (id: string): T; 13 | keys(): string[]; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting(), 21 | ); 22 | 23 | // Then we find all the tests. 24 | const context = require.context('./', true, /\.spec\.ts$/); 25 | // And load the modules. 26 | context.keys().map(context); 27 | -------------------------------------------------------------------------------- /projects/api-mock/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'; 4 | import 'zone.js/testing'; 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { 7 | BrowserDynamicTestingModule, 8 | platformBrowserDynamicTesting 9 | } from '@angular/platform-browser-dynamic/testing'; 10 | 11 | declare const require: { 12 | context(path: string, deep?: boolean, filter?: RegExp): { 13 | (id: string): T; 14 | keys(): string[]; 15 | }; 16 | }; 17 | 18 | // First, initialize the Angular testing environment. 19 | getTestBed().initTestEnvironment( 20 | BrowserDynamicTestingModule, 21 | platformBrowserDynamicTesting(), 22 | ); 23 | 24 | // Then we find all the tests. 25 | const context = require.context('./', true, /\.spec\.ts$/); 26 | // And load the modules. 27 | context.keys().map(context); 28 | -------------------------------------------------------------------------------- /projects/forms/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'; 4 | import 'zone.js/testing'; 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { 7 | BrowserDynamicTestingModule, 8 | platformBrowserDynamicTesting 9 | } from '@angular/platform-browser-dynamic/testing'; 10 | 11 | declare const require: { 12 | context(path: string, deep?: boolean, filter?: RegExp): { 13 | (id: string): T; 14 | keys(): string[]; 15 | }; 16 | }; 17 | 18 | // First, initialize the Angular testing environment. 19 | getTestBed().initTestEnvironment( 20 | BrowserDynamicTestingModule, 21 | platformBrowserDynamicTesting(), 22 | ); 23 | 24 | // Then we find all the tests. 25 | const context = require.context('./', true, /\.spec\.ts$/); 26 | // And load the modules. 27 | context.keys().map(context); 28 | -------------------------------------------------------------------------------- /projects/contenteditable/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'; 4 | import 'zone.js/testing'; 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { 7 | BrowserDynamicTestingModule, 8 | platformBrowserDynamicTesting 9 | } from '@angular/platform-browser-dynamic/testing'; 10 | 11 | declare const require: { 12 | context(path: string, deep?: boolean, filter?: RegExp): { 13 | (id: string): T; 14 | keys(): string[]; 15 | }; 16 | }; 17 | 18 | // First, initialize the Angular testing environment. 19 | getTestBed().initTestEnvironment( 20 | BrowserDynamicTestingModule, 21 | platformBrowserDynamicTesting(), 22 | ); 23 | 24 | // Then we find all the tests. 25 | const context = require.context('./', true, /\.spec\.ts$/); 26 | // And load the modules. 27 | context.keys().map(context); 28 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async () => { 7 | await TestBed.configureTestingModule({ 8 | imports: [ 9 | RouterTestingModule 10 | ], 11 | declarations: [ 12 | AppComponent 13 | ], 14 | }).compileComponents(); 15 | }); 16 | 17 | it('should create the app', () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.componentInstance; 20 | expect(app).toBeTruthy(); 21 | }); 22 | 23 | it(`should have as title 'ng-stack-13'`, () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | const app = fixture.componentInstance; 26 | expect(app.title).toEqual('ng-stack-13'); 27 | }); 28 | 29 | it('should render title', () => { 30 | const fixture = TestBed.createComponent(AppComponent); 31 | fixture.detectChanges(); 32 | const compiled = fixture.nativeElement as HTMLElement; 33 | expect(compiled.querySelector('.content span')?.textContent).toContain('ng-stack-13 app is running!'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /projects/api-mock/src/lib/api-mock.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpBackend } from '@angular/common/http'; 2 | import { ModuleWithProviders, NgModule, Type } from '@angular/core'; 3 | 4 | import { HttpBackendService } from './http-backend.service'; 5 | import { ApiMockConfig, ApiMockService } from './types'; 6 | 7 | @NgModule() 8 | export class ApiMockModule { 9 | static forRoot( 10 | apiMockService: Type, 11 | apiMockConfig?: ApiMockConfig 12 | ): ModuleWithProviders { 13 | return { 14 | ngModule: ApiMockModule, 15 | providers: [ 16 | { provide: ApiMockService, useClass: apiMockService }, 17 | { provide: ApiMockConfig, useValue: apiMockConfig }, 18 | { provide: HttpBackend, useClass: HttpBackendService }, 19 | ], 20 | }; 21 | } 22 | 23 | /** 24 | * Enable and configure the `@ng-stack/api-mock` in a lazy-loaded feature module. 25 | * Same as `forRoot`. 26 | * This is a feel-good method so you can follow the Angular style guide for lazy-loaded modules. 27 | */ 28 | static forFeature( 29 | apiMockService: Type, 30 | apiMockConfig?: ApiMockConfig 31 | ): ModuleWithProviders { 32 | return ApiMockModule.forRoot(apiMockService, apiMockConfig); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-stack-13", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "14.2.0", 14 | "@angular/common": "14.2.0", 15 | "@angular/compiler": "14.2.0", 16 | "@angular/core": "14.2.0", 17 | "@angular/forms": "14.2.0", 18 | "@angular/platform-browser": "14.2.0", 19 | "@angular/platform-browser-dynamic": "14.2.0", 20 | "@angular/router": "14.2.0", 21 | "rxjs": "~7.4.0", 22 | "tslib": "^2.3.0", 23 | "zone.js": "~0.11.4" 24 | }, 25 | "devDependencies": { 26 | "@angular-devkit/build-angular": "14.2.0", 27 | "@angular/cli": "14.2.0", 28 | "@angular/compiler-cli": "14.2.0", 29 | "@types/assert-plus": "^1.0.4", 30 | "@types/jasmine": "~3.10.0", 31 | "@types/node": "^12.11.1", 32 | "assert-plus": "^1.0.0", 33 | "jasmine-core": "~3.10.0", 34 | "karma": "~6.3.0", 35 | "karma-chrome-launcher": "~3.1.0", 36 | "karma-coverage": "~2.1.0", 37 | "karma-jasmine": "~4.0.0", 38 | "karma-jasmine-html-reporter": "~1.7.0", 39 | "ng-packagr": "14.2.0", 40 | "typescript": "4.8.2" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /projects/api-mock/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # [1.3.0](https://github.com/KostyaTretyak/ng-stack/releases/tag/api-mock%401.3.0) (2020-06-25) 3 | 4 | ### Features 5 | 6 | * **peer dependencies:** added support for Angular v10. 7 | 8 | 9 | ## [1.2.1](https://github.com/KostyaTretyak/ng-stack/releases/tag/api-mock%401.2.1) (2020-04-23) 10 | 11 | ### Bug Fixes 12 | 13 | * **queryParams:** fixed issue which incorrectly parses the absolute URL. 14 | 15 | 16 | ## [1.2.0](https://github.com/KostyaTretyak/ng-stack/releases/tag/api-mock%401.2.0) (2020-04-12) 17 | 18 | ### Features 19 | 20 | * **ApiMockDataCallbackOptions:** Added `reqHeaders` [#65](https://github.com/KostyaTretyak/ng-stack/issues/65). 21 | 22 | 23 | ## [1.1.0](https://github.com/KostyaTretyak/ng-stack/releases/tag/api-mock%401.1.0) (2020-04-08) 24 | 25 | ### Features 26 | 27 | * **ApiMockConfig:** Not need to set `*NoAction` option to have a successful response for routes without `dataCallback`. 28 | Now this functionality is "by default". See [this commit](https://github.com/KostyaTretyak/ng-stack/commit/269be2d). 29 | 30 | 31 | ## 0.0.0-beta.1 (2020-03-17) 32 | 33 | ### Features 34 | 35 | * **npm pack:** `@ng-stack/api-mock` use Angular-CLI v9 git mono repository and build npm pack with it. 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "strictPropertyInitialization": false, 11 | "paths": { 12 | "@ng-stack/forms": [ 13 | "dist/forms/forms", 14 | "dist/forms" 15 | ], 16 | "@ng-stack/contenteditable": [ 17 | "dist/contenteditable/contenteditable", 18 | "dist/contenteditable" 19 | ], 20 | "@ng-stack/api-mock": [ 21 | "dist/api-mock/api-mock", 22 | "dist/api-mock" 23 | ] 24 | }, 25 | "noPropertyAccessFromIndexSignature": true, 26 | "noImplicitReturns": true, 27 | "noFallthroughCasesInSwitch": true, 28 | "sourceMap": true, 29 | "declaration": false, 30 | "downlevelIteration": true, 31 | "experimentalDecorators": true, 32 | "moduleResolution": "node", 33 | "importHelpers": true, 34 | "target": "es2017", 35 | "module": "es2020", 36 | "lib": [ 37 | "es2020", 38 | "dom" 39 | ] 40 | }, 41 | "angularCompilerOptions": { 42 | "enableI18nLegacyMessageIdFormat": false, 43 | "strictInjectionParameters": true, 44 | "strictInputAccessModifiers": true, 45 | "strictTemplates": true 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /projects/forms/src/lib/assert.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert-plus'; 2 | 3 | export type Diff = T extends X ? never : T; 4 | 5 | export type hasType = Diff> & U; 6 | export type Fn = (...args: any[]) => any; 7 | export type NonFn = Diff; 8 | export type IsArray = T extends (infer Item)[] ? Item : never; 9 | export type NonArray = Diff>; 10 | 11 | // A bit rewrited TypeScript definitions for the type checking functions. 12 | 13 | export function isString(value: hasType) { 14 | assert.string(value); 15 | } 16 | 17 | export function isNumber(value: hasType) { 18 | assert.number(value); 19 | } 20 | 21 | export function isBoolean(value: hasType) { 22 | assert.bool(value); 23 | } 24 | 25 | export function isFunction(value: hasType) { 26 | assert.func(value); 27 | } 28 | 29 | export function isArray(value: hasType) { 30 | assert.array(value); 31 | } 32 | 33 | export function isObject(value: hasType & NonArray, object>) { 34 | assert.object(value); 35 | } 36 | 37 | export function isSymbol(value: hasType) { 38 | if (typeof value != 'symbol') { 39 | throw new TypeError(`${typeof value} (symbol) is required`); 40 | } 41 | } 42 | 43 | export function isNever(value: never) { 44 | if (value !== undefined) { 45 | throw new TypeError(`${typeof value} (undefined) is required`); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /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'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, './coverage/ng-stack-13'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /projects/forms/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'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, '../../coverage/forms'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /projects/api-mock/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'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, '../../coverage/api-mock'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /projects/contenteditable/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'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, '../../coverage/contenteditable'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /projects/contenteditable/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # [2.0.0](https://github.com/KostyaTretyak/ng-stack/releases/tag/contenteditable-2.0.0) (2022-02-17) 3 | 4 | ### Breaking Changes 5 | 6 | - Changed module name to `NgsContenteditableModule`. 7 | 8 | 9 | ## [2.0.0-beta.2](https://github.com/KostyaTretyak/ng-stack/releases/tag/contenteditable-2.0.0-beta.2) (2022-02-14) 10 | 11 | ### Breaking Changes 12 | 13 | - Changed selector to `editable`. 14 | - The lib builded with Ivy Renderer. 15 | 16 | 17 | ## [1.1.1](https://github.com/KostyaTretyak/ng-stack/releases/tag/contenteditable%401.1.1) (2020-10-15) 18 | 19 | ### Fix 20 | 21 | - Refactoring of the method to support the `unformattedPaste` attribute. 22 | 23 | 24 | ## [1.1.0](https://github.com/KostyaTretyak/ng-stack/releases/tag/contenteditable%401.1.0) (2020-08-22) 25 | 26 | ### Features 27 | 28 | - Added experimental `[unformattedPaste]` attribute that allow copy formated text (from anywhere) and paste unformated text into HTML element with `contenteditable="true"` attribute. 29 | 30 | 31 | ## [1.0.2](https://github.com/KostyaTretyak/ng-stack/releases/tag/contenteditable%401.0.2) (2019-05-04) 32 | 33 | ### Fix 34 | 35 | - **directive selector:** Specified selector for directive: 36 | 37 | before: 38 | 39 | ```ts 40 | @Directive({ 41 | selector: '[contenteditable]', 42 | //... 43 | }) 44 | ``` 45 | 46 | now: 47 | 48 | ```ts 49 | @Directive({ 50 | selector: '[contenteditable][formControlName],[contenteditable][formControl],[contenteditable][ngModel]', 51 | //... 52 | }) 53 | ``` 54 | 55 | 56 | ## 1.0.0 (2019-02-06) 57 | 58 | ### Features 59 | 60 | * **npm pack:** `@ng-stack/contenteditable` use Angular-CLI v7 git mono repository and build npm pack with it. 61 | * **testing:** Added Unit tests. 62 | * **contenteditable:** Since version 1.0.0, `@ng-stack/contenteditable` accepts `contenteditable` as @Input property. ([#12](https://github.com/KostyaTretyak/ng-contenteditable/issues/12)) 63 | -------------------------------------------------------------------------------- /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 recent versions of Safari, Chrome (including 12 | * Opera), Edge on the desktop, and iOS and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | 51 | /*************************************************************************************************** 52 | * APPLICATION IMPORTS 53 | */ 54 | -------------------------------------------------------------------------------- /projects/api-mock/src/lib/pick-properties.ts: -------------------------------------------------------------------------------- 1 | import { ObjectAny } from './types'; 2 | 3 | export function pickProperties>(targetObject: T, ...sourceObjects: S[]) { 4 | sourceObjects.forEach(sourceObj => { 5 | sourceObj = (sourceObj || {}) as S; 6 | for (const prop in targetObject) { 7 | if (Array.isArray(sourceObj[prop])) { 8 | targetObject[prop] = sourceObj[prop].slice(); 9 | } else if (sourceObj[prop] !== undefined) { 10 | targetObject[prop] = sourceObj[prop] as any; 11 | } 12 | } 13 | }); 14 | 15 | return targetObject; 16 | } 17 | 18 | /** 19 | * Pick all properties from a `targetObject` and replace 20 | * them with getters that takes values from corresponding properties of `sourceObjects`. 21 | * This is symplified version of `pickPropertiesAsGetters()`. 22 | * 23 | * If one of `sourceObjects` is equal to `targetObject`, 24 | * from start the function will do this: 25 | * 26 | ```ts 27 | targetObject = JSON.parse(JSON.stringify(targetObject)); 28 | ``` 29 | */ 30 | export function pickAllPropertiesAsGetters(targetObject: T, ...sourceObjects: ObjectAny[]) { 31 | if (sourceObjects.length) { 32 | return pickPropertiesAsGetters(targetObject, {}, ...sourceObjects); 33 | } 34 | return pickPropertiesAsGetters(targetObject, {}, targetObject); 35 | } 36 | 37 | /** 38 | * Pick given properties from a `targetObject` and replace 39 | * them with getters that takes values from corresponding properties of `sourceObjects`. 40 | * 41 | * If one of `sourceObjects` is equal to `targetObject`, 42 | * from start the function will do this: 43 | * 44 | ```ts 45 | targetObject = JSON.parse(JSON.stringify(targetObject)); 46 | ``` 47 | */ 48 | export function pickPropertiesAsGetters>( 49 | targetObject: T, 50 | properties: { includeProperties?: K[]; excludeProperties?: K[] }, 51 | ...sourceObjects: ObjectAny[] 52 | ) { 53 | properties = properties || {}; 54 | const incl = properties.includeProperties; 55 | const excl = properties.excludeProperties; 56 | 57 | for (const sourceObj of sourceObjects) { 58 | if (targetObject === sourceObj) { 59 | targetObject = JSON.parse(JSON.stringify(targetObject)); 60 | break; 61 | } 62 | } 63 | 64 | sourceObjects.forEach(sourceObj => { 65 | sourceObj = sourceObj || {}; 66 | Object.keys(targetObject) 67 | .filter(callback as any) 68 | .forEach(prop => { 69 | if (sourceObj.hasOwnProperty(prop)) { 70 | Object.defineProperty(targetObject, prop, { 71 | get() { 72 | return sourceObj[prop]; 73 | }, 74 | }); 75 | } 76 | }); 77 | }); 78 | 79 | return targetObject; 80 | 81 | function callback(property: K) { 82 | if (incl && excl) { 83 | return incl.includes(property) && !excl.includes(property); 84 | } else if (incl) { 85 | return incl.includes(property); 86 | } else if (excl) { 87 | return !excl.includes(property); 88 | } else { 89 | return true; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /projects/forms/src/lib/form-array.spec.ts: -------------------------------------------------------------------------------- 1 | import { Control, ExtractModelValue } from './types'; 2 | import { FormArray } from './form-array'; 3 | import { FormControl } from './form-control'; 4 | import { FormGroup } from './form-group'; 5 | 6 | describe('FormArray', () => { 7 | class Profile { 8 | firstName: string; 9 | lastName: string; 10 | addresses: Address[]; 11 | } 12 | 13 | class Address { 14 | street: string; 15 | city: string; 16 | state: string; 17 | zip: string; 18 | } 19 | 20 | xdescribe('checking types only', () => { 21 | // Waiting for resolving https://github.com/Microsoft/TypeScript/issues/30207 22 | 23 | describe('constructor', () => { 24 | it('FormArray -> FormControl', () => { 25 | const formArray = new FormArray([new FormControl('one')]); 26 | const arr: string[] = formArray.value; 27 | formArray.reset(['one', 'two']); 28 | formArray.setValue(['one', 'two']); 29 | formArray.patchValue(['one', 'two']); 30 | }); 31 | 32 | it(`FormArray -> FormArray -> FormControl -> string`, () => { 33 | const formArray = new FormArray([new FormArray([new FormControl('one')])]); 34 | const val1: string[][] = formArray.value; 35 | formArray.reset([['one', 'two']]); 36 | formArray.setValue([['one', 'two']]); 37 | formArray.patchValue([['one', 'two']]); 38 | }); 39 | 40 | it(`FormArray -> FormControl -> string[]`, () => { 41 | const formArray = new FormArray>([new FormControl(['one', 'two'])]); 42 | const val1: string[][] = formArray.value; 43 | formArray.reset([['one', 'two']]); 44 | formArray.setValue([['one', 'two']]); 45 | formArray.patchValue([['one', 'two']]); 46 | }); 47 | 48 | it(`'one' property as an array of FormArrays`, () => { 49 | class FormModel { 50 | one: string[]; 51 | } 52 | const formArray = new FormArray([ 53 | new FormGroup({ one: new FormArray([new FormControl('one'), new FormControl('two')]) }), 54 | ]); 55 | const arr1: FormModel[] = formArray.value; 56 | const arr2: string[] = formArray.value[0].one; 57 | formArray.reset([{ one: ['1'] }]); 58 | formArray.setValue([{ one: ['1'] }]); 59 | formArray.patchValue([{ one: ['1'] }]); 60 | }); 61 | 62 | it(`'one' property as an array of FormControls`, () => { 63 | class FormModel { 64 | one: Control; 65 | } 66 | const formArray = new FormArray([ 67 | new FormGroup({ one: new FormControl(['one', 'two']) }), 68 | ]); 69 | const val1: ExtractModelValue[] = formArray.value; 70 | formArray.reset([{ one: ['1'] }]); 71 | formArray.setValue([{ one: ['1'] }]); 72 | formArray.patchValue([{ one: ['1'] }]); 73 | const val2: string[] = formArray.value[0].one; 74 | }); 75 | }); 76 | }); 77 | 78 | describe(`checking runtime work`, () => { 79 | it('case 1', () => { 80 | const formArray = new FormArray([new FormControl()]); 81 | formArray.reset(['one']); 82 | expect(formArray.value).toEqual(['one']); 83 | 84 | formArray.reset([{ value: 'two', disabled: false }]); 85 | expect(formArray.value).toEqual(['two']); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /projects/contenteditable/README.md: -------------------------------------------------------------------------------- 1 | ## What is this library? 2 | 3 | This is micro Angular v4+ contenteditable directive for integration with Angular forms. 4 | It just implements [ControlValueAccessor](https://angular.io/api/forms/ControlValueAccessor) for this purpose. 5 | 6 | ## Install 7 | 8 | ```bash 9 | npm install @ng-stack/contenteditable --save 10 | ``` 11 | 12 | ## Usage 13 | 14 | Import and add `NgsContenteditableModule` to your project: 15 | 16 | ```ts 17 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 18 | import { NgsContenteditableModule } from '@ng-stack/contenteditable'; 19 | 20 | // ... 21 | 22 | @NgModule({ 23 | // ... 24 | imports: [ 25 | // Import this module to get available work angular with `contenteditable` 26 | NgsContenteditableModule, 27 | // Import one or both of this modules 28 | FormsModule, 29 | ReactiveFormsModule 30 | ] 31 | 32 | // ... 33 | 34 | }) 35 | ``` 36 | 37 | And then you can to use it in [template-driven forms](https://angular.io/guide/forms) 38 | or [reactive forms](https://angular.io/guide/reactive-forms) like this: 39 | 40 | ```ts 41 | // In your component 42 | import { Component, OnInit } from '@angular/core'; 43 | import { FormControl } from '@angular/forms'; 44 | 45 | export class MyComponent implements OnInit { 46 | templateDrivenForm = 'This is contenteditable text for template-driven form'; 47 | myControl = new FormControl(); 48 | 49 | ngOnInit() { 50 | this.myControl.setValue(`This is contenteditable text for reactive form`); 51 | } 52 | } 53 | ``` 54 | 55 | ```html 56 |
57 |

62 |
63 | 64 |
 65 |   {{ testForm.value | json }}
 66 | 
67 | 68 |
69 | 70 |

71 | 72 |
 73 |   {{ myControl.value | json }}
 74 | 
75 | ``` 76 | 77 | ## Options 78 | 79 | ### propValueAccessor 80 | 81 | With `editable` directive you can pass optional `@Input` value for `propValueAccessor`: 82 | 83 | ```html 84 |

89 | ``` 90 | 91 | Internally, `ContenteditableDirective` uses this value as follows: 92 | 93 | ```ts 94 | this.elementRef.nativeElement[this.propValueAccessor] 95 | ``` 96 | 97 | By default it using `textContent`. 98 | 99 | ### `editable` as @Input property 100 | 101 | Since version 2.0.0, `@ng-stack/contenteditable` accepts `editable` as @Input property (note the square brackets): 102 | 103 | ```html 104 |

105 | ``` 106 | 107 | where `isContenteditable` is a boolean variable. 108 | 109 | ### unformattedPaste 110 | 111 | Since version 1.1.0, `@ng-stack/contenteditable` takes into account experimental `unformattedPaste` attribute: 112 | 113 | ```html 114 |

119 | ``` 120 | 121 | This allow copy formated text (from anywhere) and paste unformated text into HTML element with `contenteditable` attribute. 122 | 123 | `unformattedPaste` attribute is experimental because here is used obsolete [document.execCommand()](https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand) method to write unformated text. So far no good alternative for this method has been found. 124 | -------------------------------------------------------------------------------- /projects/forms/src/lib/input-file.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Directive, 3 | ElementRef, 4 | Renderer2, 5 | HostListener, 6 | forwardRef, 7 | Input, 8 | Output, 9 | EventEmitter, 10 | HostBinding, 11 | } from '@angular/core'; 12 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; 13 | 14 | @Directive({ 15 | selector: ` 16 | input[type=file][ngModel], 17 | input[type=file][formControl], 18 | input[type=file][formControlName]`, 19 | providers: [ 20 | { 21 | provide: NG_VALUE_ACCESSOR, 22 | useExisting: forwardRef(() => InputFileDirective), 23 | multi: true, 24 | }, 25 | ], 26 | }) 27 | export class InputFileDirective implements ControlValueAccessor { 28 | private _multiple: boolean | string | undefined; 29 | 30 | @HostBinding('attr.multiple') 31 | @Input() 32 | get multiple(): boolean | string | undefined { 33 | if ( 34 | this._multiple !== undefined && 35 | this._multiple !== false && 36 | this._multiple !== 'false' 37 | ) { 38 | return ''; 39 | } else { 40 | return undefined; 41 | } 42 | } 43 | 44 | set multiple(value: boolean | string | undefined) { 45 | this._multiple = value; 46 | } 47 | @HostBinding('attr.preserveValue') @Input() preserveValue: boolean | string; 48 | @Output() select = new EventEmitter(); 49 | private onChange = (value: FormData) => {}; 50 | private onTouched = () => {}; 51 | 52 | constructor(private elementRef: ElementRef, private renderer: Renderer2) {} 53 | 54 | /** 55 | * Callback function that should be called when 56 | * the control's value changes in the UI. 57 | */ 58 | @HostListener('change', ['$event']) 59 | callOnChange(event: any) { 60 | this.onTouched(); 61 | const files = Array.from(this.elementRef.nativeElement.files); 62 | const formData = new FormData(); 63 | 64 | let formInputName = this.elementRef.nativeElement.name || 'uploadFile'; 65 | if ( 66 | this.multiple !== undefined && 67 | this.multiple !== false && 68 | this.multiple !== 'false' 69 | ) { 70 | formInputName += '[]'; 71 | } 72 | files.forEach((file) => formData.append(formInputName, file)); 73 | 74 | this.onChange(formData); 75 | this.select.next(files); 76 | if ( 77 | this.preserveValue === undefined || 78 | this.preserveValue === false || 79 | this.preserveValue === 'false' 80 | ) { 81 | event.target.value = null; 82 | } 83 | } 84 | 85 | /** 86 | * Writes a new value to the element. 87 | * This method will be called by the forms API to write 88 | * to the view when programmatic (model -> view) changes are requested. 89 | * 90 | * See: [ControlValueAccessor](https://angular.io/api/forms/ControlValueAccessor#members) 91 | */ 92 | writeValue(fileList: FileList): void { 93 | if (fileList && !(fileList instanceof FileList)) { 94 | throw new TypeError( 95 | 'Value for input[type=file] must be an instance of FileList' 96 | ); 97 | } 98 | this.renderer.setProperty(this.elementRef.nativeElement, 'files', fileList); 99 | } 100 | 101 | /** 102 | * Registers a callback function that should be called when 103 | * the control's value changes in the UI. 104 | * 105 | * This is called by the forms API on initialization so it can update 106 | * the form model when values propagate from the view (view -> model). 107 | */ 108 | registerOnChange(fn: () => void): void { 109 | this.onChange = fn; 110 | } 111 | 112 | /** 113 | * Registers a callback function that should be called when the control receives a change event. 114 | * This is called by the forms API on initialization so it can update the form model on change. 115 | */ 116 | registerOnTouched(fn: () => void): void { 117 | this.onTouched = fn; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /projects/forms/src/lib/assert.spec.ts: -------------------------------------------------------------------------------- 1 | import { Fn, isString, isNumber, isBoolean, isSymbol, isFunction, isObject, isArray } from './assert'; 2 | 3 | // Uncomment some row to see that tsc checking types correctly. 4 | 5 | describe('assertions', () => { 6 | let value: any; 7 | 8 | it('should not to throw when assert string', () => { 9 | expect(callback).not.toThrow(); 10 | 11 | function callback() { 12 | value = ''; 13 | isString(value as string); 14 | // isString(value as any); 15 | // isString(null); 16 | // isString(undefined); 17 | // isString(value as number); 18 | // isString(value as boolean); 19 | // isString(value as symbol); 20 | // isString(value as object); 21 | // isString(value as Fn); 22 | } 23 | }); 24 | 25 | it('should not to throw when assert number', () => { 26 | expect(callback).not.toThrow(); 27 | 28 | function callback() { 29 | value = 1; 30 | isNumber(value as number); 31 | // isNumber(value as any); 32 | // isNumber(null); 33 | // isNumber(undefined); 34 | // isNumber(value as string); 35 | // isNumber(value as boolean); 36 | // isNumber(value as symbol); 37 | // isNumber(value as object); 38 | // isNumber(value as Fn); 39 | } 40 | }); 41 | 42 | it('should not to throw when assert boolean', () => { 43 | expect(callback).not.toThrow(); 44 | 45 | function callback() { 46 | value = true; 47 | isBoolean(value as boolean); 48 | // isBoolean(value as any); 49 | // isBoolean(null); 50 | // isBoolean(undefined); 51 | // isBoolean(value as string); 52 | // isBoolean(value as number); 53 | // isBoolean(value as symbol); 54 | // isBoolean(value as object); 55 | // isBoolean(value as Fn); 56 | } 57 | }); 58 | 59 | it('should not to throw when assert symbol', () => { 60 | expect(callback).not.toThrow(); 61 | 62 | function callback() { 63 | value = Symbol(); 64 | isSymbol(value as symbol); 65 | // isSymbol(value as any); 66 | // isSymbol(null); 67 | // isSymbol(undefined); 68 | // isSymbol(value as string); 69 | // isSymbol(value as number); 70 | // isSymbol(value as boolean); 71 | // isSymbol(value as object); 72 | // isSymbol(value as Fn); 73 | } 74 | }); 75 | 76 | it('should not to throw when assert Function', () => { 77 | expect(callback).not.toThrow(); 78 | 79 | function callback() { 80 | value = () => {}; 81 | isFunction(value as Fn); 82 | // isFunction(value as any); 83 | // isFunction(null); 84 | // isFunction(undefined); 85 | // isFunction(value as string); 86 | // isFunction(value as number); 87 | // isFunction(value as boolean); 88 | // isFunction(value as symbol); 89 | // isFunction(value as object); 90 | } 91 | }); 92 | 93 | it('should not to throw when assert array', () => { 94 | expect(callback).not.toThrow(); 95 | 96 | function callback() { 97 | value = []; 98 | isArray(value as any[]); 99 | // isArray(value as object); 100 | // isObject(value as Fn); 101 | // isObject(value as any); 102 | // isObject(null); 103 | // isObject(undefined); 104 | // isObject(value as string); 105 | // isObject(value as number); 106 | // isObject(value as boolean); 107 | // isObject(value as symbol); 108 | } 109 | }); 110 | 111 | it('should not to throw when assert object', () => { 112 | expect(callback).not.toThrow(); 113 | 114 | function callback() { 115 | value = {}; 116 | isObject(value as object); 117 | // isObject(value as any[]); 118 | // isObject(value as Fn); 119 | // isObject(value as any); 120 | // isObject(null); 121 | // isObject(undefined); 122 | // isObject(value as string); 123 | // isObject(value as number); 124 | // isObject(value as boolean); 125 | // isObject(value as symbol); 126 | } 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /projects/contenteditable/src/lib/editable.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Directive, 3 | ElementRef, 4 | Renderer2, 5 | HostListener, 6 | HostBinding, 7 | forwardRef, 8 | Input, 9 | Inject, 10 | Attribute, 11 | } from '@angular/core'; 12 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; 13 | import { DOCUMENT } from '@angular/common'; 14 | 15 | @Directive({ 16 | selector: 17 | '[editable][formControlName],[editable][formControl],[editable][ngModel]', 18 | providers: [ 19 | { 20 | provide: NG_VALUE_ACCESSOR, 21 | useExisting: forwardRef(() => EditableDirective), 22 | multi: true, 23 | }, 24 | ], 25 | }) 26 | export class EditableDirective implements ControlValueAccessor { 27 | @Input() propValueAccessor = 'textContent'; 28 | @HostBinding('attr.contenteditable') @Input() editable: boolean | string = 29 | true; 30 | 31 | private onChange: (value: string) => void; 32 | private onTouched: () => void; 33 | private removeDisabledState: () => void; 34 | 35 | constructor( 36 | private elementRef: ElementRef, 37 | private renderer: Renderer2, 38 | @Attribute('unformattedPaste') private unformattedPaste: string, 39 | @Inject(DOCUMENT) private document: Document 40 | ) {} 41 | 42 | @HostListener('input') 43 | callOnChange() { 44 | if (typeof this.onChange == 'function') { 45 | this.onChange(this.elementRef.nativeElement[this.propValueAccessor]); 46 | } 47 | } 48 | 49 | @HostListener('blur') 50 | callOnTouched() { 51 | if (typeof this.onTouched == 'function') { 52 | this.onTouched(); 53 | } 54 | } 55 | 56 | /** 57 | * Writes a new value to the element. 58 | * This method will be called by the forms API to write 59 | * to the view when programmatic (model -> view) changes are requested. 60 | * 61 | * See: [ControlValueAccessor](https://angular.io/api/forms/ControlValueAccessor#members) 62 | */ 63 | writeValue(value: any): void { 64 | const normalizedValue = value == null ? '' : value; 65 | this.renderer.setProperty( 66 | this.elementRef.nativeElement, 67 | this.propValueAccessor, 68 | normalizedValue 69 | ); 70 | } 71 | 72 | /** 73 | * Registers a callback function that should be called when 74 | * the control's value changes in the UI. 75 | * 76 | * This is called by the forms API on initialization so it can update 77 | * the form model when values propagate from the view (view -> model). 78 | */ 79 | registerOnChange(fn: () => void): void { 80 | this.onChange = fn; 81 | } 82 | 83 | /** 84 | * Registers a callback function that should be called when the control receives a blur event. 85 | * This is called by the forms API on initialization so it can update the form model on blur. 86 | */ 87 | registerOnTouched(fn: () => void): void { 88 | this.onTouched = fn; 89 | } 90 | 91 | /** 92 | * This function is called by the forms API when the control status changes to or from "DISABLED". 93 | * Depending on the value, it should enable or disable the appropriate DOM element. 94 | */ 95 | setDisabledState(isDisabled: boolean): void { 96 | if (isDisabled) { 97 | this.renderer.setAttribute( 98 | this.elementRef.nativeElement, 99 | 'disabled', 100 | 'true' 101 | ); 102 | this.removeDisabledState = this.renderer.listen( 103 | this.elementRef.nativeElement, 104 | 'keydown', 105 | this.listenerDisabledState 106 | ); 107 | } else { 108 | if (this.removeDisabledState) { 109 | this.renderer.removeAttribute( 110 | this.elementRef.nativeElement, 111 | 'disabled' 112 | ); 113 | this.removeDisabledState(); 114 | } 115 | } 116 | } 117 | 118 | @HostListener('paste', ['$event']) 119 | preventFormatedPaste(event: ClipboardEvent) { 120 | if ( 121 | this.unformattedPaste === null || 122 | this.unformattedPaste == 'false' || 123 | !this.document.execCommand 124 | ) { 125 | return; 126 | } 127 | event.preventDefault(); 128 | const { clipboardData } = event; 129 | const text = 130 | clipboardData?.getData('text/plain') || clipboardData?.getData('text'); 131 | this.document.execCommand('insertText', false, text); 132 | } 133 | 134 | private listenerDisabledState(e: KeyboardEvent) { 135 | e.preventDefault(); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /projects/forms/src/lib/form-builder.spec.ts: -------------------------------------------------------------------------------- 1 | import { FormBuilder } from './form-builder'; 2 | import { FormControl } from './form-control'; 3 | import { Validators } from './validators'; 4 | import { AbstractControl } from '@angular/forms'; 5 | import { FormGroup } from './form-group'; 6 | 7 | describe('FormBuilder', () => { 8 | xdescribe('checking types only', () => { 9 | class Address { 10 | street?: string; 11 | city?: string; 12 | state?: string; 13 | zip?: string; 14 | } 15 | 16 | class SomeArray { 17 | item1?: string; 18 | item2?: number; 19 | } 20 | 21 | class UserForm { 22 | userName: string; 23 | userEmail: string; 24 | password?: string; 25 | addresses: Address; 26 | someArray: SomeArray[]; 27 | otherArray: (string | number)[]; 28 | } 29 | 30 | class FormProps { 31 | userEmail: string; 32 | token: string; 33 | iAgree: boolean; 34 | } 35 | 36 | const fb = new FormBuilder(); 37 | 38 | it('common', () => { 39 | const formGroup1 = fb.group({ 40 | userName: 'SomeOne', 41 | // userName: 123, 42 | // userEmail: new FormGroup({}), 43 | userEmail: new FormControl('some-one@gmail.com'), 44 | addresses: fb.group({ city: 'Kyiv' }), 45 | someArray: fb.array([ 46 | fb.group({ item1: 'value1' }), 47 | fb.group({ item1: 'value2' }), 48 | fb.group({ item1: 'value3' }), 49 | fb.group({ item1: 'value4' }), 50 | ]), 51 | // otherArray: fb.array([new FormControl(5), new FormGroup({}), 'three']), 52 | 53 | // otherArray: fb.array([new FormControl('one'), 2, 'three']), 54 | // Error --> Why? See https://github.com/Microsoft/TypeScript/issues/30207 55 | 56 | otherArray: fb.array([new FormControl('one'), ['two', Validators.required], 'three']), 57 | }); 58 | 59 | const addresses: Address = formGroup1.value.addresses; 60 | 61 | const formGroup2 = fb.group({ 62 | userEmail: [null, (control: AbstractControl) => ({ required: true, other: 1 })], 63 | token: [null, [Validators.required]], 64 | iAgree: [null, Validators.required], 65 | }); 66 | 67 | formGroup1.get('otherArray')!.setValue(['string value', 2, 'three']); 68 | 69 | fb.array([ 70 | fb.group({ item1: 'value1' }), 71 | fb.group({ item1: 'value2' }), 72 | fb.group({ item1: 'value3' }), 73 | fb.group({ item1: 'value4' }), 74 | ]); 75 | }); 76 | 77 | it('nesting validation model', () => { 78 | interface FormGroupModel { 79 | control: string; 80 | } 81 | 82 | interface ValidModel1 { 83 | wrongPassword?: { returnedValue: boolean }; 84 | wrongEmail?: { returnedValue: boolean }; 85 | } 86 | 87 | interface ValidModel2 { 88 | wrongPassword?: { returnedValue: boolean }; 89 | otherKey?: { returnedValue: boolean }; 90 | } 91 | 92 | const form = fb.group({ 93 | control: new FormControl('some value'), 94 | }); 95 | 96 | const formError = form.getError('wrongEmail'); 97 | const controlError = form.get('control')!.getError('email'); // Without errror, but it's wrong. 98 | }); 99 | }); 100 | 101 | fdescribe(`checking runtime work`, () => { 102 | describe(`control()`, () => { 103 | it('case 1', () => { 104 | const fb = new FormBuilder(); 105 | expect(fb.control('one').value).toBe('one'); 106 | expect(fb.control({ value: 'two', disabled: false }).value).toBe('two'); 107 | }); 108 | }); 109 | 110 | describe(`array()`, () => { 111 | it('case 1', () => { 112 | const fb = new FormBuilder(); 113 | let control = new FormControl('one'); 114 | expect(fb.array([control]).value).toEqual(['one']); 115 | control = new FormControl({ value: 'two', disabled: false }); 116 | expect(fb.array([control]).value).toEqual(['two']); 117 | }); 118 | 119 | it('case 2', () => { 120 | const fb = new FormBuilder(); 121 | const control = new FormControl('one'); 122 | const group = new FormGroup({ one: control }); 123 | expect(fb.array([group]).value).toEqual([{ one: 'one' }]); 124 | }); 125 | }); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /projects/forms/src/lib/form-builder.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { UntypedFormBuilder as NativeFormBuilder } from '@angular/forms'; 3 | 4 | import { 5 | FbControlConfig, 6 | AbstractControlOptions, 7 | ValidatorFn, 8 | AsyncValidatorFn, 9 | ValidatorsModel, 10 | FormControlState, 11 | } from './types'; 12 | import { FormGroup } from './form-group'; 13 | import { FormControl } from './form-control'; 14 | import { FormArray } from './form-array'; 15 | 16 | @Injectable() 17 | export class FormBuilder extends NativeFormBuilder { 18 | /** 19 | * Construct a new `FormGroup` instance. 20 | * 21 | * @param controlsConfig A collection of child controls. The key for each child is the name 22 | * under which it is registered. 23 | * 24 | * @param options Configuration options object for the `FormGroup`. The object can 25 | * have two shapes: 26 | * 27 | * 1) `AbstractControlOptions` object (preferred), which consists of: 28 | * - `validators`: A synchronous validator function, or an array of validator functions 29 | * - `asyncValidators`: A single async validator or array of async validator functions 30 | * - `updateOn`: The event upon which the control should be updated (options: 'change' | 'blur' | 31 | * submit') 32 | * 33 | * 2) Legacy configuration object, which consists of: 34 | * - `validator`: A synchronous validator function, or an array of validator functions 35 | * - `asyncValidator`: A single async validator or array of async validator functions 36 | */ 37 | override group( 38 | controlsConfig: { [P in keyof T]: FbControlConfig }, 39 | options: AbstractControlOptions | null = null 40 | ): FormGroup { 41 | return super.group(controlsConfig, options) as FormGroup; 42 | } 43 | 44 | /** 45 | * @description 46 | * Construct a new `FormControl` with the given state, validators and options. 47 | * 48 | * @param formState Initializes the control with an initial state value, or 49 | * with an object that contains both a value and a disabled status. 50 | * 51 | * @param validatorOrOpts A synchronous validator function, or an array of 52 | * such functions, or an `AbstractControlOptions` object that contains 53 | * validation functions and a validation trigger. 54 | * 55 | * @param asyncValidator A single async validator or array of async validator 56 | * functions. 57 | * 58 | * ### Initialize a control as disabled 59 | * 60 | * The following example returns a control with an initial value in a disabled state. 61 | ```ts 62 | import {Component, Inject} from '@angular/core'; 63 | import {FormBuilder, FormControl, FormGroup, Validators} from '@angular/forms'; 64 | // ... 65 | @Component({ 66 | selector: 'app-disabled-form-control', 67 | template: ` 68 | 69 | ` 70 | }) 71 | export class DisabledFormControlComponent { 72 | control: FormControl; 73 | 74 | constructor(private fb: FormBuilder) { 75 | this.control = fb.control({value: 'my val', disabled: true}); 76 | } 77 | } 78 | ``` 79 | */ 80 | override control( 81 | formState: FormControlState = null, 82 | validatorOrOpts?: 83 | | ValidatorFn 84 | | ValidatorFn[] 85 | | AbstractControlOptions 86 | | null, 87 | asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null 88 | ): FormControl { 89 | return super.control( 90 | formState, 91 | validatorOrOpts, 92 | asyncValidator 93 | ) as FormControl; 94 | } 95 | 96 | /** 97 | * Constructs a new `FormArray` from the given array of configurations, 98 | * validators and options. 99 | * 100 | * @param controlsConfig An array of child controls or control configs. Each 101 | * child control is given an index when it is registered. 102 | * 103 | * @param validatorOrOpts A synchronous validator function, or an array of 104 | * such functions, or an `AbstractControlOptions` object that contains 105 | * validation functions and a validation trigger. 106 | * 107 | * @param asyncValidator A single async validator or array of async validator 108 | * functions. 109 | */ 110 | override array( 111 | controlsConfig: FbControlConfig[], 112 | validatorOrOpts?: 113 | | ValidatorFn 114 | | ValidatorFn[] 115 | | AbstractControlOptions 116 | | null, 117 | asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null 118 | ): FormArray { 119 | return super.array( 120 | controlsConfig, 121 | validatorOrOpts, 122 | asyncValidator 123 | ) as FormArray; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /projects/api-mock/src/lib/pick-properties.spec.ts: -------------------------------------------------------------------------------- 1 | import { pickAllPropertiesAsGetters, pickPropertiesAsGetters } from './pick-properties'; 2 | 3 | describe('pickAllPropertiesAsGetters', () => { 4 | it('signature1: result = pickAllPropertiesAsGetters(targetObj, sourceObj)', () => { 5 | const targetObj: any = { one: null }; 6 | const sourceObj = { one: 1, two: 2 }; 7 | const result = pickAllPropertiesAsGetters(targetObj, sourceObj); 8 | expect(targetObj).toBe(result); 9 | expect(sourceObj).toEqual({ one: 1, two: 2 }); 10 | expect(targetObj.one).toBe(1); 11 | sourceObj.one = 11; 12 | expect(targetObj.one).toBe(11); 13 | expect(targetObj).toBe(result); 14 | }); 15 | 16 | it('signature2: targetObj = pickAllPropertiesAsGetters(sourceObj)', () => { 17 | const sourceObj: any = { one: null }; 18 | const targetObj = pickAllPropertiesAsGetters(sourceObj); 19 | expect(sourceObj).not.toBe(targetObj); 20 | expect(sourceObj.one).toBe(null); 21 | expect(targetObj.one).toBe(null); 22 | sourceObj.one = 11; 23 | expect(targetObj.one).toBe(11); 24 | }); 25 | 26 | it('signature3: result = pickAllPropertiesAsGetters(targetObj, sourceObj1, sourceObj2)', () => { 27 | const targetObj: any = { one: null, two: null, other: null }; 28 | const sourceObj1 = { one: 1, two: 2, other: 'other value' }; 29 | const sourceObj2 = { one: 11, two: 22 }; 30 | const result = pickAllPropertiesAsGetters(targetObj, sourceObj1, sourceObj2); 31 | expect(targetObj).toBe(result); 32 | expect(sourceObj1).toEqual({ one: 1, two: 2, other: 'other value' }); 33 | expect(sourceObj2).toEqual({ one: 11, two: 22 }); 34 | expect(targetObj.one).toBe(11); 35 | expect(targetObj.two).toBe(22); 36 | expect(targetObj.other).toBe('other value'); 37 | sourceObj1.one = 111; 38 | sourceObj1.two = 222; 39 | sourceObj1.other = 'some thing else'; 40 | expect(targetObj.one).toBe(11); 41 | expect(targetObj.two).toBe(22); 42 | expect(targetObj.other).toBe('some thing else'); 43 | sourceObj2.one = 1010; 44 | sourceObj2.two = 2020; 45 | expect(targetObj.one).toBe(1010); 46 | expect(targetObj.two).toBe(2020); 47 | expect(targetObj).toBe(result); 48 | }); 49 | }); 50 | 51 | describe('pickPropertiesAsGetters', () => { 52 | it('signature1: result = pickPropertiesAsGetters(targetObj, null, sourceObj)', () => { 53 | const targetObj: any = { one: null }; 54 | const sourceObj = { one: 1, two: 2 }; 55 | const result = pickPropertiesAsGetters(targetObj, {}, sourceObj); 56 | expect(targetObj).toBe(result); 57 | expect(sourceObj).toEqual({ one: 1, two: 2 }); 58 | expect(targetObj.one).toBe(1); 59 | sourceObj.one = 11; 60 | expect(targetObj.one).toBe(11); 61 | expect(targetObj).toBe(result); 62 | }); 63 | 64 | it(`signature2: result = pickPropertiesAsGetters(targetObj, { includeProperties:... }, sourceObj)`, () => { 65 | const targetObj: any = { one: null, two: null }; 66 | const sourceObj = { one: 1, two: 2 }; 67 | const result = pickPropertiesAsGetters(targetObj, { includeProperties: ['one'] }, sourceObj); 68 | expect(targetObj).toBe(result); 69 | expect(sourceObj).toEqual({ one: 1, two: 2 }); 70 | expect(targetObj.one).toBe(1); 71 | expect(targetObj.two).toBe(null); 72 | sourceObj.one = 11; 73 | sourceObj.two = 22; 74 | expect(targetObj.one).toBe(11); 75 | expect(targetObj.two).toBe(null); 76 | expect(targetObj).toBe(result); 77 | }); 78 | 79 | it(`signature3: result = pickPropertiesAsGetters(targetObj, { excludeProperties:... }, sourceObj)`, () => { 80 | const targetObj: any = { one: null, two: null }; 81 | const sourceObj = { one: 1, two: 2 }; 82 | const result = pickPropertiesAsGetters(targetObj, { excludeProperties: ['one'] }, sourceObj); 83 | expect(targetObj).toBe(result); 84 | expect(sourceObj).toEqual({ one: 1, two: 2 }); 85 | expect(targetObj.one).toBe(null); 86 | expect(targetObj.two).toBe(2); 87 | sourceObj.one = 11; 88 | sourceObj.two = 22; 89 | expect(targetObj.one).toBe(null); 90 | expect(targetObj.two).toBe(22); 91 | expect(targetObj).toBe(result); 92 | }); 93 | 94 | it(`signature4: result = pickPropertiesAsGetters(targetObj, { excludeProperties:..., includeProperties:... }, sourceObj)`, () => { 95 | const targetObj: any = { one: null, two: null }; 96 | const sourceObj = { one: 1, two: 2 }; 97 | const result = pickPropertiesAsGetters( 98 | targetObj, 99 | { excludeProperties: ['one'], includeProperties: ['one', 'two'] }, 100 | sourceObj 101 | ); 102 | expect(targetObj).toBe(result); 103 | expect(sourceObj).toEqual({ one: 1, two: 2 }); 104 | expect(targetObj.one).toBe(null); 105 | expect(targetObj.two).toBe(2); 106 | sourceObj.one = 11; 107 | sourceObj.two = 22; 108 | expect(targetObj.one).toBe(null); 109 | expect(targetObj.two).toBe(22); 110 | expect(targetObj).toBe(result); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /projects/forms/src/lib/validators.spec.ts: -------------------------------------------------------------------------------- 1 | import { Validators } from './validators'; 2 | 3 | describe('Validators', () => { 4 | describe('fileRequired', () => { 5 | it('null as value', () => { 6 | const validator = Validators.fileRequired; 7 | expect(validator({ value: null } as any)).toEqual({ fileRequired: true }); 8 | }); 9 | 10 | it('some object as value', () => { 11 | const validator = Validators.fileRequired; 12 | expect(validator({ value: { one: 1 } } as any)).toEqual({ fileRequired: true }); 13 | }); 14 | 15 | it('one file as value', () => { 16 | const content = '1'.repeat(10); 17 | 18 | const formData = new FormData(); 19 | const blob = new Blob([content], { type: 'text/plain' }); 20 | const file = new File([blob], 'any-name.jpg'); 21 | formData.append('fileUpload', file); 22 | const validator = Validators.fileRequired; 23 | expect(validator({ value: formData } as any)).toBe(null); 24 | }); 25 | }); 26 | 27 | describe('filesMinLength', () => { 28 | it('one file when expected min one file', () => { 29 | const minLength = 1; 30 | 31 | const formData = new FormData(); 32 | const file = new File([], 'any-name.jpg'); 33 | formData.append('fileUpload', file); 34 | const validator = Validators.filesMinLength(minLength); 35 | expect(validator({ value: formData } as any)).toBe(null); 36 | }); 37 | 38 | it('one file when expected min two files', () => { 39 | const minLength = 2; 40 | const actualLength = 1; 41 | 42 | const formData = new FormData(); 43 | const file = new File([], 'any-name.jpg'); 44 | formData.append('fileUpload', file); 45 | const validator = Validators.filesMinLength(minLength); 46 | const err = { filesMinLength: { requiredLength: minLength, actualLength } }; 47 | expect(validator({ value: formData } as any)).toEqual(err); 48 | }); 49 | }); 50 | 51 | describe('filesMaxLength', () => { 52 | it('one file when expected max one file', () => { 53 | const maxLength = 1; 54 | 55 | const formData = new FormData(); 56 | const file = new File([], 'any-name.jpg'); 57 | formData.append('fileUpload', file); 58 | const validator = Validators.filesMaxLength(maxLength); 59 | expect(validator({ value: formData } as any)).toBe(null); 60 | }); 61 | 62 | it('three files when expected max two files', () => { 63 | const maxLength = 2; 64 | const actualLength = 3; 65 | 66 | const formData = new FormData(); 67 | const file = new File([], 'any-name.jpg'); 68 | formData.append('fileUpload', file); 69 | formData.append('fileUpload', file); 70 | formData.append('fileUpload', file); 71 | const validator = Validators.filesMaxLength(maxLength); 72 | const err = { filesMaxLength: { requiredLength: maxLength, actualLength } }; 73 | expect(validator({ value: formData } as any)).toEqual(err); 74 | }); 75 | }); 76 | 77 | describe('fileMaxSize', () => { 78 | it('null as content', () => { 79 | const maxSize = 10; 80 | const content = null as any; 81 | 82 | const formData = new FormData(); 83 | const blob = new Blob([content], { type: 'text/plain' }); 84 | const file = new File([blob], 'any-name.jpg'); 85 | formData.append('fileUpload', file); 86 | const validator = Validators.fileMaxSize(maxSize); 87 | expect(validator({ value: formData } as any)).toBe(null); 88 | }); 89 | 90 | it('empty content', () => { 91 | const maxSize = 10; 92 | const content = ''; 93 | 94 | const formData = new FormData(); 95 | const blob = new Blob([content], { type: 'text/plain' }); 96 | const file = new File([blob], 'any-name.jpg'); 97 | formData.append('fileUpload', file); 98 | const validator = Validators.fileMaxSize(maxSize); 99 | expect(validator({ value: formData } as any)).toBe(null); 100 | }); 101 | 102 | it('acceptable content', () => { 103 | const maxSize = 10; 104 | const content = '1'.repeat(10); 105 | 106 | const formData = new FormData(); 107 | const blob = new Blob([content], { type: 'text/plain' }); 108 | const file = new File([blob], 'any-name.jpg'); 109 | formData.append('fileUpload', file); 110 | const validator = Validators.fileMaxSize(maxSize); 111 | expect(validator({ value: formData } as any)).toBe(null); 112 | }); 113 | 114 | it('exceeded max size', () => { 115 | const maxSize = 10; 116 | const actualSize = 11; 117 | const content = '1'.repeat(actualSize); 118 | 119 | const formData = new FormData(); 120 | const blob = new Blob([content], { type: 'text/plain' }); 121 | const file = new File([blob], 'any-name.jpg'); 122 | formData.append('fileUpload', file); 123 | const validator = Validators.fileMaxSize(maxSize); 124 | const err = { fileMaxSize: { requiredSize: maxSize, actualSize, file } }; 125 | expect(validator({ value: formData } as any)).toEqual(err); 126 | }); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ng-stack-13": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | }, 12 | "@schematics/angular:application": { 13 | "strict": true 14 | } 15 | }, 16 | "root": "", 17 | "sourceRoot": "src", 18 | "prefix": "app", 19 | "architect": { 20 | "build": { 21 | "builder": "@angular-devkit/build-angular:browser", 22 | "options": { 23 | "outputPath": "dist/ng-stack-13", 24 | "index": "src/index.html", 25 | "main": "src/main.ts", 26 | "polyfills": "src/polyfills.ts", 27 | "tsConfig": "tsconfig.app.json", 28 | "inlineStyleLanguage": "scss", 29 | "assets": [ 30 | "src/favicon.ico", 31 | "src/assets" 32 | ], 33 | "styles": [ 34 | "src/styles.scss" 35 | ], 36 | "scripts": [] 37 | }, 38 | "configurations": { 39 | "production": { 40 | "budgets": [ 41 | { 42 | "type": "initial", 43 | "maximumWarning": "500kb", 44 | "maximumError": "1mb" 45 | }, 46 | { 47 | "type": "anyComponentStyle", 48 | "maximumWarning": "2kb", 49 | "maximumError": "4kb" 50 | } 51 | ], 52 | "fileReplacements": [ 53 | { 54 | "replace": "src/environments/environment.ts", 55 | "with": "src/environments/environment.prod.ts" 56 | } 57 | ], 58 | "outputHashing": "all" 59 | }, 60 | "development": { 61 | "buildOptimizer": false, 62 | "optimization": false, 63 | "vendorChunk": true, 64 | "extractLicenses": false, 65 | "sourceMap": true, 66 | "namedChunks": true 67 | } 68 | }, 69 | "defaultConfiguration": "production" 70 | }, 71 | "serve": { 72 | "builder": "@angular-devkit/build-angular:dev-server", 73 | "configurations": { 74 | "production": { 75 | "browserTarget": "ng-stack-13:build:production" 76 | }, 77 | "development": { 78 | "browserTarget": "ng-stack-13:build:development" 79 | } 80 | }, 81 | "defaultConfiguration": "development" 82 | }, 83 | "extract-i18n": { 84 | "builder": "@angular-devkit/build-angular:extract-i18n", 85 | "options": { 86 | "browserTarget": "ng-stack-13:build" 87 | } 88 | }, 89 | "test": { 90 | "builder": "@angular-devkit/build-angular:karma", 91 | "options": { 92 | "main": "src/test.ts", 93 | "polyfills": "src/polyfills.ts", 94 | "tsConfig": "tsconfig.spec.json", 95 | "karmaConfig": "karma.conf.js", 96 | "inlineStyleLanguage": "scss", 97 | "assets": [ 98 | "src/favicon.ico", 99 | "src/assets" 100 | ], 101 | "styles": [ 102 | "src/styles.scss" 103 | ], 104 | "scripts": [] 105 | } 106 | } 107 | } 108 | }, 109 | "forms": { 110 | "projectType": "library", 111 | "root": "projects/forms", 112 | "sourceRoot": "projects/forms/src", 113 | "prefix": "lib", 114 | "architect": { 115 | "build": { 116 | "builder": "@angular-devkit/build-angular:ng-packagr", 117 | "options": { 118 | "project": "projects/forms/ng-package.json" 119 | }, 120 | "configurations": { 121 | "production": { 122 | "tsConfig": "projects/forms/tsconfig.lib.prod.json" 123 | }, 124 | "development": { 125 | "tsConfig": "projects/forms/tsconfig.lib.json" 126 | } 127 | }, 128 | "defaultConfiguration": "production" 129 | }, 130 | "test": { 131 | "builder": "@angular-devkit/build-angular:karma", 132 | "options": { 133 | "main": "projects/forms/src/test.ts", 134 | "tsConfig": "projects/forms/tsconfig.spec.json", 135 | "karmaConfig": "projects/forms/karma.conf.js" 136 | } 137 | } 138 | } 139 | }, 140 | "contenteditable": { 141 | "projectType": "library", 142 | "root": "projects/contenteditable", 143 | "sourceRoot": "projects/contenteditable/src", 144 | "prefix": "lib", 145 | "architect": { 146 | "build": { 147 | "builder": "@angular-devkit/build-angular:ng-packagr", 148 | "options": { 149 | "project": "projects/contenteditable/ng-package.json" 150 | }, 151 | "configurations": { 152 | "production": { 153 | "tsConfig": "projects/contenteditable/tsconfig.lib.prod.json" 154 | }, 155 | "development": { 156 | "tsConfig": "projects/contenteditable/tsconfig.lib.json" 157 | } 158 | }, 159 | "defaultConfiguration": "production" 160 | }, 161 | "test": { 162 | "builder": "@angular-devkit/build-angular:karma", 163 | "options": { 164 | "main": "projects/contenteditable/src/test.ts", 165 | "tsConfig": "projects/contenteditable/tsconfig.spec.json", 166 | "karmaConfig": "projects/contenteditable/karma.conf.js" 167 | } 168 | } 169 | } 170 | }, 171 | "api-mock": { 172 | "projectType": "library", 173 | "root": "projects/api-mock", 174 | "sourceRoot": "projects/api-mock/src", 175 | "prefix": "lib", 176 | "architect": { 177 | "build": { 178 | "builder": "@angular-devkit/build-angular:ng-packagr", 179 | "options": { 180 | "project": "projects/api-mock/ng-package.json" 181 | }, 182 | "configurations": { 183 | "production": { 184 | "tsConfig": "projects/api-mock/tsconfig.lib.prod.json" 185 | }, 186 | "development": { 187 | "tsConfig": "projects/api-mock/tsconfig.lib.json" 188 | } 189 | }, 190 | "defaultConfiguration": "production" 191 | }, 192 | "test": { 193 | "builder": "@angular-devkit/build-angular:karma", 194 | "options": { 195 | "main": "projects/api-mock/src/test.ts", 196 | "tsConfig": "projects/api-mock/tsconfig.spec.json", 197 | "karmaConfig": "projects/api-mock/karma.conf.js" 198 | } 199 | } 200 | } 201 | } 202 | }, 203 | "defaultProject": "ng-stack-13" 204 | } 205 | -------------------------------------------------------------------------------- /projects/forms/src/lib/form-control.ts: -------------------------------------------------------------------------------- 1 | import { UntypedFormControl as NativeFormControl } from '@angular/forms'; 2 | 3 | import { Observable } from 'rxjs'; 4 | 5 | import { 6 | Status, 7 | ValidationErrors, 8 | StringKeys, 9 | ValidatorFn, 10 | AsyncValidatorFn, 11 | AbstractControlOptions, 12 | ValidatorsModel, 13 | ExtractControlValue, 14 | FormControlState, 15 | } from './types'; 16 | 17 | export class FormControl extends NativeFormControl { 18 | override readonly value: ExtractControlValue; 19 | override readonly valueChanges: Observable>; 20 | override readonly status: Status; 21 | override readonly statusChanges: Observable; 22 | override readonly errors: ValidationErrors | null; 23 | 24 | /** 25 | * Creates a new `FormControl` instance. 26 | * 27 | * @param formState Initializes the control with an initial value, 28 | * or an object that defines the initial value and disabled state. 29 | * 30 | * @param validatorOrOpts A synchronous validator function, or an array of 31 | * such functions, or an `AbstractControlOptions` object that contains validation functions 32 | * and a validation trigger. 33 | * 34 | * @param asyncValidator A single async validator or array of async validator functions 35 | * 36 | */ 37 | constructor( 38 | formState: FormControlState = null, 39 | validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, 40 | asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null 41 | ) { 42 | super(formState, validatorOrOpts, asyncValidator); 43 | } 44 | 45 | /** 46 | * Sets a new value for the form control. 47 | * 48 | * @param value The new value for the control. 49 | * @param options Configuration options that determine how the control proopagates changes 50 | * and emits events when the value changes. 51 | * The configuration options are passed to the 52 | * [updateValueAndValidity](https://angular.io/api/forms/AbstractControl#updateValueAndValidity) method. 53 | * 54 | * * `onlySelf`: When true, each change only affects this control, and not its parent. Default is 55 | * false. 56 | * * `emitEvent`: When true or not supplied (the default), both the `statusChanges` and 57 | * `valueChanges` 58 | * observables emit events with the latest status and value when the control value is updated. 59 | * When false, no events are emitted. 60 | * * `emitModelToViewChange`: When true or not supplied (the default), each change triggers an 61 | * `onChange` event to 62 | * update the view. 63 | * * `emitViewToModelChange`: When true or not supplied (the default), each change triggers an 64 | * `ngModelChange` 65 | * event to update the model. 66 | * 67 | */ 68 | override setValue( 69 | value: ExtractControlValue, 70 | options: { 71 | onlySelf?: boolean; 72 | emitEvent?: boolean; 73 | emitModelToViewChange?: boolean; 74 | emitViewToModelChange?: boolean; 75 | } = {} 76 | ) { 77 | return super.setValue(value, options); 78 | } 79 | 80 | /** 81 | * Patches the value of a control. 82 | * 83 | * This function is functionally the same as [setValue](https://angular.io/api/forms/FormControl#setValue) at this level. 84 | * It exists for symmetry with [patchValue](https://angular.io/api/forms/FormGroup#patchValue) on `FormGroups` and 85 | * `FormArrays`, where it does behave differently. 86 | * 87 | * See also: `setValue` for options 88 | */ 89 | override patchValue( 90 | value: ExtractControlValue, 91 | options: { 92 | onlySelf?: boolean; 93 | emitEvent?: boolean; 94 | emitModelToViewChange?: boolean; 95 | emitViewToModelChange?: boolean; 96 | } = {} 97 | ) { 98 | return super.patchValue(value, options); 99 | } 100 | 101 | /** 102 | * Resets the form control, marking it `pristine` and `untouched`, and setting 103 | * the value to null. 104 | * 105 | * @param formState Resets the control with an initial value, 106 | * or an object that defines the initial value and disabled state. 107 | * 108 | * @param options Configuration options that determine how the control propagates changes 109 | * and emits events after the value changes. 110 | * 111 | * * `onlySelf`: When true, each change only affects this control, and not its parent. Default is 112 | * false. 113 | * * `emitEvent`: When true or not supplied (the default), both the `statusChanges` and 114 | * `valueChanges` 115 | * observables emit events with the latest status and value when the control is reset. 116 | * When false, no events are emitted. 117 | * 118 | */ 119 | override reset( 120 | formState: FormControlState = null, 121 | options: { 122 | onlySelf?: boolean; 123 | emitEvent?: boolean; 124 | } = {} 125 | ) { 126 | return super.reset(formState, options); 127 | } 128 | 129 | /** 130 | * In `FormControl`, this method always returns `null`. 131 | */ 132 | override get(): null { 133 | return null; 134 | } 135 | 136 | /** 137 | * Sets the synchronous validators that are active on this control. Calling 138 | * this overwrites any existing sync validators. 139 | */ 140 | override setValidators(newValidator: ValidatorFn | ValidatorFn[] | null) { 141 | return super.setValidators(newValidator); 142 | } 143 | 144 | /** 145 | * Sets the async validators that are active on this control. Calling this 146 | * overwrites any existing async validators. 147 | */ 148 | override setAsyncValidators(newValidator: AsyncValidatorFn | AsyncValidatorFn[] | null) { 149 | return super.setAsyncValidators(newValidator); 150 | } 151 | 152 | /** 153 | * Sets errors on a form control when running validations manually, rather than automatically. 154 | * 155 | * Calling `setErrors` also updates the validity of the parent control. 156 | * 157 | * ### Manually set the errors for a control 158 | * 159 | * ```ts 160 | * const login = new FormControl('someLogin'); 161 | * login.setErrors({ 162 | * notUnique: true 163 | * }); 164 | * 165 | * expect(login.valid).toEqual(false); 166 | * expect(login.errors).toEqual({ notUnique: true }); 167 | * 168 | * login.setValue('someOtherLogin'); 169 | * 170 | * expect(login.valid).toEqual(true); 171 | * ``` 172 | */ 173 | override setErrors(errors: ValidationErrors | null, opts: { emitEvent?: boolean } = {}) { 174 | return super.setErrors(errors, opts); 175 | } 176 | 177 | /** 178 | * Reports error data for the current control. 179 | * 180 | * @param errorCode The code of the error to check. 181 | * 182 | * @returns error data for that particular error. If an error is not present, 183 | * null is returned. 184 | */ 185 | override getError = any>(errorCode: K) { 186 | return super.getError(errorCode) as V[K] | null; 187 | } 188 | 189 | /** 190 | * Reports whether the current control has the error specified. 191 | * 192 | * @param errorCode The code of the error to check. 193 | * 194 | * @returns whether the given error is present in the current control. 195 | * 196 | * If an error is not present, false is returned. 197 | */ 198 | override hasError = any>(errorCode: K) { 199 | return super.hasError(errorCode); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /projects/forms/src/lib/types.spec.ts: -------------------------------------------------------------------------------- 1 | import { FormArray } from './form-array'; 2 | import { FormGroup } from './form-group'; 3 | import { FormControl } from './form-control'; 4 | import { 5 | ControlType, 6 | FbControlConfig, 7 | FbControl, 8 | Control, 9 | ExtractGroupValue, 10 | ExtractGroupStateValue, 11 | ExtractModelValue, 12 | ExtractControlValue, 13 | FormControlState, 14 | } from './types'; 15 | 16 | xdescribe('checking types only', () => { 17 | // tslint:disable: prefer-const 18 | 19 | class Model { 20 | street: string; 21 | city: string; 22 | state: string; 23 | zip: string; 24 | } 25 | 26 | describe('Control of FormGroup', () => { 27 | describe('ControlType', () => { 28 | it('should return FormArray type', () => { 29 | let valueWithType: ControlType = {} as any; 30 | const formArray: FormArray = valueWithType; 31 | }); 32 | 33 | it('should return FormControl', () => { 34 | let valueWithType: ControlType> = {} as any; 35 | const formControl: FormControl = valueWithType; 36 | }); 37 | 38 | it('should return FormControl', () => { 39 | let valueWithType: ControlType> = {} as any; 40 | const formControl: FormControl = valueWithType; 41 | }); 42 | 43 | it('should return FormGroup', () => { 44 | let valueWithType: ControlType = {} as any; 45 | const formGroup: FormGroup = valueWithType; 46 | }); 47 | 48 | it('should return FormControl', () => { 49 | let valueWithType: ControlType = {} as any; 50 | const formControl: FormControl = valueWithType; 51 | }); 52 | }); 53 | 54 | describe('ExtractControlValue', () => { 55 | it('case 1', () => { 56 | let value: ExtractControlValue = ''; 57 | const val: string = value; 58 | }); 59 | 60 | it('case 2', () => { 61 | let value: ExtractControlValue = ['']; 62 | const val: string[] = value; 63 | }); 64 | 65 | it('case 3', () => { 66 | let value: ExtractControlValue = true; 67 | const val: boolean = value; 68 | }); 69 | 70 | it('case 4', () => { 71 | let value: ExtractControlValue = [true]; 72 | const val: boolean[] = value; 73 | }); 74 | 75 | it('case 5', () => { 76 | let value: ExtractControlValue<{ one: string }> = {one: ''}; 77 | const val: { one: string } = value; 78 | }); 79 | 80 | it('case 6', () => { 81 | let value: ExtractControlValue> = ['one']; 82 | const val: string[] = value; 83 | }); 84 | 85 | it('case 7', () => { 86 | let value: ExtractControlValue> = [true]; 87 | const val: boolean[] = value; 88 | }); 89 | 90 | it('case 8', () => { 91 | let value: ExtractControlValue> = { one: 'val' }; 92 | const val: { one: string } = value; 93 | }); 94 | 95 | it('case 9', () => { 96 | let value: ExtractControlValue<{ one: Control }> = { one: [''] } as any; 97 | const val: { one: string[] } = value; 98 | }); 99 | }); 100 | 101 | describe('FormControlState', () => { 102 | let value1: FormControlState; 103 | value1 = ''; 104 | value1 = { value: '', disabled: false }; 105 | }); 106 | 107 | describe('ControlValue', () => { 108 | it('should clear value outside an object (for FormControl)', () => { 109 | interface FormModel { 110 | one: string; 111 | } 112 | let value: ExtractGroupValue> = {} as any; 113 | const obj1: FormModel = value; 114 | const str1: string = value.one; 115 | const control = new FormControl(value); 116 | const obj2: FormModel = control.value; 117 | const str2: string = control.value.one; 118 | }); 119 | 120 | it('should clear value inside an object (for FormGroup)', () => { 121 | interface FormModel { 122 | one: Control; 123 | } 124 | let value: ExtractGroupValue= {} as any; 125 | const arr1: string[] = value.one; 126 | }); 127 | }); 128 | 129 | describe('ExtractFormValue', () => { 130 | it('case 1', () => { 131 | let value: ExtractModelValue; 132 | const val1: string = value; 133 | const val2: number = value; 134 | const val3: string[] = value; 135 | const val4: { one: 1 } = value; 136 | }); 137 | 138 | it('case 2', () => { 139 | let value: ExtractModelValue = ''; 140 | const val: string = value; 141 | }); 142 | 143 | it('case 3', () => { 144 | let value: ExtractModelValue = ['one'] 145 | const val: string[] = value; 146 | }); 147 | 148 | it('case 4', () => { 149 | interface FormModel { 150 | one: string[]; 151 | } 152 | let value: ExtractModelValue[]> = {} as any; 153 | const val: FormModel[] = value; 154 | }); 155 | 156 | it('case 5', () => { 157 | interface FormModel { 158 | one: Control; 159 | } 160 | let value: ExtractModelValue = ['one'] as any; 161 | const val: string[] = value.one; 162 | }); 163 | 164 | it('case 6', () => { 165 | interface FormModel { 166 | value: { other: string; city: string; street: string }; 167 | disabled: false; 168 | } 169 | let value: ExtractModelValue = {} as any; 170 | const val: FormModel = value; 171 | }); 172 | }); 173 | 174 | describe('FormGroupState', () => { 175 | it('case 1', () => { 176 | let some: ExtractGroupStateValue<{ one: number }> = {} as any;; 177 | some.one = 1; 178 | some.one = { value: 1, disabled: true }; 179 | }); 180 | }); 181 | 182 | type T1 = ControlType; 183 | type T2 = ControlType; 184 | type T3 = ControlType; 185 | type T4 = ControlType<'one' | 'two'>; 186 | type T5 = ControlType<{ one: string; two: number }>; 187 | }); 188 | 189 | describe('Control of FormBuilder', () => { 190 | it('should return FormArray type', () => { 191 | let valueWithType: FbControlConfig = {} as any; 192 | const formArray: FormArray = valueWithType; 193 | }); 194 | 195 | it('should return FbControl', () => { 196 | let valueWithType: FbControlConfig> = {} as any; 197 | const formControl: FbControl = valueWithType; 198 | }); 199 | 200 | it('should return FormGroup', () => { 201 | let valueWithType: FbControlConfig; 202 | const formGroup: FormGroup = valueWithType = {} as any; 203 | }); 204 | 205 | it('should return FbControl', () => { 206 | let valueWithType: FbControlConfig; 207 | const fbControl: FbControl = valueWithType = {} as any; 208 | }); 209 | 210 | type T1 = FbControlConfig; 211 | type T2 = FbControlConfig; 212 | type T3 = FbControlConfig; 213 | type T4 = FbControlConfig<'one' | 'two'>; 214 | type T5 = FbControlConfig<{ one: string; two: number }>; 215 | }); 216 | }); 217 | -------------------------------------------------------------------------------- /projects/api-mock/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Params } from '@angular/router'; 3 | import { HttpHeaders } from '@angular/common/http'; 4 | import { Status } from './http-status-codes'; 5 | import { pickProperties } from './pick-properties'; 6 | 7 | export abstract class ApiMockService { 8 | abstract getRoutes(): ApiMockRootRoute[]; 9 | } 10 | 11 | /** 12 | * Interface for `HttpBackendService` configuration options. 13 | */ 14 | @Injectable() 15 | export class ApiMockConfig { 16 | /** 17 | * - `true` - should pass unrecognized request URL through to original backend. 18 | * - `false` - (default) return 404 code. 19 | */ 20 | passThruUnknownUrl? = false; 21 | /** 22 | * - Do you need to clear previous console logs? 23 | * 24 | * Clears logs between previous route `NavigationStart` and current `NavigationStart` events. 25 | */ 26 | clearPrevLog? = false; 27 | showLog? = true; 28 | cacheFromLocalStorage? = false; 29 | /** 30 | * By default `apiMockCachedData`. 31 | */ 32 | localStorageKey? = 'apiMockCachedData'; 33 | /** 34 | * Simulate latency by delaying response (in milliseconds). 35 | */ 36 | delay? = 500; 37 | /** 38 | * - `true` - (default) 204 code - should NOT return the item after a `POST` an item with existing ID. 39 | * - `false` - 200 code - return the item. 40 | * 41 | * Tip: 42 | * > **204 No Content** 43 | * 44 | * > The server successfully processed the request and is not returning any content. 45 | */ 46 | postUpdate204? = true; 47 | /** 48 | * - `true` - 409 code - should NOT update existing item with `POST`. 49 | * - `false` - (default) 200 code - OK to update. 50 | * 51 | * Tip: 52 | * > **409 Conflict** 53 | * 54 | * > Indicates that the request could not be processed because of conflict in the current 55 | * > state of the resource, such as an edit conflict between multiple simultaneous updates. 56 | */ 57 | postUpdate409? = false; 58 | /** 59 | * - `true` - (default) 204 code - should NOT return the item after a `PUT` an item with existing ID. 60 | * - `false` - 200 code - return the item. 61 | * 62 | * Tip: 63 | * > **204 No Content** 64 | * 65 | * > The server successfully processed the request and is not returning any content. 66 | */ 67 | putUpdate204? = true; 68 | /** 69 | * - `true` - (default) 404 code - if `PUT` item with that ID not found. 70 | * - `false` - create new item. 71 | */ 72 | putUpdate404? = true; 73 | /** 74 | * - `true` - (default) 204 code - should NOT return the item after a `PATCH` an item with existing ID. 75 | * - `false` - 200 code - return the item. 76 | * 77 | * Tip: 78 | * > **204 No Content** 79 | * 80 | * > The server successfully processed the request and is not returning any content. 81 | */ 82 | patchUpdate204? = true; 83 | /** 84 | * - `true` - (default) 404 code - if item with that ID not found. 85 | * - `false` - 204 code. 86 | * 87 | * Tip: 88 | * > **204 No Content** 89 | * 90 | * > The server successfully processed the request and is not returning any content. 91 | */ 92 | deleteNotFound404? = true; 93 | 94 | constructor(apiMockConfig?: ApiMockConfig) { 95 | pickProperties(this, apiMockConfig as any); 96 | } 97 | } 98 | 99 | /** 100 | * It is just `{ [key: string]: any }` an object interface. 101 | */ 102 | export interface ObjectAny { 103 | [key: string]: any; 104 | } 105 | 106 | export type CallbackAny = (...params: any[]) => any; 107 | 108 | /** 109 | * For more info, see [HTTP Request methods](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods) 110 | */ 111 | export type HttpMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'TRACE' | 'OPTIONS' | 'CONNECT' | 'PATCH'; 112 | 113 | export interface ApiMockDataCallbackOptions { 114 | items?: I; 115 | itemId?: string; 116 | httpMethod?: HttpMethod; 117 | parents?: P; 118 | queryParams?: Params; 119 | /** 120 | * Request body. 121 | */ 122 | reqBody?: any; 123 | reqHeaders?: ObjectAny; 124 | } 125 | 126 | export interface ApiMockResponseCallbackOptions< 127 | I extends ObjectAny[] = ObjectAny[], 128 | P extends ObjectAny[] = ObjectAny[] 129 | > extends ApiMockDataCallbackOptions { 130 | /** 131 | * Response body. 132 | */ 133 | resBody?: any; 134 | } 135 | 136 | export type ApiMockDataCallback = ( 137 | opts?: ApiMockDataCallbackOptions 138 | ) => I; 139 | 140 | export type ApiMockResponseCallback = ( 141 | opts?: ApiMockResponseCallbackOptions 142 | ) => any; 143 | 144 | export interface ApiMockRoute { 145 | path: string; 146 | dataCallback?: ApiMockDataCallback; 147 | /** 148 | * Properties for a list items that returns from `dataCallback()`, but 149 | * you need init this properties: `propertiesForList: { firstProp: null, secondProp: null }`. 150 | */ 151 | propertiesForList?: ObjectAny; 152 | responseCallback?: ApiMockResponseCallback; 153 | /** 154 | * You can store almost all mockData in localStorage, but with exception of store 155 | * from individual routes with `ignoreDataFromLocalStorage == true`. 156 | * 157 | * By default `ignoreDataFromLocalStorage == false`. 158 | */ 159 | ignoreDataFromLocalStorage?: boolean; 160 | children?: ApiMockRoute[]; 161 | } 162 | 163 | export interface ApiMockRootRoute extends ApiMockRoute { 164 | host?: string; 165 | } 166 | 167 | export class CacheData { 168 | [routeKey: string]: MockData; 169 | } 170 | 171 | export type PartialRoutes = Array<{ path: string; length: number; index: number }>; 172 | 173 | export interface RouteDryMatch { 174 | splitedUrl: string[]; 175 | splitedRoute: string[]; 176 | routes: ApiMockRoute[]; 177 | hasLastRestId: boolean; 178 | lastPrimaryKey?: string; 179 | } 180 | 181 | /** 182 | * If we have URL `api/posts/123/comments/456` with route `api/posts/:postId/comments/:commentId`, 183 | * we have two "chain params" for `api/posts` and for `api/posts/123/comments`. 184 | */ 185 | export interface ChainParam { 186 | cacheKey: string; 187 | route: ApiMockRoute; 188 | primaryKey: string; 189 | restId?: string; 190 | } 191 | 192 | export interface MockData { 193 | /** 194 | * Array of full version of items from REST resource, 195 | * it is a single resource of true for given REST resource. 196 | * 197 | * - If HTTP-request have `GET` method with restId, we returns item from this array. 198 | * - If HTTP-request have `POST`, `PUT`, `PATCH` or `DELETE` method, 199 | * this actions will be doing with item from this array. 200 | */ 201 | writeableData: ObjectAny[]; 202 | /** 203 | * Array of composed objects with properties as getters (readonly properties). 204 | * 205 | * - If HTTP-request have `GET` method without restId, we return this array, 206 | * where items may have reduce version of REST resource. 207 | */ 208 | readonlyData: ObjectAny[]; 209 | } 210 | 211 | /** 212 | * Http Response Options. 213 | */ 214 | export interface ResponseOptions { 215 | headers: HttpHeaders; 216 | status: number; 217 | body?: any; 218 | statusText?: string; 219 | url?: string; 220 | } 221 | 222 | export interface ResponseOptionsLog { 223 | status: Status; 224 | body: any; 225 | headers?: ObjectAny; 226 | } 227 | 228 | export function isFormData(formData: FormData): formData is FormData { 229 | return FormData !== undefined && formData instanceof FormData; 230 | } 231 | -------------------------------------------------------------------------------- /projects/forms/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import { AbstractControl } from '@angular/forms'; 2 | 3 | import { Observable } from 'rxjs'; 4 | 5 | import { FormArray } from './form-array'; 6 | import { FormGroup } from './form-group'; 7 | import { FormControl } from './form-control'; 8 | 9 | /** 10 | * This type marks a property of a form model as property 11 | * which is intended for an instance of `FormControl`. 12 | * 13 | * If a property of your form model have a primitive type, 14 | * in appropriate form field the instance of `FormControl` will be automatically assigned. 15 | * But if the property have a type that extends `object` - you need `Control`. 16 | * 17 | * ### Example: 18 | ```ts 19 | import { FormBuilder, Control } from '@ng-stack/forms'; 20 | 21 | const fb = new FormBuilder(); 22 | 23 | // Form Model 24 | interface Person { 25 | id: number; 26 | name: string; 27 | birthDate: Control; // Here should be FormControl, instead of a FormGroup 28 | } 29 | 30 | const form = fb.group({ 31 | id: 123, 32 | name: 'John Smith', 33 | birthDate: new Date(1977, 6, 30), 34 | }); 35 | 36 | const birthDate: Date = form.value.birthDate; 37 | ``` 38 | * ## External form model 39 | * 40 | * If the form model interface comes from an external library, you can do the following: 41 | * 42 | ```ts 43 | import { FormBuilder, Control } from '@ng-stack/forms'; 44 | 45 | const fb = new FormBuilder(); 46 | 47 | // External Form Model 48 | interface ExternalPerson { 49 | id: number; 50 | name: string; 51 | birthDate: Date; 52 | } 53 | 54 | const configForm: ExternalPerson = { 55 | id: 123, 56 | name: 'John Smith', 57 | birthDate: new Date(1977, 6, 30), 58 | }; 59 | 60 | interface Person extends ExternalPerson { 61 | birthDate: Control; 62 | } 63 | 64 | const form = fb.group(configForm); // `Control` type is compatible with `Date` type. 65 | 66 | const birthDate: Date = form.value.birthDate; // `Control` type is compatible with `Date` type. 67 | ``` 68 | */ 69 | export type Control = T & UniqToken; 70 | 71 | const sym = Symbol(); 72 | 73 | interface UniqToken { 74 | [sym]: never; 75 | } 76 | 77 | /** 78 | * Extract `keyof T` with string keys. 79 | */ 80 | export type StringKeys = Extract; 81 | 82 | type ExtractAny = T extends Extract ? any : never; 83 | 84 | /** 85 | * This type is a conditional type that automatically detects 86 | * appropriate types for form controls by given type for its generic. 87 | */ 88 | export type ControlType = [T] extends [ExtractAny] 89 | ? FormGroup | FormControl | FormArray 90 | : [T] extends [Control] 91 | ? FormControl 92 | : [T] extends [Array] 93 | ? FormArray 94 | : [T] extends [object] 95 | ? FormGroup 96 | : FormControl; 97 | 98 | export type FormControlState = 99 | | null 100 | | ExtractModelValue 101 | | { 102 | value: null | ExtractModelValue; 103 | disabled: boolean; 104 | }; 105 | 106 | /** 107 | * Clears the form model from `Control` type. 108 | */ 109 | export type ExtractModelValue = [T] extends [ExtractAny] 110 | ? any 111 | : [T] extends [Array] 112 | ? Array> 113 | : [T] extends [Control] 114 | ? ControlModel 115 | : [T] extends [object] 116 | ? ExtractGroupValue 117 | : T; 118 | 119 | export type ExtractControlValue = [T] extends [Control] ? ControlModel : T; 120 | 121 | /** 122 | * Clears the form model (as object) from `Control` type. 123 | */ 124 | export type ExtractGroupValue = { 125 | [P in keyof T]: ExtractModelValue; 126 | }; 127 | 128 | export type ExtractGroupStateValue = { 129 | [P in keyof T]: FormControlState; 130 | }; 131 | 132 | /** 133 | * Form builder control config. 134 | */ 135 | export type FbControlConfig = [T] extends [ExtractAny] 136 | ? FormGroup | FbControl | FormArray 137 | : [T] extends [Control] 138 | ? FbControl 139 | : [T] extends [Array] 140 | ? FormArray 141 | : [T] extends [object] 142 | ? FormGroup 143 | : FbControl; 144 | 145 | /** 146 | * Form builder control. 147 | */ 148 | export type FbControl = 149 | | ExtractModelValue 150 | | FormControlState 151 | | [ 152 | FormControlState, 153 | (ValidatorFn | ValidatorFn[] | AbstractControlOptions)?, 154 | (AsyncValidatorFn | AsyncValidatorFn[])? 155 | ] 156 | | FormControl; 157 | 158 | /** 159 | * The validation status of the control. There are four possible 160 | * validation status values: 161 | * 162 | * * **VALID**: This control has passed all validation checks. 163 | * * **INVALID**: This control has failed at least one validation check. 164 | * * **PENDING**: This control is in the midst of conducting a validation check. 165 | * * **DISABLED**: This control is exempt from validation checks. 166 | * 167 | * These status values are mutually exclusive, so a control cannot be 168 | * both valid AND invalid or invalid AND disabled. 169 | */ 170 | export type Status = 'VALID' | 'INVALID' | 'PENDING' | 'DISABLED'; 171 | 172 | /** 173 | * Interface for options provided to an `AbstractControl`. 174 | */ 175 | export interface AbstractControlOptions { 176 | /** 177 | * The list of validators applied to a control. 178 | */ 179 | validators?: ValidatorFn | ValidatorFn[] | null; 180 | /** 181 | * The list of async validators applied to control. 182 | */ 183 | asyncValidators?: AsyncValidatorFn | AsyncValidatorFn[] | null; 184 | /** 185 | * The event name for control to update upon. 186 | */ 187 | updateOn?: 'change' | 'blur' | 'submit'; 188 | } 189 | 190 | /** 191 | * A function that receives a control and synchronously returns a map of 192 | * validation errors if present, otherwise null. 193 | */ 194 | export type ValidatorFn = (control: AbstractControl) => ValidationErrors | null; 195 | 196 | /** 197 | * A function that receives a control and returns a Promise or observable 198 | * that emits validation errors if present, otherwise null. 199 | */ 200 | export type AsyncValidatorFn = ( 201 | control: AbstractControl 202 | ) => Promise | null> | Observable | null>; 203 | 204 | /** 205 | * Defines the map of errors returned from failed validation checks. 206 | */ 207 | export type ValidationErrors = T; 208 | 209 | /** 210 | * The default validators model, it includes almost all static properties of `Validators`, 211 | * excludes: `prototype`, `compose`, `composeAsync` and `nullValidator`. 212 | * 213 | * ### Usage 214 | * 215 | ```ts 216 | const formControl = new FormControl('some value'); 217 | // OR 218 | const formGroup = new FormGroup({}); 219 | // OR 220 | const formArray = new FormArray([]); 221 | ``` 222 | */ 223 | export class ValidatorsModel { 224 | min: { min: number; actual: number }; 225 | max: { max: number; actual: number }; 226 | required: true; 227 | email: true; 228 | minlength: { requiredLength: number; actualLength: number }; 229 | maxlength: { requiredLength: number; actualLength: number }; 230 | pattern: { requiredPattern: string; actualValue: string }; 231 | fileRequired: { requiredSize: number; actualSize: number; file: File }; 232 | filesMinLength: { requiredLength: number; actualLength: number }; 233 | filesMaxLength: { requiredLength: number; actualLength: number }; 234 | fileMaxSize: { requiredSize: number; actualSize: number; file: File }; 235 | } 236 | -------------------------------------------------------------------------------- /projects/forms/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # [3.1.0](https://github.com/KostyaTretyak/ng-stack/releases/tag/forms-3.1.0) (2022-08-30) 3 | 4 | ### Features 5 | 6 | - [Added](https://github.com/KostyaTretyak/ng-stack/pull/110) support for Angular v14. 7 | 8 | 9 | ## [3.0.0](https://github.com/KostyaTretyak/ng-stack/releases/tag/forms-3.0.0) (2022-02-17) 10 | 11 | ### BREAKING CHANGES 12 | 13 | - Changed module name to `NgsFormsModule`. 14 | 15 | 16 | ## [3.0.0-beta.2](https://github.com/KostyaTretyak/ng-stack/releases/tag/forms-3.0.0-beta.2) (2022-01-07) 17 | 18 | ### Features 19 | 20 | - Changed `peerDependencies` to support angular v13. 21 | 22 | 23 | ## [2.4.0](https://github.com/KostyaTretyak/ng-stack/releases/tag/forms-2.4.0) (2021-05-15) 24 | 25 | ### Features 26 | 27 | - Changed `peerDependencies` to support angular v12. 28 | 29 | 30 | ## [2.3.0](https://github.com/KostyaTretyak/ng-stack/releases/tag/forms%402.3.0) (2020-11-14) 31 | 32 | ### Features 33 | 34 | - Changed `peerDependencies` to support angular v11. 35 | 36 | 37 | ## [2.2.3](https://github.com/KostyaTretyak/ng-stack/releases/tag/forms%402.2.3) (2020-08-24) 38 | 39 | ### Bug Fixes 40 | 41 | - Fixed peerDependencies (see [#79](https://github.com/KostyaTretyak/ng-stack/issues/79)). `@ng-stack/forms` with version > 2.1.0 is not compatible with Angular v8 and below. 42 | 43 | 44 | ## [2.2.2](https://github.com/KostyaTretyak/ng-stack/releases/tag/forms%402.2.2) (2020-07-05) 45 | 46 | ### Bug Fixes 47 | 48 | - Fixed FormGroup API (see [70ea07](https://github.com/KostyaTretyak/ng-stack/commit/70ea0737bcf)) 49 | 50 | 51 | ## [2.2.1](https://github.com/KostyaTretyak/ng-stack/releases/tag/forms%402.2.1) (2020-07-04) 52 | 53 | ### Bug Fixes 54 | 55 | - Fixed clearing form model from `Control`, closes [#76](https://github.com/KostyaTretyak/ng-stack/issues/76) 56 | 57 | 58 | ## [2.2.0](https://github.com/KostyaTretyak/ng-stack/releases/tag/forms%402.2.0) (2020-06-25) 59 | 60 | ### Features 61 | 62 | - **peer dependencies:** added support for Angular v10. 63 | 64 | 65 | ## [2.1.1](https://github.com/KostyaTretyak/ng-stack/releases/tag/forms%402.1.1) (2020-06-23) 66 | 67 | ### Bug Fixes 68 | 69 | - Fixed `` feature, see [#75](https://github.com/KostyaTretyak/ng-stack/issues/75). 70 | 71 | 72 | ## [2.1.0](https://github.com/KostyaTretyak/ng-stack/releases/tag/forms%402.1.0) (2020-06-23) 73 | 74 | ### Bug Fixes 75 | 76 | - Fixed `` feature, see [#75](https://github.com/KostyaTretyak/ng-stack/issues/75). 77 | 78 | ### Features 79 | 80 | - Added `preserveValue` option, see [docs](./README.md#preserveValue-option) and [#74](https://github.com/KostyaTretyak/ng-stack/issues/74) 81 | 82 | 83 | ## [2.0.1](https://github.com/KostyaTretyak/ng-stack/releases/tag/forms%402.0.1) (2020-06-03) 84 | 85 | ### Bug Fixes 86 | 87 | - Fixed first parameter type for `formArray.patchValue()` (value: `Partial[]` -> `value: Item[]`), see [5bf943b](https://github.com/KostyaTretyak/ng-stack/commit/5bf943bfad4770e5ba26b4132ee6c53049922dde) 88 | 89 | 90 | ## [2.0.0](https://github.com/KostyaTretyak/ng-stack/releases/tag/forms%402.0.0) (2020-04-28) 91 | 92 | ### Notes 93 | 94 | - Release of stable version. 95 | 96 | 97 | ## [2.0.0-beta.4](https://github.com/KostyaTretyak/ng-stack/releases/tag/forms%402.0.0-beta.4) (2020-04-18) 98 | 99 | ### Feature 100 | 101 | - **Form builder** Added support for this signature: 102 | ```ts 103 | fb.group({ 104 | id: {value: 1, disabled: true} 105 | name: [{value: '', disabled: false}] 106 | }) 107 | ``` 108 | 109 | (See [#67](https://github.com/KostyaTretyak/ng-stack/pull/67)). 110 | 111 | 112 | ## [2.0.0-beta.3](https://github.com/KostyaTretyak/ng-stack/releases/tag/forms%402.0.0-beta.3) (2020-04-09) 113 | 114 | ### Bug Fixes 115 | 116 | - **auto detecting controls types** Fixed issue with `Control`. (Closes [#64](https://github.com/KostyaTretyak/ng-stack/issues/64)). 117 | 118 | 119 | ## [2.0.0-beta.2](https://github.com/KostyaTretyak/ng-stack/releases/tag/forms%402.0.0-beta.2) (2020-03-28) 120 | 121 | ### Bug Fixes 122 | 123 | - **support strict mode for `ng build`** Removed `Required` for `reset()` and `setValue()`. (See [9e2408](https://github.com/KostyaTretyak/ng-stack/commit/9e2408)). 124 | 125 | 126 | ## [2.0.0-beta.1](https://github.com/KostyaTretyak/ng-stack/releases/tag/forms%402.0.0-beta.1) (2020-03-20) 127 | 128 | ### Features and BREAKING CHANGES 129 | 130 | - **support strict mode for `ng build`** Added support strict mode for `ng build`. (See [#45](https://github.com/KostyaTretyak/ng-stack/pull/45)). 131 | You cannot now partially pass fields in `FormBuilder` or `FormGroup` unless the form model has optional fields. 132 | 133 | For example: 134 | 135 | ```ts 136 | import { FormBuilder } from '@ng-stack/forms'; 137 | 138 | // Form model 139 | class Address { 140 | city: string; 141 | street: string; // If you make this field optional, the error will disappear. 142 | } 143 | 144 | const formBuilder = new FormBuilder(); 145 | 146 | formBuilder.group
({ 147 | city: 'Mykolaiv', 148 | }); 149 | 150 | // Argument of type '{ city: string; }' is not assignable to parameter of type ... 151 | // Property 'street' is missing in type '{ city: string; }' but required in type ... 152 | ``` 153 | 154 | 155 | ## [1.4.0-beta](https://github.com/KostyaTretyak/ng-stack/releases/tag/forms%401.4.0-beta) (2020-03-11) 156 | 157 | ### Features 158 | 159 | - **support Control with an object model type** See [Automatically detect appropriate types for form controls](README.md#automatically-detect-appropriate-types-for-form-controls) (See [commit](https://github.com/KostyaTretyak/ng-stack/commit/faafda)). 160 | 161 | 162 | ## [1.3.4](https://github.com/KostyaTretyak/ng-stack/releases/tag/forms%401.3.4) (2020-01-08) 163 | 164 | ### Fix peer dependencies 165 | 166 | - **install the library** Now peer dependencies included v8 of `@angular/core`. 167 | 168 | 169 | ## [1.3.3](https://github.com/KostyaTretyak/ng-stack/releases/tag/forms%401.3.3) (2019-07-23) 170 | 171 | ### Bug Fixes 172 | 173 | - **validation model:** Fixed default `ValidatorsModel`. ([#53](https://github.com/KostyaTretyak/ng-stack/pull/53)). 174 | 175 | 176 | ## [1.3.2](https://github.com/KostyaTretyak/ng-stack/releases/tag/forms%401.3.2) (2019-05-09) 177 | 178 | ### Bug Fixes and BREAKING CHANGES 179 | 180 | - **Control type:** Removed buggy `Control` type support. ([#48](https://github.com/KostyaTretyak/ng-stack/pull/48)). 181 | 182 | 183 | ## [1.3.1](https://github.com/KostyaTretyak/ng-stack/releases/tag/forms%401.3.1) (2019-05-01) 184 | 185 | ### Bug Fixes 186 | 187 | - **validation model:** fixed issues with nested validation models ([#44](https://github.com/KostyaTretyak/ng-stack/pull/44)). 188 | 189 | 190 | ## [1.3.0](https://github.com/KostyaTretyak/ng-stack/releases/tag/forms%401.3.0) (2019-04-17) 191 | 192 | ### Features 193 | 194 | - **types:** added support for union types ([#39](https://github.com/KostyaTretyak/ng-stack/pull/39)). 195 | 196 | 197 | 198 | ## [1.2.1](https://github.com/KostyaTretyak/ng-stack/releases/tag/forms%401.2.1) (2019-04-09) 199 | 200 | ### Features and BREAKING CHANGES 201 | 202 | - **input[type=file] directive:** added new @Output `select` event, 203 | and removed @Output `selectedFiles` event ([#35](https://github.com/KostyaTretyak/ng-stack/pull/35)). 204 | 205 | 206 | 207 | ## [1.2.0-beta.1](https://github.com/KostyaTretyak/ng-stack/releases/tag/forms%401.2.0-beta.1) (2019-04-07) 208 | 209 | ### Features and BREAKING CHANGES 210 | 211 | - **input[type=file] directive:** added new @Output `selectedFiles` event, 212 | and removed @Input() `valueAsFileList` ([#32](https://github.com/KostyaTretyak/ng-stack/pull/32)). 213 | 214 | 215 | 216 | ## [1.1.0-beta.2](https://github.com/KostyaTretyak/ng-stack/releases/tag/forms%401.1.0-beta.2) (2019-04-06) 217 | 218 | ### Bug Fixes 219 | 220 | - **Validators:** fixed filesMinLength and filesMaxLength methods names ([#29](https://github.com/KostyaTretyak/ng-stack/pull/29)). 221 | 222 | 223 | 224 | ## [1.1.0-beta.1](https://github.com/KostyaTretyak/ng-stack/releases/tag/forms%401.1.0-beta.1) (2019-04-06) 225 | 226 | ### Features 227 | 228 | - **new directive:** added support for `input[type="file"]` ([#25](https://github.com/KostyaTretyak/ng-stack/pull/25)). 229 | 230 | 231 | 232 | ## [1.0.0-beta.6](https://github.com/KostyaTretyak/ng-stack/releases/tag/forms%401.0.0-beta.6) (2019-03-10) 233 | 234 | ### Bug Fixes 235 | 236 | - **form model:** Type mismatch ([#18](https://github.com/KostyaTretyak/ng-stack/issues/18)) 237 | 238 | 239 | 240 | ## [1.0.0-beta.5](https://github.com/KostyaTretyak/ng-stack/releases/tag/forms%401.0.0-beta.5) (2019-03-09) 241 | 242 | ### Bug Fixes 243 | 244 | - **validation model:** Issue with interpreting of a validation model ([#15](https://github.com/KostyaTretyak/ng-stack/issues/15)) 245 | 246 | 247 | 248 | ## [1.0.0-beta.4](https://github.com/KostyaTretyak/ng-stack/releases/tag/forms%401.0.0-beta.4) (2019-03-08) 249 | 250 | ### Features 251 | 252 | - **validation model:** added support for a validation model ([#14](https://github.com/KostyaTretyak/ng-stack/pull/14)). 253 | 254 | 255 | 256 | ## 0.0.0-alpha.1 (2019-02-25) 257 | 258 | ### Features 259 | 260 | - **npm pack:** `@ng-stack/forms` use Angular-CLI v7 git mono repository and build npm pack with it. 261 | -------------------------------------------------------------------------------- /projects/forms/src/lib/form-control.spec.ts: -------------------------------------------------------------------------------- 1 | import { AbstractControl } from '@angular/forms'; 2 | 3 | import { FormControl } from './form-control'; 4 | import { isString, isNumber, isArray, isObject } from './assert'; 5 | import { FormGroup } from './form-group'; 6 | import { ValidatorFn, Control } from './types'; 7 | 8 | describe('FormControl', () => { 9 | xdescribe('checking types only', () => { 10 | describe('constructor()', () => { 11 | isString(new FormControl('').value); 12 | isString(new FormControl('').value); 13 | isNumber(new FormControl(1).value); 14 | isNumber(new FormControl(1).value); 15 | isArray(new FormControl([]).value); 16 | isArray( 17 | new FormControl([1]).value 18 | ); 19 | isObject(new FormControl({}).value); 20 | isObject(new FormControl({}).value); 21 | 22 | const formState1 = { value: '', disabled: false }; 23 | const control1 = new FormControl(formState1); 24 | isString(control1.value); 25 | 26 | const formState2 = { value: 2, disabled: false }; 27 | const control2 = new FormControl(formState2); 28 | isNumber(control2.value); 29 | 30 | const formState3 = { value: [], disabled: false }; 31 | const control3 = new FormControl(formState3); 32 | isArray(control3.value); 33 | 34 | const formState4 = { value: {}, disabled: false }; 35 | const control4 = new FormControl(formState4); 36 | isObject(control4.value); 37 | }); 38 | 39 | describe('setValue()', () => { 40 | new FormControl().setValue(10); 41 | 42 | const control1 = new FormControl(''); 43 | 44 | control1.setValue(''); 45 | // control1.setValue(2); 46 | // control1.setValue([]); 47 | // control1.setValue({}); 48 | 49 | const control2 = new FormControl(2); 50 | control2.setValue(2); 51 | // control2.setValue(''); 52 | // control2.setValue([]); 53 | // control2.setValue({}); 54 | 55 | const formState3 = { value: '', disabled: false }; 56 | const control3 = new FormControl(formState3); 57 | control3.setValue(''); 58 | // control3.setValue(2); 59 | // control3.setValue([]); 60 | // control3.setValue({}); 61 | 62 | const formState4 = { value: 2, disabled: false }; 63 | const control4 = new FormControl(formState4); 64 | control4.setValue(2); 65 | // control4.setValue(''); 66 | // control4.setValue([]); 67 | // control4.setValue({}); 68 | 69 | class FormModel { 70 | requiredProp: string; 71 | optionalProp1?: number; 72 | optionalProp2?: any[]; 73 | } 74 | 75 | const formState5 = { requiredProp: 'some string', optionalProp1: 123, optionalProp2: [] }; 76 | const control5 = new FormControl(formState5); 77 | control5.setValue({ requiredProp: 'other string', optionalProp1: 456, optionalProp2: [] }); 78 | // control5.setValue({ requiredProp: 2, optionalProp1: 2, optionalProp2: 2 }); 79 | // control5.setValue({ requiredProp: 'other string' }); 80 | // control5.setValue(2); 81 | // control5.setValue(''); 82 | // control5.setValue([]); 83 | 84 | it('', () => { 85 | const formControl = new FormControl>(); 86 | formControl.setValue(['one', 'two']); 87 | const val: string[] = formControl.value; 88 | }); 89 | }); 90 | 91 | describe('patchValue()', () => { 92 | new FormControl().setValue(10); 93 | 94 | const control1 = new FormControl(''); 95 | 96 | control1.patchValue(''); 97 | // control1.patchValue(2); 98 | // control1.patchValue([]); 99 | // control1.patchValue({}); 100 | 101 | const control2 = new FormControl(2); 102 | 103 | control2.patchValue(2); 104 | // control2.patchValue(''); 105 | // control2.patchValue([]); 106 | // control2.patchValue({}); 107 | 108 | const formState3 = { value: '', disabled: false }; 109 | const control3 = new FormControl(formState3); 110 | 111 | control3.patchValue(''); 112 | // control3.patchValue(2); 113 | // control3.patchValue([]); 114 | // control3.patchValue({}); 115 | 116 | const formState4 = { value: 2, disabled: false }; 117 | const control4 = new FormControl(formState4); 118 | 119 | control4.patchValue(2); 120 | // control4.patchValue(''); 121 | // control4.patchValue([]); 122 | // control4.patchValue({}); 123 | 124 | class FormModel { 125 | requiredProp: string; 126 | optionalProp1?: number; 127 | optionalProp2?: any[]; 128 | } 129 | 130 | const formState5 = { requiredProp: 'some string', optionalProp1: 123, optionalProp2: [] }; 131 | const control5 = new FormControl(formState5); 132 | 133 | // control5.patchValue({ prop22: 456 }); 134 | // control5.patchValue(2); 135 | // control5.patchValue(''); 136 | // control5.patchValue([]); 137 | 138 | it('', () => { 139 | const formControl = new FormControl>(); 140 | formControl.patchValue(['one', 'two']); 141 | const val: string[] = formControl.value; 142 | }); 143 | }); 144 | 145 | describe('reset()', () => { 146 | new FormControl().setValue(10); 147 | 148 | const control1 = new FormControl(''); 149 | 150 | control1.reset(''); 151 | // control1.reset(2); 152 | // control1.reset([]); 153 | // control1.reset({}); 154 | 155 | const control2 = new FormControl(2); 156 | 157 | control2.reset(2); 158 | // control2.reset(''); 159 | // control2.reset([]); 160 | // control2.reset({}); 161 | 162 | const formState3 = { value: '', disabled: false }; 163 | const control3 = new FormControl(formState3); 164 | 165 | control3.reset(''); 166 | // control3.reset(2); 167 | // control3.reset([]); 168 | // control3.reset({}); 169 | 170 | const formState4 = { value: 2, disabled: false }; 171 | const control4 = new FormControl(formState4); 172 | 173 | control4.reset(2); 174 | // control4.reset(''); 175 | // control4.reset([]); 176 | // control4.reset({}); 177 | 178 | class FormModel { 179 | requiredProp: string; 180 | optionalProp1?: number; 181 | optionalProp2?: any[]; 182 | } 183 | 184 | const formState5 = { requiredProp: 'some string', optionalProp1: 123, optionalProp2: [] }; 185 | const control5 = new FormControl(formState5); 186 | 187 | control5.reset({ requiredProp: 'other string', optionalProp1: 456, optionalProp2: [] }); 188 | // control5.reset({ requiredProp: 2, optionalProp1: 2, optionalProp2: 2 }); 189 | // control5.reset({ requiredProp: 'other string' }); 190 | // control5.reset(2); 191 | // control5.reset(''); 192 | // control5.reset([]); 193 | 194 | it('', () => { 195 | const formControl = new FormControl>(); 196 | formControl.reset(['one', 'two']); 197 | const val: string[] = formControl.value; 198 | }); 199 | }); 200 | 201 | it('setValidators() with unappropriate ValidatorFn to a validation model', () => { 202 | const control = new FormControl('some value'); 203 | const validatorFn: ValidatorFn = (c: AbstractControl) => ({ otherErrorCode: { returnedValue: 456 } }); 204 | 205 | control.setValidators(validatorFn); 206 | // Without error, but it's not checking match 207 | // between model error code (someErrorCode) and actual entered ValidatorFn error code (otherErrorCode) 208 | }); 209 | }); 210 | 211 | describe(`checking runtime work`, () => { 212 | describe(`constructor()`, () => { 213 | it('passing primitive types as params', () => { 214 | const str = 'some string'; 215 | 216 | // Mapping between param and expected 217 | const map = new Map([ 218 | [str, str], 219 | [2, 2], 220 | [null, null], 221 | ]); 222 | 223 | map.forEach((expected, param) => { 224 | let control: FormControl; 225 | 226 | expect(() => new FormControl(param)).not.toThrow(); 227 | 228 | const value = new FormControl(param).value; 229 | expect(value).toBe(expected); 230 | 231 | control = new FormControl(); 232 | control.setValue(param); 233 | expect(control.value).toBe(expected); 234 | 235 | control = new FormControl(); 236 | control.patchValue(param); 237 | expect(control.value).toBe(expected); 238 | 239 | control = new FormControl(); 240 | control.reset(param); 241 | expect(control.value).toBe(expected); 242 | }); 243 | }); 244 | 245 | it('passing object as params', () => { 246 | const str = 'some string'; 247 | 248 | // Mapping between param and expected 249 | const map = new Map([ 250 | [ 251 | { prop1: 1, prop2: str }, 252 | { prop1: 1, prop2: str }, 253 | ], 254 | [{ value: str, disabled: false }, str], 255 | [{ value: 2, disabled: false }, 2], 256 | [{ value: null, disabled: false }, null], 257 | ]); 258 | 259 | map.forEach((expected, param) => { 260 | let control: FormControl; 261 | 262 | expect(() => new FormControl(param)).not.toThrow(); 263 | 264 | const value = new FormControl(param).value; 265 | expect(value).toEqual(expected); 266 | 267 | control = new FormControl(); 268 | control.reset(param); 269 | expect(control.value).toEqual(expected); 270 | }); 271 | }); 272 | }); 273 | 274 | describe(`other methods`, () => { 275 | it('get() after passing primitive type to constructor()', () => { 276 | const control = new FormControl('some value'); 277 | const validatorFn: ValidatorFn = (c: AbstractControl) => ({ otherErrorCode: 123 }); 278 | control.setValidators(validatorFn); // Withot error, but it's not checking match to `{ someErrorCode: true }` 279 | // control.getError('notExistingErrorCode'); 280 | // control.errors.email 281 | // control.errors.notExistingErrorCode 282 | expect(control.status).toBe('VALID'); 283 | expect((control as any).get()).toBe(null); 284 | expect((control as any).get('some value')).toBe(null); 285 | }); 286 | 287 | it('get() after passing an object to constructor()', () => { 288 | const formGroup = new FormGroup({ one: new FormControl(1), two: new FormControl(2) }); 289 | expect(formGroup.status).toBe('VALID'); 290 | const control = new FormControl(formGroup); 291 | expect(control.status).toBe('VALID'); 292 | expect((control as any).get()).toBe(null); 293 | expect((control as any).get('one')).toBe(null); 294 | }); 295 | }); 296 | }); 297 | }); 298 | -------------------------------------------------------------------------------- /projects/forms/src/lib/validators.ts: -------------------------------------------------------------------------------- 1 | import { Validators as NativeValidators, AbstractControl } from '@angular/forms'; 2 | 3 | import { ValidatorFn, ValidationErrors, AsyncValidatorFn } from './types'; 4 | import { FormControl } from './form-control'; 5 | 6 | // Next flag used because of this https://github.com/ng-packagr/ng-packagr/issues/696#issuecomment-373487183 7 | // @dynamic 8 | /** 9 | * Provides a set of built-in validators that can be used by form controls. 10 | * 11 | * A validator is a function that processes a `FormControl` or collection of 12 | * controls and returns an error map or null. A null map means that validation has passed. 13 | * 14 | * See also [Form Validation](https://angular.io/guide/form-validation). 15 | */ 16 | export class Validators extends NativeValidators { 17 | /** 18 | * Validator that requires the control's value to be greater than or equal to the provided number. 19 | * The validator exists only as a function and not as a directive. 20 | * 21 | * ### Validate against a minimum of 3 22 | * 23 | * ```ts 24 | * const control = new FormControl(2, Validators.min(3)); 25 | * 26 | * console.log(control.errors); // {min: {min: 3, actual: 2}} 27 | * ``` 28 | * 29 | * @returns A validator function that returns an error map with the 30 | * `min` property if the validation check fails, otherwise `null`. 31 | * 32 | */ 33 | static override min(min: number) { 34 | return super.min(min) as ValidatorFn<{ min: { min: number; actual: number } }>; 35 | } 36 | 37 | /** 38 | * Validator that requires the control's value to be less than or equal to the provided number. 39 | * The validator exists only as a function and not as a directive. 40 | * 41 | * ### Validate against a maximum of 15 42 | * 43 | * ```ts 44 | * const control = new FormControl(16, Validators.max(15)); 45 | * 46 | * console.log(control.errors); // {max: {max: 15, actual: 16}} 47 | * ``` 48 | * 49 | * @returns A validator function that returns an error map with the 50 | * `max` property if the validation check fails, otherwise `null`. 51 | * 52 | */ 53 | static override max(max: number) { 54 | return super.max(max) as ValidatorFn<{ max: { max: number; actual: number } }>; 55 | } 56 | 57 | /** 58 | * Validator that requires the control have a non-empty value. 59 | * 60 | * ### Validate that the field is non-empty 61 | * 62 | * ```ts 63 | * const control = new FormControl('', Validators.required); 64 | * 65 | * console.log(control.errors); // {required: true} 66 | * ``` 67 | * 68 | * @returns An error map with the `required` property 69 | * if the validation check fails, otherwise `null`. 70 | * 71 | */ 72 | static override required(control: AbstractControl) { 73 | return super.required(control) as ValidationErrors<{ required: true }> | null; 74 | } 75 | 76 | /** 77 | * Validator that requires the control's value be true. This validator is commonly 78 | * used for required checkboxes. 79 | * 80 | * ### Validate that the field value is true 81 | * 82 | * ```typescript 83 | * const control = new FormControl('', Validators.requiredTrue); 84 | * 85 | * console.log(control.errors); // {required: true} 86 | * ``` 87 | * 88 | * @returns An error map that contains the `required` property 89 | * set to `true` if the validation check fails, otherwise `null`. 90 | */ 91 | static override requiredTrue(control: AbstractControl) { 92 | return super.requiredTrue(control) as ValidationErrors<{ required: true }> | null; 93 | } 94 | 95 | /** 96 | * Validator that requires the control's value pass an email validation test. 97 | * 98 | * ### Validate that the field matches a valid email pattern 99 | * 100 | * ```typescript 101 | * const control = new FormControl('bad@', Validators.email); 102 | * 103 | * console.log(control.errors); // {email: true} 104 | * ``` 105 | * 106 | * @returns An error map with the `email` property 107 | * if the validation check fails, otherwise `null`. 108 | * 109 | */ 110 | static override email(control: AbstractControl) { 111 | return super.email(control) as ValidationErrors<{ email: true }> | null; 112 | } 113 | 114 | /** 115 | * Validator that requires the length of the control's value to be greater than or equal 116 | * to the provided minimum length. This validator is also provided by default if you use the 117 | * the HTML5 `minlength` attribute. 118 | * 119 | * ### Validate that the field has a minimum of 3 characters 120 | * 121 | * ```typescript 122 | * const control = new FormControl('ng', Validators.minLength(3)); 123 | * 124 | * console.log(control.errors); // {minlength: {requiredLength: 3, actualLength: 2}} 125 | * ``` 126 | * 127 | * ```html 128 | * 129 | * ``` 130 | * 131 | * @returns A validator function that returns an error map with the 132 | * `minlength` if the validation check fails, otherwise `null`. 133 | */ 134 | static override minLength(minLength: number) { 135 | return super.minLength(minLength) as ValidatorFn<{ 136 | minlength: { requiredLength: number; actualLength: number }; 137 | }>; 138 | } 139 | 140 | /** 141 | * Validator that requires the length of the control's value to be less than or equal 142 | * to the provided maximum length. This validator is also provided by default if you use the 143 | * the HTML5 `maxlength` attribute. 144 | * 145 | * ### Validate that the field has maximum of 5 characters 146 | * 147 | * ```typescript 148 | * const control = new FormControl('Angular', Validators.maxLength(5)); 149 | * 150 | * console.log(control.errors); // {maxlength: {requiredLength: 5, actualLength: 7}} 151 | * ``` 152 | * 153 | * ```html 154 | * 155 | * ``` 156 | * 157 | * @returns A validator function that returns an error map with the 158 | * `maxlength` property if the validation check fails, otherwise `null`. 159 | */ 160 | static override maxLength(maxLength: number) { 161 | return super.maxLength(maxLength) as ValidatorFn<{ 162 | maxlength: { requiredLength: number; actualLength: number }; 163 | }>; 164 | } 165 | 166 | /** 167 | * Validator that requires the control's value to match a regex pattern. This validator is also 168 | * provided by default if you use the HTML5 `pattern` attribute. 169 | * 170 | * Note that if a Regexp is provided, the Regexp is used as is to test the values. On the other 171 | * hand, if a string is passed, the `^` character is prepended and the `$` character is 172 | * appended to the provided string (if not already present), and the resulting regular 173 | * expression is used to test the values. 174 | * 175 | * ### Validate that the field only contains letters or spaces 176 | * 177 | * ```typescript 178 | * const control = new FormControl('1', Validators.pattern('[a-zA-Z ]*')); 179 | * 180 | * console.log(control.errors); // {pattern: {requiredPattern: '^[a-zA-Z ]*$', actualValue: '1'}} 181 | * ``` 182 | * 183 | * ```html 184 | * 185 | * ``` 186 | * 187 | * @returns A validator function that returns an error map with the 188 | * `pattern` property if the validation check fails, otherwise `null`. 189 | */ 190 | static override pattern(pattern: string | RegExp) { 191 | return super.pattern(pattern) as ValidatorFn<{ 192 | pattern: { requiredPattern: string; actualValue: string }; 193 | }>; 194 | } 195 | 196 | /** 197 | * Validator that performs no operation. 198 | */ 199 | static override nullValidator(control: AbstractControl): null { 200 | return null; 201 | } 202 | 203 | /** 204 | * Compose multiple validators into a single function that returns the union 205 | * of the individual error maps for the provided control. 206 | * 207 | * @returns A validator function that returns an error map with the 208 | * merged error maps of the validators if the validation check fails, otherwise `null`. 209 | */ 210 | static override compose(validators: null): null; 211 | static override compose(validators: (ValidatorFn | null | undefined)[]): ValidatorFn | null; 212 | static override compose(validators: (ValidatorFn | null | undefined)[] | null): ValidatorFn | null { 213 | return super.compose(validators as any); 214 | } 215 | 216 | /** 217 | * Compose multiple async validators into a single function that returns the union 218 | * of the individual error objects for the provided control. 219 | * 220 | * @returns A validator function that returns an error map with the 221 | * merged error objects of the async validators if the validation check fails, otherwise `null`. 222 | */ 223 | static override composeAsync(validators: (AsyncValidatorFn | null)[]) { 224 | return super.composeAsync(validators) as AsyncValidatorFn | null; 225 | } 226 | 227 | /** 228 | * At least one file should be. 229 | * 230 | * **Note**: use this validator when `formControl.value` is an instance of `FormData` only. 231 | */ 232 | static fileRequired(formControl: FormControl): ValidationErrors<{ fileRequired: true }> | null { 233 | if (!(formControl.value instanceof FormData)) { 234 | return { fileRequired: true }; 235 | } 236 | 237 | const files: FormDataEntryValue[] = []; 238 | formControl.value.forEach((file) => files.push(file)); 239 | 240 | for (const file of files) { 241 | if (file instanceof File) { 242 | return null; 243 | } 244 | } 245 | 246 | return { fileRequired: true }; 247 | } 248 | 249 | /** 250 | * Minimal number of files. 251 | * 252 | * **Note**: use this validator when `formControl.value` is an instance of `FormData` only. 253 | */ 254 | static filesMinLength( 255 | minLength: number 256 | ): ValidatorFn<{ filesMinLength: { requiredLength: number; actualLength: number } }> { 257 | return (formControl: any) => { 258 | const value = formControl.value as FormData; 259 | 260 | if (minLength < 1) { 261 | return null; 262 | } 263 | 264 | if (!value || !(value instanceof FormData)) { 265 | return { filesMinLength: { requiredLength: minLength, actualLength: 0 } }; 266 | } 267 | 268 | const files: FormDataEntryValue[] = []; 269 | value.forEach((file) => files.push(file)); 270 | const len = files.length; 271 | if (len < minLength) { 272 | return { filesMinLength: { requiredLength: minLength, actualLength: len } }; 273 | } 274 | 275 | return null; 276 | }; 277 | } 278 | 279 | /** 280 | * Maximal number of files. 281 | * 282 | * **Note**: use this validator when `formControl.value` is an instance of `FormData` only. 283 | */ 284 | static filesMaxLength( 285 | maxLength: number 286 | ): ValidatorFn<{ filesMaxLength: { requiredLength: number; actualLength: number } }> { 287 | return (formControl: any) => { 288 | if (!(formControl.value instanceof FormData)) { 289 | return null; 290 | } 291 | 292 | const files: FormDataEntryValue[] = []; 293 | (formControl.value as FormData).forEach((file) => files.push(file)); 294 | const len = files.length; 295 | if (len > maxLength) { 296 | return { filesMaxLength: { requiredLength: maxLength, actualLength: len } }; 297 | } 298 | 299 | return null; 300 | }; 301 | } 302 | 303 | /** 304 | * Maximal size of a file. 305 | * 306 | * **Note**: use this validator when `formControl.value` is an instance of `FormData` only. 307 | */ 308 | static fileMaxSize( 309 | maxSize: number 310 | ): ValidatorFn<{ fileMaxSize: { requiredSize: number; actualSize: number; file: File } }> { 311 | return (formControl: any) => { 312 | if (!(formControl.value instanceof FormData)) { 313 | return null; 314 | } 315 | 316 | const files: FormDataEntryValue[] = []; 317 | (formControl.value as FormData).forEach((file) => files.push(file)); 318 | for (const file of files) { 319 | if (file instanceof File && file.size > maxSize) { 320 | return { fileMaxSize: { requiredSize: maxSize, actualSize: file.size, file } }; 321 | } 322 | } 323 | 324 | return null; 325 | }; 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /projects/forms/src/lib/form-array.ts: -------------------------------------------------------------------------------- 1 | import { UntypedFormArray as NativeFormArray } from '@angular/forms'; 2 | 3 | import { Observable } from 'rxjs'; 4 | 5 | import { 6 | ControlType, 7 | Status, 8 | ValidatorFn, 9 | AsyncValidatorFn, 10 | ValidatorsModel, 11 | ValidationErrors, 12 | AbstractControlOptions, 13 | StringKeys, 14 | ExtractModelValue, 15 | FormControlState, 16 | } from './types'; 17 | 18 | export class FormArray< 19 | Item = any, 20 | V extends object = ValidatorsModel 21 | > extends NativeFormArray { 22 | override readonly value: ExtractModelValue[]; 23 | override readonly valueChanges: Observable[]>; 24 | override readonly status: Status; 25 | override readonly statusChanges: Observable; 26 | override readonly errors: ValidationErrors | null; 27 | 28 | /** 29 | * Creates a new `FormArray` instance. 30 | * 31 | * @param controls An array of child controls. Each child control is given an index 32 | * where it is registered. 33 | * 34 | * @param validatorOrOpts A synchronous validator function, or an array of 35 | * such functions, or an `AbstractControlOptions` object that contains validation functions 36 | * and a validation trigger. 37 | * 38 | * @param asyncValidator A single async validator or array of async validator functions 39 | * 40 | */ 41 | constructor( 42 | public override controls: ControlType[], 43 | validatorOrOpts?: 44 | | ValidatorFn 45 | | ValidatorFn[] 46 | | AbstractControlOptions 47 | | null, 48 | asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null 49 | ) { 50 | super(controls, validatorOrOpts, asyncValidator); 51 | } 52 | 53 | /** 54 | * Get the Control at the given `index` in the array. 55 | * 56 | * @param index Index in the array to retrieve the control 57 | */ 58 | override at(index: number) { 59 | return super.at(index) as ControlType; 60 | } 61 | 62 | /** 63 | * Insert a new Control at the end of the array. 64 | * 65 | * @param control Form control to be inserted 66 | */ 67 | override push(control: ControlType) { 68 | return super.push(control); 69 | } 70 | 71 | /** 72 | * Insert a new Control at the given `index` in the array. 73 | * 74 | * @param index Index in the array to insert the control 75 | * @param control Form control to be inserted 76 | */ 77 | override insert(index: number, control: ControlType) { 78 | return super.insert(index, control); 79 | } 80 | 81 | /** 82 | * Replace an existing control. 83 | * 84 | * @param index Index in the array to replace the control 85 | * @param control The Control control to replace the existing control 86 | */ 87 | override setControl(index: number, control: ControlType) { 88 | return super.setControl(index, control); 89 | } 90 | 91 | /** 92 | * Sets the value of the `FormArray`. It accepts an array that matches 93 | * the structure of the control. 94 | * 95 | * This method performs strict checks, and throws an error if you try 96 | * to set the value of a control that doesn't exist or if you exclude the 97 | * value of a control. 98 | * 99 | * ### Set the values for the controls in the form array 100 | * 101 | ```ts 102 | const arr = new FormArray([ 103 | new FormControl(), 104 | new FormControl() 105 | ]); 106 | console.log(arr.value); // [null, null] 107 | 108 | arr.setValue(['Nancy', 'Drew']); 109 | console.log(arr.value); // ['Nancy', 'Drew'] 110 | ``` 111 | * 112 | * @param value Array of values for the controls 113 | * @param options Configure options that determine how the control propagates changes and 114 | * emits events after the value changes 115 | * 116 | * * `onlySelf`: When true, each change only affects this control, and not its parent. Default 117 | * is false. 118 | * * `emitEvent`: When true or not supplied (the default), both the `statusChanges` and 119 | * `valueChanges` 120 | * observables emit events with the latest status and value when the control value is updated. 121 | * When false, no events are emitted. 122 | * The configuration options are passed to the 123 | * [updateValueAndValidity](https://angular.io/api/forms/AbstractControl#updateValueAndValidity) method. 124 | */ 125 | override setValue( 126 | value: ExtractModelValue[], 127 | options: { onlySelf?: boolean; emitEvent?: boolean } = {} 128 | ) { 129 | return super.setValue(value, options); 130 | } 131 | 132 | /** 133 | * Patches the value of the `FormArray`. It accepts an array that matches the 134 | * structure of the control, and does its best to match the values to the correct 135 | * controls in the group. 136 | * 137 | * It accepts both super-sets and sub-sets of the array without throwing an error. 138 | * 139 | * ### Patch the values for controls in a form array 140 | * 141 | ```ts 142 | const arr = new FormArray([ 143 | new FormControl(), 144 | new FormControl() 145 | ]); 146 | console.log(arr.value); // [null, null] 147 | 148 | arr.patchValue(['Nancy']); 149 | console.log(arr.value); // ['Nancy', null] 150 | ``` 151 | * 152 | * @param value Array of latest values for the controls 153 | * @param options Configure options that determine how the control propagates changes and 154 | * emits events after the value changes 155 | * 156 | * * `onlySelf`: When true, each change only affects this control, and not its parent. Default 157 | * is false. 158 | * * `emitEvent`: When true or not supplied (the default), both the `statusChanges` and 159 | * `valueChanges` 160 | * observables emit events with the latest status and value when the control value is updated. 161 | * When false, no events are emitted. 162 | * The configuration options are passed to the 163 | * [updateValueAndValidity](https://angular.io/api/forms/AbstractControl#updateValueAndValidity) method. 164 | */ 165 | override patchValue( 166 | value: ExtractModelValue[], 167 | options: { onlySelf?: boolean; emitEvent?: boolean } = {} 168 | ) { 169 | return super.patchValue(value, options); 170 | } 171 | 172 | /** 173 | * Resets the `FormArray` and all descendants are marked `pristine` and `untouched`, and the 174 | * value of all descendants to null or null maps. 175 | * 176 | * You reset to a specific form state by passing in an array of states 177 | * that matches the structure of the control. The state is a standalone value 178 | * or a form state object with both a value and a disabled status. 179 | * 180 | * ### Reset the values in a form array 181 | * 182 | ```ts 183 | const arr = new FormArray([ 184 | new FormControl(), 185 | new FormControl() 186 | ]); 187 | arr.reset(['name', 'last name']); 188 | 189 | console.log(this.arr.value); // ['name', 'last name'] 190 | ``` 191 | * 192 | * ### Reset the values in a form array and the disabled status for the first control 193 | * 194 | ``` 195 | this.arr.reset([ 196 | {value: 'name', disabled: true}, 197 | 'last' 198 | ]); 199 | 200 | console.log(this.arr.value); // ['name', 'last name'] 201 | console.log(this.arr.get(0).status); // 'DISABLED' 202 | ``` 203 | * 204 | * @param value Array of values for the controls 205 | * @param options Configure options that determine how the control propagates changes and 206 | * emits events after the value changes 207 | * 208 | * * `onlySelf`: When true, each change only affects this control, and not its parent. Default 209 | * is false. 210 | * * `emitEvent`: When true or not supplied (the default), both the `statusChanges` and 211 | * `valueChanges` 212 | * observables emit events with the latest status and value when the control is reset. 213 | * When false, no events are emitted. 214 | * The configuration options are passed to the 215 | * [updateValueAndValidity](https://angular.io/api/forms/AbstractControl#updateValueAndValidity) method. 216 | */ 217 | override reset( 218 | value: FormControlState[] = [], 219 | options: { onlySelf?: boolean; emitEvent?: boolean } = {} 220 | ) { 221 | return super.reset(value, options); 222 | } 223 | 224 | /** 225 | * The aggregate value of the array, including any disabled controls. 226 | * 227 | * Reports all values regardless of disabled status. 228 | * For enabled controls only, the `value` property is the best way to get the value of the array. 229 | */ 230 | override getRawValue() { 231 | return super.getRawValue() as ExtractModelValue[]; 232 | } 233 | 234 | /** 235 | * Sets the synchronous validators that are active on this control. Calling 236 | * this overwrites any existing sync validators. 237 | */ 238 | override setValidators(newValidator: ValidatorFn | ValidatorFn[] | null) { 239 | return super.setValidators(newValidator); 240 | } 241 | 242 | /** 243 | * Sets the async validators that are active on this control. Calling this 244 | * overwrites any existing async validators. 245 | */ 246 | override setAsyncValidators( 247 | newValidator: AsyncValidatorFn | AsyncValidatorFn[] | null 248 | ) { 249 | return super.setAsyncValidators(newValidator); 250 | } 251 | 252 | /** 253 | * Sets errors on a form control when running validations manually, rather than automatically. 254 | * 255 | * Calling `setErrors` also updates the validity of the parent control. 256 | * 257 | * ### Manually set the errors for a control 258 | * 259 | * ```ts 260 | * const login = new FormControl('someLogin'); 261 | * login.setErrors({ 262 | * notUnique: true 263 | * }); 264 | * 265 | * expect(login.valid).toEqual(false); 266 | * expect(login.errors).toEqual({ notUnique: true }); 267 | * 268 | * login.setValue('someOtherLogin'); 269 | * 270 | * expect(login.valid).toEqual(true); 271 | * ``` 272 | */ 273 | override setErrors( 274 | errors: ValidationErrors | null, 275 | opts: { emitEvent?: boolean } = {} 276 | ) { 277 | return super.setErrors(errors, opts); 278 | } 279 | 280 | /** 281 | * Reports error data for the control with the given controlName. 282 | * 283 | * @param errorCode The code of the error to check 284 | * @param controlName A control name that designates how to move from the current control 285 | * to the control that should be queried for errors. 286 | * 287 | * For example, for the following `FormGroup`: 288 | * 289 | ```ts 290 | form = new FormGroup({ 291 | address: new FormGroup({ street: new FormControl() }) 292 | }); 293 | ``` 294 | * 295 | * The controlName to the 'street' control from the root form would be 'address' -> 'street'. 296 | * 297 | * It can be provided to this method in combination with `get()` method: 298 | * 299 | ```ts 300 | form.get('address').getError('someErrorCode', 'street'); 301 | ``` 302 | * 303 | * @returns error data for that particular error. If the control or error is not present, 304 | * null is returned. 305 | */ 306 | override getError

, K extends StringKeys>( 307 | errorCode: P, 308 | controlName?: K 309 | ) { 310 | return super.getError(errorCode, controlName) as V[P] | null; 311 | } 312 | 313 | /** 314 | * Reports whether the control with the given controlName has the error specified. 315 | * 316 | * @param errorCode The code of the error to check 317 | * @param controlName A control name that designates how to move from the current control 318 | * to the control that should be queried for errors. 319 | * 320 | * For example, for the following `FormGroup`: 321 | * 322 | ```ts 323 | form = new FormGroup({ 324 | address: new FormGroup({ street: new FormControl() }) 325 | }); 326 | ``` 327 | * 328 | * The controlName to the 'street' control from the root form would be 'address' -> 'street'. 329 | * 330 | * It can be provided to this method in combination with `get()` method: 331 | ```ts 332 | form.get('address').hasError('someErrorCode', 'street'); 333 | ``` 334 | * 335 | * If no controlName is given, this method checks for the error on the current control. 336 | * 337 | * @returns whether the given error is present in the control at the given controlName. 338 | * 339 | * If the control is not present, false is returned. 340 | */ 341 | override hasError

, K extends StringKeys>( 342 | errorCode: P, 343 | controlName?: K 344 | ) { 345 | return super.hasError(errorCode, controlName); 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /projects/forms/src/lib/form-group.ts: -------------------------------------------------------------------------------- 1 | import { UntypedFormGroup as NativeFormGroup } from '@angular/forms'; 2 | 3 | import { Observable } from 'rxjs'; 4 | 5 | import { 6 | Status, 7 | StringKeys, 8 | ValidatorFn, 9 | AsyncValidatorFn, 10 | ValidatorsModel, 11 | ValidationErrors, 12 | AbstractControlOptions, 13 | ControlType, 14 | ExtractGroupValue, 15 | } from './types'; 16 | 17 | export class FormGroup< 18 | T extends object = any, 19 | V extends object = ValidatorsModel 20 | > extends NativeFormGroup { 21 | override readonly value: ExtractGroupValue; 22 | override readonly valueChanges: Observable>; 23 | override readonly status: Status; 24 | override readonly statusChanges: Observable; 25 | override readonly errors: ValidationErrors | null; 26 | 27 | /** 28 | * Creates a new `FormGroup` instance. 29 | * 30 | * @param controls A collection of child controls. The key for each child is the name 31 | * under which it is registered. 32 | * 33 | * @param validatorOrOpts A synchronous validator function, or an array of 34 | * such functions, or an `AbstractControlOptions` object that contains validation functions 35 | * and a validation trigger. 36 | * 37 | * @param asyncValidator A single async validator or array of async validator functions 38 | * 39 | * @todo Chechout how to respect optional and require properties modifyers for the controls. 40 | */ 41 | constructor( 42 | public override controls: { [P in keyof T]: ControlType }, 43 | validatorOrOpts?: 44 | | ValidatorFn 45 | | ValidatorFn[] 46 | | AbstractControlOptions 47 | | null, 48 | asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null 49 | ) { 50 | super(controls, validatorOrOpts, asyncValidator); 51 | } 52 | 53 | /** 54 | * Registers a control with the group's list of controls. 55 | * 56 | * This method does not update the value or validity of the control. 57 | * Use [addControl](https://angular.io/api/forms/FormGroup#addControl) instead. 58 | * 59 | * @param name The control name to register in the collection 60 | * @param control Provides the control for the given name 61 | */ 62 | override registerControl< 63 | K extends StringKeys, 64 | CV extends object = ValidatorsModel 65 | >(name: K, control: ControlType) { 66 | return super.registerControl(name, control) as ControlType; 67 | } 68 | 69 | /** 70 | * Add a control to this group. 71 | * 72 | * This method also updates the value and validity of the control. 73 | * 74 | * @param name The control name to add to the collection 75 | * @param control Provides the control for the given name 76 | */ 77 | override addControl< 78 | K extends StringKeys, 79 | CV extends object = ValidatorsModel 80 | >(name: K, control: ControlType) { 81 | return super.addControl(name, control); 82 | } 83 | 84 | /** 85 | * Remove a control from this group. 86 | * 87 | * @param name The control name to remove from the collection 88 | */ 89 | override removeControl>(name: K) { 90 | return super.removeControl(name); 91 | } 92 | 93 | /** 94 | * Replace an existing control. 95 | * 96 | * @param name The control name to replace in the collection 97 | * @param control Provides the control for the given name 98 | */ 99 | override setControl< 100 | K extends StringKeys, 101 | CV extends object = ValidatorsModel 102 | >(name: K, control: ControlType) { 103 | return super.setControl(name, control); 104 | } 105 | 106 | /** 107 | * Check whether there is an enabled control with the given name in the group. 108 | * 109 | * Reports false for disabled controls. If you'd like to check for existence in the group 110 | * only, use [get](https://angular.io/api/forms/AbstractControl#get) instead. 111 | * 112 | * @param name The control name to check for existence in the collection 113 | * 114 | * @returns false for disabled controls, true otherwise. 115 | */ 116 | override contains>(name: K) { 117 | return super.contains(name); 118 | } 119 | 120 | /** 121 | * Sets the value of the `FormGroup`. It accepts an object that matches 122 | * the structure of the group, with control names as keys. 123 | * 124 | * ### Set the complete value for the form group 125 | * 126 | ```ts 127 | const form = new FormGroup({ 128 | first: new FormControl(), 129 | last: new FormControl() 130 | }); 131 | 132 | console.log(form.value); // {first: null, last: null} 133 | 134 | form.setValue({first: 'Nancy', last: 'Drew'}); 135 | console.log(form.value); // {first: 'Nancy', last: 'Drew'} 136 | ``` 137 | * 138 | * @throws When strict checks fail, such as setting the value of a control 139 | * that doesn't exist or if you excluding the value of a control. 140 | * 141 | * @param value The new value for the control that matches the structure of the group. 142 | * @param options Configuration options that determine how the control propagates changes 143 | * and emits events after the value changes. 144 | * The configuration options are passed to the 145 | * [updateValueAndValidity](https://angular.io/api/forms/AbstractControl#updateValueAndValidity) method. 146 | * 147 | * * `onlySelf`: When true, each change only affects this control, and not its parent. Default is 148 | * false. 149 | * * `emitEvent`: When true or not supplied (the default), both the `statusChanges` and 150 | * `valueChanges` 151 | * observables emit events with the latest status and value when the control value is updated. 152 | * When false, no events are emitted. 153 | */ 154 | override setValue( 155 | value: ExtractGroupValue, 156 | options: { onlySelf?: boolean; emitEvent?: boolean } = {} 157 | ) { 158 | return super.setValue(value, options); 159 | } 160 | 161 | /** 162 | * Patches the value of the `FormGroup`. It accepts an object with control 163 | * names as keys, and does its best to match the values to the correct controls 164 | * in the group. 165 | * 166 | * It accepts both super-sets and sub-sets of the group without throwing an error. 167 | * 168 | * ### Patch the value for a form group 169 | * 170 | ```ts 171 | const form = new FormGroup({ 172 | first: new FormControl(), 173 | last: new FormControl() 174 | }); 175 | console.log(form.value); // {first: null, last: null} 176 | 177 | form.patchValue({first: 'Nancy'}); 178 | console.log(form.value); // {first: 'Nancy', last: null} 179 | ``` 180 | * 181 | * @param value The object that matches the structure of the group. 182 | * @param options Configuration options that determine how the control propagates changes and 183 | * emits events after the value is patched. 184 | * * `onlySelf`: When true, each change only affects this control and not its parent. Default is 185 | * true. 186 | * * `emitEvent`: When true or not supplied (the default), both the `statusChanges` and 187 | * `valueChanges` 188 | * observables emit events with the latest status and value when the control value is updated. 189 | * When false, no events are emitted. 190 | * The configuration options are passed to the 191 | * [updateValueAndValidity](https://angular.io/api/forms/AbstractControl#updateValueAndValidity) method. 192 | */ 193 | override patchValue( 194 | value: Partial>, 195 | options: { onlySelf?: boolean; emitEvent?: boolean } = {} 196 | ) { 197 | return super.patchValue(value, options); 198 | } 199 | 200 | /** 201 | * Resets the `FormGroup`, marks all descendants are marked `pristine` and `untouched`, and 202 | * the value of all descendants to null. 203 | * 204 | * You reset to a specific form state by passing in a map of states 205 | * that matches the structure of your form, with control names as keys. The state 206 | * is a standalone value or a form state object with both a value and a disabled 207 | * status. 208 | * 209 | * @param formState Resets the control with an initial value, 210 | * or an object that defines the initial value and disabled state. 211 | * 212 | * @param options Configuration options that determine how the control propagates changes 213 | * and emits events when the group is reset. 214 | * * `onlySelf`: When true, each change only affects this control, and not its parent. Default is 215 | * false. 216 | * * `emitEvent`: When true or not supplied (the default), both the `statusChanges` and 217 | * `valueChanges` 218 | * observables emit events with the latest status and value when the control is reset. 219 | * When false, no events are emitted. 220 | * The configuration options are passed to the 221 | * [updateValueAndValidity](https://angular.io/api/forms/AbstractControl#updateValueAndValidity) method. 222 | * 223 | * 224 | * ### Reset the form group values 225 | * 226 | ```ts 227 | const form = new FormGroup({ 228 | first: new FormControl('first name'), 229 | last: new FormControl('last name') 230 | }); 231 | 232 | console.log(form.value); // {first: 'first name', last: 'last name'} 233 | 234 | form.reset({ first: 'name', last: 'last name' }); 235 | 236 | console.log(form.value); // {first: 'name', last: 'last name'} 237 | ``` 238 | * 239 | * ### Reset the form group values and disabled status 240 | * 241 | ```ts 242 | const form = new FormGroup({ 243 | first: new FormControl('first name'), 244 | last: new FormControl('last name') 245 | }); 246 | 247 | form.reset({ 248 | first: {value: 'name', disabled: true}, 249 | last: 'last' 250 | }); 251 | 252 | console.log(this.form.value); // {first: 'name', last: 'last name'} 253 | console.log(this.form.get('first').status); // 'DISABLED' 254 | ``` 255 | */ 256 | override reset( 257 | value: ExtractGroupValue = {} as any, 258 | options: { onlySelf?: boolean; emitEvent?: boolean } = {} 259 | ) { 260 | return super.reset(value, options); 261 | } 262 | 263 | /** 264 | * The aggregate value of the `FormGroup`, including any disabled controls. 265 | * 266 | * Retrieves all values regardless of disabled status. 267 | * The `value` property is the best way to get the value of the group, because 268 | * it excludes disabled controls in the `FormGroup`. 269 | */ 270 | override getRawValue() { 271 | return super.getRawValue() as ExtractGroupValue; 272 | } 273 | 274 | /** 275 | * Retrieves a child control given the control's name. 276 | * 277 | * ### Retrieve a nested control 278 | * 279 | * For example, to get a `name` control nested within a `person` sub-group: 280 | ```ts 281 | this.form.get('person').get('name'); 282 | ``` 283 | */ 284 | override get, CV extends object = ValidatorsModel>( 285 | controlName: K 286 | ): ControlType | null { 287 | return super.get(controlName) as ControlType | null; 288 | } 289 | 290 | /** 291 | * Sets the synchronous validators that are active on this control. Calling 292 | * this overwrites any existing sync validators. 293 | */ 294 | override setValidators(newValidator: ValidatorFn | ValidatorFn[] | null) { 295 | return super.setValidators(newValidator); 296 | } 297 | 298 | /** 299 | * Sets the async validators that are active on this control. Calling this 300 | * overwrites any existing async validators. 301 | */ 302 | override setAsyncValidators( 303 | newValidator: AsyncValidatorFn | AsyncValidatorFn[] | null 304 | ) { 305 | return super.setAsyncValidators(newValidator); 306 | } 307 | 308 | /** 309 | * Sets errors on a form control when running validations manually, rather than automatically. 310 | * 311 | * Calling `setErrors` also updates the validity of the parent control. 312 | * 313 | * ### Manually set the errors for a control 314 | * 315 | * ```ts 316 | * const login = new FormControl('someLogin'); 317 | * login.setErrors({ 318 | * notUnique: true 319 | * }); 320 | * 321 | * expect(login.valid).toEqual(false); 322 | * expect(login.errors).toEqual({ notUnique: true }); 323 | * 324 | * login.setValue('someOtherLogin'); 325 | * 326 | * expect(login.valid).toEqual(true); 327 | * ``` 328 | */ 329 | override setErrors( 330 | errors: ValidationErrors | null, 331 | opts: { emitEvent?: boolean } = {} 332 | ) { 333 | return super.setErrors(errors, opts); 334 | } 335 | 336 | /** 337 | * Reports error data for the control with the given controlName. 338 | * 339 | * @param errorCode The code of the error to check 340 | * @param controlName A control name that designates how to move from the current control 341 | * to the control that should be queried for errors. 342 | * 343 | * For example, for the following `FormGroup`: 344 | * 345 | ```ts 346 | form = new FormGroup({ 347 | address: new FormGroup({ street: new FormControl() }) 348 | }); 349 | ``` 350 | * 351 | * The controlName to the 'street' control from the root form would be 'address' -> 'street'. 352 | * 353 | * It can be provided to this method in combination with `get()` method: 354 | * 355 | ```ts 356 | form.get('address').getError('someErrorCode', 'street'); 357 | ``` 358 | * 359 | * @returns error data for that particular error. If the control or error is not present, 360 | * null is returned. 361 | */ 362 | override getError

, K extends StringKeys>( 363 | errorCode: P, 364 | controlName?: K 365 | ) { 366 | return super.getError(errorCode, controlName) as V[P] | null; 367 | } 368 | 369 | /** 370 | * Reports whether the control with the given controlName has the error specified. 371 | * 372 | * @param errorCode The code of the error to check 373 | * @param controlName A control name that designates how to move from the current control 374 | * to the control that should be queried for errors. 375 | * 376 | * For example, for the following `FormGroup`: 377 | * 378 | ```ts 379 | form = new FormGroup({ 380 | address: new FormGroup({ street: new FormControl() }) 381 | }); 382 | ``` 383 | * 384 | * The controlName to the 'street' control from the root form would be 'address' -> 'street'. 385 | * 386 | * It can be provided to this method in combination with `get()` method: 387 | ```ts 388 | form.get('address').hasError('someErrorCode', 'street'); 389 | ``` 390 | * 391 | * If no controlName is given, this method checks for the error on the current control. 392 | * 393 | * @returns whether the given error is present in the control at the given controlName. 394 | * 395 | * If the control is not present, false is returned. 396 | */ 397 | override hasError

, K extends StringKeys>( 398 | errorCode: P, 399 | controlName?: K 400 | ) { 401 | return super.hasError(errorCode, controlName); 402 | } 403 | } 404 | -------------------------------------------------------------------------------- /projects/forms/README.md: -------------------------------------------------------------------------------- 1 | # @ng-stack/forms 2 | 3 | > provides wrapped Angular's Reactive Forms to write its more strongly typed. 4 | 5 | ## Table of contents 6 | - [Install](#install) 7 | - [Usage](#usage) 8 | - [Using form model](#using-form-model) 9 | - [Automatically detect appropriate types for form controls](#automatically-detect-appropriate-types-for-form-controls) 10 | - [Typed Validations](#typed-validations) 11 | - [Support input[type="file"]](#support-input-with-file-type) 12 | - [`preserveValue` option](#preserveValue-option) 13 | - [Known issues](#known-issues) 14 | - [Known issues with ValidatorFn](#known-issues-with-validatorfn) 15 | - [Known issues with data type infer](#known-issues-with-data-type-infer) 16 | - [How does it works](#how-does-it-works) 17 | - [Changes API](#api-changes) 18 | 19 | ## Install 20 | 21 | ```bash 22 | npm i @ng-stack/forms 23 | ``` 24 | 25 | OR 26 | 27 | ```bash 28 | yarn add @ng-stack/forms 29 | ``` 30 | 31 | ## Usage 32 | 33 | Import into your module `NgsFormsModule`, and no need import `ReactiveFormsModule` because it's already 34 | reexported by `NgsFormsModule`. 35 | 36 | ```ts 37 | import { NgsFormsModule } from '@ng-stack/forms'; 38 | 39 | // ... 40 | 41 | @NgModule({ 42 | // ... 43 | imports: [ 44 | NgsFormsModule 45 | ] 46 | 47 | // ... 48 | 49 | }) 50 | ``` 51 | Then you should be able just import and using classes from `@ng-stack/forms`. 52 | 53 | ### Using form model 54 | 55 | ```ts 56 | import { FormGroup, FormControl, FormArray } from '@ng-stack/forms'; 57 | 58 | const formControl = new FormControl('some string'); 59 | const value = formControl.value; // some string 60 | 61 | formControl.setValue(123); // Error: Argument of type '123' is not assignable... 62 | 63 | // Form model 64 | class Address { 65 | city: string; 66 | street: string; 67 | zip: string; 68 | other: string; 69 | } 70 | 71 | const formGroup = new FormGroup

({ 72 | city: new FormControl('Kyiv'), 73 | street: new FormControl('Khreshchatyk'), 74 | zip: new FormControl('01001'), 75 | // other: new FormControl(123), // Error: Type 'number' is not assignable to type 'string' 76 | }); 77 | 78 | // Note: form model hints for generic without [] 79 | const formArray = new FormArray
([ 80 | formGroup, 81 | formGroup, 82 | formGroup, 83 | 84 | new FormGroup({ someProp: new FormControl('') }), 85 | // Error: Type '{ someProp: string; }' is missing 86 | // the following properties from type 'Address': city, street, other 87 | ]); 88 | ``` 89 | 90 | ### Automatically detect appropriate types for form controls 91 | 92 | `FormGroup()`, `formBuilder.group()`, `FormArray()` and `formBuilder.array()` attempt to automatically detect 93 | appropriate types for form controls by their form models. 94 | 95 | Simple example: 96 | 97 | ```ts 98 | import { FormControl, FormGroup } from '@ng-stack/forms'; 99 | 100 | // Form model 101 | class Address { 102 | city: string; 103 | street: string; 104 | zip: string; 105 | other: string; 106 | } 107 | 108 | const formGroup = new FormGroup
({ 109 | city: new FormControl('Mykolaiv'), // OK 110 | 111 | street: new FormGroup({}), 112 | // Error: Type 'FormGroup' is missing 113 | // the following properties from type 'FormControl' 114 | }); 115 | ``` 116 | 117 | As you can see, constructor of `FormGroup` accept form model `Address` for its generic and knows that 118 | property `street` have primitive type and should to have a value only with instance of `FormControl`. 119 | 120 | If some property of a form model have type that extends `object`, then an appropriate property in a form 121 | should to have a value with instance of `FormGroup`. So for an array - instance of `FormArray`. 122 | 123 | But maybe you want for `FormControl` to accept an object in its constructor, instead of a primitive value. 124 | What to do in this case? For this purpose a special type `Control` was intended. 125 | 126 | For example: 127 | 128 | ```ts 129 | import { FormBuilder, Control } from '@ng-stack/forms'; 130 | 131 | // Form Model 132 | interface Person { 133 | id: number; 134 | name: string; 135 | birthDate: Control; // Here should be FormControl, instead of a FormGroup 136 | } 137 | 138 | const fb = new FormBuilder(); 139 | 140 | const form = fb.group({ 141 | id: 123, 142 | name: 'John Smith', 143 | birthDate: new Date(1977, 6, 30), 144 | }); 145 | 146 | const birthDate: Date = form.value.birthDate; // As you can see, `Control` type is compatible with `Date` type. 147 | ``` 148 | 149 | If the form model interface comes from an external library, you can do the following: 150 | 151 | ```ts 152 | import { FormBuilder, Control } from '@ng-stack/forms'; 153 | 154 | // External Form Model 155 | interface ExternalPerson { 156 | id: number; 157 | name: string; 158 | birthDate: Date; 159 | } 160 | 161 | const formConfig: ExternalPerson = { 162 | id: 123, 163 | name: 'John Smith', 164 | birthDate: new Date(1977, 6, 30), 165 | }; 166 | 167 | interface Person extends ExternalPerson { 168 | birthDate: Control; 169 | } 170 | 171 | 172 | const fb = new FormBuilder(); 173 | const form = fb.group(formConfig); // `Control` type is compatible with `Date` type. 174 | 175 | const birthDate: Date = form.value.birthDate; // `Control` type is compatible with `Date` type. 176 | ``` 177 | 178 | So, if your `FormGroup` knows about types of properties a form model, it inferring appropriate types of form controls 179 | for their values. 180 | 181 | And no need to do `as FormControl` or `as FormGroup` in your components: 182 | 183 | ```ts 184 | get userName() { 185 | return this.formGroup.get('userName') as FormControl; 186 | } 187 | 188 | get addresses() { 189 | return this.formGroup.get('addresses') as FormGroup; 190 | } 191 | ``` 192 | 193 | Now do this: 194 | 195 | ```ts 196 | // Note here form model UserForm 197 | formGroup: FormGroup; 198 | 199 | get userName() { 200 | return this.formGroup.get('userName'); 201 | } 202 | 203 | get addresses() { 204 | return this.formGroup.get('addresses'); 205 | } 206 | ``` 207 | 208 | ### Typed Validations 209 | 210 | Classes `FormControl`, `FormGroup`, `FormArray` and all methods of `FormBuilder` 211 | accept a "validation model" as second parameter for their generics: 212 | 213 | ```ts 214 | interface ValidationModel { 215 | someErrorCode: { returnedValue: 123 }; 216 | } 217 | const control = new FormControl('some value'); 218 | control.getError('someErrorCode'); // OK 219 | control.errors.someErrorCode; // OK 220 | control.getError('notExistingErrorCode'); // Error: Argument of type '"notExistingErrorCode"' is not... 221 | control.errors.notExistingErrorCode; // Error: Property 'notExistingErrorCode' does not exist... 222 | ``` 223 | 224 | By default is used class `ValidatorsModel`. 225 | 226 | ```ts 227 | const control = new FormControl('some value'); 228 | control.getError('required'); // OK 229 | control.getError('email'); // OK 230 | control.errors.required // OK 231 | control.errors.email // OK 232 | control.getError('notExistingErrorCode'); // Error: Argument of type '"notExistingErrorCode"' is not... 233 | control.errors.notExistingErrorCode // Error: Property 'notExistingErrorCode' does not exist... 234 | ``` 235 | 236 | `ValidatorsModel` contains a list of properties extracted from `typeof Validators`, 237 | additional validators to support `input[type=file]`, and expected returns types: 238 | 239 | ```ts 240 | class ValidatorsModel { 241 | min: { min: number; actual: number }; 242 | max: { max: number; actual: number }; 243 | required: true; 244 | email: true; 245 | minlength: { requiredLength: number; actualLength: number }; 246 | maxlength: { requiredLength: number; actualLength: number }; 247 | 248 | // Additional validators to support `input[type=file]` 249 | fileRequired: { requiredSize: number; actualSize: number; file: File }; 250 | filesMinLength: { requiredLength: number; actualLength: number }; 251 | filesMaxLength: { requiredLength: number; actualLength: number }; 252 | fileMaxSize: { requiredSize: number; actualSize: number; file: File }; 253 | } 254 | ``` 255 | See also [Known issues with ValidatorFn](#known-issues-with-validatorFn). 256 | 257 | ### Support input with "file" type 258 | 259 | Since version 1.1.0, `@ng-stack/forms` supports `input[type=file]`. 260 | 261 | The module will be set instance of `FormData` to `formControl.value`, 262 | and output event `select` with type `File[]`: 263 | 264 | For example, if you have this component template: 265 | 266 | ```html 267 | 268 | ``` 269 | 270 | In your component class, you can get selected files from `select` output event: 271 | 272 | ```ts 273 | // ... 274 | 275 | onSelect(files: File[]) { 276 | console.log('selected files:', files); 277 | } 278 | 279 | // ... 280 | ``` 281 | 282 | You can validate the `formControl` with four methods: 283 | 284 | ```ts 285 | import { Validators, FormControl } from '@ng-stack/forms'; 286 | 287 | // ... 288 | 289 | const validators = [ 290 | Validators.fileRequired; 291 | Validators.filesMinLength(2); 292 | Validators.filesMaxLength(10); 293 | Validators.fileMaxSize(1024 * 1024); 294 | ]; 295 | 296 | this.formControl = new FormControl(null, validators); 297 | 298 | // ... 299 | 300 | const validErr = this.formControl.getError('fileMaxSize'); 301 | 302 | if (validErr) { 303 | const msg = `Every file should not exceed ${validErr.requiredSize} kB (you upload ${validErr.actualSize} kB)`; 304 | this.showMsg(msg); 305 | return; 306 | } 307 | 308 | // ... 309 | ``` 310 | 311 | A more complete example can be seen on github [example-input-file](https://github.com/KostyaTretyak/example-input-file) 312 | and on [stackblitz](https://stackblitz.com/github/KostyaTretyak/example-input-file). 313 | 314 | #### `preserveValue` option 315 | 316 | Since version 2.1.0, with `input[type=file]` you can also pass `preserveValue` attribute to preserve the field's native value of HTML form control: 317 | 318 | ```html 319 | 320 | ``` 321 | 322 | Without `preserveValue`, you may see unwanted text near the input control - "No file chosen". As a workaround, you can do the following: 323 | 324 | ```html 325 | 326 | 327 | ``` 328 | 329 | So you can change the output text to the desired one. 330 | 331 | By default `preserveValue="false"` but if you want set `preserveValue="true"`, keep in mind that when you need to re-select the same file after changing it in the file system (for example, reduce the size of the avatar image), you will not be able to see the changes. This is how the browser cache works. 332 | 333 | ## Known issues 334 | 335 | #### Known issues with data type infer 336 | 337 | Without a data type hint, there is a limitation of the TypeScript that does not allow you to correctly infer the data type for nested form controls based on the usage: 338 | 339 | ```ts 340 | import { FormControl, FormGroup, FormArray } from '@ng-stack/forms'; 341 | 342 | // Next block code tested with TypeScript 4.1.2 343 | 344 | const formGroup1 = new FormGroup({ prop: new FormArray([]) }); // Error, but it's wrong 345 | const formGroup2 = new FormGroup<{ prop: any[] }>({ prop: new FormArray([]) }); // OK 346 | 347 | interface NestedModel { 348 | one: number; 349 | } 350 | 351 | interface Model { 352 | prop: NestedModel; 353 | } 354 | 355 | // Here error "Type 'number' is not assignable to type '123'" 356 | // because design limitation, see https://github.com/microsoft/TypeScript/issues/22596 357 | const formGroup3 = new FormGroup({ prop: new FormGroup({ one: new FormControl(123) }) }); 358 | const formGroup4 = new FormGroup({ prop: new FormGroup({ one: new FormControl(123) }) }); // OK 359 | 360 | // Here without errors, but it's wrong, 361 | // because the nested `FormGroup` does not have the `two` property in the `Model`. 362 | const formGroup5 = new FormGroup({ 363 | prop: new FormGroup({ one: new FormControl(123), two: new FormControl('') }), 364 | }); 365 | 366 | // To see error in the previous example, add a type hint for the nested FormGroup: 367 | const formGroup8 = new FormGroup({ 368 | prop: new FormGroup({ one: new FormControl(123), two: new FormControl('') }), 369 | }); 370 | 371 | const formState1 = { value: 2, disabled: false }; 372 | const control1 = new FormControl(formState1); 373 | control1.patchValue(2); // Argument of type '2' is not assignable to parameter of type '{ value: number; disabled: boolean; }' 374 | 375 | // To fix previous example, add a type hint for the FormControl generic: 376 | const formState2 = { value: 2, disabled: false }; 377 | const control2 = new FormControl(formState2); 378 | control2.patchValue(2); // OK 379 | ``` 380 | 381 | See [bug(generics): errors of inferring types for an array](https://github.com/microsoft/TypeScript/issues/30207). 382 | 383 | #### Known issues with ValidatorFn 384 | 385 | For now, the functionality - when a match between a validation model and actually entered validator's functions is checked - is not supported. 386 | 387 | For example: 388 | 389 | ```ts 390 | interface ValidationModel { 391 | someErrorCode: { returnedValue: 123 }; 392 | } 393 | const control = new FormControl('some value'); 394 | const validatorFn: ValidatorFn = (c: AbstractControl) => ({ otherErrorCode: { returnedValue: 456 } }); 395 | 396 | control.setValidators(validatorFn); 397 | // Without error, but it's not checking 398 | // match between `someErrorCode` and `otherErrorCode` 399 | ``` 400 | 401 | See: [bug(forms): issue with interpreting of a validation model](https://github.com/KostyaTretyak/ng-stack/issues/15). 402 | 403 | ## How does it works 404 | 405 | In almost all cases, this module absolutely does not change the runtime behavior of native Angular methods. 406 | 407 | Classes are overrided as follows: 408 | 409 | ```ts 410 | import { FormGroup as NativeFormGroup } from '@angular/forms'; 411 | 412 | export class FormGroup extends NativeFormGroup { 413 | get(path) { 414 | return super.get(path); 415 | } 416 | } 417 | ``` 418 | 419 | The following section describes the changes that have occurred. All of the following restrictions apply only because of the need to more clearly control the data entered by developers. 420 | 421 | ## API Changes 422 | 423 | ### get() 424 | 425 | - `formGroup.get()` supporting only signature: 426 | 427 | ```ts 428 | formGroup.get('address').get('street'); 429 | ``` 430 | 431 | and not supporting: 432 | 433 | ```ts 434 | formGroup.get('address.street'); 435 | // OR 436 | formGroup.get(['address', 'street']); 437 | ``` 438 | 439 | - Angular's native `formControl.get()` method always returns `null`. Because of this, supporting signature only `get()` (without arguments). 440 | See also issue on github [feat(forms): hide get() method of FormControl from public API](https://github.com/angular/angular/issues/29091). 441 | 442 | ### getError() and hasError() 443 | 444 | - `formGroup.getError()` and `formGroup.hasError()` supporting only this signature: 445 | 446 | ```ts 447 | formGroup.get('address').getError('someErrorCode', 'street'); 448 | ``` 449 | 450 | And not supporting this signature: 451 | 452 | ```ts 453 | formGroup.getError('someErrorCode', 'address.street'); 454 | // OR 455 | formGroup.getError('someErrorCode', ['address', 'street']); 456 | ``` 457 | 458 | - `formControl.getError()` and `formControl.hasError()` supporting only this signature (without second argument): 459 | 460 | ```ts 461 | formControl.getError('someErrorCode'); 462 | ``` 463 | 464 | ### ValidatorFn and AsyncValidatorFn 465 | 466 | Native `ValidatorFn` and `AsyncValidatorFn` are interfaces, in `@ng-stack/forms` they are types. -------------------------------------------------------------------------------- /projects/api-mock/README.md: -------------------------------------------------------------------------------- 1 | # @ng-stack/api-mock 2 | 3 | This module is an alternative of [angular-in-memory-web-api](https://github.com/angular/in-memory-web-api), it's intended for Angular demos and tests that simulates CRUD operations over a RESTy API. It intercepts Angular `HttpClient` requests that would otherwise go to the remote server and redirects them to `@ng-stack/api-mock` data store that you control. 4 | 5 | You can also view the [Ukrainian version of the documentation](./README.uk.md). 6 | 7 | ## Table of contents 8 | - [Use cases](#use-cases) 9 | - [Install](#install) 10 | - [HTTP request handling](#http-request-handling) 11 | - [Basic setup](#basic-setup) 12 | - [Import the `@ng-stack/api-mock` module](#import-the-ng-stackapi-mock-module) 13 | - [API](#api) 14 | - [ApiMockService and ApiMockRoute](#apimockservice-and-apimockroute) 15 | - [dataCallback](#datacallback) 16 | - [responseCallback](#responsecallback) 17 | - [ApiMockConfig](#apimockconfig) 18 | 19 | ## Use cases 20 | 21 | - When Angular applications are developed faster than the API backend for these applications. This module allows you to simulate the data as if it were on the backend. Later, when the required functionality is implemented on a real dev/test server, this module can be switched off seamlessly, thus directing requests to the real backend. 22 | - Demo apps that need to simulate CRUD data persistence operations without a real server. You won't have to build and start a test server. 23 | - Whip up prototypes and proofs of concept. 24 | - Share examples with the community in a web coding environment such as [StackBlitz](https://stackblitz.com/) or [CodePen](https://codepen.io/). Create Angular issues and StackOverflow answers supported by live code. 25 | - Write unit test apps that read and write data. Avoid the hassle of intercepting multiple http calls and manufacturing sequences of responses. The `@ng-stack/api-mock` data store resets for each test so there is no cross-test data pollution. 26 | - End-to-end tests. If you can toggle the app into test mode using the `@ng-stack/api-mock`, you won't disturb the real database. This can be especially useful for CI (continuous integration) builds. 27 | 28 | ## Install 29 | 30 | ```bash 31 | npm i -D @ng-stack/api-mock 32 | ``` 33 | 34 | where switch `-D` mean - "save to devDependencies in package.json". 35 | 36 | ## HTTP request handling 37 | 38 | `@ng-stack/api-mock` processes an HTTP request in the manner of a RESTy web api. 39 | 40 | Examples: 41 | 42 | ```text 43 | GET api/posts // all posts 44 | GET api/posts/42 // the post with id=42 45 | GET api/posts/42/comments // all comments of post with id=42 46 | GET api/authors/10/books/3 // a book with id=3 whose author has id=10 47 | GET api/one/two/three // endpoint without primary key 48 | ``` 49 | 50 | Supporting any level of nesting routes. 51 | 52 | ## Basic setup 53 | 54 | > Source code of this example see 55 | on [github](https://github.com/KostyaTretyak/angular-example-simple-service) 56 | or on [stackblitz](https://stackblitz.com/github/KostyaTretyak/angular-example-simple-service) 57 | 58 | Create `SimpleService` class that implements `ApiMockService`. 59 | 60 | At minimum it must implement `getRoutes()` which returns an array whose items are collection routes to return or update. 61 | For example: 62 | 63 | ```ts 64 | import { ApiMockService, ApiMockDataCallback, ApiMockRootRoute } from '@ng-stack/api-mock'; 65 | 66 | interface Model { 67 | id: number; 68 | name: string; 69 | } 70 | 71 | export class SimpleService implements ApiMockService { 72 | getRoutes(): ApiMockRootRoute[] { 73 | return [ 74 | { 75 | path: 'api/heroes/:id', 76 | dataCallback: this.getDataCallback(), 77 | }, 78 | ]; 79 | } 80 | 81 | /** 82 | * The callback called when URL is like `api/heroes` or `api/heroes/3`. 83 | */ 84 | private getDataCallback(): ApiMockDataCallback { 85 | return ({ httpMethod, items }) => { 86 | if (httpMethod == 'GET') { 87 | return [ 88 | { id: 1, name: 'Windstorm' }, 89 | { id: 2, name: 'Bombasto' }, 90 | { id: 3, name: 'Magneta' }, 91 | { id: 4, name: 'Tornado' }, 92 | ]; 93 | } else { 94 | return items; 95 | } 96 | }; 97 | } 98 | } 99 | ``` 100 | 101 | ## Import the `@ng-stack/api-mock` module 102 | 103 | Register `SimpleService` with the `ApiMockModule` in your `imports` array calling the `forRoot` static method with `SimpleService` and an optional configuration object: 104 | 105 | ```ts 106 | import { NgModule } from '@angular/core'; 107 | import { ApiMockModule } from '@ng-stack/api-mock'; 108 | import { environment } from '../environments/environment'; 109 | import { SimpleService } from './simple.service'; 110 | 111 | const apiMockModule = ApiMockModule.forRoot(SimpleService, { delay: 1000 }); 112 | 113 | @NgModule({ 114 | // ... 115 | imports: [ 116 | HttpClientModule, 117 | !environment.production ? apiMockModule : [] 118 | // ... 119 | ], 120 | // ... 121 | }) 122 | export class AppModule { } 123 | ``` 124 | 125 | ### _Notes 1_ 126 | - Always import the `ApiMockModule` after the `HttpClientModule` to ensure that the `@ng-stack/api-mock` backend provider supersedes the Angular version. 127 | - You can setup the `@ng-stack/api-mock` within a lazy loaded feature module by calling the `.forFeature()` method as you would `.forRoot()`. 128 | 129 | ## API 130 | 131 | ### ApiMockService and ApiMockRoute 132 | 133 | ```ts 134 | abstract class ApiMockService { 135 | abstract getRoutes(): ApiMockRootRoute[]; 136 | } 137 | ``` 138 | 139 | The `ApiMockService` is an interface that must be implemented by any` @ng-stack/api-mock` service. For example: 140 | 141 | ```ts 142 | class SomeService implements ApiMockService { 143 | getRoutes(): ApiMockRootRoute[] { 144 | return [{ path: 'api/login' }]; 145 | } 146 | } 147 | ``` 148 | 149 | If we look at the definition of the `ApiMockRootRoute` interface, 150 | we will see that it differs from the `ApiMockRoute` interface only by having the `host` property: 151 | 152 | ```ts 153 | interface ApiMockRootRoute extends ApiMockRoute { 154 | host?: string; 155 | } 156 | 157 | interface ApiMockRoute { 158 | path: string; 159 | dataCallback?: ApiMockDataCallback; 160 | /** 161 | * Properties for a list items that returns from `dataCallback()`, but 162 | * you need init this properties: `propertiesForList: { firstProp: null, secondProp: null }`. 163 | */ 164 | propertiesForList?: ObjectAny; 165 | responseCallback?: ApiMockResponseCallback; 166 | /** 167 | * You can store almost all mockData in localStorage, but with exception of store 168 | * from individual routes with `ignoreDataFromLocalStorage == true`. 169 | * 170 | * By default `ignoreDataFromLocalStorage == false`. 171 | */ 172 | ignoreDataFromLocalStorage?: boolean; 173 | children?: ApiMockRoute[]; 174 | } 175 | ``` 176 | 177 | So, if you use all the possible properties for a route, it will look something like this: 178 | 179 | ```ts 180 | class SomeService implements ApiMockService { 181 | getRoutes(): ApiMockRootRoute[] { 182 | return [ 183 | { 184 | host: 'https://example.com', 185 | path: 'api/posts/:postId', 186 | dataCallback: ({ items }) => items.length ? items : [{ postId: 1, body: 'one' }, { postId: 2, body: 'two' }], 187 | propertiesForList: { body: null }, 188 | responseCallback: ({ resBody }) => resBody, 189 | ignoreDataFromLocalStorage: false, 190 | children: [] 191 | } 192 | ]; 193 | } 194 | } 195 | ``` 196 | 197 | Where `:postId` in `path` property indicates that this key should be used as the "primary key" for the items returned from `dataCallback` function. 198 | And `propertiesForList` contains an object with initialized properties, 199 | it is used for the list of items returned from `dataCallback` function. 200 | 201 | In the example above, the `dataCallback` function returns items with type `{ postId: number, body: string }`, 202 | and if we have a request with an `URL == '/api/posts/1'`, the items returned in the `resBody` argument will have same type. 203 | 204 | But if we have a request with an `URL == '/api/posts'` (without primary key), 205 | the items returned in the `resBody` argument will have type `{ body: string }` 206 | because we have `propertiesForList: { body: null }` in the route. 207 | 208 | ### dataCallback 209 | 210 | `dataCallback` contains a function that is called on a specific route for the very first `HttpClient` request: 211 | - once if `httpMethod == 'GET'`. 212 | - twice if `httpMethod != 'GET'`. The first call automatically comes with `httpMethod == 'GET'` and with an empty array in the `items` argument. The result from the first call is then passed to an array by the `items` argument with **mutable** elements for further calls with the original HTTP method. 213 | 214 | That is, for example, if the backend receives `HttpClient` request with the `POST` method, first `dataCallback` is called with the `GET` method and then with the `POST` method, and the `items` argument contains the result returned from the first call. 215 | - if we have a nesting route, for example: 216 | ```ts 217 | { 218 | path: 'api/posts/:postId', 219 | dataCallback: firstCallback, 220 | children: [ 221 | { 222 | path: 'comments/:commentId', 223 | dataCallback: secondCallback 224 | } 225 | ] 226 | } 227 | ``` 228 | and if `HttpClient` request comes with `URL == 'api/posts/123/comments'`, it will first call `firstCallback()` with `httpMethod == 'GET'`, then in result of this call will search for the item with `postId == 123`. Then `secondCallback()` will be called according to the algorithm described in the first two points, but with the `parents` argument, where there will be an array with one element `postId == 123`. 229 | 230 | Recall - so will work `dataCallback` only for the very first `HttpClient` request on a specific route. For the second and subsequent requests, if `httpMethod == 'GET'`, the data will be retrieved from the cache (or from `localStorage`, if configured). 231 | 232 | For example, if `HttpClient` had a request to `URL == 'api/posts/:postId'` before, then `dataCallback` would no longer be called along the same route with `httpMethod == 'GET'`. 233 | 234 | The same applies to nested routes from the example above. If the first `HttpClient` request comes to `URL == 'api/posts/:postId/comments/:commentId'`, next - to `URL == 'api/posts/:postId'` with `httpMethod == 'GET'`, then this second request will no longer be called `firstCallback()` because the data will be retrieved from the cache. 235 | 236 | Also worth noting - if your route does not have the `dataCallback` property and not have `responseCallback` property, and not have primary key in the `path`, you will always receive `{ status: 200 }` as response. 237 | 238 | The `dataCallback` property must contain a function of the following type: 239 | 240 | ```ts 241 | /** 242 | * Simplified version. 243 | */ 244 | type ApiMockDataCallback = (opts?: ApiMockDataCallbackOptions) => I; 245 | /** 246 | * Simplified version. 247 | */ 248 | interface ApiMockDataCallbackOptions { 249 | httpMethod?: HttpMethod; 250 | items?: I; 251 | itemId?: string; 252 | parents?: P; 253 | queryParams?: Params; 254 | /** 255 | * Request body. 256 | */ 257 | reqBody?: any; 258 | /** 259 | * Request headers. 260 | */ 261 | reqHeaders?: any; 262 | } 263 | ``` 264 | 265 | So, if you use all the possible properties for `ApiMockDataCallbackOptions`, it will look something like this: 266 | 267 | ```ts 268 | export class SomeService implements ApiMockService { 269 | getRoutes(): ApiMockRootRoute[] { 270 | return [ 271 | { 272 | path: 'api/heroes/:id', 273 | dataCallback: ({ httpMethod, items, itemId, parents, queryParams, reqBody, reqHeaders }) => [], 274 | }, 275 | ]; 276 | } 277 | } 278 | ``` 279 | 280 | ### responseCallback 281 | 282 | The `responseCallback` it's property of `ApiMockRoute` that contains function, and it's called after call `dataCallback`. 283 | 284 | This property must contain a function of the following type: 285 | 286 | ```ts 287 | /** 288 | * Simplified version. 289 | */ 290 | type ApiMockResponseCallback = (opts?: ApiMockResponseCallbackOptions) => any; 291 | 292 | /** 293 | * Simplified version. 294 | */ 295 | interface ApiMockResponseCallbackOptions extends ApiMockDataCallbackOptions { 296 | /** 297 | * Response body. 298 | */ 299 | resBody?: any; 300 | } 301 | ``` 302 | 303 | So, if you use all the possible properties for `ApiMockResponseCallbackOptions`, it will look something like this: 304 | 305 | ```ts 306 | export class SomeService implements ApiMockService { 307 | getRoutes(): ApiMockRootRoute[] { 308 | return [ 309 | { 310 | path: 'api/login', 311 | responseCallback: ({ httpMethod, items, itemId, parents, queryParams, reqBody, reqHeaders, resBody }) => [], 312 | }, 313 | ]; 314 | } 315 | } 316 | ``` 317 | 318 | The data returned by the `responseCallback` function is then substituted as a `body` property in the HTTP response. The exception to this rule applies to data of type `HttpResponse` or `HttpErrorResponse`, this data returns as is - without changes. 319 | 320 | For example: 321 | 322 | ```ts 323 | import { HttpErrorResponse, HttpHeaders } from '@angular/common/http'; 324 | import { ApiMockService, ApiMockRootRoute, ApiMockResponseCallback } from '@ng-stack/api-mock'; 325 | 326 | export class SomeService implements ApiMockService { 327 | getRoutes(): ApiMockRootRoute[] { 328 | return [ 329 | { 330 | path: 'api/login', 331 | responseCallback: this.getResponseCallback(), 332 | }, 333 | ]; 334 | } 335 | 336 | private getResponseCallback(): ApiMockResponseCallback { 337 | return ({ reqBody }) => { 338 | const { login, password } = reqBody; 339 | 340 | if (login != 'admin' || password != 'qwerty') { 341 | return new HttpErrorResponse({ 342 | url: 'api/login', 343 | status: 400, 344 | statusText: 'some error message', 345 | headers: new HttpHeaders({ 'Content-Type': 'application/json' }), 346 | error: 'other description', 347 | }); 348 | } 349 | }; 350 | } 351 | } 352 | ``` 353 | 354 | ### ApiMockConfig 355 | 356 | The `ApiMockConfig` defines a set of options for `ApiMockModule`. Add them as the second `forRoot` argument: 357 | 358 | ```ts 359 | ApiMockModule.forRoot(SimpleService, { delay: 1000 }); 360 | ``` 361 | 362 | Read the `ApiMockConfig` class to learn about these options: 363 | 364 | ```ts 365 | class ApiMockConfig { 366 | /** 367 | * - `true` - should pass unrecognized request URL through to original backend. 368 | * - `false` - (default) return 404 code. 369 | */ 370 | passThruUnknownUrl? = false; 371 | /** 372 | * - Do you need to clear previous console logs? 373 | * 374 | * Clears logs between previous route `NavigationStart` and current `NavigationStart` events. 375 | */ 376 | clearPrevLog? = false; 377 | showLog? = true; 378 | cacheFromLocalStorage? = false; 379 | /** 380 | * By default `apiMockCachedData`. 381 | */ 382 | localStorageKey? = 'apiMockCachedData'; 383 | /** 384 | * Simulate latency by delaying response (in milliseconds). 385 | */ 386 | delay? = 500; 387 | /** 388 | * - `true` - (default) 204 code - should NOT return the item after a `POST` an item with existing ID. 389 | * - `false` - 200 code - return the item. 390 | * 391 | * Tip: 392 | * > **204 No Content** 393 | * 394 | * > The server successfully processed the request and is not returning any content. 395 | */ 396 | postUpdate204? = true; 397 | /** 398 | * - `true` - 409 code - should NOT update existing item with `POST`. 399 | * - `false` - (default) 200 code - OK to update. 400 | * 401 | * Tip: 402 | * > **409 Conflict** 403 | * 404 | * > Indicates that the request could not be processed because of conflict in the current 405 | * > state of the resource, such as an edit conflict between multiple simultaneous updates. 406 | */ 407 | postUpdate409? = false; 408 | /** 409 | * - `true` - (default) 204 code - should NOT return the item after a `PUT` an item with existing ID. 410 | * - `false` - 200 code - return the item. 411 | * 412 | * Tip: 413 | * > **204 No Content** 414 | * 415 | * > The server successfully processed the request and is not returning any content. 416 | */ 417 | putUpdate204? = true; 418 | /** 419 | * - `true` - (default) 404 code - if `PUT` item with that ID not found. 420 | * - `false` - create new item. 421 | */ 422 | putUpdate404? = true; 423 | /** 424 | * - `true` - (default) 204 code - should NOT return the item after a `PATCH` an item with existing ID. 425 | * - `false` - 200 code - return the item. 426 | * 427 | * Tip: 428 | * > **204 No Content** 429 | * 430 | * > The server successfully processed the request and is not returning any content. 431 | */ 432 | patchUpdate204? = true; 433 | /** 434 | * - `true` - (default) 404 code - if item with that ID not found. 435 | * - `false` - 204 code. 436 | * 437 | * Tip: 438 | * > **204 No Content** 439 | * 440 | * > The server successfully processed the request and is not returning any content. 441 | */ 442 | deleteNotFound404? = true; 443 | } 444 | ``` 445 | 446 | ## Peer dependencies 447 | 448 | Compatible with `@angular/core` >= v4.3.6 and `rxjs` > v6 449 | 450 | --------------------------------------------------------------------------------