├── .editorconfig
├── .github
└── workflows
│ ├── ci.yml
│ └── release.yml
├── .gitignore
├── LICENSE
├── README.md
├── angular.json
├── package-lock.json
├── package.json
├── projects
└── rx-form-mapper
│ ├── karma.conf.js
│ ├── ng-package.json
│ ├── package.json
│ ├── src
│ ├── lib
│ │ ├── bind
│ │ │ ├── index.ts
│ │ │ ├── model-binder.ts
│ │ │ └── reflect-metadata-design-types.ts
│ │ ├── decorators
│ │ │ ├── custom-control.decorator.ts
│ │ │ ├── form-array.decorator.ts
│ │ │ ├── form-control.decorator.ts
│ │ │ ├── form-group.decorator.ts
│ │ │ ├── form.decorator.ts
│ │ │ └── index.ts
│ │ ├── index.ts
│ │ ├── interfaces
│ │ │ ├── custom-control-mapper.ts
│ │ │ └── index.ts
│ │ ├── metadata
│ │ │ ├── control-metadata.ts
│ │ │ ├── control-visitor.ts
│ │ │ ├── custom-control-metadata.ts
│ │ │ ├── form-array-metadata.ts
│ │ │ ├── form-control-metadata.ts
│ │ │ ├── form-group-metadata.ts
│ │ │ ├── form-metadata.ts
│ │ │ ├── index.ts
│ │ │ └── validable-metadata.ts
│ │ ├── rx-form-mapper.module.ts
│ │ ├── services
│ │ │ ├── custom-mapper-resolver.ts
│ │ │ ├── form-reader.ts
│ │ │ ├── form-writer.ts
│ │ │ ├── index.ts
│ │ │ ├── rx-form-mapper.service.ts
│ │ │ └── validator-resolver.ts
│ │ ├── tests
│ │ │ ├── custom-control-decorator.spec.ts
│ │ │ ├── custom-mapper-resolver.spec.ts
│ │ │ ├── form-array-decorator.spec.ts
│ │ │ ├── form-control-decorator.spec.ts
│ │ │ ├── form-decorator.spec.ts
│ │ │ ├── form-group-decorator.spec.ts
│ │ │ ├── rx-form-mapper-module.spec.ts
│ │ │ ├── rx-form-mapper.spec.ts
│ │ │ ├── utils.spec.ts
│ │ │ └── validator-resolver.spec.ts
│ │ ├── types.ts
│ │ └── utils.ts
│ ├── public_api.ts
│ └── test.ts
│ ├── tsconfig.lib.json
│ ├── tsconfig.lib.prod.json
│ ├── tsconfig.spec.json
│ └── tslint.json
├── renovate.json
├── resources
└── logo_big.png
├── tsconfig.json
└── tslint.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see https://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = tab
7 | indent_size = 4
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: pull_request
4 |
5 | jobs:
6 | lint:
7 | name: lint
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v1
11 | - uses: actions/setup-node@v1
12 | with:
13 | node-version: "14.x"
14 | - run: npm ci
15 | - run: npm run lint
16 | test:
17 | name: Test
18 | runs-on: ubuntu-latest
19 | steps:
20 | - uses: actions/checkout@v1
21 | - uses: actions/setup-node@v1
22 | with:
23 | node-version: "14.x"
24 | - run: npm ci
25 | - run: npm run test-ci
26 | - uses: codecov/codecov-action@v1
27 | with:
28 | fail_ci_if_error: true
29 | directory: coverage
30 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release npm package
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | release:
10 | name: Release
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@master
14 | - uses: actions/setup-node@v1
15 | with:
16 | node-version: "14.x"
17 | - run: npm ci
18 | - run: npm run lint
19 | - run: npm run test-ci
20 | - run: npm run build-ci
21 | - run: cp README.md dist/rx-form-mapper/
22 | - run: cp LICENSE dist/rx-form-mapper/
23 | - run: npx semantic-release
24 | env:
25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
26 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
27 |
--------------------------------------------------------------------------------
/.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 |
8 | # dependencies
9 | /node_modules
10 |
11 | # profiling files
12 | chrome-profiler-events.json
13 | speed-measure-plugin.json
14 |
15 | # IDEs and editors
16 | /.idea
17 | .project
18 | .classpath
19 | .c9/
20 | *.launch
21 | .settings/
22 | *.sublime-workspace
23 |
24 | # IDE - VSCode
25 | .vscode/*
26 | !.vscode/settings.json
27 | !.vscode/tasks.json
28 | !.vscode/launch.json
29 | !.vscode/extensions.json
30 |
31 | # misc
32 | /.sass-cache
33 | /connect.lock
34 | /coverage
35 | /libpeerconnection.log
36 | npm-debug.log
37 | yarn-error.log
38 | testem.log
39 | /typings
40 |
41 | # System Files
42 | .DS_Store
43 | Thumbs.db
44 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Iacopo Ciao
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | [](https://codecov.io/gh/KernelPanic92/rx-form-mapper)
4 | [](https://badge.fury.io/js/rx-form-mapper)
5 | [](https://david-dm.org/KernelPanic92/rx-form-mapper)
6 | [](https://img.shields.io/npm/l/rx-form-mapper.svg)
7 | [](https://img.shields.io/bundlephobia/min/rx-form-mapper.svg)
8 | [](https://github.com/semantic-release/semantic-release)
9 |
10 | RxFormMapper is a framework developed for angular and allows you to convert, by annotation, classes into reactive form and vice versa.
11 |
12 | ## What is RxFormMapper
13 |
14 | Reactive forms use an explicit and immutable approach to managing the state of a form at a given point in time. Each change to the form state returns a new state, which maintains the integrity of the model between changes. Reactive forms are built around observable streams, where form inputs and values are provided as streams of input values, which can be accessed synchronously.
15 |
16 | So... Why RxFormMapper?
17 |
18 | Sometimes you want to transform the classes you have into reactive forms, for example you have a user model that you want to have filled out by a form:
19 |
20 | ```typescript
21 |
22 | export class User {
23 | name: string;
24 | surname: string;
25 | age: number;
26 | }
27 |
28 | ```
29 |
30 | So what to do? How to make a user form ? Solution is to create new instances of Reactive Form object and manually copy all properties to new object. But things may go wrong very fast once you have a more complex object hierarchy.
31 |
32 | ```typescript
33 |
34 | new FormGroup(
35 | name: new FormControl(user.name),
36 | surname: new FormControl(user.surname),
37 | age: new FormControl(user.age),
38 | );
39 |
40 | ```
41 |
42 | To avoid all this you can use RxFormMapper:
43 |
44 | ```typescript
45 |
46 | export class User {
47 |
48 | @FormControl()
49 | name: string;
50 |
51 | @FormControl()
52 | surname: string;
53 |
54 | @FormControl()
55 | age: number;
56 | }
57 |
58 | ```
59 |
60 | ```typescript
61 |
62 | import { Component } from '@angular/core';
63 | import { FormControl } from '@angular/forms';
64 | import { User } from 'src/app/models/user.model';
65 |
66 | @Component({
67 | selector: 'app-user-editor',
68 | templateUrl: './user-editor.component.html',
69 | styleUrls: ['./user-editor.component.css']
70 | })
71 | export class UserEditorComponent {
72 |
73 | public form: FormGroup;
74 | constructor(rxFormMapper: RxFormMapper) {
75 | this.form = rxFormMapper.writeForm(User);
76 | }
77 | }
78 |
79 | ```
80 |
81 | ## Try it
82 |
83 | See it in action at https://stackblitz.com/edit/rx-form-mapper-example?file=src/app/user-registration.ts
84 |
85 | ## Getting started
86 |
87 |
88 | ### Install npm package
89 |
90 | ```bash
91 | npm i rx-form-mapper --save
92 |
93 | ```
94 |
95 | `reflect-metadata` is required (with angular+ you should already have this dependency installed.)
96 |
97 | ```bash
98 | npm i reflect-metadata --save
99 |
100 | ```
101 |
102 | ### Import the component modules
103 | Import the NgModule for RxFormMapper
104 |
105 | ```typescript
106 | import { RxFormMapperModule } from 'rx-form-mapper';
107 |
108 | @NgModule({
109 | ...
110 | imports: [RxFormMapperModule.forRoot()],
111 | ...
112 | })
113 | export class MyAppModule { }
114 | ```
115 |
116 | ### Inject RxFormMapper in your component
117 |
118 | ```typescript
119 | import { RxFormMapper } from 'rx-form-mapper';
120 |
121 | @Component({ ... })
122 | export class MyComponent {
123 | constructor(private readonly rxFormMapper: RxFormMapper) {}
124 | }
125 | ```
126 |
127 | ### Build your form
128 |
129 | ```typescript
130 | import { RxFormMapper } from 'rx-form-mapper';
131 | import { Component } from '@angular/core';
132 | import { FormGroup } from '@angular/forms';
133 | import { User } from 'src/app/models/user.model';
134 |
135 | @Component({ ... })
136 | export class MyComponent {
137 | public myForm: FormGroup;
138 | constructor(rxFormMapper: RxFormMapper) {
139 | this.myForm = rxFormMapper.writeForm(new User());
140 | }
141 | }
142 | ```
143 |
144 | ## Modules
145 |
146 | ### RxFormMapperModule
147 |
148 | This module enables RxFormMapper features
149 |
150 | ## Services
151 |
152 | ### RxFormMapper
153 |
154 | This service provides the methods to serialize and deserialize our objects
155 |
156 | ## Methods
157 |
158 | ### writeForm
159 |
160 | This method converts our class instance into reactive form instance
161 |
162 | ```typescript
163 | this.form = formMapper.writeForm(new Post());
164 | ```
165 |
166 | ### fromType
167 |
168 | This method converts our class type into reactive form instance
169 |
170 | ```typescript
171 | this.form = formMapper.fromType(Post);
172 | ```
173 |
174 | ### readForm
175 |
176 | This method converts our form instance into specific class instance
177 |
178 | ```typescript
179 | const post: Post = formMapper.readForm(this.form, Post);
180 | ```
181 |
182 | ## Decorators
183 |
184 | ### @FormControl
185 |
186 | If you want to expose some of properties as a FormControl, you can do it by @FormControl decorator
187 |
188 | ```typescript
189 |
190 | import { FormControl } from 'rx-form-mapper';
191 |
192 | export class User {
193 |
194 | @FormControl()
195 | name: string;
196 |
197 | @FormControl()
198 | surname: string;
199 |
200 | @FormControl()
201 | age: number;
202 | }
203 |
204 | ```
205 |
206 | ### @FormGroup
207 |
208 | If you want to expose some of properties as a FormGroup, you can do it by @FormGroup decorator
209 |
210 | ```typescript
211 |
212 | import { FormGroup } from 'rx-form-mapper';
213 |
214 | export class Child {}
215 |
216 | export class User {
217 | @FormGroup()
218 | child: Child;
219 | }
220 |
221 | ```
222 |
223 | ### @FormArray
224 |
225 | If you want to expose some of properties as a FormArray, you can do it by @FormArray decorator
226 |
227 | ```typescript
228 |
229 | import { FormGroup } from 'rx-form-mapper';
230 |
231 | export class Child {}
232 |
233 | export class User {
234 | @FormArray(Child)
235 | children: Child[];
236 | }
237 |
238 | ```
239 |
240 | When you're trying to serialize a property into FormArray its required to known what type of object you are trying to convert.
241 |
242 | ### @Form
243 |
244 | If you want to add extra data to your form, you can do it by optional @Form decorator
245 |
246 | ```typescript
247 |
248 | import { Form } from 'rx-form-mapper';
249 |
250 | @Form({
251 | validators: Validators.required
252 | })
253 | export class User {
254 |
255 | @FormControl()
256 | name: string;
257 |
258 | @FormControl()
259 | surname: string;
260 |
261 | @FormControl()
262 | age: number;
263 | }
264 |
265 | ```
266 |
267 | ### @CustomControl
268 |
269 | If you want to create custom forms for specific fields, you can do it by @CustomControl decorator
270 |
271 | Declare your custom mapper class implementing `CustomControlMapper` interface
272 |
273 | ```typescript
274 |
275 | import { CustomControlMapper } from 'rx-form-mapper';
276 | import { AbstractControlOptions, FormControl } from '@angular/forms';
277 |
278 | export class CustomAuthorControlMapper implements CustomControlMapper {
279 |
280 | public writeForm(value: any, abstractControlOptions: AbstractControlOptions): AbstractControl {
281 | return new FormControl(value, abstractControlOptions);
282 | }
283 |
284 | public readForm(control: AbstractControl): ChildTestClass {
285 | return control.value;
286 | }
287 |
288 | }
289 |
290 | ```
291 |
292 | And pass it's type as argument of CustomControl decorator
293 |
294 |
295 | ```typescript
296 |
297 | import { Form } from 'rx-form-mapper';
298 | import { CustomAuthorControlMapper } from '.';
299 |
300 | export class Post {
301 |
302 | @CustomControl(CustomAuthorControlMapper)
303 | author: Person;
304 |
305 | }
306 |
307 | ```
308 |
309 | ## Injectable CustomMapper
310 |
311 | Sometimes you want to injects other services into your CustomMapper, RxFormMapper allows you to do it simple:
312 |
313 | Declare your CustomControlMapper class, decorate with `@Injectable` and includes it in a module as a normal service.
314 |
315 | ```typescript
316 |
317 | import { CustomControlMapper } from 'rx-form-mapper';
318 | import { AbstractControlOptions, FormControl } from '@angular/forms';
319 |
320 | @Injectable()
321 | export class CustomAuthorControlMapper implements CustomControlMapper {
322 |
323 | public writeForm(value: any, abstractControlOptions: AbstractControlOptions): AbstractControl {
324 | return new FormControl(value, abstractControlOptions);
325 | }
326 |
327 | public readForm(control: AbstractControl): ChildTestClass {
328 | return control.value;
329 | }
330 |
331 | }
332 |
333 | ```
334 |
335 | And pass it's type as validator or asyncValidator option
336 |
337 | ```typescript
338 |
339 | import { Form } from 'rx-form-mapper';
340 | import { CustomAuthorControlMapper } from '.';
341 |
342 | export class Post {
343 |
344 | @CustomControl(CustomAuthorControlMapper)
345 | author: Person;
346 |
347 | }
348 |
349 | ```
350 |
351 | ## Validators
352 |
353 | If you want to set a validator on a class or a property, you can do it by specifying `validators` option to `@Form`, `@FormControl`,`@CustomControl` or `@FormArray` decorators
354 |
355 | ```typescript
356 |
357 | import { FormControl } from 'rx-form-mapper';
358 |
359 | export class User {
360 |
361 | @FormControl({
362 | validators: Validators.required
363 | })
364 | completeName: string;
365 |
366 | }
367 |
368 | ```
369 |
370 | ## Async validators
371 |
372 | If you want to set an AsyncValidator on a class or a property, you can do it by specifying `asyncValidators` option to `@Form`, `@FormControl`,`@CustomControl` or `@FormArray` decorators
373 |
374 | ```typescript
375 |
376 | import { FormControl } from 'rx-form-mapper';
377 |
378 | const asyncValidator = (control: AbstractControl) => return of(undefined);
379 |
380 | export class User {
381 |
382 |
383 | @FormControl({
384 | asyncValidators: asyncValidator
385 | })
386 | name: string;
387 |
388 | }
389 |
390 | ```
391 |
392 | ## Injectable validators
393 |
394 | Sometimes you want to injects other services into your validator or asyncValidator, RxFormMapper allows you to do it simple with Angular Forms interfaces:
395 |
396 | Declare your validator class implementing `Validator` or `AsyncValidator` interfaces, decorate with `@Injectable` and includes it in a module as a normal service.
397 |
398 | ```typescript
399 |
400 | import { AsyncValidator } from '@angular/forms';
401 |
402 | @Injectable()
403 | export class UniqueNameValidator implements AsyncValidator {
404 |
405 | constructor(private readonly http: HttpProvider) {}
406 |
407 | public validate(control: AbstractControl): Promise | Observable {
408 | // implementation
409 | }
410 |
411 | }
412 |
413 | ```
414 |
415 | And pass it's type as validator or asyncValidator option
416 |
417 | ```typescript
418 |
419 | import { FormControl } from 'rx-form-mapper';
420 | import { UniqueNameValidator } from 'src/app/validators/unique-Name.validator';
421 |
422 | export class User {
423 |
424 | @FormControl({
425 | asyncValidators: UniqueNameValidator
426 | })
427 | name: string;
428 |
429 | }
430 |
431 | ```
432 |
433 | ## Validation strategy
434 |
435 | Sometimes you want to change the default strategy of form validation, you can do it specifying `updateOn` option to `@Form`, `@FormControl`,`@CustomControl` or `@FormArray` decorators
436 |
437 | ```typescript
438 |
439 | import { FormControl } from 'rx-form-mapper';
440 |
441 | export class User {
442 |
443 | @FormControl({
444 | validators: Validators.required,
445 | updateOn: 'blur'
446 | })
447 | name: string;
448 |
449 | }
450 |
451 | ```
452 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "rx-form-mapper": {
7 | "root": "projects/rx-form-mapper",
8 | "sourceRoot": "projects/rx-form-mapper/src",
9 | "projectType": "library",
10 | "prefix": "lib",
11 | "architect": {
12 | "build": {
13 | "builder": "@angular-devkit/build-angular:ng-packagr",
14 | "options": {
15 | "tsConfig": "projects/rx-form-mapper/tsconfig.lib.json",
16 | "project": "projects/rx-form-mapper/ng-package.json"
17 | },
18 | "configurations": {
19 | "production": {
20 | "tsConfig": "projects/rx-form-mapper/tsconfig.lib.prod.json"
21 | }
22 | }
23 | },
24 | "test": {
25 | "builder": "@angular-devkit/build-angular:karma",
26 | "options": {
27 | "main": "projects/rx-form-mapper/src/test.ts",
28 | "tsConfig": "projects/rx-form-mapper/tsconfig.spec.json",
29 | "karmaConfig": "projects/rx-form-mapper/karma.conf.js",
30 | "codeCoverage": true
31 | }
32 | },
33 | "lint": {
34 | "builder": "@angular-devkit/build-angular:tslint",
35 | "options": {
36 | "tsConfig": [
37 | "projects/rx-form-mapper/tsconfig.lib.json",
38 | "projects/rx-form-mapper/tsconfig.spec.json"
39 | ],
40 | "exclude": [
41 | "**/node_modules/**"
42 | ]
43 | }
44 | }
45 | }
46 | }
47 | },
48 | "defaultProject": "rx-form-mapper"
49 | }
50 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rx-form-mapper",
3 | "version": "0.0.0-development",
4 | "description": "Proper decorator-based transformation / serialization / deserialization of plain javascript classes to angular reactive forms",
5 | "author": {
6 | "email": "iacopociao1992@gmail.com",
7 | "name": "KernelPanic92",
8 | "url": "https://github.com/KernelPanic92"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "https://github.com/KernelPanic92/rx-form-mapper"
13 | },
14 | "license": "MIT",
15 | "keywords": [
16 | "angular",
17 | "reactive-form",
18 | "mapper",
19 | "converter",
20 | "decorator-pattern",
21 | "typescript-library"
22 | ],
23 | "scripts": {
24 | "ng": "ng",
25 | "start": "ng serve",
26 | "build": "ng build",
27 | "build-ci": "ng build --prod",
28 | "test": "ng test",
29 | "test-ci": "ng test --watch=false --browsers=ChromeHeadless",
30 | "lint": "ng lint",
31 | "e2e": "ng e2e",
32 | "semantic-release": "semantic-release",
33 | "postinstall": "ngcc"
34 | },
35 | "private": false,
36 | "dependencies": {
37 | "@angular/animations": "11.2.8",
38 | "@angular/common": "11.2.8",
39 | "@angular/compiler": "11.2.8",
40 | "@angular/core": "11.2.8",
41 | "@angular/forms": "11.2.8",
42 | "@angular/platform-browser": "11.2.8",
43 | "@angular/platform-browser-dynamic": "11.2.8",
44 | "@angular/router": "11.2.8",
45 | "@types/lodash": "4.14.168",
46 | "lodash": "4.17.21",
47 | "rxjs": "6.6.7",
48 | "tslib": "2.1.0",
49 | "zone.js": "0.11.4"
50 | },
51 | "devDependencies": {
52 | "@angular-devkit/build-angular": "0.1102.6",
53 | "@angular/cli": "11.2.6",
54 | "@angular/compiler-cli": "11.2.8",
55 | "@types/jasmine": "3.6.9",
56 | "@types/node": "12.20.6",
57 | "codelyzer": "6.0.1",
58 | "cz-conventional-changelog": "3.3.0",
59 | "jasmine-core": "3.7.1",
60 | "jasmine-spec-reporter": "6.0.0",
61 | "karma": "6.3.1",
62 | "karma-chrome-launcher": "3.1.0",
63 | "karma-coverage": "2.0.3",
64 | "karma-jasmine": "4.0.1",
65 | "karma-jasmine-html-reporter": "1.5.4",
66 | "ng-packagr": "11.2.4",
67 | "protractor": "7.0.0",
68 | "semantic-release": "17.4.2",
69 | "ts-node": "9.1.1",
70 | "tslint": "6.1.3",
71 | "typescript": "4.1.5"
72 | },
73 | "release": {
74 | "pkgRoot": "dist/rx-form-mapper"
75 | },
76 | "config": {
77 | "commitizen": {
78 | "path": "./node_modules/cz-conventional-changelog"
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/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 | clearContext: false // leave Jasmine Spec Runner output visible in browser
17 | },
18 | jasmineHtmlReporter: {
19 | suppressAll: true // removes the duplicated traces
20 | },
21 | coverageReporter: {
22 | dir: require('path').join(__dirname, '../../coverage'),
23 | subdir: '.',
24 | reporters: [
25 | { type: 'html' },
26 | { type: 'text-summary' },
27 | {type: 'lcovonly'}
28 | ]
29 | },
30 | reporters: ['progress', 'kjhtml'],
31 | port: 9876,
32 | colors: true,
33 | logLevel: config.LOG_INFO,
34 | autoWatch: true,
35 | browsers: ['Chrome'],
36 | singleRun: false
37 | });
38 | };
39 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/ng-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3 | "dest": "../../dist/rx-form-mapper",
4 | "lib": {
5 | "entryFile": "src/public_api.ts"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rx-form-mapper",
3 | "description" : "Proper decorator-based transformation / serialization / deserialization of plain javascript classes to angular reactive forms",
4 | "version": "0.9.0",
5 | "author": {
6 | "email": "iacopociao1992@gmail.com",
7 | "name": "KernelPanic92",
8 | "url": "https://github.com/KernelPanic92"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "https://github.com/KernelPanic92/rx-form-mapper"
13 | },
14 | "license": "MIT",
15 | "keywords": [
16 | "angular",
17 | "reactive-form",
18 | "mapper",
19 | "converter",
20 | "decorator-pattern",
21 | "typescript-library"
22 | ],
23 | "dependencies": {
24 | "tslib": "2.1.0"
25 | },
26 | "peerDependencies": {
27 | "@angular/common": "^7.1.0 || ^8.0.0 || ^11.0.0",
28 | "@angular/core": "^7.1.0 || ^8.0.0 || ^11.0.0"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/bind/index.ts:
--------------------------------------------------------------------------------
1 | export * from './model-binder';
2 | export * from './reflect-metadata-design-types';
3 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/bind/model-binder.ts:
--------------------------------------------------------------------------------
1 | import { Type } from '@angular/core';
2 | import 'reflect-metadata';
3 | import { MetadataDesignTypes } from '.';
4 | import { CustomControlOpts, FormArrayOpts, FormControlOpts, FormOpts } from '../decorators';
5 | import { FormMetadata } from '../metadata';
6 |
7 | export class ModelBinder {
8 |
9 | private metadataKey = 'rx-form-mapper-metadata';
10 |
11 | public static readonly instance = new ModelBinder();
12 |
13 | private constructor() {}
14 |
15 | public getMetadata(target: Type): FormMetadata {
16 | if (!Reflect.hasMetadata(this.metadataKey, target)) {
17 | Reflect.defineMetadata(this.metadataKey, new FormMetadata(target), target);
18 | }
19 |
20 | return Reflect.getMetadata(this.metadataKey, target);
21 | }
22 |
23 | public bindForm(target: Type, opts: FormOpts) {
24 | const formMetadata: FormMetadata = this.getMetadata(target);
25 | formMetadata.setValidators(opts);
26 | }
27 |
28 | public bindCustomControl(target: {constructor: Type}, propertyName: string, opts: CustomControlOpts) {
29 | this.getMetadata(target.constructor).setCustomControl(propertyName, opts.mapper, opts);
30 | }
31 |
32 | public bindFormControl(target: {constructor: Type}, propertyName: string, opts?: FormControlOpts): void {
33 | this.getMetadata(target.constructor).setFormControl(propertyName, opts);
34 | }
35 |
36 | public bindFormGroup(target: {constructor: Type}, propertyName: string, type?: Type): void {
37 | const propertyType = type ?? Reflect.getMetadata(MetadataDesignTypes.TYPE, target, propertyName);
38 | const propertyFormMetadata = this.getMetadata(propertyType);
39 | this.getMetadata(target.constructor).setFormGroup(propertyName, propertyFormMetadata);
40 | }
41 |
42 | public bindFormArray(target: {constructor: Type}, propertyName: string, opts: FormArrayOpts): void {
43 | const itemFormMetadata = this.getMetadata(opts.type);
44 | this.getMetadata(target.constructor).setFormArray(propertyName, itemFormMetadata, opts);
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/bind/reflect-metadata-design-types.ts:
--------------------------------------------------------------------------------
1 | export enum MetadataDesignTypes {
2 | TYPE = 'design:type',
3 | RETURN_TYPE = 'design:returntype',
4 | PARAM_TYPE = 'design:paramtypes'
5 | }
6 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/decorators/custom-control.decorator.ts:
--------------------------------------------------------------------------------
1 | import { Type } from '@angular/core';
2 | import { isNil } from 'lodash';
3 | import 'reflect-metadata';
4 | import { RxValidator, RxAsyncValidator } from '..';
5 | import { CustomControlMapper } from '../interfaces/custom-control-mapper';
6 | import { UpdateOn } from '../types';
7 | import { ModelBinder } from './../bind/model-binder';
8 |
9 | export interface CustomControlOpts {
10 | mapper: Type,
11 | validators?: RxValidator | RxValidator[];
12 | asyncValidators?: RxAsyncValidator | RxAsyncValidator[];
13 | updateOn?: UpdateOn;
14 | }
15 |
16 | export function CustomControl(mapper: Type): (target: any, propertyName: string) => void;
17 | export function CustomControl(opts: CustomControlOpts): (target: any, propertyName: string) => void;
18 | export function CustomControl(optsOrMapper: Type | CustomControlOpts): (target: any, propertyName: string) => void {
19 | return (target: any, propertyName: string) => {
20 |
21 | if (isNil(optsOrMapper)) {
22 | throw new Error(`unexpected CustomControl configuration: ${optsOrMapper}`);
23 | }
24 |
25 | if (typeof(optsOrMapper) !== 'object') {
26 | optsOrMapper = { mapper: optsOrMapper };
27 | }
28 |
29 | ModelBinder.instance.bindCustomControl(target, propertyName, optsOrMapper);
30 | };
31 | }
32 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/decorators/form-array.decorator.ts:
--------------------------------------------------------------------------------
1 | import { Type } from '@angular/core';
2 | import { isFunction, isNil } from 'lodash';
3 | import 'reflect-metadata';
4 | import { RxValidator, RxAsyncValidator, UpdateOn, isType } from '..';
5 | import { ModelBinder } from '../bind/model-binder';
6 |
7 | export interface FormArrayOpts {
8 | validators?: RxValidator | RxValidator[];
9 | asyncValidators?: RxAsyncValidator | RxAsyncValidator[];
10 | updateOn?: UpdateOn;
11 | type: Type;
12 | }
13 |
14 | export function FormArray(type: Type): (target: Object, propertyName: string) => void;
15 | export function FormArray(opts: FormArrayOpts): (target: Object, propertyName: string) => void;
16 | export function FormArray(optsOrType: FormArrayOpts | Type): (target: Object, propertyName: string) => void {
17 | return (target: any, propertyName: string) => {
18 |
19 | if (isNil(optsOrType)) {
20 | throw new Error(`unexpected FormArray configuration: ${optsOrType}`);
21 | }
22 |
23 | if (isType(optsOrType)) {
24 | optsOrType = { type: optsOrType };
25 | }
26 |
27 | ModelBinder.instance.bindFormArray(target, propertyName, optsOrType);
28 | };
29 | }
30 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/decorators/form-control.decorator.ts:
--------------------------------------------------------------------------------
1 | import 'reflect-metadata';
2 | import { RxAsyncValidator, RxValidator, UpdateOn } from '../types';
3 | import { ModelBinder } from './../bind/model-binder';
4 |
5 | export interface FormControlOpts {
6 | validators?: RxValidator | RxValidator[];
7 | asyncValidators?: RxAsyncValidator | RxAsyncValidator[];
8 | updateOn?: UpdateOn;
9 | }
10 |
11 | export function FormControl(opts?: FormControlOpts): (target: any, propertyName: string) => void {
12 | return (target: any, propertyName: string) => {
13 | ModelBinder.instance.bindFormControl(target, propertyName, opts);
14 | };
15 | }
16 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/decorators/form-group.decorator.ts:
--------------------------------------------------------------------------------
1 | import { Type } from '@angular/core';
2 | import 'reflect-metadata';
3 | import { ModelBinder } from '../bind/model-binder';
4 |
5 | export function FormGroup(type?: Type): (target: any, propertyName: string) => void {
6 | return (target: any, propertyName: string) => {
7 | ModelBinder.instance.bindFormGroup(target, propertyName, type);
8 | };
9 | }
10 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/decorators/form.decorator.ts:
--------------------------------------------------------------------------------
1 | import 'reflect-metadata';
2 | import { RxValidator, RxAsyncValidator, UpdateOn } from '..';
3 | import { ModelBinder } from '../bind/model-binder';
4 |
5 | export interface FormOpts {
6 | validators?: RxValidator | RxValidator[];
7 | asyncValidators?: RxAsyncValidator | RxAsyncValidator[];
8 | updateOn?: UpdateOn;
9 | }
10 |
11 | export function Form(opts?: FormOpts): (target: any) => void {
12 | return (target: any) => {
13 | ModelBinder.instance.bindForm(target, opts);
14 | };
15 | }
16 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/decorators/index.ts:
--------------------------------------------------------------------------------
1 | export * from './custom-control.decorator';
2 | export * from './form-array.decorator';
3 | export * from './form-control.decorator';
4 | export * from './form-group.decorator';
5 | export * from './form.decorator';
6 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/index.ts:
--------------------------------------------------------------------------------
1 | export * from './bind';
2 | export * from './decorators';
3 | export * from './interfaces';
4 | export * from './metadata';
5 | export * from './rx-form-mapper.module';
6 | export * from './services';
7 | export * from './types';
8 | export * from './utils';
9 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/interfaces/custom-control-mapper.ts:
--------------------------------------------------------------------------------
1 | import { AbstractControl, AbstractControlOptions } from '@angular/forms';
2 |
3 |
4 | export interface CustomControlMapper {
5 | writeForm(value: any, abstractControlOptions: AbstractControlOptions): AbstractControl;
6 | readForm(control: AbstractControl): any;
7 | }
8 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/interfaces/index.ts:
--------------------------------------------------------------------------------
1 | export * from './custom-control-mapper';
2 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/metadata/control-metadata.ts:
--------------------------------------------------------------------------------
1 | import { ControlVisitor } from './control-visitor';
2 |
3 | export interface ControlMetadata {
4 | accept(visitor: ControlVisitor): T;
5 | }
6 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/metadata/control-visitor.ts:
--------------------------------------------------------------------------------
1 | import { FormControlMetadata } from './form-control-metadata';
2 | import { FormGroupMetadata } from './form-group-metadata';
3 | import { FormMetadata } from './form-metadata';
4 | import { FormArrayMetadata } from './form-array-metadata';
5 | import { CustomControlMetadata } from './custom-control-metadata';
6 |
7 | export interface ControlVisitor {
8 | visitCustomControlMetadata(customControlMetadata: CustomControlMetadata): T;
9 | visitFormArrayMetadata(formArrayMetadata: FormArrayMetadata): T;
10 | visitFormControlMetadata(formControlMetadata: FormControlMetadata): T;
11 | visitFormGroupMetadata(formGroupMetadata: FormGroupMetadata): T;
12 | visitFormMetadata(formMetadata: FormMetadata): T;
13 | }
14 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/metadata/custom-control-metadata.ts:
--------------------------------------------------------------------------------
1 |
2 | import { Type } from '@angular/core';
3 | import { CustomControlMapper } from '../interfaces';
4 | import { ControlVisitor } from './control-visitor';
5 | import { ValidableMetadata } from './validable-metadata';
6 |
7 | export class CustomControlMetadata extends ValidableMetadata {
8 |
9 | public constructor(public readonly mapper: Type) {
10 | super();
11 | }
12 |
13 | public accept(visitor: ControlVisitor): T {
14 | return visitor.visitCustomControlMetadata(this);
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/metadata/form-array-metadata.ts:
--------------------------------------------------------------------------------
1 | import { ControlVisitor } from './control-visitor';
2 | import { FormMetadata } from './form-metadata';
3 | import { ValidableMetadata } from './validable-metadata';
4 |
5 | export class FormArrayMetadata extends ValidableMetadata {
6 |
7 | public constructor(public readonly itemForm: FormMetadata) {
8 | super();
9 | }
10 |
11 | public accept(visitor: ControlVisitor): T {
12 | return visitor.visitFormArrayMetadata(this);
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/metadata/form-control-metadata.ts:
--------------------------------------------------------------------------------
1 | import { ControlVisitor } from './control-visitor';
2 | import { ValidableMetadata } from './validable-metadata';
3 |
4 | export class FormControlMetadata extends ValidableMetadata {
5 |
6 | public accept(visitor: ControlVisitor): T {
7 | return visitor.visitFormControlMetadata(this);
8 | }
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/metadata/form-group-metadata.ts:
--------------------------------------------------------------------------------
1 | import { ControlMetadata } from './control-metadata';
2 | import { ControlVisitor } from './control-visitor';
3 | import { FormMetadata } from './form-metadata';
4 |
5 | export class FormGroupMetadata implements ControlMetadata {
6 |
7 | public constructor(public readonly form: FormMetadata) {}
8 |
9 | public accept(visitor: ControlVisitor): T {
10 | return visitor.visitFormGroupMetadata(this);
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/metadata/form-metadata.ts:
--------------------------------------------------------------------------------
1 | import { Type } from '@angular/core';
2 | import { CustomControlMapper } from '../interfaces';
3 | import { ControlVisitor } from './control-visitor';
4 | import { CustomControlMetadata } from './custom-control-metadata';
5 | import { FormArrayMetadata } from './form-array-metadata';
6 | import { FormControlMetadata } from './form-control-metadata';
7 | import { FormGroupMetadata } from './form-group-metadata';
8 | import { ValidableMetadata, ValidationOpts } from './validable-metadata';
9 |
10 | type FormPropertyMetadata = FormControlMetadata | FormGroupMetadata | FormArrayMetadata;
11 |
12 | export class FormMetadata extends ValidableMetadata {
13 |
14 | private _controls: {[key: string]: FormPropertyMetadata} = {};
15 |
16 | public constructor(public readonly type: Type) {
17 | super();
18 | }
19 |
20 | public accept(visitor: ControlVisitor): T {
21 | return visitor.visitFormMetadata(this);
22 | }
23 |
24 | public get controls(): {[key: string]: FormPropertyMetadata} {
25 | return this._controls;
26 | }
27 |
28 | public setFormControl(name: string, opts: ValidationOpts): void {
29 | const control = new FormControlMetadata();
30 | control.setValidators(opts);
31 | this.controls[name] = control;
32 | }
33 |
34 | public setFormGroup(name: string, form: FormMetadata): void {
35 | const control = new FormGroupMetadata(form);
36 | this.controls[name] = control;
37 | }
38 |
39 | public setCustomControl(name: string, mapper: Type, opts: ValidationOpts): void {
40 | const control = new CustomControlMetadata(mapper);
41 | control.setValidators(opts);
42 | this.controls[name] = control;
43 | }
44 |
45 | public setFormArray(name: string, itemform: FormMetadata, opts: ValidationOpts): void {
46 | const control = new FormArrayMetadata(itemform);
47 | control.setValidators(opts);
48 | this.controls[name] = control;
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/metadata/index.ts:
--------------------------------------------------------------------------------
1 | export * from './control-metadata';
2 | export * from './control-visitor';
3 | export * from './custom-control-metadata';
4 | export * from './form-array-metadata';
5 | export * from './form-control-metadata';
6 | export * from './form-group-metadata';
7 | export * from './form-metadata';
8 | export * from './validable-metadata';
9 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/metadata/validable-metadata.ts:
--------------------------------------------------------------------------------
1 | import { RxValidator, RxAsyncValidator, UpdateOn } from '..';
2 | import { coerceArray } from '../utils';
3 | import { ControlMetadata } from './control-metadata';
4 | import { ControlVisitor } from './control-visitor';
5 |
6 | export interface ValidationOpts {
7 | validators?: RxValidator | RxValidator[];
8 | asyncValidators?: RxAsyncValidator | RxAsyncValidator[];
9 | updateOn?: UpdateOn;
10 | }
11 |
12 | export abstract class ValidableMetadata implements ControlMetadata {
13 |
14 | private _validators: RxValidator[] = [];
15 | private _asyncValidators: RxAsyncValidator[] = [];
16 | private _updateOn: UpdateOn;
17 |
18 | public abstract accept(visitor: ControlVisitor): T;
19 |
20 | public setValidators(opts?: ValidationOpts): void {
21 | this._validators = coerceArray(opts?.validators);
22 | this._asyncValidators = coerceArray(opts?.asyncValidators);
23 | this._updateOn = opts?.updateOn;
24 | }
25 |
26 | public get validators(): RxValidator[] {
27 | return this._validators;
28 | }
29 |
30 | public get asyncValidators(): RxAsyncValidator[] {
31 | return this._asyncValidators;
32 | }
33 |
34 | public get updateOn(): UpdateOn {
35 | return this._updateOn;
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/rx-form-mapper.module.ts:
--------------------------------------------------------------------------------
1 | import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
2 | import { RxFormMapper, ValidatorResolver, CustomMapperResolver } from './services';
3 |
4 | @NgModule()
5 | export class RxFormMapperModule {
6 |
7 | public static forRoot(): ModuleWithProviders {
8 | return {
9 | ngModule: RxFormMapperModule,
10 | providers: [
11 | RxFormMapper,
12 | CustomMapperResolver,
13 | ValidatorResolver
14 | ]
15 | };
16 | }
17 |
18 | public constructor(@Optional() @SkipSelf() parentModule?: RxFormMapperModule) {
19 | if (parentModule) {
20 | throw new Error('RxFormMapperModule is already loaded. Import it in the AppModule only');
21 | }
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/services/custom-mapper-resolver.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, InjectFlags, Injector, Type } from '@angular/core';
2 | import { CustomControlMapper } from '../interfaces';
3 |
4 | @Injectable()
5 | export class CustomMapperResolver {
6 | public constructor(private readonly injector: Injector) {}
7 |
8 | public resolve(type: Type): CustomControlMapper {
9 | return this.injector.get(type, null, InjectFlags.Optional) ?? new type();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/services/form-reader.ts:
--------------------------------------------------------------------------------
1 | import { Type } from '@angular/core';
2 | import { AbstractControl, FormArray, FormControl, FormGroup } from '@angular/forms';
3 | import { ControlVisitor, CustomControlMetadata, FormArrayMetadata, FormControlMetadata, FormGroupMetadata, FormMetadata } from '../metadata';
4 | import { CustomMapperResolver } from './custom-mapper-resolver';
5 |
6 | export class FormReader implements ControlVisitor {
7 |
8 | public constructor(private readonly control: AbstractControl, private readonly customMapperResolver: CustomMapperResolver) {}
9 |
10 | public visitCustomControlMetadata(customControlMetadata: CustomControlMetadata): any {
11 | return this.customMapperResolver.resolve(customControlMetadata.mapper).readForm(this.control);
12 | }
13 |
14 | public visitFormArrayMetadata(formArrayMetadata: FormArrayMetadata): any {
15 | if (!this.control) {
16 | return void(0);
17 | }
18 |
19 | this.checkControlType(this.control, FormArray);
20 |
21 | return (this.control as FormArray).controls.map(control => {
22 | const subFormReader = this.copyPrototype(control);
23 | return formArrayMetadata.itemForm.accept(subFormReader);
24 | });
25 | }
26 |
27 | public visitFormControlMetadata(formControlMetadata: FormControlMetadata): any {
28 | if (!this.control) {
29 | return void(0);
30 | }
31 |
32 | this.checkControlType(this.control, FormControl);
33 |
34 | return this.control.value;
35 | }
36 |
37 | public visitFormGroupMetadata(formGroupMetadata: FormGroupMetadata): any {
38 | return this.visitFormMetadata(formGroupMetadata.form);
39 | }
40 |
41 | public visitFormMetadata(formMetadata: FormMetadata): any {
42 | if (!this.control) {
43 | return void(0);
44 | }
45 |
46 | this.checkControlType(this.control, FormGroup);
47 |
48 | const value = new formMetadata.type();
49 | const formGroup: FormGroup = this.control as FormGroup;
50 |
51 | for (const [key, controlMetadata] of Object.entries(formMetadata.controls)) {
52 | const formField = formGroup.controls[key];
53 | const formFieldReader = this.copyPrototype(formField);
54 | value[key] = controlMetadata.accept(formFieldReader);
55 | }
56 |
57 | return value;
58 | }
59 |
60 | private copyPrototype(control: AbstractControl): FormReader {
61 | return new FormReader(control, this.customMapperResolver);
62 | }
63 |
64 | private checkControlType(control: AbstractControl, type: Type): void {
65 | if (control instanceof type) {
66 | return;
67 | }
68 |
69 | throw new Error(`control is not ${type.name} instance`);
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/services/form-writer.ts:
--------------------------------------------------------------------------------
1 | import { InjectFlags, Injector, Type } from '@angular/core';
2 | import { AbstractControl, AbstractControlOptions, FormArray, Validator, ValidatorFn, FormControl, FormGroup } from '@angular/forms';
3 |
4 | import { CustomControlMapper } from '../interfaces';
5 | import { ControlVisitor, CustomControlMetadata, FormArrayMetadata, FormControlMetadata, FormGroupMetadata, FormMetadata, ValidableMetadata } from '../metadata';
6 | import { CustomMapperResolver } from './custom-mapper-resolver';
7 | import { ValidatorResolver } from './validator-resolver';
8 |
9 | export class FormWriter implements ControlVisitor {
10 |
11 | public constructor(private readonly value: any, private readonly customMapperResolver: CustomMapperResolver, private readonly validatorResolver: ValidatorResolver) {}
12 |
13 | public visitCustomControlMetadata(customControlMetadata: CustomControlMetadata): AbstractControl {
14 | const mapper = this.customMapperResolver.resolve(customControlMetadata.mapper);
15 | return mapper.writeForm(this.value, this.buildAbstractControlOptions(customControlMetadata))
16 | }
17 |
18 | public visitFormArrayMetadata(formArrayMetadata: FormArrayMetadata): AbstractControl {
19 | const values: any[] = this.value ?? [];
20 | const controls: AbstractControl[] = [];
21 |
22 | for (const item of values) {
23 | const subWriter = this.copyPrototype(item);
24 | const control = formArrayMetadata.itemForm.accept(subWriter);
25 | controls.push(control);
26 | }
27 |
28 | return new FormArray(controls, this.buildAbstractControlOptions(formArrayMetadata));
29 | }
30 |
31 | public visitFormControlMetadata(formControlMetadata: FormControlMetadata): AbstractControl {
32 | return new FormControl(this.value, this.buildAbstractControlOptions(formControlMetadata));
33 | }
34 |
35 | public visitFormGroupMetadata(formGroupMetadata: FormGroupMetadata): AbstractControl {
36 | return this.visitFormMetadata(formGroupMetadata.form);
37 | }
38 |
39 | public visitFormMetadata(formMetadata: FormMetadata): AbstractControl {
40 | const controls: { [key: string]: AbstractControl } = {};
41 |
42 | for (const [key, controlMetadata] of Object.entries(formMetadata.controls)) {
43 | const fieldValue = this.value?.[key];
44 | const subWriter = this.copyPrototype(fieldValue);
45 | controls[key] = controlMetadata.accept(subWriter);
46 | }
47 |
48 | return new FormGroup(controls, this.buildAbstractControlOptions(formMetadata));
49 | }
50 |
51 |
52 | private buildAbstractControlOptions(metadata: ValidableMetadata): AbstractControlOptions {
53 | return {
54 | validators: metadata.validators.map(v => this.validatorResolver.resolve(v)),
55 | asyncValidators: metadata.asyncValidators.map(v => this.validatorResolver.resolve(v)),
56 | updateOn: metadata.updateOn
57 | };
58 | }
59 |
60 | private copyPrototype(value: any): FormWriter {
61 | return new FormWriter(value, this.customMapperResolver, this.validatorResolver);
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/services/index.ts:
--------------------------------------------------------------------------------
1 | export * from './custom-mapper-resolver';
2 | export * from './form-reader';
3 | export * from './form-writer';
4 | export * from './rx-form-mapper.service';
5 | export * from './validator-resolver';
6 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/services/rx-form-mapper.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Injector, Type } from '@angular/core';
2 | import { FormGroup } from '@angular/forms';
3 | import { isNil } from 'lodash';
4 | import { ModelBinder } from '../bind';
5 | import { CustomMapperResolver } from './custom-mapper-resolver';
6 | import { FormReader } from './form-reader';
7 | import { FormWriter } from './form-writer';
8 | import { ValidatorResolver } from './validator-resolver';
9 |
10 | @Injectable()
11 | export class RxFormMapper {
12 | public constructor(
13 | private readonly customMapperResolver: CustomMapperResolver,
14 | private readonly validatorResolver: ValidatorResolver
15 | ) {}
16 |
17 | public fromType(type: Type): FormGroup {
18 | if (isNil(type)) {
19 | throw new Error('type cannot be inferred implicitly');
20 | }
21 |
22 | const formWriter = new FormWriter(void(0), this.customMapperResolver, this.validatorResolver);
23 | return ModelBinder.instance.getMetadata(type).accept(formWriter) as FormGroup;
24 | }
25 |
26 | public writeForm(value: T): FormGroup;
27 | public writeForm(value: T, type: Type): FormGroup;
28 | public writeForm(value: T, type?: Type): FormGroup {
29 | if (isNil(value) && isNil(type)) {
30 | throw new Error('type cannot be inferred implicitly');
31 | }
32 |
33 | const valueType = type ?? Object.getPrototypeOf(value).constructor;
34 | const formWriter = new FormWriter(value, this.customMapperResolver, this.validatorResolver);
35 | return ModelBinder.instance.getMetadata(valueType).accept(formWriter) as FormGroup;
36 |
37 | }
38 |
39 | public readForm(form: FormGroup, type: Type ): T {
40 | if (isNil(type)) {
41 | throw new Error('type cannot be inferred implicitly');
42 | }
43 |
44 | const formReader = new FormReader(form, this.customMapperResolver);
45 | return ModelBinder.instance.getMetadata(type).accept(formReader);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/services/validator-resolver.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, InjectFlags, Injector } from '@angular/core';
2 | import { AbstractControl, AsyncValidatorFn, ValidatorFn } from '@angular/forms';
3 | import { isFunction } from 'lodash';
4 | import { RxAsyncValidator, RxValidator } from '../types';
5 |
6 | @Injectable()
7 | export class ValidatorResolver {
8 |
9 | public constructor(private readonly injector: Injector) {}
10 |
11 | public resolve(validator: RxAsyncValidator): AsyncValidatorFn;
12 | public resolve(validator: RxValidator): ValidatorFn;
13 | public resolve(validator: any): any {
14 |
15 | if (this.isValidatorFn(validator)) {
16 | return validator;
17 | }
18 |
19 | let validatorInstance = null;
20 |
21 | if (this.isValidatorInstance(validator)) {
22 | validatorInstance = validator;
23 | } else {
24 | validatorInstance = this.injector.get(validator as any, null, InjectFlags.Optional);
25 | validatorInstance ??= new validator();
26 | }
27 |
28 | return (c: AbstractControl) => validatorInstance.validate(c);
29 |
30 | }
31 |
32 | private isValidatorFn(value: any): boolean {
33 | return isFunction(value) && !value.prototype.validate;
34 | }
35 |
36 | private isValidatorInstance(value: any): boolean {
37 | return 'validate' in value;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/tests/custom-control-decorator.spec.ts:
--------------------------------------------------------------------------------
1 | import { AbstractControl, AbstractControlOptions, FormControl } from '@angular/forms';
2 | import { CustomControl, CustomControlMapper } from '..';
3 | import { ModelBinder } from '../bind';
4 | import { CustomControlMetadata } from '../metadata';
5 |
6 | describe('CustomControl decorator', () => {
7 |
8 | it('should decorate with type', () => {
9 |
10 | class CustomControlMapperImpl implements CustomControlMapper {
11 | public writeForm(value: any, abstractControlOptions: AbstractControlOptions): AbstractControl {
12 | return new FormControl(value, abstractControlOptions);
13 | }
14 |
15 | public readForm(control: AbstractControl): any {
16 | return control.value;
17 | }
18 | }
19 |
20 | class Test {
21 | @CustomControl(CustomControlMapperImpl)
22 | public field: string[];
23 | }
24 |
25 | expect(ModelBinder.instance.getMetadata(Test).controls.field instanceof CustomControlMetadata).toBeTruthy();
26 | });
27 |
28 | it('should decorate with opts', () => {
29 |
30 | class CustomControlMapperImpl implements CustomControlMapper {
31 | public writeForm(value: any, abstractControlOptions: AbstractControlOptions): AbstractControl {
32 | return new FormControl(value, abstractControlOptions);
33 | }
34 |
35 | public readForm(control: AbstractControl): any {
36 | return control.value;
37 | }
38 | }
39 |
40 | class Test {
41 | @CustomControl({
42 | mapper: CustomControlMapperImpl
43 | })
44 | public field: string[];
45 | }
46 |
47 | expect(ModelBinder.instance.getMetadata(Test).controls.field instanceof CustomControlMetadata).toBeTruthy();
48 | });
49 |
50 | it('should throw error when configuration is invalid', ()=> {
51 |
52 | expect(() => {
53 |
54 | class ChildTestClass {
55 | public field: string;
56 | }
57 |
58 | class TestClass {
59 | @CustomControl(null)
60 | public field: ChildTestClass;
61 | }
62 |
63 | }).toThrow();
64 | });
65 |
66 | });
67 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/tests/custom-mapper-resolver.spec.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Injector } from '@angular/core';
2 | import { inject, TestBed } from '@angular/core/testing';
3 | import { AbstractControl, AbstractControlOptions, FormGroup } from '@angular/forms';
4 | import { RxFormMapperModule } from '..';
5 | import { CustomControlMapper } from '../interfaces';
6 | import { RxFormMapper } from '../services';
7 | import { CustomMapperResolver } from '../services/custom-mapper-resolver';
8 |
9 | class UninstantiableCustomControlMapper implements CustomControlMapper {
10 |
11 | public constructor() {
12 | throw new Error('invalid operation');
13 | }
14 |
15 | public writeForm(value: any, abstractControlOptions: AbstractControlOptions): AbstractControl {
16 | return null;
17 | }
18 |
19 | public readForm(control: AbstractControl) {
20 | return null;
21 | }
22 |
23 | }
24 |
25 | class InstantiableCustomControlMapper extends UninstantiableCustomControlMapper {
26 | public constructor() {
27 | try{
28 | super();
29 | } catch (error) { /* do nothing */ }
30 | }
31 | }
32 |
33 | describe('CustomMapperResolver', () => {
34 | beforeEach(() => {
35 | TestBed.configureTestingModule({
36 | imports: [RxFormMapperModule.forRoot()],
37 | }).compileComponents();
38 | });
39 |
40 | it('should be created', inject([CustomMapperResolver], (resolver: CustomMapperResolver) => {
41 | expect(resolver).toBeTruthy();
42 | }));
43 |
44 | it('should resolve injected mapper', inject([Injector], (injector: Injector) => {
45 | spyOn(injector, 'get').and.returnValue(new InstantiableCustomControlMapper());
46 |
47 |
48 | const mapper = new CustomMapperResolver(injector).resolve(UninstantiableCustomControlMapper);
49 |
50 | expect(mapper).toBeTruthy();
51 | // tslint:disable-next-line: deprecation
52 | expect(injector.get).toHaveBeenCalled();
53 | }));
54 |
55 | it('should instantiate mapper', inject([Injector], (injector: Injector) => {
56 | spyOn(injector, 'get').and.returnValue(null);
57 |
58 | const mapper = new CustomMapperResolver(injector).resolve(InstantiableCustomControlMapper);
59 |
60 | expect(mapper).toBeTruthy();
61 | }));
62 | });
63 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/tests/form-array-decorator.spec.ts:
--------------------------------------------------------------------------------
1 | import { ModelBinder } from '../bind';
2 | import { FormArray } from '../decorators';
3 | import { FormArrayMetadata } from '../metadata';
4 |
5 | describe('FormArray decorator', () => {
6 |
7 | it('should decorate with type', () => {
8 | class Test {
9 | @FormArray(String)
10 | public field: string[];
11 | }
12 |
13 | expect(ModelBinder.instance.getMetadata(Test).controls.field instanceof FormArrayMetadata).toBeTruthy();
14 | const formArrayMetadata = ModelBinder.instance.getMetadata(Test).controls.field as FormArrayMetadata;
15 | expect(formArrayMetadata.itemForm.type === String).toBeTruthy();
16 | });
17 |
18 | it('should decorate with opts', () => {
19 | class Test {
20 | @FormArray({type: String})
21 | public field: string[];
22 | }
23 |
24 | const formArrayMetadata = ModelBinder.instance.getMetadata(Test).controls.field as FormArrayMetadata;
25 | expect(formArrayMetadata.itemForm.type === String).toBeTruthy();
26 | });
27 |
28 | it('should throw error when configuration is invalid', ()=> {
29 |
30 | expect(() => {
31 |
32 | class TestClass {
33 | @FormArray(null)
34 | public field: [];
35 | }
36 |
37 | }).toThrow();
38 | });
39 |
40 | });
41 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/tests/form-control-decorator.spec.ts:
--------------------------------------------------------------------------------
1 | import { ModelBinder } from '../bind';
2 | import { FormControl } from '../decorators';
3 | import { FormControlMetadata } from '../metadata';
4 |
5 | describe('FormControl decorator', () => {
6 |
7 | it('should decorate', () => {
8 | class Test {
9 | @FormControl()
10 | public field: string[];
11 | }
12 |
13 | expect(ModelBinder.instance.getMetadata(Test).controls.field instanceof FormControlMetadata).toBeTruthy();
14 | });
15 |
16 | });
17 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/tests/form-decorator.spec.ts:
--------------------------------------------------------------------------------
1 | import { Validators } from '@angular/forms';
2 | import { ModelBinder } from '../bind';
3 | import { Form } from '../decorators';
4 |
5 | describe('Form decorator', () => {
6 |
7 | it('should decorate', () => {
8 |
9 | @Form({ validators: Validators.required })
10 | class Test {
11 |
12 | public field: string[];
13 |
14 | }
15 |
16 | expect(ModelBinder.instance.getMetadata(Test).validators).toHaveSize(1);
17 | });
18 |
19 | });
20 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/tests/form-group-decorator.spec.ts:
--------------------------------------------------------------------------------
1 | import { ModelBinder } from '../bind';
2 | import { FormGroup } from '../decorators';
3 | import { FormGroupMetadata } from '../metadata';
4 |
5 | describe('FormGroup decorator', () => {
6 |
7 | it('should decorate', () => {
8 | class Test {
9 | @FormGroup()
10 | public field: string[];
11 | }
12 |
13 | expect(ModelBinder.instance.getMetadata(Test).controls.field instanceof FormGroupMetadata).toBeTruthy();
14 | });
15 |
16 | it('should decorate with type', () => {
17 | class Test {
18 | @FormGroup(String)
19 | public field: string[];
20 | }
21 |
22 | expect(ModelBinder.instance.getMetadata(Test).controls.field instanceof FormGroupMetadata).toBeTruthy();
23 | });
24 |
25 | });
26 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/tests/rx-form-mapper-module.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component, NgModule, NgModuleFactoryLoader } from '@angular/core';
2 | import { TestBed } from '@angular/core/testing';
3 | import { Router, RouterModule } from '@angular/router';
4 | import { RxFormMapperModule } from '..';
5 | import { RouterTestingModule } from '@angular/router/testing';
6 |
7 | describe('RxFormMapperModule', () => {
8 |
9 | @Component({ template: '' })
10 | class TestComponent { }
11 |
12 | @NgModule({
13 | imports: [RxFormMapperModule.forRoot(), RouterModule.forChild([{ path: '', component: TestComponent }])]
14 | })
15 | class ChildModule { }
16 |
17 | beforeEach(() => {
18 | TestBed.configureTestingModule({
19 | imports: [RxFormMapperModule.forRoot(), RouterTestingModule.withRoutes([{ path: '', loadChildren: './test/ChildModule#ChildModule' }])],
20 | }).compileComponents();
21 |
22 |
23 | });
24 |
25 | it('Should not provide twice', async () => {
26 | // tslint:disable-next-line: deprecation
27 | const loader: any = TestBed.inject(NgModuleFactoryLoader);
28 | const router = TestBed.inject(Router);
29 |
30 | loader.stubbedModules = {
31 | './test/ChildModule#ChildModule': ChildModule,
32 | };
33 |
34 | let error: Error = null;
35 |
36 | try {
37 | await router.navigate([]);
38 | } catch (e) {
39 | error = e;
40 | }
41 |
42 | expect(error.message).toEqual('RxFormMapperModule is already loaded. Import it in the AppModule only');
43 | });
44 |
45 | });
46 |
47 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/tests/rx-form-mapper.spec.ts:
--------------------------------------------------------------------------------
1 | import { inject, TestBed } from '@angular/core/testing';
2 | import { CustomControl, CustomControlMapper, Form, FormArray, FormControl, FormGroup, RxFormMapperModule } from '..';
3 | import { RxFormMapper } from '../services';
4 | import {
5 | AbstractControl,
6 | AbstractControlOptions,
7 | FormArray as NgFormArray,
8 | FormControl as NgFormControl,
9 | FormGroup as NgFormGroup,
10 | Validators,
11 | } from '@angular/forms';
12 | import { of } from 'rxjs';
13 |
14 | describe('RxFormMapper', () => {
15 | beforeEach(() => {
16 | TestBed.configureTestingModule({
17 | imports: [RxFormMapperModule.forRoot()]
18 | }).compileComponents();
19 | });
20 |
21 | it('should be created', inject([RxFormMapper], (formMapper: RxFormMapper) => {
22 | expect(formMapper).toBeTruthy();
23 | }));
24 |
25 | it('writeForm should not detect type', inject([RxFormMapper], (formMapper: RxFormMapper) => {
26 | expect(() => formMapper.writeForm(null)).toThrow();
27 | }));
28 |
29 | it('writeForm should auto detect type', inject([RxFormMapper], (formMapper: RxFormMapper) => {
30 | class Test {
31 |
32 | }
33 | expect(formMapper.writeForm(new Test())).toBeTruthy();
34 | }));
35 |
36 | it('writeForm should write with specific type', inject([RxFormMapper], (formMapper: RxFormMapper) => {
37 | class Test {
38 |
39 | }
40 | expect(formMapper.writeForm(new Test(), Test)).toBeTruthy();
41 | }));
42 |
43 | it('fromType should create form', inject([RxFormMapper], (formMapper: RxFormMapper) => {
44 | class Test {
45 | @FormControl()
46 | public name: string;
47 | }
48 |
49 | expect(formMapper.fromType(Test).get('name')).toBeTruthy();
50 | }));
51 |
52 | it('fromType should throw error', inject([RxFormMapper], (formMapper: RxFormMapper) => {
53 | class Test {
54 | @FormControl()
55 | public name: string;
56 | }
57 |
58 | expect(() => formMapper.fromType(null)).toThrow();
59 | }));
60 |
61 | it('writeForm should not accept undefined type', inject([RxFormMapper], (mapper: RxFormMapper) => {
62 | expect(() => mapper.writeForm(null, null)).toThrow();
63 | }));
64 |
65 | it('writeForm should return FormGroup', inject([RxFormMapper], (mapper: RxFormMapper) => {
66 | class TestClass {}
67 | expect(mapper.writeForm(null, TestClass) instanceof NgFormGroup).toBeTruthy();
68 | }));
69 |
70 | it('should write FormControl field', inject([RxFormMapper], (mapper: RxFormMapper) => {
71 | class TestClass {
72 | @FormControl()
73 | public field: string;
74 | }
75 | const form = mapper.writeForm(new TestClass(), TestClass);
76 | expect(form.get('field') instanceof NgFormControl).toBeTruthy();
77 | }));
78 |
79 | it('should write FormControl value', inject([RxFormMapper], (mapper: RxFormMapper) => {
80 | class TestClass {
81 | @FormControl()
82 | public field: string;
83 | }
84 | const testValue = new TestClass();
85 | testValue.field = 'test';
86 | expect(mapper.writeForm(testValue, TestClass).get('field').value).toEqual('test');
87 | }));
88 |
89 | it('should write FormGroup field', inject([RxFormMapper], (mapper: RxFormMapper) => {
90 |
91 | class ChildTestClass {
92 | @FormControl()
93 | public field: string;
94 | }
95 |
96 | class TestClass {
97 | @FormGroup()
98 | public field: ChildTestClass;
99 | }
100 |
101 | expect(mapper.writeForm(new TestClass(), TestClass).get('field') instanceof NgFormGroup).toBeTruthy();
102 | }));
103 |
104 | it('should write FormGroup value', inject([RxFormMapper], (mapper: RxFormMapper) => {
105 |
106 | class ChildTestClass {
107 | @FormControl()
108 | public field: string;
109 | }
110 |
111 | class TestClass {
112 | @FormGroup()
113 | public field: ChildTestClass;
114 | }
115 |
116 | const testValue = new TestClass();
117 | testValue.field = new ChildTestClass();
118 | testValue.field.field = 'test';
119 | expect(mapper.writeForm(testValue, TestClass).get('field.field').value).toEqual('test');
120 | }));
121 |
122 | it('should write FormArray field', inject([RxFormMapper], (mapper: RxFormMapper) => {
123 |
124 | class ChildTestClass {
125 | @FormControl()
126 | public field: string;
127 | }
128 |
129 | class TestClass {
130 | @FormArray(ChildTestClass)
131 | public fields: ChildTestClass[];
132 | }
133 |
134 | expect(mapper.writeForm(new TestClass(), TestClass).get('fields') instanceof NgFormArray).toBeTruthy();
135 | }));
136 |
137 | it('should write FormArray value', inject([RxFormMapper], (mapper: RxFormMapper) => {
138 |
139 | class ChildTestClass {
140 | @FormControl()
141 | public field: string;
142 | }
143 |
144 | class TestClass {
145 | @FormArray(ChildTestClass)
146 | public fields: ChildTestClass[];
147 | }
148 |
149 | const testValue = new TestClass();
150 | testValue.fields = [new ChildTestClass()];
151 | testValue.fields[0].field = 'test';
152 | const form = mapper.writeForm(testValue, TestClass);
153 | expect((form.get('fields') as NgFormArray).controls[0].get('field').value).toEqual('test');
154 | }));
155 |
156 | it('should write CustomControl value', inject([RxFormMapper], (mapper: RxFormMapper) => {
157 |
158 | class ChildTestClass {
159 | public field: string;
160 | }
161 |
162 | class CustomControlMapperTest implements CustomControlMapper {
163 | public writeForm(value: ChildTestClass, abstractControlOptions: AbstractControlOptions): AbstractControl {
164 | return new NgFormControl(value, abstractControlOptions);
165 | }
166 |
167 | public readForm(control: AbstractControl): ChildTestClass {
168 | return control.value;
169 | }
170 | }
171 |
172 | class TestClass {
173 | @CustomControl(CustomControlMapperTest)
174 | public field: ChildTestClass;
175 | }
176 |
177 | const form = mapper.writeForm(new TestClass(), TestClass);
178 | expect(form.get('field') instanceof NgFormControl).toBeTruthy();
179 | }));
180 |
181 | it('readForm should return null', inject([RxFormMapper], (formMapper: RxFormMapper) => {
182 | class Test {
183 |
184 | }
185 |
186 | expect(formMapper.readForm(null, Test)).toBeUndefined();
187 | }));
188 |
189 | it('readForm should not detect type', inject([RxFormMapper], (formMapper: RxFormMapper) => {
190 |
191 | expect(() => formMapper.readForm(new NgFormGroup({}), null)).toThrow();
192 | }));
193 |
194 | it('should return undefined when control not exists', inject([RxFormMapper], (mapper: RxFormMapper) => {
195 | class TestClass {
196 | @FormControl()
197 | public field: string;
198 | }
199 |
200 | const formGroup = new NgFormGroup({});
201 | expect(mapper.readForm(formGroup, TestClass).field).toBeUndefined();
202 | }));
203 |
204 | it('should throw error when field is not FormControl', inject([RxFormMapper], (mapper: RxFormMapper) => {
205 | class TestClass {
206 | @FormControl()
207 | public field: string;
208 | }
209 |
210 | const formGroup = new NgFormGroup({field: new NgFormGroup({})});
211 | expect(() => mapper.readForm(formGroup, TestClass)).toThrow();
212 | }));
213 |
214 | it('should read FormControl field', inject([RxFormMapper], (mapper: RxFormMapper) => {
215 | class TestClass {
216 | @FormControl()
217 | public field: string;
218 | }
219 |
220 | const formGroup = new NgFormGroup({field: new NgFormControl('test')});
221 | expect(mapper.readForm(formGroup, TestClass).field).toEqual('test');
222 | }));
223 |
224 | it('should throw error when field is not FormGroup', inject([RxFormMapper], (mapper: RxFormMapper) => {
225 | class TestClass {
226 | @FormGroup()
227 | public field: string;
228 | }
229 |
230 | const formGroup = new NgFormGroup({field: new NgFormControl({})});
231 | expect(() => mapper.readForm(formGroup, TestClass)).toThrow();
232 | }));
233 |
234 | it('should read FormGroup field', inject([RxFormMapper], (mapper: RxFormMapper) => {
235 | class TestClass {
236 | @FormControl()
237 | public name: string;
238 | @FormGroup()
239 | public field: TestClass;
240 | }
241 |
242 | const formGroup = new NgFormGroup({field: new NgFormGroup({name: new NgFormControl('test')})});
243 | expect(mapper.readForm(formGroup, TestClass).field.name).toEqual('test');
244 | }));
245 |
246 | it('should throw error when field is not FormArray', inject([RxFormMapper], (mapper: RxFormMapper) => {
247 | class TestClass {
248 | @FormArray(TestClass)
249 | public field: TestClass[];
250 | }
251 |
252 | const formGroup = new NgFormGroup({field: new NgFormControl({})});
253 | expect(() => mapper.readForm(formGroup, TestClass)).toThrow();
254 | }));
255 |
256 | it('should read FormArray field', inject([RxFormMapper], (mapper: RxFormMapper) => {
257 | class TestClass {
258 | @FormControl()
259 | public name: string;
260 | @FormArray(TestClass)
261 | public field: TestClass[];
262 | }
263 |
264 | const formGroup = new NgFormGroup({field: new NgFormArray([new NgFormGroup({name: new NgFormControl('test')})])});
265 | expect(mapper.readForm(formGroup, TestClass).field[0].name).toEqual('test');
266 | }));
267 |
268 | it('should read CustomControl value', inject([RxFormMapper], (mapper: RxFormMapper) => {
269 |
270 | class ChildTestClass {
271 | constructor(public field: string) {}
272 | }
273 |
274 | class CustomControlMapperTest implements CustomControlMapper {
275 | public writeForm(value: ChildTestClass, abstractControlOptions: AbstractControlOptions): AbstractControl {
276 | return new NgFormControl(value, abstractControlOptions);
277 | }
278 |
279 | public readForm(control: AbstractControl): ChildTestClass {
280 | return control.value;
281 | }
282 | }
283 |
284 | class TestClass {
285 | @CustomControl(CustomControlMapperTest)
286 | public field: ChildTestClass;
287 | }
288 |
289 | const form = new NgFormGroup({field: new NgFormControl(new ChildTestClass('hello'))});
290 |
291 | expect(mapper.readForm(form, TestClass).field.field).toEqual('hello');
292 | }));
293 |
294 | it('should set validator', inject([RxFormMapper], (mapper: RxFormMapper) => {
295 |
296 | @Form({validators: Validators.required, asyncValidators: c => of(null)})
297 | class TestClass {
298 | @FormControl()
299 | public field: string;
300 | }
301 |
302 | expect(mapper.writeForm(new TestClass(), TestClass).validator).toBeTruthy();
303 | }));
304 |
305 | });
306 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/tests/utils.spec.ts:
--------------------------------------------------------------------------------
1 | import { coerceArray } from '../utils';
2 |
3 | describe('utils', () => {
4 |
5 | it('coerceArray should return array when value is array', () => {
6 | expect(coerceArray([1])).toEqual([1]);
7 | });
8 |
9 | it('coerceArray should return array when value is empty array', () => {
10 | expect(coerceArray([])).toEqual([]);
11 | });
12 |
13 | it('coerceArray should return array when value is not array', () => {
14 | expect(coerceArray(1)).toEqual([1]);
15 | });
16 |
17 | it('coerceArray should return array when value is undefined', () => {
18 | expect(coerceArray(undefined)).toEqual([]);
19 | });
20 |
21 | });
22 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/tests/validator-resolver.spec.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Injector } from '@angular/core';
2 | import { inject, TestBed } from '@angular/core/testing';
3 | import { AbstractControl, AbstractControlOptions, FormGroup, ValidationErrors, Validator } from '@angular/forms';
4 | import { RxFormMapperModule } from '..';
5 | import { CustomControlMapper } from '../interfaces';
6 | import { RxFormMapper, ValidatorResolver } from '../services';
7 | import { CustomMapperResolver } from '../services/custom-mapper-resolver';
8 |
9 | describe('ValidatorResolver', () => {
10 | beforeEach(() => {
11 | TestBed.configureTestingModule({
12 | imports: [RxFormMapperModule.forRoot()],
13 | }).compileComponents();
14 | });
15 |
16 | it('should be created', inject([ValidatorResolver], (resolver: ValidatorResolver) => {
17 | expect(resolver).toBeTruthy();
18 | }));
19 |
20 | it('should resolve validatorFn', inject([ValidatorResolver], (resolver: ValidatorResolver) => {
21 | const validatorFn = (c) => ({});
22 |
23 | const actual = resolver.resolve(validatorFn);
24 |
25 | expect(actual).toEqual(validatorFn);
26 | }));
27 |
28 | it('should resolve validator instance', inject([ValidatorResolver], (resolver: ValidatorResolver) => {
29 |
30 | class SimpleValidator implements Validator {
31 | validate(control: AbstractControl): ValidationErrors {
32 | return { error: true };
33 | }
34 | }
35 |
36 | const actual = resolver.resolve(new SimpleValidator());
37 |
38 | expect(actual(null).error).toEqual(true);
39 | }));
40 |
41 | it('should resolve validator type', inject([ValidatorResolver], (resolver: ValidatorResolver) => {
42 |
43 | class SimpleValidator implements Validator {
44 | validate(control: AbstractControl): ValidationErrors {
45 | return { error: true };
46 | }
47 | }
48 |
49 | const actual = resolver.resolve(SimpleValidator);
50 |
51 | expect(actual(null).error).toEqual(true);
52 | }));
53 |
54 | it('should resolve injectable validator type', inject([ValidatorResolver, Injector], (resolver: ValidatorResolver, injector: Injector) => {
55 |
56 | class NotInjectableValidator implements Validator {
57 | validate(control: AbstractControl): ValidationErrors {
58 | return { error: true }
59 | }
60 | }
61 |
62 | class InjectableValidator extends NotInjectableValidator {
63 | validate(control: AbstractControl): ValidationErrors {
64 | return { error: false }
65 | }
66 | }
67 |
68 | spyOn(injector, 'get').and.returnValue(new InjectableValidator());
69 |
70 | const actual = new ValidatorResolver(injector).resolve(NotInjectableValidator);
71 |
72 | expect(actual(null).error).toEqual(false);
73 | }));
74 |
75 | });
76 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/types.ts:
--------------------------------------------------------------------------------
1 | import { Type } from '@angular/core';
2 | import { Validator, ValidatorFn, AsyncValidator, AsyncValidatorFn } from '@angular/forms';
3 |
4 | export type RxValidator = Validator | ValidatorFn | Type;
5 | export type RxAsyncValidator = AsyncValidator | AsyncValidatorFn | Type;
6 | export type UpdateOn = 'change' | 'blur' | 'submit' | null | undefined;
7 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { Type } from '@angular/core';
2 | import { castArray, isFunction, isNil, negate } from 'lodash';
3 |
4 | const isNotNil = negate(isNil);
5 |
6 | export function coerceArray(value: T | ReadonlyArray): Array {
7 | return castArray(value).filter(isNotNil);
8 | }
9 |
10 | export function isType(value: any): value is Type {
11 | return isFunction(value);
12 | }
13 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/public_api.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Public API Surface of rx-form-mapper
3 | */
4 |
5 | export * from './lib/index';
6 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 | import 'zone.js/dist/zone';
3 | import 'zone.js/dist/zone-testing';
4 | import { getTestBed } from '@angular/core/testing';
5 | import {
6 | BrowserDynamicTestingModule,
7 | platformBrowserDynamicTesting
8 | } from '@angular/platform-browser-dynamic/testing';
9 |
10 | declare const require: {
11 | context(path: string, deep?: boolean, filter?: RegExp): {
12 | keys(): string[];
13 | (id: string): T;
14 | };
15 | };
16 |
17 | // First, initialize the Angular testing environment.
18 | getTestBed().initTestEnvironment(
19 | BrowserDynamicTestingModule,
20 | platformBrowserDynamicTesting()
21 | );
22 | // Then we find all the tests.
23 | const context = require.context('./', true, /\.spec\.ts$/);
24 |
25 | // And load the modules.
26 | context.keys().forEach(context);
27 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../out-tsc/lib",
5 | "declarationMap": true,
6 | "target": "es2015",
7 | "module": "es2015",
8 | "moduleResolution": "node",
9 | "declaration": true,
10 | "sourceMap": true,
11 | "inlineSources": true,
12 | "emitDecoratorMetadata": true,
13 | "experimentalDecorators": true,
14 | "importHelpers": true,
15 | "types": [],
16 | "lib": [
17 | "dom",
18 | "es2018"
19 | ]
20 | },
21 | "angularCompilerOptions": {
22 | "skipTemplateCodegen": true,
23 | "strictMetadataEmit": true,
24 | "fullTemplateTypeCheck": true,
25 | "strictInjectionParameters": true,
26 | "enableResourceInlining": true
27 | },
28 | "exclude": [
29 | "src/test.ts",
30 | "**/*.spec.ts"
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/tsconfig.lib.prod.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.lib.json",
3 | "compilerOptions": {
4 | "declarationMap": false
5 | },
6 | "angularCompilerOptions": {
7 | "enableIvy": false
8 | }
9 | }
--------------------------------------------------------------------------------
/projects/rx-form-mapper/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../out-tsc/spec",
5 | "types": [
6 | "jasmine",
7 | "node"
8 | ]
9 | },
10 | "files": [
11 | "src/test.ts"
12 | ],
13 | "include": [
14 | "**/*.spec.ts",
15 | "**/*.d.ts"
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/projects/rx-form-mapper/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tslint.json",
3 | "rules": {
4 | "directive-selector": [
5 | true,
6 | "attribute",
7 | "lib",
8 | "camelCase"
9 | ],
10 | "component-selector": [
11 | true,
12 | "element",
13 | "lib",
14 | "kebab-case"
15 | ]
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "config:base"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/resources/logo_big.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KernelPanic92/rx-form-mapper/4661e56a30c33b71c65d57baea710431b20302a8/resources/logo_big.png
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "baseUrl": "./",
5 | "outDir": "./dist/out-tsc",
6 | "sourceMap": true,
7 | "declaration": false,
8 | "module": "es2020",
9 | "moduleResolution": "node",
10 | "emitDecoratorMetadata": true,
11 | "experimentalDecorators": true,
12 | "importHelpers": true,
13 | "target": "es5",
14 | "typeRoots": [
15 | "node_modules/@types"
16 | ],
17 | "lib": [
18 | "es2018",
19 | "dom"
20 | ],
21 | "paths": {
22 | "rx-form-mapper": [
23 | "dist/rx-form-mapper"
24 | ],
25 | "rx-form-mapper/*": [
26 | "dist/rx-form-mapper/*"
27 | ]
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "error",
3 | "extends": [
4 | "tslint:recommended"
5 | ],
6 | "rules": {
7 | "unified-signatures": false,
8 | "indent": [true, "tabs"],
9 | "no-var-keyword": true,
10 | "quotemark": [
11 | true,
12 | "single"
13 | ],
14 | "deprecation": {
15 | "severity": "warning"
16 | },
17 | "array-type": false,
18 | "interface-name": false,
19 | "no-shadowed-variable": false,
20 | "arrow-parens": false,
21 | "trailing-comma": true,
22 | "ban-types": false,
23 | "max-line-length": false,
24 | "variable-name": false,
25 | "max-classes-per-file": false,
26 | "object-literal-sort-keys": false,
27 | "curly": false
28 | }
29 | }
30 |
--------------------------------------------------------------------------------