diff --git a/README.md b/README.md index 0e99886..3916b1f 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,8 @@ ```bash npm install ngx-strongly-typed-forms ``` -**Attention:** Since version 7.2 this project does no longer follow semver version numbers. -The major and minor version represents the compatible Angular version and patch versions are bugfixes in this library. +**Attention:** Since version 7.2 this project does no longer follow semver version numbers. +The major and minor version represents the compatible Angular version and patch versions are bugfixes in this library. Now you can import generic FormControl, FormGroup and FormArray and use them instead of the classes from `@angular/forms` @@ -36,19 +36,36 @@ For convenience it re-exports these classes directly from Angular. ### Hints -* When working with FormBuilder and FormGroups always mention the type you want, or else the TypeScript compiler tries to match -every property, which does not work with nested FormArrays or FormGroups. +* When working with `FormControl` always mention the type you want, or else the TypeScript compiler loses type safety by inferring `any`. +For the same reason the `FormArray` sub-control type should be specified unless at least one sub-control is included. ``` -form = fb.group({ - foo: null, - bar: ["bar", Validators.required], - baz: fb.array([]) -}) +form = fb.group({ + foo: fb.control("foo"), + bar: fb.control("bar", Validators.required), + baz1: fb.array>([]), + baz2: fb.array([fb.control(undefined)]) +}); ``` * `FormArray` extends `AbstractControl`. So if you have a `FormArray` you can assign it to an `AbstractControl`. This is necessary, because for instance `FormArray.get` returns a single instance of type `T` but `FormArray.value` returns `T[]`. It's also important when working with FormArrays as part of complex FormGroups. The generic type of the FormArray must always be the same as the generic of the Array in the model. +* Specifying a type parameter in the `FormBuilder` methods will provide the original functionality of all inner controls being typed as `AbstractControl`s. Full control type inference is supported by removing the type parameter, eg., + +``` typescript +form = fb.group({ + array: fb.array([ + fb.control("") + ]) +}); +// this typechecks +stringcontrol: FormControl = form.controls.array[0]; + +* Unfortunately the type errors when using full control type inference are not good as of Typescript v3.4 + * `Types of property 'controls' are incompatible` means that the types do not align exactly - FormControl will not match a model which is of ype string | undefined or an optional field such as `{ foo?: string }` + * `Argument of type 'string[]' is not assignable to parameter of type ...` when calling `control.get([...])` means that the control "path" does not match the sub-controls defined. +``` + ## Alternatives * Angulars own effort to create typed forms (https://github.com/angular/angular/pull/20040). diff --git a/gulpfile.js b/gulpfile.js index 855a06b..b36a210 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -9,6 +9,9 @@ var gulp = require('gulp'), bump = require('gulp-bump'); const version = require('./package').version; +// for the side effect of configuring ngc +require('@angular/compiler-cli'); + const rootFolder = path.join(__dirname); const srcFolder = path.join(rootFolder, 'src'); const tmpFolder = path.join(rootFolder, '.tmp'); diff --git a/package-lock.json b/package-lock.json index 8e40c3f..5ce3acb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,27 +5,27 @@ "requires": true, "dependencies": { "@angular/common": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-8.0.2.tgz", - "integrity": "sha512-9lwrKso0XjyS7wu+8dEWa5yN1kCTdbelP6JElFhh0kAt0TbPVHJ/dXEwvIFk9/2MjYv2PbooQo1zsc5kAB2Rlg==", + "version": "8.2.14", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-8.2.14.tgz", + "integrity": "sha512-Qmt+aX2quUW54kaNT7QH7WGXnFxr/cC2C6sf5SW5SdkZfDQSiz8IaItvieZfXVQUbBOQKFRJ7TlSkt0jI/yjvw==", "dev": true, "requires": { "tslib": "^1.9.0" } }, "@angular/compiler": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-8.0.2.tgz", - "integrity": "sha512-ktobrxpWX1eCwbDKOIUm5GRj8WGlHW/8MAQvDDFUnsGqXBHfOGiaySiEYX/XjeN8qu34IfXs736QkdzpMM4+iw==", + "version": "8.2.14", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-8.2.14.tgz", + "integrity": "sha512-ABZO4E7eeFA1QyJ2trDezxeQM5ZFa1dXw1Mpl/+1vuXDKNjJgNyWYwKp/NwRkLmrsuV0yv4UDCDe4kJOGbPKnw==", "dev": true, "requires": { "tslib": "^1.9.0" } }, "@angular/compiler-cli": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-8.0.2.tgz", - "integrity": "sha512-9jdpB8WC47oSgQ/jA+ExTYqbe4xw3ZCEhgLhPd8BQukBOHodaIHKnkinrVJAPZORpY1CKRaImoAHieSvRhiPjA==", + "version": "8.2.14", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-8.2.14.tgz", + "integrity": "sha512-XDrTyrlIZM+0NquVT+Kbg5bn48AaWFT+B3bAT288PENrTdkuxuF9AhjFRZj8jnMdmaE4O2rioEkXBtl6z3zptA==", "dev": true, "requires": { "canonical-path": "1.0.0", @@ -35,34 +35,33 @@ "magic-string": "^0.25.0", "minimist": "^1.2.0", "reflect-metadata": "^0.1.2", - "shelljs": "^0.8.1", "source-map": "^0.6.1", "tslib": "^1.9.0", "yargs": "13.1.0" } }, "@angular/core": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-8.0.2.tgz", - "integrity": "sha512-g8BRvGZxTXb5GZ/xoC5Z94DGK3wMiD2jbmEQEbXGNM+c8E/Mo/W8GF44P7EU2d+V1oJoUh75SRK6U/StC+rLqA==", + "version": "8.2.14", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-8.2.14.tgz", + "integrity": "sha512-zeePkigi+hPh3rN7yoNENG/YUBUsIvUXdxx+AZq+QPaFeKEA2FBSrKn36ojHFrdJUjKzl0lPMEiGC2b6a6bo6g==", "dev": true, "requires": { "tslib": "^1.9.0" } }, "@angular/forms": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-8.0.2.tgz", - "integrity": "sha512-LGu3b/wjNMCki5PnMUsfQlyaVZVOedNO+XccfluP4ZBQ5G/E2cz2tJ0UIHg3RhLbbpWntmqokpYLyd7leUPpIQ==", + "version": "8.2.14", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-8.2.14.tgz", + "integrity": "sha512-zhyKL3CFIqcyHJ/TQF/h1OZztK611a6rxuPHCrt/5Sn1SuBTJJQ1pPTkOYIDy6IrCrtyANc8qB6P17Mao71DNQ==", "dev": true, "requires": { "tslib": "^1.9.0" } }, "@angular/platform-browser": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-8.0.2.tgz", - "integrity": "sha512-iUoyhJ81jqvpmQI6Lu5NzRZR8azmnb2kX2FQ+LbwCvWQLfkLbTaa/Jl09/qN6KWpTsMogNQXVnjjgwoeaObvBw==", + "version": "8.2.14", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-8.2.14.tgz", + "integrity": "sha512-MtJptptyKzsE37JZ2VB/tI4cvMrdAH+cT9pMBYZd66YSZfKjIj5s+AZo7z8ncoskQSB1o3HMfDjSK7QXGx1mLQ==", "dev": true, "requires": { "tslib": "^1.9.0" @@ -306,6 +305,16 @@ "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", "dev": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -430,9 +439,9 @@ } }, "chokidar": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz", - "integrity": "sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", "dev": true, "requires": { "anymatch": "^2.0.0", @@ -580,9 +589,9 @@ "dev": true }, "convert-source-map": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", - "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", "dev": true, "requires": { "safe-buffer": "~5.1.1" @@ -614,9 +623,9 @@ }, "dependencies": { "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true } } @@ -970,6 +979,13 @@ "time-stamp": "^1.0.0" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -1363,21 +1379,16 @@ "universalify": "^0.1.0" } }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, "fsevents": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", - "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.11.tgz", + "integrity": "sha512-+ux3lx6peh0BpvY0JebGyZoiR4D+oYzdPZMKJwkZ+sFkNJzpL7tXc/wehS49gUAxg3tmMHPHZkA8JU2rhhgDHw==", "dev": true, "optional": true, "requires": { + "bindings": "^1.5.0", "nan": "^2.12.1", - "node-pre-gyp": "^0.12.0" + "node-pre-gyp": "*" }, "dependencies": { "abbrev": { @@ -1425,7 +1436,7 @@ } }, "chownr": { - "version": "1.1.1", + "version": "1.1.3", "bundled": true, "dev": true, "optional": true @@ -1455,7 +1466,7 @@ "optional": true }, "debug": { - "version": "4.1.1", + "version": "3.2.6", "bundled": true, "dev": true, "optional": true, @@ -1482,12 +1493,12 @@ "optional": true }, "fs-minipass": { - "version": "1.2.5", + "version": "1.2.7", "bundled": true, "dev": true, "optional": true, "requires": { - "minipass": "^2.2.1" + "minipass": "^2.6.0" } }, "fs.realpath": { @@ -1513,7 +1524,7 @@ } }, "glob": { - "version": "7.1.3", + "version": "7.1.6", "bundled": true, "dev": true, "optional": true, @@ -1542,7 +1553,7 @@ } }, "ignore-walk": { - "version": "3.0.1", + "version": "3.0.3", "bundled": true, "dev": true, "optional": true, @@ -1561,7 +1572,7 @@ } }, "inherits": { - "version": "2.0.3", + "version": "2.0.4", "bundled": true, "dev": true, "optional": true @@ -1603,7 +1614,7 @@ "optional": true }, "minipass": { - "version": "2.3.5", + "version": "2.9.0", "bundled": true, "dev": true, "optional": true, @@ -1613,12 +1624,12 @@ } }, "minizlib": { - "version": "1.2.1", + "version": "1.3.3", "bundled": true, "dev": true, "optional": true, "requires": { - "minipass": "^2.2.1" + "minipass": "^2.9.0" } }, "mkdirp": { @@ -1631,24 +1642,24 @@ } }, "ms": { - "version": "2.1.1", + "version": "2.1.2", "bundled": true, "dev": true, "optional": true }, "needle": { - "version": "2.3.0", + "version": "2.4.0", "bundled": true, "dev": true, "optional": true, "requires": { - "debug": "^4.1.0", + "debug": "^3.2.6", "iconv-lite": "^0.4.4", "sax": "^1.2.4" } }, "node-pre-gyp": { - "version": "0.12.0", + "version": "0.14.0", "bundled": true, "dev": true, "optional": true, @@ -1662,7 +1673,7 @@ "rc": "^1.2.7", "rimraf": "^2.6.1", "semver": "^5.3.0", - "tar": "^4" + "tar": "^4.4.2" } }, "nopt": { @@ -1676,13 +1687,22 @@ } }, "npm-bundled": { - "version": "1.0.6", + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-normalize-package-bin": { + "version": "1.0.1", "bundled": true, "dev": true, "optional": true }, "npm-packlist": { - "version": "1.4.1", + "version": "1.4.7", "bundled": true, "dev": true, "optional": true, @@ -1753,7 +1773,7 @@ "optional": true }, "process-nextick-args": { - "version": "2.0.0", + "version": "2.0.1", "bundled": true, "dev": true, "optional": true @@ -1794,7 +1814,7 @@ } }, "rimraf": { - "version": "2.6.3", + "version": "2.7.1", "bundled": true, "dev": true, "optional": true, @@ -1821,7 +1841,7 @@ "optional": true }, "semver": { - "version": "5.7.0", + "version": "5.7.1", "bundled": true, "dev": true, "optional": true @@ -1874,18 +1894,18 @@ "optional": true }, "tar": { - "version": "4.4.8", + "version": "4.4.13", "bundled": true, "dev": true, "optional": true, "requires": { "chownr": "^1.1.1", "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", "mkdirp": "^0.5.0", "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" + "yallist": "^3.0.3" } }, "util-deprecate": { @@ -1910,7 +1930,7 @@ "optional": true }, "yallist": { - "version": "3.0.3", + "version": "3.1.1", "bundled": true, "dev": true, "optional": true @@ -2908,9 +2928,9 @@ "dev": true }, "magic-string": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.2.tgz", - "integrity": "sha512-iLs9mPjh9IuTtRsqqhNGYcZXGei0Nh/A4xirrsqW7c+QhKVFL2vm7U09ru6cHRD22azaP/wMDgI+HCqbETMTtg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.5.tgz", + "integrity": "sha512-vIO/BOm9odBHBAGwv0gZPLJeO9IpwliiIc0uPeAW93rrFMJ/R3M665IAEfOU/IW3kD4S9AtEn76lfTn1Yif+9A==", "dev": true, "requires": { "sourcemap-codec": "^1.4.4" @@ -3035,15 +3055,6 @@ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, "minimist": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", @@ -3147,10 +3158,9 @@ } }, "natives": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/natives/-/natives-1.1.4.tgz", - "integrity": "sha512-Q29yeg9aFKwhLVdkTAejM/HvYG0Y1Am1+HUkFQGn5k2j8GS+v60TVmZh6nujpEAj/qql+wGUrlryO8bF+b1jEg==", - "dev": true + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/natives/-/natives-1.1.6.tgz", + "integrity": "sha512-6+TDFewD4yxY14ptjKaS63GVdtKiES1pTPyxn9Jb0rBqPMZ7VcCiooEhPNsr+mqHtMGxa/5c/HhcC4uPEUw/nA==" }, "nice-try": { "version": "1.0.5", @@ -3365,9 +3375,9 @@ "dev": true }, "p-limit": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", - "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", + "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", "dev": true, "requires": { "p-try": "^2.0.0" @@ -3571,9 +3581,9 @@ }, "dependencies": { "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "dev": true, "requires": { "once": "^1.4.0" @@ -3852,33 +3862,6 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, - "shelljs": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.3.tgz", - "integrity": "sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A==", - "dev": true, - "requires": { - "glob": "^7.0.0", - "interpret": "^1.0.0", - "rechoir": "^0.6.2" - }, - "dependencies": { - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, "sigmund": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", @@ -4031,9 +4014,9 @@ "dev": true }, "sourcemap-codec": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.4.tgz", - "integrity": "sha512-CYAPYdBu34781kLHkaW3m6b/uUSyMOC2R61gcYMWooeuaGtjof86ZA/8T+qVPPt7np1085CR9hmMGrySwEc8Xg==", + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.7.tgz", + "integrity": "sha512-RuN23NzhAOuUtaivhcrjXx1OPXsFeH9m5sI373/U7+tGLKihjUyboZAzOadytMjnqHp1f45RGk1IzDKCpDpSYA==", "dev": true }, "sparkles": { @@ -4284,9 +4267,9 @@ "dev": true }, "typescript": { - "version": "3.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.5.tgz", - "integrity": "sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz", + "integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==", "dev": true }, "unc-path-regex": { @@ -4389,9 +4372,9 @@ } }, "upath": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz", - "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", "dev": true }, "urix": { diff --git a/package.json b/package.json index 95c16d3..6ea115d 100644 --- a/package.json +++ b/package.json @@ -21,16 +21,16 @@ "type definition" ], "peerDependencies": { - "@angular/forms": "^8.0.0", - "rxjs": "^5.x || ^6.x" + "@angular/forms": "^8.2.0", + "rxjs": "^6.4" }, "devDependencies": { - "@angular/common": "^8.0.1", - "@angular/compiler": "^8.0.1", - "@angular/compiler-cli": "^8.0.1", - "@angular/core": "^8.0.1", - "@angular/forms": "^8.0.1", - "@angular/platform-browser": "^8.0.1", + "@angular/common": "^8.2.14", + "@angular/compiler": "^8.2.14", + "@angular/compiler-cli": "^8.2.14", + "@angular/core": "^8.2.14", + "@angular/forms": "^8.2.14", + "@angular/platform-browser": "^8.2.14", "fs-extra": "^5.0.0", "gulp": "^3.9.1", "gulp-bump": "^3.1.1", @@ -39,7 +39,7 @@ "rollup": "^0.49.3", "run-sequence": "^1.2.2", "rxjs": "^6.4.0", - "typescript": "~3.4", + "typescript": "3.5.3", "zone.js": "^0.9.1" }, "repository": { diff --git a/src/interop.ts b/src/interop.ts index eee347f..869b503 100644 --- a/src/interop.ts +++ b/src/interop.ts @@ -4,12 +4,12 @@ import { FormControl as NgFormControl, FormGroup as NgFormGroup } from '@angular/forms'; -import {AbstractControl, FormArray, FormControl, FormGroup} from './model'; +import { AbstractControl, FormArray, FormControl, FormGroup, Controls} from './model'; export function toUntyped(control: FormControl): NgFormControl; -export function toUntyped(control: FormGroup): NgFormGroup; -export function toUntyped(control: FormArray): NgFormArray; -export function toUntyped(control: AbstractControl): NgAbstractControl; +export function toUntyped>(control: FormGroup): NgFormGroup; +export function toUntyped>(control: FormArray): NgFormArray; +export function toUntyped(control: AbstractControl): NgAbstractControl; export function toUntyped(control: any): any { return control as any; } diff --git a/src/model.ts b/src/model.ts index 78bfad4..07401c5 100644 --- a/src/model.ts +++ b/src/model.ts @@ -6,7 +6,7 @@ export type FormHooks = 'change' | 'blur' | 'submit'; export type ValidatorFn = NgValidatorFN | TypedValidatorFn export type AsyncValidatorFn = NgAsyncValidatorFn | TypedAsyncValidatorFn -export type Controls = { [P in keyof T]: AbstractControl }; +export type Controls = { [P in keyof T]: AbstractControl }; export type FormState = T | { value: T, disabled: boolean }; export type StateAndValidators = [FormState] | [FormState, ValidatorFn | ValidatorFn[]] | @@ -39,6 +39,17 @@ export interface AsyncValidator extends Validator { validate(c: AbstractControl): Promise | Observable; } +// helpers to support typing 'get' +type HasControls = FormArray | FormGroup; +type GetControls = T['controls']; +type GetControlKeys = keyof GetControls; +type GetControl> = GetControls[K]; + +/** + * Get the type of the data stored by a control. + */ +export type Static> = T['value']; + /** * This is the base class for `FormControl`, `FormGroup`, and `FormArray`. * @@ -52,7 +63,8 @@ export interface AsyncValidator extends Validator { * @see [Dynamic Forms Guide](/guide/dynamic-form) * */ -export abstract class AbstractControl { +// is the type of the value stored by the control, is the type of the inner controls, if any +export abstract class AbstractControl { /** * The parent control. @@ -376,7 +388,7 @@ export abstract class AbstractControl { /** * Patches the value of the control. Abstract method (implemented in sub-classes). */ - abstract patchValue(value: Partial, options?: Object): void; + abstract patchValue(value: T, options?: Object): void; /** * Resets the control. Abstract method (implemented in sub-classes). @@ -446,12 +458,16 @@ export abstract class AbstractControl { * * * `this.form.get(['person', 'name']);` */ - abstract get(path: K): AbstractControl | null; - abstract get(path: [K1]): AbstractControl | null; - abstract get(path: [K1, K2]): AbstractControl | null; - abstract get(path: [K1, K2, K3]): AbstractControl | null; - abstract get(path: [K1, K2, K3, K4]): AbstractControl | null; - abstract get(path: [K1, K2, K3, K4, K5]): AbstractControl | null; + get(path: [K1]): C[K1]; + get>(path: [K1, K2]): GetControl; + get, C3 extends GetControl & HasControls, K3 extends GetControlKeys>(path: [K1, K2, K3]): GetControl; + get, C3 extends GetControl & HasControls, K3 extends GetControlKeys, C4 extends GetControl & HasControls, K4 extends GetControlKeys>(path: [K1, K2, K3, K4]): GetControl; + get, C3 extends GetControl & HasControls, K3 extends GetControlKeys, C4 extends GetControl & HasControls, K4 extends GetControlKeys, C5 extends GetControl & HasControls, K5 extends GetControlKeys>(path: [K1, K2, K3, K4, K5]): GetControl; + + get(path: K1): C[K1]; + + get(path: any): any { + } /** * @description @@ -583,7 +599,8 @@ export abstract class AbstractControl { * * */ -export class FormArray extends AbstractControl { +export class FormArray = AbstractControl> extends AbstractControl>, Array> { + controls: Array; /** * Creates a new `FormArray` instance. @@ -598,14 +615,12 @@ export class FormArray extends AbstractControl { * @param asyncValidator A single async validator or array of async validator functions * */ - constructor(controls: AbstractControl[], - validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, - asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null) { + constructor(controls: Array, + validatorOrOpts?: ValidatorFn> | ValidatorFn>[] | AbstractControlOptions> | null, + asyncValidator?: AsyncValidatorFn> | AsyncValidatorFn>[] | null) { super() }; - controls: AbstractControl[]; - /** * Length of the control array. */ @@ -616,24 +631,16 @@ export class FormArray extends AbstractControl { * * @param index Index in the array to retrieve the control */ - at(index: number): AbstractControl { + at(index: number): T { return this.controls[index] }; - get(path: [number]): AbstractControl | null; - get(path: [number, K1]): AbstractControl | null; - get(path: [number, K1, K2]): AbstractControl | null; - get(path: [number, K1, K2, K3]): AbstractControl | null; - get(path: [number, K1, K2, K3, K4]): AbstractControl | null; - get(path: any): any { - } - /** * Insert a new `AbstractControl` at the end of the array. * * @param control Form control to be inserted */ - push(control: AbstractControl): void { + push(control: T): void { }; /** @@ -642,7 +649,7 @@ export class FormArray extends AbstractControl { * @param index Index in the array to insert the control * @param control Form control to be inserted */ - insert(index: number, control: AbstractControl): void { + insert(index: number, control: T): void { }; /** @@ -659,7 +666,7 @@ export class FormArray extends AbstractControl { * @param index Index in the array to replace the control * @param control The `AbstractControl` control to replace the existing control */ - setControl(index: number, control: AbstractControl): void { + setControl(index: number, control: T): void { }; /** @@ -696,7 +703,7 @@ export class FormArray extends AbstractControl { * The configuration options are passed to the {@link AbstractControl#updateValueAndValidity * updateValueAndValidity} method. */ - setValue(value: T[], options?: { + setValue(value: Static, options?: { onlySelf?: boolean; emitEvent?: boolean; }): void { @@ -735,7 +742,7 @@ export class FormArray extends AbstractControl { * The configuration options are passed to the {@link AbstractControl#updateValueAndValidity * updateValueAndValidity} method. */ - patchValue(value: T[], options?: { + patchValue(value: Static, options?: { onlySelf?: boolean; emitEvent?: boolean; }): void { @@ -786,7 +793,7 @@ export class FormArray extends AbstractControl { * The configuration options are passed to the {@link AbstractControl#updateValueAndValidity * updateValueAndValidity} method. */ - reset(value?: T[], options?: { + reset(value?: Static, options?: { onlySelf?: boolean; emitEvent?: boolean; }): void { @@ -798,7 +805,7 @@ export class FormArray extends AbstractControl { * Reports all values regardless of disabled status. * For enabled controls only, the `value` property is the best way to get the value of the array. */ - getRawValue(): T[] { + getRawValue(): Static { throw undefined }; @@ -907,8 +914,9 @@ export class FormArray extends AbstractControl { * }, { updateOn: 'blur' }); * ``` */ -export class FormGroup extends AbstractControl { - controls: Controls; +type ControlsStaticType = { [K in keyof T]: Static }; +export class FormGroup = Controls> extends AbstractControl, T> { + controls: T; /** * Creates a new `FormGroup` instance. @@ -923,9 +931,9 @@ export class FormGroup extends AbstractControl { * @param asyncValidator A single async validator or array of async validator functions * */ - constructor(controls: Controls, - validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, - asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null) { + constructor(controls: T, + validatorOrOpts?: ValidatorFn> | ValidatorFn>[] | AbstractControlOptions> | null, + asyncValidator?: AsyncValidatorFn> | AsyncValidatorFn>[] | null) { super() }; @@ -967,7 +975,7 @@ export class FormGroup extends AbstractControl { * @param name The control name to replace in the collection * @param control Provides the control for the given name */ - setControl(name: K, control: AbstractControl): void { + setControl(name: K, control: T[K]): void { }; /** @@ -1018,7 +1026,7 @@ export class FormGroup extends AbstractControl { * observables emit events with the latest status and value when the control value is updated. * When false, no events are emitted. */ - setValue(value: T, options?: { + setValue(value: Static, options?: { onlySelf?: boolean; emitEvent?: boolean; }): void { @@ -1057,7 +1065,7 @@ export class FormGroup extends AbstractControl { * The configuration options are passed to the {@link AbstractControl#updateValueAndValidity * updateValueAndValidity} method. */ - patchValue(value: Partial, options?: { + patchValue(value: Partial>, options?: { onlySelf?: boolean; emitEvent?: boolean; }): void { @@ -1120,7 +1128,7 @@ export class FormGroup extends AbstractControl { * console.log(this.form.get('first').status); // 'DISABLED' * ``` */ - reset(value?: Partial, options?: { + reset(value?: Partial>, options?: { onlySelf?: boolean; emitEvent?: boolean; }): void { @@ -1133,18 +1141,9 @@ export class FormGroup extends AbstractControl { * The `value` property is the best way to get the value of the group, because * it excludes disabled controls in the `FormGroup`. */ - getRawValue(): T { + getRawValue(): Static { throw undefined }; - - get(path: [K1]): AbstractControl; - get(path: [K1, K2]): AbstractControl | null; - get(path: [K1, K2, K3]): AbstractControl | null; - get(path: [K1, K2, K3, K4]): AbstractControl | null; - get(path: [K1, K2, K3, K4, K5]): AbstractControl | null; - get(path: K): AbstractControl; - get(path: any): any { - } } @@ -1243,8 +1242,8 @@ export class FormGroup extends AbstractControl { * console.log(control.status); // 'DISABLED' * */ -export class FormControl extends AbstractControl { - +export class FormControl extends AbstractControl { + /** * Creates a new `FormControl` instance. * @@ -1397,7 +1396,9 @@ export class FormBuilder { * * `asyncValidator`: A single async validator or array of async validator functions * */ - group(controlsConfig: ControlsConfig, options?: FormBuilderFormGroupOptions | AbstractControlOptions): FormGroup { + group>(controlsConfig: T, options?: FormBuilderFormGroupOptions> | AbstractControlOptions>): FormGroup; + group(controlsConfig: ControlsConfig, options?: FormBuilderFormGroupOptions | AbstractControlOptions): FormGroup>; + group(controlsConfig: any, options?: any): any { throw undefined }; @@ -1445,9 +1446,13 @@ export class FormBuilder { * @param asyncValidator A single async validator or array of async validator * functions. */ + array>(controlsConfig: Array, + validatorOrOpts?: ValidatorFn> | ValidatorFn>[] | null, + asyncValidator?: AsyncValidatorFn> | AsyncValidatorFn>[] | null): FormArray; array(controlsConfig: ControlConfig[], validatorOrOpts?: ValidatorFn | ValidatorFn[] | null, - asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormArray { + asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormArray>; + array(controlsConfig: any): any { throw undefined }; } diff --git a/test/compile-test-form-array.ts b/test/compile-test-form-array.ts index 81a3b4e..611cfa6 100644 --- a/test/compile-test-form-array.ts +++ b/test/compile-test-form-array.ts @@ -1,5 +1,5 @@ import {AbstractControl, FormArray, FormControl} from '../src/model'; import {Bar} from './interfaces'; -let barArray: FormArray = new FormArray([new FormControl({prop: ""}), new FormControl(undefined)]); +let barArray: FormArray> = new FormArray([new FormControl({prop: ""}), new FormControl(undefined)]); let barsControll: AbstractControl = barArray; diff --git a/test/compile-test-form-builder.ts b/test/compile-test-form-builder.ts index 60c7af5..f10fb5c 100644 --- a/test/compile-test-form-builder.ts +++ b/test/compile-test-form-builder.ts @@ -1,6 +1,6 @@ import {Validators} from '@angular/forms' -import {AbstractControl, FormArray, FormBuilder, FormControl, FormGroup} from '../src/model'; -import {Address, Bar, Foo, Hero} from './interfaces'; +import { AbstractControl, FormArray, FormBuilder, FormControl, FormGroup, Controls} from '../src/model'; +import {Address, Bar, Hero} from './interfaces'; const fb = new FormBuilder(); // FormControl @@ -9,29 +9,31 @@ let stringFormControl: FormControl; stringFormControl = fb.control(''); stringFormControl = fb.control({value: '', disabled: true}); + // FormArray -let stringFormArray: FormArray; +let stringFormArray: FormArray>; -stringFormArray = fb.array(['2345', 'ieiae', fb.control('uieie')]); -stringFormArray = fb.array([fb.control('uieie')]); +const stringFormArrayGeneric: FormArray> = fb.array(['2345', 'ieiae', fb.control('uieie')]); +stringFormArray = fb.array([fb.control('uieie')]); stringFormArray.setValue(['foo', 'bar']); -let barFormArray: FormArray; + +let barFormArray: FormArray>; barFormArray = fb.array([fb.control({prop: ''})]); barFormArray = fb.array([[{prop: ''}, Validators.required]]); barFormArray = fb.array([[{prop: ''}, [Validators.required]]]); barFormArray = fb.array([[{prop: ''}, [Validators.required, Validators.minLength(5)]]]); // FormGroup -const heroFormGroup: FormGroup = fb.group({ +const heroFormGroup = fb.group({ name: '', secretLairs: fb.array
([new Address(), {name: 'foo'}]), power: '', sidekick: '' }); -const namecontrol: AbstractControl | null = heroFormGroup.get('name'); -const lairs: AbstractControl | null = heroFormGroup.get('secretLairs'); +const namecontrol: AbstractControl = heroFormGroup.get('name'); +const lairs: AbstractControl = heroFormGroup.get('secretLairs'); -let barFormGroup: FormGroup; +let barFormGroup: FormGroup>; barFormGroup= fb.group({prop: ''}); barFormGroup = fb.group({prop: {value: '', disabled: true}}); barFormGroup = fb.group({prop: ['', Validators.required]}); diff --git a/test/compile-test-form-group.ts b/test/compile-test-form-group.ts index e6e2a46..3053026 100644 --- a/test/compile-test-form-group.ts +++ b/test/compile-test-form-group.ts @@ -1,27 +1,48 @@ -import {AbstractControl, FormArray, FormControl, FormGroup} from '../src/model'; -import {Address, Bar, Foo, Hero} from './interfaces'; +import { FormArray, FormControl, FormGroup, Static } from '../src/model'; +import { Bar } from './interfaces'; -let fooFormGroup: FormGroup = new FormGroup({ +let fooFormGroup = new FormGroup({ field: new FormControl(undefined), - array: new FormArray([]) + group: new FormGroup({ + prop: new FormControl('') + }), + array: new FormArray(new Array>()) }); -fooFormGroup = new FormGroup({ +fooFormGroup = new FormGroup({ field: new FormControl(undefined), + group: new FormGroup({ + prop: new FormControl('') + }), array: new FormArray([new FormControl({prop: ""}), new FormControl(undefined)]) }); -let barControl: AbstractControl = fooFormGroup.get(['field']); +let barControl: FormControl = fooFormGroup.get(['field']); barControl = fooFormGroup.get('field'); -// should be null as 'prop' is only a field in the 'field' FormControl and FormControl.get always returns null -let stringControl: AbstractControl | null = fooFormGroup.get(['field', 'prop']); -// should be null as 'prop' is only a field in the 'field' FormControl and FormControl.get always returns null -let s: string = fooFormGroup.get(['field', 'prop'])!.value; -// should be null as 'prop' is only a field in the 'field' FormControl and FormControl.get always returns null -stringControl = fooFormGroup.get('field')!.get('prop'); -const barArray: AbstractControl | null = fooFormGroup.get('array'); +barControl = fooFormGroup.controls.field; -const heroFormGroup: FormGroup = {} as FormGroup; -const namecontrol: AbstractControl | null = heroFormGroup.get('name'); -const lairsControl: AbstractControl | null = heroFormGroup.get('secretLairs'); +// This is correctly recognised as an error +// Unfortunately the error message isn't very helpful: Argument of type 'string[]' is not assignable to parameter of type '"field" | "array"' +// let stringControl = fooFormGroup.get(['field', 'prop']); + +// the group version works as expected +let stringControl: FormControl = fooFormGroup.get(['group', 'prop']); +stringControl = fooFormGroup.controls.group.controls.prop; + +const nullControl: null = fooFormGroup.get('field')!.get('prop'); + +let barArray: FormArray> = fooFormGroup.get('array'); +barArray = fooFormGroup.controls.array; + +// Typescript array dereference is never null so the "get" must also not return null to be consistent, even if sometimes incorrect. +barControl = fooFormGroup.get(['array', 0]); +barControl = fooFormGroup.controls.array.controls[0]; // partial Reset fooFormGroup.reset({field: new Bar()}); +type Foo = Static; +const foo: Foo = { + field: new Bar(), + group: { + prop: '' + }, + array: [new Bar()] +} diff --git a/test/tsconfig-test.json b/test/tsconfig-test.json index 6734878..89800b9 100644 --- a/test/tsconfig-test.json +++ b/test/tsconfig-test.json @@ -24,7 +24,7 @@ // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* Enable strict null checks. */ // "strictFunctionTypes": true, /* Enable strict checking of function types. */ - // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + "strictPropertyInitialization": false, /* Enable strict checking of property initialization in classes. */ // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */