├── .browserlistrc
├── .editorconfig
├── .gitignore
├── .idea
├── .gitignore
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── encodings.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── misc.xml
├── modules.xml
├── runConfigurations
│ ├── Build.xml
│ ├── Docs.xml
│ ├── E2E_Tests.xml
│ ├── Serve.xml
│ ├── Test_Coverage.xml
│ ├── Tests.xml
│ └── lib__dtslint.xml
├── s-ng-utils-platform.iml
├── vcs.xml
└── watcherTasks.xml
├── .prettierignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── angular.json
├── docs
└── typedoc
│ └── index.html
├── e2e
├── protractor-ci.conf.js
├── protractor.conf.js
├── src
│ ├── app.e2e-spec.ts
│ └── app.po.ts
└── tsconfig.json
├── karma.conf.js
├── package.json
├── projects
└── s-ng-utils
│ ├── karma.conf.js
│ ├── ng-package.json
│ ├── package.json
│ ├── src
│ ├── lib
│ │ ├── directive-superclass.spec.ts
│ │ ├── directive-superclass.ts
│ │ ├── form-control-superclass.spec.ts
│ │ ├── form-control-superclass.ts
│ │ ├── injectable-superclass.spec.ts
│ │ ├── injectable-superclass.ts
│ │ ├── wrapped-form-control-superclass.spec.ts
│ │ └── wrapped-form-control-superclass.ts
│ ├── public-api.ts
│ ├── test-helpers.ts
│ ├── test-polyfills.ts
│ ├── test.ts
│ ├── to-replace
│ │ ├── angular-context.ts
│ │ └── component-context.ts
│ └── typing-tests
│ │ ├── form-control-superclass.dts-spec.ts
│ │ ├── index.d.ts
│ │ └── tsconfig.json
│ ├── tsconfig.lib.json
│ ├── tsconfig.lib.prod.json
│ ├── tsconfig.spec.json
│ └── tslint.json
├── src
├── app
│ ├── app.component.css
│ ├── app.component.html
│ ├── app.component.ts
│ ├── app.module.ts
│ ├── error-displaying-input.component.ts
│ ├── location.component.ts
│ └── package.json
├── assets
│ └── .gitkeep
├── environments
│ ├── environment.prod.ts
│ └── environment.ts
├── favicon.ico
├── index.html
├── main.ts
├── polyfills.ts
├── styles.css
└── test.ts
├── standard-version-postbump.ts
├── tsconfig.app.json
├── tsconfig.base.json
├── tsconfig.json
├── tsconfig.spec.json
├── tslint.json
└── yarn.lock
/.browserlistrc:
--------------------------------------------------------------------------------
1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
2 | # For additional information regarding the format and rule options, please see:
3 | # https://github.com/browserslist/browserslist#queries
4 |
5 | # For the full list of supported browsers by the Angular framework, please see:
6 | # https://angular.io/guide/browser-support
7 |
8 | # You can see what browsers were selected by your queries by running:
9 | # npx browserslist
10 |
11 | last 1 Chrome version
12 | last 1 Firefox version
13 | last 2 Edge major versions
14 | last 2 Safari major version
15 | last 2 iOS major versions
16 | Firefox ESR
17 | not IE 9-11 # For IE 9-11 support, remove 'not'.
18 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see https://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.ts]
12 | quote_type = single
13 |
14 | [*.md]
15 | max_line_length = off
16 | trim_trailing_whitespace = false
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 | # Only exists if Bazel was run
8 | /bazel-out
9 |
10 | # dependencies
11 | /node_modules
12 |
13 | # profiling files
14 | chrome-profiler-events*.json
15 | speed-measure-plugin*.json
16 |
17 | # IDEs and editors
18 | .project
19 | .classpath
20 | .c9/
21 | *.launch
22 | .settings/
23 | *.sublime-workspace
24 |
25 | # IDE - VSCode
26 | .vscode/*
27 | !.vscode/settings.json
28 | !.vscode/tasks.json
29 | !.vscode/launch.json
30 | !.vscode/extensions.json
31 | .history/*
32 |
33 | # misc
34 | /.sass-cache
35 | /connect.lock
36 | /coverage
37 | /libpeerconnection.log
38 | npm-debug.log
39 | yarn-error.log
40 | testem.log
41 | /typings
42 |
43 | # System Files
44 | .DS_Store
45 | Thumbs.db
46 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /workspace.xml
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/Build.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/Docs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/E2E_Tests.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/Serve.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/Test_Coverage.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/Tests.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/lib__dtslint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/s-ng-utils-platform.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.idea/watcherTasks.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | coverage/
3 | dist/
4 | node_modules/
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | dist: trusty
2 | sudo: false
3 |
4 | language: node_js
5 | node_js:
6 | - "10"
7 |
8 | addons:
9 | apt:
10 | sources:
11 | - google-chrome
12 | packages:
13 | - google-chrome-stable
14 |
15 | cache:
16 | directories:
17 | - ./node_modules
18 |
19 | before_install:
20 | - npm install -g yarn
21 |
22 | install:
23 | - yarn
24 |
25 | script:
26 | - yarn test s-ng-utils --code-coverage --watch=false --progress=false --browsers=ChromeHeadlessCI
27 |
28 | - mkdir ~/.dts
29 | - yarn dtslint
30 |
31 | - yarn build s-ng-utils
32 | - yarn e2e --protractor-config=./e2e/protractor-ci.conf.js
33 |
34 | after_success:
35 | - cat coverage/s-ng-utils/lcov.info | node_modules/coveralls/bin/coveralls.js
36 |
37 | notifications:
38 | email: false
39 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 | ## [3.0.0](https://github.com/simontonsoftware/s-ng-utils/compare/v2.0.0...v3.0.0) (2020-06-27)
6 |
7 | ### ⚠ BREAKING CHANGES
8 |
9 | - requires micro-dash 8.0
10 | - requires s-js-utils 7.0
11 | - requires s-rxjs-utils 4.0
12 | - requires angular 10
13 | - requires typescript 3.9
14 |
15 | ### Features
16 |
17 | - upgrade angular ([831fcb5](https://github.com/simontonsoftware/s-ng-utils/commit/831fcb5207cab03ba919fc19760293f7a9ff4af4))
18 |
19 | * upgrade other dependencies ([a9cd092](https://github.com/simontonsoftware/s-ng-utils/commit/a9cd092ad84fbf53e93771506c3b6bb0614112b0))
20 |
21 | ## [2.0.0](https://github.com/simontonsoftware/s-ng-utils/compare/v1.0.0...v2.0.0) (2020-02-08)
22 |
23 | ### ⚠ BREAKING CHANGES
24 |
25 | - requires micro-dash 7
26 | - requires s-rxjs-utils 3
27 | - requires s-js-utils
28 | - requires Typescript 3.7
29 | - requires RxJS 6.5
30 | - removed `DirectiveSuperclass.lastChangedKeys$` in favor of `.inputChanges$`
31 |
32 | ### Features
33 |
34 | - upgrade to Angular 9 ([d5537e8](https://github.com/simontonsoftware/s-ng-utils/commit/d5537e81cc231b3f7971560166211226b25aa399))
35 |
36 | ### Bug Fixes
37 |
38 | - `DirectiveSuperclass.getInput$()` now always starts with a value ([ae6ffb2](https://github.com/simontonsoftware/s-ng-utils/commit/ae6ffb2c42f2af041091f23d3093645d1358fa04))
39 |
40 | * upgrade buildchain ([fce37eb](https://github.com/simontonsoftware/s-ng-utils/commit/fce37ebd8e9bd362f1ac085a8cca2f9509d20a6e))
41 | * upgrade dependencies ([00b3811](https://github.com/simontonsoftware/s-ng-utils/commit/00b38110ffd829646a4c50c1e5c2dd8e739b9f2f))
42 |
43 | ## [1.0.0](https://github.com/simontonsoftware/s-ng-utils/compare/v0.4.1...v1.0.0) (2019-07-17)
44 |
45 | ### Features
46 |
47 | - add `AutoDestroyable.destroy$` ([d6e798f](https://github.com/simontonsoftware/s-ng-utils/commit/d6e798f))
48 |
49 | ### [0.4.1](https://github.com/simontonsoftware/s-ng-utils/compare/v0.4.0...v0.4.1) (2019-06-26)
50 |
51 | ### Bug Fixes
52 |
53 | - remove accidental dependency on `s-ng-dev-utils` ([6da82c3](https://github.com/simontonsoftware/s-ng-utils/commit/6da82c3))
54 |
55 | ## [0.4.0](https://github.com/simontonsoftware/s-ng-utils/compare/v0.3.1...v0.4.0) (2019-05-30)
56 |
57 | ### Features
58 |
59 | - Upgrade dependencies ([b0de2a1](https://github.com/simontonsoftware/s-ng-utils/commit/b0de2a1))
60 |
61 | ### BREAKING CHANGES
62 |
63 | - Uses Typescript 3.4 (up from 3.1)
64 | - Requires Rxjs 6.4 (up from 6.3)
65 | - Requires Angular 8 (up from 7)
66 | - Requires micro-dash 6 (up from 5)
67 | - Requires s-rxjs-utils 2 (up from 1)
68 |
69 |
70 |
71 | ## [0.3.1](https://github.com/simontonsoftware/s-ng-utils/compare/v0.3.0...v0.3.1) (2019-01-09)
72 |
73 | ### Bug Fixes
74 |
75 | - allow `DirectiveSuperclass.getInput$()` to be used in templates ([8e67212](https://github.com/simontonsoftware/s-ng-utils/commit/8e67212))
76 | - expose `DirectiveSuperclass` in the public api ([b00f390](https://github.com/simontonsoftware/s-ng-utils/commit/b00f390))
77 |
78 |
79 |
80 | # [0.3.0](https://github.com/simontonsoftware/s-ng-utils/compare/v0.2.0...v0.3.0) (2018-12-15)
81 |
82 | ### Features
83 |
84 | - `FormControlSuperclass` now extends `DirectiveSuperclass` ([f23a535](https://github.com/simontonsoftware/s-ng-utils/commit/f23a535))
85 | - add `DirectiveSuperclass` ([b2d0213](https://github.com/simontonsoftware/s-ng-utils/commit/b2d0213))
86 |
87 |
88 |
89 | # [0.2.0](https://github.com/simontonsoftware/s-ng-utils/compare/v0.1.0...v0.2.0) (2018-11-12)
90 |
91 | ### Bug Fixes
92 |
93 | - **WrappedFormControlSuperclass:** it is no longer necessary to call `onTouched()` ([53f9c24](https://github.com/simontonsoftware/s-ng-utils/commit/53f9c24))
94 |
95 | ### Chores
96 |
97 | - upgrade dependencies ([27aad30](https://github.com/simontonsoftware/s-ng-utils/commit/27aad30))
98 |
99 | ### BREAKING CHANGES
100 |
101 | - uses Typescript 3 (up from Typescript 2)
102 | - requires Angular 7 (up from Angular 6)
103 | - requires s-rxjs-utils 1 (up from s-rxjs-utils 0.2)
104 |
105 |
106 |
107 | # 0.1.0 (2018-09-04)
108 |
109 | ### Features
110 |
111 | - add `AutoDestroyable` ([16cb7d8](https://github.com/simontonsoftware/s-ng-utils/commit/16cb7d8))
112 | - add `FormControlSuperclass` and `provideValueAccessor()` ([57314f2](https://github.com/simontonsoftware/s-ng-utils/commit/57314f2))
113 | - add `WrappedFormControlSuperclass` ([de118dd](https://github.com/simontonsoftware/s-ng-utils/commit/de118dd))
114 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Moved
2 |
3 | This is an old version. The library moved to a new repo [here](https://github.com/simontonsoftware/s-libs/tree/master/projects/ng-core), as in now published under the name `@s-libs/ng-core` in NPM.
4 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "s-ng-utils-platform": {
7 | "projectType": "application",
8 | "schematics": {},
9 | "root": "",
10 | "sourceRoot": "src",
11 | "prefix": "app",
12 | "architect": {
13 | "build": {
14 | "builder": "@angular-devkit/build-angular:browser",
15 | "options": {
16 | "outputPath": "dist/s-ng-utils-platform",
17 | "index": "src/index.html",
18 | "main": "src/main.ts",
19 | "polyfills": "src/polyfills.ts",
20 | "tsConfig": "tsconfig.app.json",
21 | "aot": true,
22 | "assets": ["src/favicon.ico", "src/assets"],
23 | "styles": ["src/styles.css"],
24 | "scripts": []
25 | },
26 | "configurations": {
27 | "production": {
28 | "fileReplacements": [
29 | {
30 | "replace": "src/environments/environment.ts",
31 | "with": "src/environments/environment.prod.ts"
32 | }
33 | ],
34 | "optimization": true,
35 | "outputHashing": "all",
36 | "sourceMap": false,
37 | "extractCss": true,
38 | "namedChunks": false,
39 | "extractLicenses": true,
40 | "vendorChunk": false,
41 | "buildOptimizer": true,
42 | "budgets": [
43 | {
44 | "type": "initial",
45 | "maximumWarning": "500kb",
46 | "maximumError": "1mb"
47 | },
48 | {
49 | "type": "anyComponentStyle",
50 | "maximumWarning": "2kb",
51 | "maximumError": "4kb"
52 | }
53 | ]
54 | }
55 | }
56 | },
57 | "serve": {
58 | "builder": "@angular-devkit/build-angular:dev-server",
59 | "options": {
60 | "browserTarget": "s-ng-utils-platform:build"
61 | },
62 | "configurations": {
63 | "production": {
64 | "browserTarget": "s-ng-utils-platform:build:production"
65 | }
66 | }
67 | },
68 | "extract-i18n": {
69 | "builder": "@angular-devkit/build-angular:extract-i18n",
70 | "options": {
71 | "browserTarget": "s-ng-utils-platform:build"
72 | }
73 | },
74 | "test": {
75 | "builder": "@angular-devkit/build-angular:karma",
76 | "options": {
77 | "main": "src/test.ts",
78 | "polyfills": "src/polyfills.ts",
79 | "tsConfig": "tsconfig.spec.json",
80 | "karmaConfig": "karma.conf.js",
81 | "assets": ["src/favicon.ico", "src/assets"],
82 | "styles": ["src/styles.css"],
83 | "scripts": []
84 | }
85 | },
86 | "lint": {
87 | "builder": "@angular-devkit/build-angular:tslint",
88 | "options": {
89 | "tsConfig": [
90 | "tsconfig.app.json",
91 | "tsconfig.spec.json",
92 | "e2e/tsconfig.json"
93 | ],
94 | "exclude": ["**/node_modules/**"]
95 | }
96 | },
97 | "e2e": {
98 | "builder": "@angular-devkit/build-angular:protractor",
99 | "options": {
100 | "protractorConfig": "e2e/protractor.conf.js",
101 | "devServerTarget": "s-ng-utils-platform:serve"
102 | },
103 | "configurations": {
104 | "production": {
105 | "devServerTarget": "s-ng-utils-platform:serve:production"
106 | }
107 | }
108 | }
109 | }
110 | },
111 | "s-ng-utils": {
112 | "projectType": "library",
113 | "root": "projects/s-ng-utils",
114 | "sourceRoot": "projects/s-ng-utils/src",
115 | "prefix": "s",
116 | "architect": {
117 | "build": {
118 | "builder": "@angular-devkit/build-ng-packagr:build",
119 | "options": {
120 | "tsConfig": "projects/s-ng-utils/tsconfig.lib.json",
121 | "project": "projects/s-ng-utils/ng-package.json"
122 | },
123 | "configurations": {
124 | "production": {
125 | "tsConfig": "projects/s-ng-utils/tsconfig.lib.prod.json"
126 | }
127 | }
128 | },
129 | "test": {
130 | "builder": "@angular-devkit/build-angular:karma",
131 | "options": {
132 | "main": "projects/s-ng-utils/src/test.ts",
133 | "tsConfig": "projects/s-ng-utils/tsconfig.spec.json",
134 | "karmaConfig": "projects/s-ng-utils/karma.conf.js",
135 | "sourceMap": true,
136 | "codeCoverageExclude": ["**/test.ts", "**/test-helpers.ts"],
137 | "polyfills": "projects/s-ng-utils/src/test-polyfills.ts"
138 | }
139 | },
140 | "lint": {
141 | "builder": "@angular-devkit/build-angular:tslint",
142 | "options": {
143 | "tsConfig": [
144 | "projects/s-ng-utils/tsconfig.lib.json",
145 | "projects/s-ng-utils/tsconfig.spec.json"
146 | ],
147 | "exclude": ["**/node_modules/**"]
148 | }
149 | }
150 | }
151 | }
152 | },
153 | "defaultProject": "s-ng-utils-platform",
154 | "cli": {
155 | "analytics": "4a7df08f-32b4-480f-86f2-edc7e4a48553"
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/docs/typedoc/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | s-ng-utils
6 |
7 |
8 | s-ng-utils
9 |
10 | This is the location for an old version of s-ng-utils
. The
11 | library is now published under the name
12 | @s-libs/ng-core
.
17 |
18 |
19 | It moved to a new repo
20 | here, with API docs
24 | here.
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/e2e/protractor-ci.conf.js:
--------------------------------------------------------------------------------
1 | const config = require("./protractor.conf").config;
2 |
3 | config.capabilities = {
4 | browserName: "chrome",
5 | chromeOptions: {
6 | args: ["--headless", "--no-sandbox", "--disable-gpu"],
7 | },
8 | };
9 |
10 | exports.config = config;
11 |
--------------------------------------------------------------------------------
/e2e/protractor.conf.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | // Protractor configuration file, see link for more information
3 | // https://github.com/angular/protractor/blob/master/lib/config.ts
4 |
5 | const { SpecReporter, StacktraceOption } = require("jasmine-spec-reporter");
6 |
7 | /**
8 | * @type { import("protractor").Config }
9 | */
10 | exports.config = {
11 | allScriptsTimeout: 11000,
12 | specs: ["./src/**/*.e2e-spec.ts"],
13 | capabilities: {
14 | browserName: "chrome",
15 | },
16 | directConnect: true,
17 | baseUrl: "http://localhost:4200/",
18 | framework: "jasmine",
19 | jasmineNodeOpts: {
20 | showColors: true,
21 | defaultTimeoutInterval: 30000,
22 | print: function() {},
23 | },
24 | onPrepare() {
25 | require("ts-node").register({
26 | project: require("path").join(__dirname, "./tsconfig.json"),
27 | });
28 | jasmine.getEnv().addReporter(
29 | new SpecReporter({
30 | spec: { displayStacktrace: StacktraceOption.PRETTY },
31 | }),
32 | );
33 | },
34 | };
35 |
--------------------------------------------------------------------------------
/e2e/src/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { browser, logging } from "protractor";
2 | import { AppPage } from "./app.po";
3 |
4 | describe("workspace-project App", () => {
5 | let page: AppPage;
6 |
7 | beforeEach(() => {
8 | page = new AppPage();
9 | });
10 |
11 | it("should display welcome message", () => {
12 | page.navigateTo();
13 | expect(page.getTitleText()).toEqual("Welcome to s-ng-utils-platform!");
14 | });
15 |
16 | afterEach(async () => {
17 | // Assert that there are no errors emitted from the browser
18 | const logs = await browser
19 | .manage()
20 | .logs()
21 | .get(logging.Type.BROWSER);
22 | expect(logs).not.toContain(
23 | jasmine.objectContaining({
24 | level: logging.Level.SEVERE,
25 | } as logging.Entry),
26 | );
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/e2e/src/app.po.ts:
--------------------------------------------------------------------------------
1 | import { browser, by, element } from "protractor";
2 |
3 | export class AppPage {
4 | navigateTo(): Promise {
5 | return browser.get(browser.baseUrl) as Promise;
6 | }
7 |
8 | getTitleText(): Promise {
9 | return element(by.css("app-root h1")).getText() as Promise;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/e2e/tsconfig.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "../tsconfig.base.json",
4 | "compilerOptions": {
5 | "outDir": "../out-tsc/e2e",
6 | "module": "commonjs",
7 | "target": "es2018",
8 | "types": ["jasmine", "jasminewd2", "node"]
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/1.0/config/configuration-file.html
3 |
4 | module.exports = function(config) {
5 | config.set({
6 | basePath: "",
7 | frameworks: ["jasmine", "@angular-devkit/build-angular"],
8 | plugins: [
9 | require("karma-jasmine"),
10 | require("karma-chrome-launcher"),
11 | require("karma-jasmine-html-reporter"),
12 | require("karma-coverage-istanbul-reporter"),
13 | require("@angular-devkit/build-angular/plugins/karma"),
14 | ],
15 | client: {
16 | clearContext: false, // leave Jasmine Spec Runner output visible in browser
17 | },
18 | coverageIstanbulReporter: {
19 | dir: require("path").join(__dirname, "./coverage/s-ng-utils-platform"),
20 | reports: ["html", "lcovonly", "text-summary"],
21 | fixWebpackSourcePaths: true,
22 | },
23 | reporters: ["progress", "kjhtml"],
24 | port: 9876,
25 | colors: true,
26 | logLevel: config.LOG_INFO,
27 | autoWatch: true,
28 | browsers: ["Chrome"],
29 | singleRun: false,
30 | restartOnFileChange: true,
31 | });
32 | };
33 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "s-ng-utils-platform",
3 | "version": "3.0.0",
4 | "scripts": {
5 | "ng": "ng",
6 | "start": "ng serve",
7 | "build": "ng build",
8 | "test": "ng test",
9 | "lint": "ng lint",
10 | "e2e": "ng e2e",
11 | "dtslint": "dtslint --expectOnly --localTs node_modules/typescript/lib projects/s-ng-utils/src/typing-tests",
12 | "docs": "cd docs && typedoc ../projects/s-ng-utils/src/lib",
13 | "prettier-all": "prettier --write \"**/*.{html,js,json,md,scss,ts,yml}\"",
14 | "release": "yarn docs && git add docs/* && standard-version --commit-all && yarn build --prod s-ng-utils"
15 | },
16 | "private": true,
17 | "dependencies": {
18 | "@angular/animations": "~10.0.0",
19 | "@angular/common": "~10.0.0",
20 | "@angular/compiler": "~10.0.0",
21 | "@angular/core": "~10.0.0",
22 | "@angular/forms": "~10.0.0",
23 | "@angular/platform-browser": "~10.0.0",
24 | "@angular/platform-browser-dynamic": "~10.0.0",
25 | "@angular/router": "~10.0.0",
26 | "micro-dash": "^8.0.0",
27 | "rxjs": "~6.5.5",
28 | "s-js-utils": "^7.0.0",
29 | "s-rxjs-utils": "^4.0.0",
30 | "tslib": "^2.0.0",
31 | "zone.js": "~0.10.3"
32 | },
33 | "devDependencies": {
34 | "@angular-devkit/build-angular": "~0.1000.0",
35 | "@angular-devkit/build-ng-packagr": "~0.1000.0",
36 | "@angular/cli": "~10.0.0",
37 | "@angular/compiler-cli": "~10.0.0",
38 | "@types/jasmine": "~3.5.0",
39 | "@types/jasminewd2": "~2.0.3",
40 | "@types/node": "^12.11.1",
41 | "@types/prettier": "^2.0.1",
42 | "codelyzer": "^6.0.0-next.1",
43 | "coveralls": "^3.0.6",
44 | "dtslint": "^3.6.12",
45 | "jasmine-core": "~3.5.0",
46 | "jasmine-spec-reporter": "~5.0.0",
47 | "karma": "~5.0.0",
48 | "karma-chrome-launcher": "~3.1.0",
49 | "karma-coverage-istanbul-reporter": "~3.0.2",
50 | "karma-jasmine": "~3.3.0",
51 | "karma-jasmine-html-reporter": "^1.5.0",
52 | "ng-packagr": "^10.0.0",
53 | "prettier": "^2.0.5",
54 | "protractor": "~7.0.0",
55 | "s-ng-dev-utils": "^2.0.0",
56 | "standard-version": "^8.0.0",
57 | "ts-node": "~8.3.0",
58 | "tslint": "~6.1.0",
59 | "typedoc": "^0.17.7",
60 | "typescript": "~3.9.5"
61 | },
62 | "prettier": {
63 | "trailingComma": "all",
64 | "arrowParens": "always"
65 | },
66 | "standard-version": {
67 | "scripts": {
68 | "postbump": "ts-node -O \"{\\\"module\\\": \\\"commonjs\\\"}\" standard-version-postbump",
69 | "postchangelog": "prettier --write CHANGELOG.md",
70 | "precommit": "git add projects/s-ng-utils/package.json"
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/projects/s-ng-utils/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/1.0/config/configuration-file.html
3 |
4 | module.exports = function(config) {
5 | config.set({
6 | basePath: "",
7 | frameworks: ["jasmine", "@angular-devkit/build-angular"],
8 | plugins: [
9 | require("karma-jasmine"),
10 | require("karma-chrome-launcher"),
11 | require("karma-jasmine-html-reporter"),
12 | require("karma-coverage-istanbul-reporter"),
13 | require("@angular-devkit/build-angular/plugins/karma"),
14 | ],
15 | client: {
16 | clearContext: false, // leave Jasmine Spec Runner output visible in browser
17 | },
18 | coverageIstanbulReporter: {
19 | dir: require("path").join(__dirname, "../../coverage/s-ng-utils"),
20 | reports: ["html", "lcovonly", "text-summary"],
21 | fixWebpackSourcePaths: true,
22 | },
23 | reporters: ["progress", "kjhtml"],
24 | port: 9876,
25 | colors: true,
26 | logLevel: config.LOG_INFO,
27 | autoWatch: true,
28 | browsers: ["Chrome"],
29 | singleRun: false,
30 | restartOnFileChange: true,
31 | customLaunchers: {
32 | ChromeHeadlessCI: {
33 | base: "ChromeHeadless",
34 | flags: ["--no-sandbox", "--disable-gpu"],
35 | },
36 | },
37 | });
38 | };
39 |
--------------------------------------------------------------------------------
/projects/s-ng-utils/ng-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3 | "dest": "../../dist/s-ng-utils",
4 | "lib": {
5 | "entryFile": "src/public-api.ts"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/projects/s-ng-utils/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "s-ng-utils",
3 | "version": "3.0.0",
4 | "author": "Simonton Software",
5 | "repository": "simontonsoftware/s-ng-utils",
6 | "license": "MIT",
7 | "peerDependencies": {
8 | "@angular/common": "^10.0.0",
9 | "@angular/core": "^10.0.0",
10 | "micro-dash": "^8.0.0",
11 | "s-js-utils": "^7.0.0",
12 | "s-rxjs-utils": "^4.0.0"
13 | },
14 | "dependencies": { "tslib": "^2.0.0" }
15 | }
16 |
--------------------------------------------------------------------------------
/projects/s-ng-utils/src/lib/directive-superclass.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ChangeDetectionStrategy,
3 | Component,
4 | Inject,
5 | Injector,
6 | Input,
7 | } from '@angular/core';
8 | import { TestBed } from '@angular/core/testing';
9 | import { By } from '@angular/platform-browser';
10 | import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
11 | import { map } from 'rxjs/operators';
12 | import { expectSingleCallAndReset } from 's-ng-dev-utils';
13 | import { click, find, findButton } from '../test-helpers';
14 | import { ComponentContext } from '../to-replace/component-context';
15 | import { DirectiveSuperclass } from './directive-superclass';
16 |
17 | @Component({
18 | template: `
19 |
20 |
21 |
24 |
25 |
30 | `,
31 | })
32 | class TestComponent {
33 | color$ = new BehaviorSubject('Green');
34 | prefix?: string;
35 | prefix2?: string;
36 | hide = false;
37 |
38 | toggle(key: 'prefix' | 'prefix2', value: string): void {
39 | this[key] = this[key] ? undefined : value;
40 | }
41 | }
42 |
43 | @Component({
44 | selector: 's-color-text',
45 | template: ` {{ color }} `,
46 | changeDetection: ChangeDetectionStrategy.OnPush,
47 | })
48 | class ColorTextComponent extends DirectiveSuperclass {
49 | @Input() prefix?: string;
50 | @Input() prefix2?: string;
51 | @Input() unspecified?: string;
52 | color!: string;
53 |
54 | constructor(
55 | @Inject('color$') color$: Observable,
56 | injector: Injector,
57 | ) {
58 | super(injector);
59 | this.bindToInstance(
60 | 'color',
61 | combineLatest([
62 | this.getInput$('prefix'),
63 | this.getInput$('prefix2'),
64 | color$,
65 | ]).pipe(map((parts) => parts.filter((p) => p).join(''))),
66 | );
67 | }
68 | }
69 |
70 | class TestComponentContext extends ComponentContext {
71 | color$ = new BehaviorSubject('Grey');
72 | protected componentType = TestComponent;
73 |
74 | constructor() {
75 | super({ declarations: [ColorTextComponent, TestComponent] });
76 | TestBed.overrideProvider('color$', { useValue: this.color$ });
77 | }
78 | }
79 |
80 | describe('DirectiveSuperclass', () => {
81 | let ctx: TestComponentContext;
82 | beforeEach(() => {
83 | ctx = new TestComponentContext();
84 | });
85 |
86 | function colorTextComponent(): ColorTextComponent {
87 | return ctx.fixture.debugElement.query(By.directive(ColorTextComponent))
88 | .componentInstance;
89 | }
90 |
91 | function darkButton(): HTMLButtonElement {
92 | return findButton(ctx.fixture, 'Dark');
93 | }
94 |
95 | function slateButton(): HTMLButtonElement {
96 | return findButton(ctx.fixture, 'Slate');
97 | }
98 |
99 | function bothButton(): HTMLButtonElement {
100 | return findButton(ctx.fixture, 'Both');
101 | }
102 |
103 | function hideButton(): HTMLButtonElement {
104 | return findButton(ctx.fixture, 'Hide');
105 | }
106 |
107 | function colorSpan(): HTMLSpanElement {
108 | return find(ctx.fixture, 's-color-text span');
109 | }
110 |
111 | /////////
112 |
113 | describe('.inputChanges$', () => {
114 | it('emits the keys that change', () => {
115 | ctx.run(() => {
116 | const stub = jasmine.createSpy();
117 | colorTextComponent().inputChanges$.subscribe(stub);
118 | expect(stub).not.toHaveBeenCalled();
119 |
120 | click(darkButton());
121 | expectSingleCallAndReset(stub, new Set(['prefix']));
122 |
123 | click(slateButton());
124 | expectSingleCallAndReset(stub, new Set(['prefix2']));
125 |
126 | click(bothButton());
127 | expectSingleCallAndReset(stub, new Set(['prefix', 'prefix2']));
128 | });
129 | });
130 | });
131 |
132 | describe('.getInput$()', () => {
133 | it('emits the value of an input when it changes', () => {
134 | ctx.run(() => {
135 | const stub = jasmine.createSpy();
136 | colorTextComponent().getInput$('prefix2').subscribe(stub);
137 | expect(stub).toHaveBeenCalledTimes(1);
138 | expect(stub.calls.argsFor(0)).toEqual([undefined]);
139 |
140 | click(darkButton());
141 | expect(stub).toHaveBeenCalledTimes(1);
142 |
143 | click(slateButton());
144 | expect(stub).toHaveBeenCalledTimes(2);
145 | expect(stub.calls.argsFor(1)).toEqual(['Slate']);
146 |
147 | click(bothButton());
148 | expect(stub).toHaveBeenCalledTimes(3);
149 | expect(stub.calls.argsFor(2)).toEqual([undefined]);
150 | });
151 | });
152 |
153 | // https://github.com/simontonsoftware/s-ng-utils/issues/10
154 | it('emits `undefined` for unspecified inputs', () => {
155 | ctx.run(() => {
156 | const stub = jasmine.createSpy();
157 | colorTextComponent().getInput$('unspecified').subscribe(stub);
158 | expectSingleCallAndReset(stub, undefined);
159 | });
160 | });
161 | });
162 |
163 | describe('.bindToInstance()', () => {
164 | it('sets the local value', () => {
165 | ctx.run(() => {
166 | expect(colorSpan().innerText).toBe('Grey');
167 | expect(colorSpan().style.backgroundColor).toBe('grey');
168 |
169 | click(darkButton());
170 | expect(colorSpan().innerText).toBe('DarkGrey');
171 | expect(colorSpan().style.backgroundColor).toBe('darkgrey');
172 |
173 | click(slateButton());
174 | expect(colorSpan().innerText).toBe('DarkSlateGrey');
175 | expect(colorSpan().style.backgroundColor).toBe('darkslategrey');
176 |
177 | click(bothButton());
178 | expect(colorSpan().innerText).toBe('Grey');
179 | expect(colorSpan().style.backgroundColor).toBe('grey');
180 | });
181 | });
182 |
183 | it('triggers change detection', () => {
184 | ctx.run(() => {
185 | ctx.color$.next('Blue');
186 | ctx.fixture.detectChanges();
187 | expect(colorSpan().innerText).toBe('Blue');
188 | expect(colorSpan().style.backgroundColor).toBe('blue');
189 |
190 | click(bothButton());
191 | expect(colorSpan().innerText).toBe('DarkSlateBlue');
192 | expect(colorSpan().style.backgroundColor).toBe('darkslateblue');
193 | });
194 | });
195 | });
196 |
197 | describe('.subscribeTo()', () => {
198 | it('cleans up subscriptions', () => {
199 | ctx.run(() => {
200 | expect(ctx.color$.observers.length).toBe(1);
201 |
202 | click(hideButton());
203 | expect(ctx.color$.observers.length).toBe(0);
204 | });
205 | });
206 | });
207 | });
208 |
--------------------------------------------------------------------------------
/projects/s-ng-utils/src/lib/directive-superclass.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ChangeDetectorRef,
3 | Injector,
4 | OnChanges,
5 | SimpleChanges,
6 | } from '@angular/core';
7 | import { Observable, Subject } from 'rxjs';
8 | import { filter, map, startWith } from 'rxjs/operators';
9 | import { InjectableSuperclass } from './injectable-superclass';
10 |
11 | /**
12 | * Extend this when creating a directive (including a component, which is a kind of directive) to gain access to the helpers demonstrated below. **Warning:** You _must_ include a constructor in your subclass.
13 | *
14 | * ```ts
15 | * @Component({
16 | * selector: "s-color-text",
17 | * template: `
18 | * {{ color }}
19 | * `,
20 | * // note that `bindToInstance()` works even with OnPush change detection
21 | * changeDetection: ChangeDetectionStrategy.OnPush,
22 | * })
23 | * class ColorTextComponent extends DirectiveSuperclass {
24 | * @Input() prefix?: string;
25 | * @Input() prefix2?: string;
26 | * color!: string;
27 | *
28 | * // Even if you don't need extra arguments injector, you must still include a constructor. It is required for angular to provide `Injector`.
29 | * constructor(
30 | * @Inject("color$") color$: Observable,
31 | * injector: Injector,
32 | * ) {
33 | * super(injector);
34 | *
35 | * // combine everything to calculate `color` and keep it up to date
36 | * this.bindToInstance(
37 | * "color",
38 | * combineLatest(
39 | * this.getInput$("prefix"),
40 | * this.getInput$("prefix2"),
41 | * color$,
42 | * ).pipe(map((parts) => parts.filter((p) => p).join(""))),
43 | * );
44 | * }
45 | * }
46 | * ```
47 | */
48 | export abstract class DirectiveSuperclass extends InjectableSuperclass
49 | implements OnChanges {
50 | /**
51 | * Emits the set of `@Input()` property names that change during each call to `ngOnChanges()`.
52 | */
53 | inputChanges$ = new Subject>();
54 |
55 | protected changeDetectorRef: ChangeDetectorRef;
56 |
57 | constructor(injector: Injector) {
58 | super();
59 | this.changeDetectorRef = injector.get(ChangeDetectorRef);
60 | }
61 |
62 | ngOnChanges(changes: SimpleChanges): void {
63 | this.inputChanges$.next(
64 | new Set(Object.getOwnPropertyNames(changes) as Array),
65 | );
66 | }
67 |
68 | /**
69 | * @return an observable of the values for one of this directive's `@Input()` properties
70 | */
71 | getInput$(key: K): Observable {
72 | return this.inputChanges$.pipe(
73 | filter((keys) => keys.has(key)),
74 | startWith(undefined),
75 | map(() => this[key]),
76 | );
77 | }
78 |
79 | /**
80 | * Binds an observable to one of this directive's instance variables. When the observable emits the instance variable will be updated, and change detection will be triggered to propagate any changes. Use this an an alternative to repeating `| async` multiple times in your template.
81 | */
82 | bindToInstance(
83 | key: K,
84 | value$: Observable,
85 | ): void {
86 | this.subscribeTo(value$, (value) => {
87 | this[key] = value;
88 | this.changeDetectorRef.markForCheck();
89 | });
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/projects/s-ng-utils/src/lib/form-control-superclass.spec.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component, Injector } from '@angular/core';
2 | import { FormsModule } from '@angular/forms';
3 | import { By } from '@angular/platform-browser';
4 | import { click, find, findButton } from '../test-helpers';
5 | import { ComponentContext } from '../to-replace/component-context';
6 | import { DirectiveSuperclass } from './directive-superclass';
7 | import {
8 | FormControlSuperclass,
9 | provideValueAccessor,
10 | } from './form-control-superclass';
11 | import { InjectableSuperclass } from './injectable-superclass';
12 |
13 | @Component({
14 | template: `
15 |
20 | Touched!
21 |
22 | `,
23 | })
24 | class TestComponent {
25 | value = 0;
26 | shouldDisable = false;
27 | }
28 |
29 | @Component({
30 | selector: `s-counter-component`,
31 | template: `
32 |
33 | `,
34 | providers: [provideValueAccessor(CounterComponent)],
35 | changeDetection: ChangeDetectionStrategy.OnPush,
36 | })
37 | class CounterComponent extends FormControlSuperclass {
38 | counter = 0;
39 |
40 | constructor(injector: Injector) {
41 | super(injector);
42 | }
43 |
44 | handleIncomingValue(value: number): void {
45 | this.counter = value;
46 | }
47 |
48 | increment(): void {
49 | this.emitOutgoingValue(++this.counter);
50 | this.onTouched();
51 | }
52 | }
53 |
54 | class TestComponentContext extends ComponentContext {
55 | protected componentType = TestComponent;
56 |
57 | constructor() {
58 | super({
59 | imports: [FormsModule],
60 | declarations: [CounterComponent, TestComponent],
61 | });
62 | }
63 | }
64 |
65 | describe('FormControlSuperclass', () => {
66 | let ctx: TestComponentContext;
67 | beforeEach(() => {
68 | ctx = new TestComponentContext();
69 | });
70 |
71 | function incrementButton(): HTMLButtonElement {
72 | return find(ctx.fixture, 's-counter-component button');
73 | }
74 |
75 | function toggleDisabledButton(): HTMLButtonElement {
76 | return findButton(ctx.fixture, 'Toggle Disabled');
77 | }
78 |
79 | ///////
80 |
81 | it('provides help for 2-way binding', () => {
82 | ctx.run({ input: { value: 15 } }, () => {
83 | expect(ctx.fixture.componentInstance.value).toBe(15);
84 | expect(ctx.fixture.nativeElement.innerText).toContain('15');
85 |
86 | click(incrementButton());
87 | expect(ctx.fixture.componentInstance.value).toBe(16);
88 | expect(ctx.fixture.nativeElement.innerText).toContain('16');
89 | });
90 | });
91 |
92 | it('provides help for `onTouched`', () => {
93 | ctx.run(() => {
94 | expect(ctx.fixture.nativeElement.innerText).not.toContain('Touched!');
95 | click(incrementButton());
96 | expect(ctx.fixture.nativeElement.innerText).toContain('Touched!');
97 | });
98 | });
99 |
100 | it('provides help for `[disabled]`', () => {
101 | ctx.run({ input: { shouldDisable: true } }, () => {
102 | expect(incrementButton().disabled).toBe(true);
103 |
104 | click(toggleDisabledButton());
105 | expect(incrementButton().disabled).toBe(false);
106 | });
107 | });
108 |
109 | it('has the right class hierarchy', () => {
110 | ctx.run(() => {
111 | const counter = ctx.fixture.debugElement.query(
112 | By.directive(CounterComponent),
113 | ).componentInstance;
114 | expect(counter instanceof InjectableSuperclass).toBe(true);
115 | expect(counter instanceof DirectiveSuperclass).toBe(true);
116 | });
117 | });
118 | });
119 |
--------------------------------------------------------------------------------
/projects/s-ng-utils/src/lib/form-control-superclass.ts:
--------------------------------------------------------------------------------
1 | import { Provider, Type } from '@angular/core';
2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
3 | import { noop } from 'micro-dash';
4 | import { DirectiveSuperclass } from './directive-superclass';
5 |
6 | /**
7 | * Use in the `providers` of a component that implements `ControlValueAccessor` to reduce some boilerplate.
8 | *
9 | * ```ts
10 | * @Component({ providers: [provideValueAccessor(MyFormControl)] }
11 | * class MyFormControl extends BaseFormControl {
12 | * // ...
13 | * }
14 | * ```
15 | */
16 | export function provideValueAccessor(type: Type): Provider {
17 | return {
18 | provide: NG_VALUE_ACCESSOR,
19 | useExisting: type,
20 | multi: true,
21 | };
22 | }
23 |
24 | /**
25 | * Extend this when creating a form control to reduce some boilerplate. **Warning:** You _must_ include a constructor in your subclass.
26 | *
27 | * This example allows 2-way binding to a number via `[(ngModel)]`, `[formControl]`, or any other technique that leverages the `ControlValueAccessor` interface.
28 | * ```ts
29 | * @Component({
30 | * template: `
31 | *
32 | * `,
33 | * providers: [provideValueAccessor(CounterComponent)],
34 | * })
35 | * class CounterComponent extends FormControlSuperclass {
36 | * counter = 0;
37 | *
38 | * // This looks unnecessary, but is required for Angular to provide `Injector`
39 | * constructor(injector: Injector) {
40 | * super(injector);
41 | * }
42 | *
43 | * handleIncomingValue(value: number) {
44 | * this.counter = value;
45 | * }
46 | *
47 | * increment() {
48 | * this.emitOutgoingValue(++this.counter);
49 | * this.onTouched();
50 | * }
51 | * }
52 | * ```
53 | */
54 | export abstract class FormControlSuperclass extends DirectiveSuperclass
55 | implements ControlValueAccessor {
56 | /** Call this to emit a new value when it changes. */
57 | emitOutgoingValue: (value: T) => void = noop;
58 |
59 | /** Call this to "commit" a change, traditionally done e.g. on blur. */
60 | onTouched = noop;
61 |
62 | /** You can bind to this in your template as needed. */
63 | isDisabled = false;
64 |
65 | /** Implement this to handle a new value coming in from outside. */
66 | abstract handleIncomingValue(value: T): void;
67 |
68 | /** Called as angular propagates value changes to this `ControlValueAccessor`. You normally do not need to use it. */
69 | writeValue(value: T): void {
70 | this.handleIncomingValue(value);
71 | this.changeDetectorRef.markForCheck();
72 | }
73 |
74 | /** Called as angular sets up the binding to this `ControlValueAccessor`. You normally do not need to use it. */
75 | registerOnChange(fn: (value: T) => void): void {
76 | this.emitOutgoingValue = fn;
77 | }
78 |
79 | /** Called as angular sets up the binding to this `ControlValueAccessor`. You normally do not need to use it. */
80 | registerOnTouched(fn: VoidFunction): void {
81 | this.onTouched = fn;
82 | }
83 |
84 | /** Called as angular propagates disabled changes to this `ControlValueAccessor`. You normally do not need to use it. */
85 | setDisabledState(isDisabled: boolean): void {
86 | this.isDisabled = isDisabled;
87 | this.changeDetectorRef.markForCheck();
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/projects/s-ng-utils/src/lib/injectable-superclass.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component, Directive, Injectable } from '@angular/core';
2 | import { TestBed } from '@angular/core/testing';
3 | import { By } from '@angular/platform-browser';
4 | import { Subject } from 'rxjs';
5 | import { expectSingleCallAndReset } from 's-ng-dev-utils';
6 | import { ComponentContext } from '../to-replace/component-context';
7 | import { InjectableSuperclass } from './injectable-superclass';
8 |
9 | @Injectable()
10 | class DestroyableService extends InjectableSuperclass {}
11 |
12 | @Directive({
13 | selector: `[sDestroyableDirective]`,
14 | providers: [DestroyableService],
15 | })
16 | class DestroyableDirective extends InjectableSuperclass {
17 | constructor(subject: Subject, public service: DestroyableService) {
18 | super();
19 | this.subscribeTo(subject);
20 | service.subscribeTo(subject);
21 | }
22 | }
23 |
24 | @Component({
25 | template: `I'm showing.
`,
26 | })
27 | class TestComponent {
28 | showThings = true;
29 | }
30 |
31 | class TestComponentContext extends ComponentContext {
32 | subject = new Subject();
33 |
34 | protected componentType = TestComponent;
35 |
36 | constructor() {
37 | super({ declarations: [DestroyableDirective, TestComponent] });
38 | TestBed.overrideProvider(Subject, { useValue: this.subject });
39 | }
40 | }
41 |
42 | describe('InjectableSuperclass', () => {
43 | let ctx: TestComponentContext;
44 | beforeEach(async () => {
45 | ctx = new TestComponentContext();
46 | });
47 |
48 | it('cleans up subscriptions when destroyed by angular', () => {
49 | ctx.run(() => {
50 | expect(ctx.subject.observers.length).toBe(2);
51 |
52 | ctx.fixture.componentInstance.showThings = false;
53 | ctx.fixture.detectChanges();
54 | expect(ctx.subject.observers.length).toBe(0);
55 | });
56 | });
57 |
58 | it('has .destruction$ which emits and completes upon destruction', () => {
59 | const next = jasmine.createSpy();
60 | const complete = jasmine.createSpy();
61 | ctx.run(() => {
62 | const host = ctx.fixture.debugElement.query(
63 | By.directive(DestroyableDirective),
64 | );
65 | const service = host.injector.get(DestroyableService);
66 | service.destruction$.subscribe({ next, complete });
67 |
68 | ctx.fixture.componentInstance.showThings = false;
69 | ctx.fixture.detectChanges();
70 | expectSingleCallAndReset(next, undefined);
71 | expectSingleCallAndReset(complete);
72 | });
73 | });
74 | });
75 |
--------------------------------------------------------------------------------
/projects/s-ng-utils/src/lib/injectable-superclass.ts:
--------------------------------------------------------------------------------
1 | import { OnDestroy } from '@angular/core';
2 | import { Observable, Subject } from 'rxjs';
3 | import { SubscriptionManager } from 's-rxjs-utils';
4 |
5 | /**
6 | * Use as the superclass for anything managed by angular's dependency injection for care-free use of `subscribeTo()`. It simply calls `unsubscribe()` during `ngOnDestroy()`. If you override `ngOnDestroy()` in your subclass, be sure to invoke the super implementation.
7 | *
8 | * ```ts
9 | * @Injectable()
10 | * // or @Component() (also consider DirectiveSuperclass)
11 | * // or @Directive() (also consider DirectiveSuperclass)
12 | * // or @Pipe()
13 | * class MyThing extends InjectableSuperclass {
14 | * constructor(somethingObservable: Observable) {
15 | * super();
16 | * this.subscribeTo(somethingObservable);
17 | * }
18 | *
19 | * ngOnDestroy() {
20 | * // if you override ngOnDestroy, be sure to call this too
21 | * super.ngOnDestroy();
22 | * }
23 | * }
24 | * ```
25 | */
26 | export abstract class InjectableSuperclass extends SubscriptionManager
27 | implements OnDestroy {
28 | /**
29 | * An observable that emits once when this object is destroyed, then completes.
30 | */
31 | destruction$: Observable;
32 |
33 | private destructionSubject = new Subject();
34 |
35 | constructor() {
36 | super();
37 | this.destruction$ = this.destructionSubject.asObservable();
38 | }
39 |
40 | ngOnDestroy(): void {
41 | this.unsubscribe();
42 | this.destructionSubject.next();
43 | this.destructionSubject.complete();
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/projects/s-ng-utils/src/lib/wrapped-form-control-superclass.spec.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component, Injector } from '@angular/core';
2 | import { flushMicrotasks } from '@angular/core/testing';
3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms';
4 | import { By } from '@angular/platform-browser';
5 | import { click, find, findButton, setValue } from '../test-helpers';
6 | import { ComponentContext } from '../to-replace/component-context';
7 | import { DirectiveSuperclass } from './directive-superclass';
8 | import {
9 | FormControlSuperclass,
10 | provideValueAccessor,
11 | } from './form-control-superclass';
12 | import { InjectableSuperclass } from './injectable-superclass';
13 | import { WrappedFormControlSuperclass } from './wrapped-form-control-superclass';
14 |
15 | @Component({
16 | template: `
17 |
23 | Touched!
24 |
25 |
26 |
27 | `,
28 | })
29 | class TestComponent {
30 | emissions = 0;
31 | string = '';
32 | date = new Date();
33 | shouldDisable = false;
34 | }
35 |
36 | @Component({
37 | selector: `s-string-component`,
38 | template: ` `,
39 | providers: [provideValueAccessor(StringComponent)],
40 | changeDetection: ChangeDetectionStrategy.OnPush,
41 | })
42 | class StringComponent extends WrappedFormControlSuperclass {
43 | constructor(injector: Injector) {
44 | super(injector);
45 | }
46 | }
47 |
48 | @Component({
49 | selector: `s-date-component`,
50 | template: ` `,
51 | providers: [provideValueAccessor(DateComponent)],
52 | changeDetection: ChangeDetectionStrategy.OnPush,
53 | })
54 | class DateComponent extends WrappedFormControlSuperclass {
55 | constructor(injector: Injector) {
56 | super(injector);
57 | }
58 |
59 | protected innerToOuter(value: string): Date {
60 | return new Date(value + 'Z');
61 | }
62 |
63 | protected outerToInner(value: Date): string {
64 | if (value === null) {
65 | return ''; // happens during initialization
66 | }
67 | return value.toISOString().substr(0, 16);
68 | }
69 | }
70 |
71 | class TestComponentContext extends ComponentContext {
72 | protected componentType = TestComponent;
73 |
74 | constructor() {
75 | super({
76 | imports: [FormsModule, ReactiveFormsModule],
77 | declarations: [DateComponent, StringComponent, TestComponent],
78 | });
79 | }
80 | }
81 |
82 | describe('WrappedFormControlSuperclass', () => {
83 | let ctx: TestComponentContext;
84 | beforeEach(() => {
85 | ctx = new TestComponentContext();
86 | });
87 |
88 | function stringInput(): HTMLInputElement {
89 | return find(ctx.fixture, 's-string-component input');
90 | }
91 |
92 | function dateInput(): HTMLInputElement {
93 | return find(ctx.fixture, 's-date-component input');
94 | }
95 |
96 | function toggleDisabledButton(): HTMLButtonElement {
97 | return findButton(ctx.fixture, 'Toggle Disabled');
98 | }
99 |
100 | ///////
101 |
102 | it('provides help for 2-way binding', () => {
103 | ctx.run({ input: { string: 'initial value' } }, () => {
104 | expect(stringInput().value).toBe('initial value');
105 |
106 | setValue(stringInput(), 'edited value');
107 | expect(ctx.fixture.componentInstance.string).toBe('edited value');
108 | });
109 | });
110 |
111 | it('can translate between inner and outer values', () => {
112 | ctx.run({ input: { date: new Date('2018-09-03T21:00Z') } }, () => {
113 | expect(dateInput().value).toBe('2018-09-03T21:00');
114 |
115 | setValue(dateInput(), '1980-11-04T10:00');
116 | expect(ctx.fixture.componentInstance.date).toEqual(
117 | new Date('1980-11-04T10:00Z'),
118 | );
119 | });
120 | });
121 |
122 | it('provides help for `onTouched`', () => {
123 | ctx.run(() => {
124 | expect(ctx.fixture.nativeElement.innerText).not.toContain('Touched!');
125 | stringInput().dispatchEvent(new Event('blur'));
126 | expect(ctx.fixture.nativeElement.innerText).toContain('Touched!');
127 | });
128 | });
129 |
130 | it('provides help for `[disabled]`', () => {
131 | ctx.run({ input: { shouldDisable: true } }, () => {
132 | expect(stringInput().disabled).toBe(true);
133 |
134 | click(toggleDisabledButton());
135 | expect(stringInput().disabled).toBe(false);
136 |
137 | click(toggleDisabledButton());
138 | expect(stringInput().disabled).toBe(true);
139 | });
140 | });
141 |
142 | it('does not emit after an incoming change', () => {
143 | ctx.run(() => {
144 | expect(ctx.fixture.componentInstance.emissions).toBe(0);
145 |
146 | setValue(stringInput(), 'changed from within');
147 | expect(ctx.fixture.componentInstance.emissions).toBe(1);
148 |
149 | ctx.fixture.componentInstance.string = 'changed from without';
150 | ctx.fixture.detectChanges();
151 | flushMicrotasks();
152 | expect(ctx.fixture.componentInstance.emissions).toBe(1);
153 |
154 | click(toggleDisabledButton());
155 | click(toggleDisabledButton());
156 | expect(ctx.fixture.componentInstance.emissions).toBe(1);
157 | });
158 | });
159 |
160 | it('has the right class hierarchy', () => {
161 | ctx.run(() => {
162 | const component = ctx.fixture.debugElement.query(
163 | By.directive(StringComponent),
164 | ).componentInstance;
165 | expect(component instanceof InjectableSuperclass).toBe(true);
166 | expect(component instanceof DirectiveSuperclass).toBe(true);
167 | expect(component instanceof FormControlSuperclass).toBe(true);
168 | });
169 | });
170 | });
171 |
--------------------------------------------------------------------------------
/projects/s-ng-utils/src/lib/wrapped-form-control-superclass.ts:
--------------------------------------------------------------------------------
1 | import { Injector } from '@angular/core';
2 | import { FormControl } from '@angular/forms';
3 | import { FormControlSuperclass } from './form-control-superclass';
4 |
5 | /**
6 | * Extend this when creating a form control that simply wraps an existing form control, to reduce a lot of boilerplate. **Warning:** You _must_ include a constructor in your subclass.
7 | *
8 | * Example when you don't need to modify the wrapped control's value:
9 | * ```ts
10 | * @Component({
11 | * template: ``,
12 | * providers: [provideValueAccessor(StringComponent)],
13 | * })
14 | * class StringComponent extends WrappedFormControlSuperclass {
15 | * // This looks unnecessary, but is required for Angular to provide `Injector`
16 | * constructor(injector: Injector) {
17 | * super(injector);
18 | * }
19 | * }
20 | * ```
21 | *
22 | * Example when you need to modify the wrapped control's value:
23 | * ```ts
24 | * @Component({
25 | * template: ``,
26 | * providers: [provideValueAccessor(DateComponent)],
27 | * })
28 | * class DateComponent extends WrappedFormControlSuperclass {
29 | * // This looks unnecessary, but is required for Angular to provide `Injector`
30 | * constructor(injector: Injector) {
31 | * super(injector);
32 | * }
33 | *
34 | * protected innerToOuter(value: string): Date {
35 | * return new Date(value + "Z");
36 | * }
37 | *
38 | * protected outerToInner(value: Date): string {
39 | * if (value === null) {
40 | * return ""; // happens during initialization
41 | * }
42 | * return value.toISOString().substr(0, 16);
43 | * }
44 | * }
45 | * ```
46 | */
47 | export abstract class WrappedFormControlSuperclass<
48 | OuterType,
49 | InnerType = OuterType
50 | > extends FormControlSuperclass {
51 | /** Bind this to your inner form control to make all the magic happen. */
52 | formControl = new FormControl();
53 |
54 | constructor(injector: Injector) {
55 | super(injector);
56 | this.subscribeTo(this.formControl.valueChanges, (value) => {
57 | this.emitOutgoingValue(this.innerToOuter(value));
58 | });
59 | this.formControl.markAsTouched = () => {
60 | this.onTouched();
61 | };
62 | }
63 |
64 | /** Called as angular propagates values changes to this `ControlValueAccessor`. You normally do not need to use it. */
65 | handleIncomingValue(value: OuterType): void {
66 | this.formControl.setValue(this.outerToInner(value), { emitEvent: false });
67 | }
68 |
69 | /** Called as angular propagates disabled changes to this `ControlValueAccessor`. You normally do not need to use it. */
70 | setDisabledState(isDisabled: boolean): void {
71 | if (isDisabled) {
72 | this.formControl.disable({ emitEvent: false });
73 | } else {
74 | this.formControl.enable({ emitEvent: false });
75 | }
76 | super.setDisabledState(this.isDisabled);
77 | }
78 |
79 | /** Override this to modify a value coming from the outside to the format needed within this component. */
80 | protected outerToInner(value: OuterType): InnerType {
81 | return (value as any) as InnerType;
82 | }
83 |
84 | /** Override this to modify a value coming from within this component to the format expected on the outside. */
85 | protected innerToOuter(value: InnerType): OuterType {
86 | return (value as any) as OuterType;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/projects/s-ng-utils/src/public-api.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Public API Surface of s-ng-utils
3 | */
4 |
5 | export { DirectiveSuperclass } from "./lib/directive-superclass";
6 | export {
7 | FormControlSuperclass,
8 | provideValueAccessor,
9 | } from "./lib/form-control-superclass";
10 | export { InjectableSuperclass } from "./lib/injectable-superclass";
11 | export {
12 | WrappedFormControlSuperclass,
13 | } from "./lib/wrapped-form-control-superclass";
14 |
--------------------------------------------------------------------------------
/projects/s-ng-utils/src/test-helpers.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, flushMicrotasks } from '@angular/core/testing';
2 |
3 | /** @hidden */
4 | export function findButton(
5 | fixture: ComponentFixture,
6 | text: string,
7 | ): HTMLButtonElement {
8 | const found = fixture.debugElement.query(
9 | (candidate) =>
10 | candidate.nativeElement.nodeName === 'BUTTON' &&
11 | candidate.nativeElement.textContent.trim() === text,
12 | );
13 | if (found) {
14 | return found.nativeElement;
15 | } else {
16 | throw new Error('No button with text ' + text);
17 | }
18 | }
19 |
20 | /** @hidden */
21 | export function find(
22 | fixture: ComponentFixture,
23 | cssSelector: string,
24 | ): T {
25 | const found = fixture.nativeElement.querySelector(cssSelector) as T;
26 | if (found) {
27 | return found;
28 | } else {
29 | throw new Error('could not find ' + cssSelector);
30 | }
31 | }
32 |
33 | /** @hidden */
34 | export function click(element: Element): void {
35 | element.dispatchEvent(new MouseEvent('click', { bubbles: true }));
36 | flushMicrotasks();
37 | }
38 |
39 | /** @hidden */
40 | export function setValue(input: HTMLInputElement, value: string): void {
41 | input.value = value;
42 | input.dispatchEvent(new Event('input', { bubbles: true }));
43 | input.dispatchEvent(new Event('change', { bubbles: true }));
44 | flushMicrotasks();
45 | }
46 |
--------------------------------------------------------------------------------
/projects/s-ng-utils/src/test-polyfills.ts:
--------------------------------------------------------------------------------
1 | // This file can be deleted after https://github.com/angular/angular-cli/issues/14432. Be sure to delete it from `angular.json`, `tsconfig.lib.json`, and `tsconfig.spec.json`, too.
2 | import 'zone.js/dist/zone';
3 |
--------------------------------------------------------------------------------
/projects/s-ng-utils/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js/dist/zone';
4 | import 'zone.js/dist/zone-testing';
5 | import { getTestBed } from '@angular/core/testing';
6 | import {
7 | BrowserDynamicTestingModule,
8 | platformBrowserDynamicTesting,
9 | } from '@angular/platform-browser-dynamic/testing';
10 |
11 | declare const require: {
12 | context(
13 | path: string,
14 | deep?: boolean,
15 | filter?: RegExp,
16 | ): {
17 | keys(): string[];
18 | (id: string): T;
19 | };
20 | };
21 |
22 | // First, initialize the Angular testing environment.
23 | getTestBed().initTestEnvironment(
24 | BrowserDynamicTestingModule,
25 | platformBrowserDynamicTesting(),
26 | );
27 | // Then we find all the tests.
28 | const context = require.context('./', true, /\.spec\.ts$/);
29 | // And load the modules.
30 | context.keys().map(context);
31 |
--------------------------------------------------------------------------------
/projects/s-ng-utils/src/to-replace/angular-context.ts:
--------------------------------------------------------------------------------
1 | import {
2 | HttpClientTestingModule,
3 | HttpTestingController,
4 | } from '@angular/common/http/testing';
5 | import {
6 | AbstractType,
7 | ApplicationRef,
8 | InjectionToken,
9 | Type,
10 | } from '@angular/core';
11 | import {
12 | discardPeriodicTasks,
13 | fakeAsync,
14 | flushMicrotasks,
15 | TestBed,
16 | TestModuleMetadata,
17 | tick,
18 | } from '@angular/core/testing';
19 | import { clone, forOwn, isFunction } from 'micro-dash';
20 | import { isArray } from 'rxjs/internal-compatibility';
21 | import { assert } from 's-js-utils';
22 |
23 | export function extendMetadata(
24 | metadata: TestModuleMetadata,
25 | toAdd: TestModuleMetadata,
26 | ): TestModuleMetadata {
27 | const result: any = clone(metadata);
28 | forOwn(toAdd, (val, key) => {
29 | result[key] = isArray(result[key]) ? result[key].concat(val) : val;
30 | });
31 | return result;
32 | }
33 |
34 | export abstract class AngularContext {
35 | startTime = new Date();
36 |
37 | constructor(moduleMetadata: TestModuleMetadata) {
38 | TestBed.configureTestingModule(
39 | extendMetadata(moduleMetadata, { imports: [HttpClientTestingModule] }),
40 | );
41 | }
42 |
43 | run(test: () => void): void;
44 | run(options: Partial, test: () => void): void;
45 | run(
46 | optionsOrTest: Partial | (() => void),
47 | test?: () => void,
48 | ): void {
49 | let options: Partial = {};
50 | if (isFunction(optionsOrTest)) {
51 | test = optionsOrTest;
52 | } else {
53 | options = optionsOrTest;
54 | }
55 |
56 | jasmine.clock().install();
57 | fakeAsync(() => {
58 | jasmine.clock().mockDate(this.startTime);
59 | assert(test);
60 |
61 | this.init(options);
62 | try {
63 | test();
64 | this.verify();
65 | } finally {
66 | this.cleanUp();
67 | }
68 | })();
69 | jasmine.clock().uninstall();
70 | }
71 |
72 | inject(token: Type | InjectionToken | AbstractType): T {
73 | return TestBed.inject(token);
74 | }
75 |
76 | tick(millis?: number): void {
77 | flushMicrotasks();
78 | this.inject(ApplicationRef).tick();
79 | tick(millis);
80 | }
81 |
82 | protected init(_options: Partial): void {}
83 |
84 | protected verify(): void {
85 | this.inject(HttpTestingController).verify();
86 | }
87 |
88 | protected cleanUp(): void {
89 | discardPeriodicTasks();
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/projects/s-ng-utils/src/to-replace/component-context.ts:
--------------------------------------------------------------------------------
1 | import { Type } from '@angular/core';
2 | import {
3 | ComponentFixture,
4 | ComponentFixtureAutoDetect,
5 | flushMicrotasks,
6 | TestBed,
7 | TestModuleMetadata,
8 | tick,
9 | } from '@angular/core/testing';
10 | import { NoopAnimationsModule } from '@angular/platform-browser/animations';
11 | import { convertTime } from 's-js-utils';
12 | import { trimLeftoverStyles } from 's-ng-dev-utils';
13 | import { AngularContext, extendMetadata } from './angular-context';
14 |
15 | export interface ComponentContextInitOptions {
16 | input?: Partial;
17 | }
18 |
19 | export abstract class ComponentContext<
20 | ComponentType = unknown,
21 | InitOptions extends ComponentContextInitOptions<
22 | ComponentType
23 | > = ComponentContextInitOptions
24 | > extends AngularContext {
25 | fixture!: ComponentFixture;
26 | protected abstract componentType: Type;
27 |
28 | constructor(moduleMetadata: TestModuleMetadata) {
29 | super(
30 | extendMetadata(moduleMetadata, {
31 | imports: [NoopAnimationsModule],
32 | providers: [{ provide: ComponentFixtureAutoDetect, useValue: true }],
33 | }),
34 | );
35 | }
36 |
37 | tick(amount = 0, unit = 'ms'): void {
38 | flushMicrotasks();
39 | this.fixture.detectChanges();
40 | tick(convertTime(amount, unit, 'ms'));
41 | }
42 |
43 | protected init(options: Partial): void {
44 | trimLeftoverStyles();
45 | super.init(options);
46 | this.fixture = TestBed.createComponent(this.componentType);
47 | Object.assign(this.fixture.componentInstance, options.input);
48 | this.fixture.detectChanges();
49 | this.tick();
50 | }
51 |
52 | protected cleanUp(): void {
53 | this.fixture.destroy();
54 | super.cleanUp();
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/projects/s-ng-utils/src/typing-tests/form-control-superclass.dts-spec.ts:
--------------------------------------------------------------------------------
1 | import { FormControlSuperclass } from "../public-api";
2 |
3 | class TestSubclass extends FormControlSuperclass {
4 | handleIncomingValue(_value: Date): void {}
5 | }
6 |
7 | const obj = new TestSubclass(null as any);
8 |
9 | // $ExpectType (value: Date) => void
10 | const emitOutgoingValue = obj.emitOutgoingValue;
11 | // $ExpectType (value: Date) => void
12 | const writeValue = obj.writeValue;
13 | // $ExpectType (fn: (value: Date) => void) => void
14 | const registerOnChange = obj.registerOnChange;
15 |
16 | console.log(emitOutgoingValue, writeValue, registerOnChange);
17 |
--------------------------------------------------------------------------------
/projects/s-ng-utils/src/typing-tests/index.d.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simontonsoftware/s-ng-utils/cbae499f6e832701193e773b776488a12663fcb2/projects/s-ng-utils/src/typing-tests/index.d.ts
--------------------------------------------------------------------------------
/projects/s-ng-utils/src/typing-tests/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "noImplicitAny": true,
5 | "noImplicitThis": true,
6 | "strictNullChecks": true,
7 | "strictFunctionTypes": true,
8 | "lib": ["es2018", "dom"],
9 | "noEmit": true,
10 | "target": "es2015",
11 |
12 | "baseUrl": "../lib"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/projects/s-ng-utils/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "../../tsconfig.base.json",
4 | "compilerOptions": {
5 | "outDir": "../../out-tsc/lib",
6 | "target": "es2015",
7 | "declaration": true,
8 | "inlineSources": true,
9 | "types": [],
10 | "lib": ["dom", "es2018"]
11 | },
12 | "angularCompilerOptions": {
13 | "skipTemplateCodegen": true,
14 | "strictMetadataEmit": true,
15 | "enableResourceInlining": true
16 | },
17 | "exclude": ["src/test.ts", "src/test-polyfills.ts", "**/*.spec.ts"]
18 | }
19 |
--------------------------------------------------------------------------------
/projects/s-ng-utils/tsconfig.lib.prod.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "./tsconfig.lib.json",
4 | "angularCompilerOptions": {
5 | "enableIvy": false
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/projects/s-ng-utils/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "../../tsconfig.base.json",
4 | "compilerOptions": {
5 | "outDir": "../../out-tsc/spec",
6 | "types": ["jasmine"]
7 | },
8 | "files": ["src/test.ts"],
9 | "include": ["src/test-polyfills.ts", "**/*.spec.ts", "**/*.d.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/projects/s-ng-utils/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tslint.json",
3 | "rules": {
4 | "directive-selector": [true, "attribute", "s", "camelCase"],
5 | "component-selector": [true, "element", "s", "kebab-case"]
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/app.component.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simontonsoftware/s-ng-utils/cbae499f6e832701193e773b776488a12663fcb2/src/app/app.component.css
--------------------------------------------------------------------------------
/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Welcome to {{ title }}!
4 |

9 |
10 |
11 |
12 | {{ location | json }}
13 |
14 |
17 |
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Injector } from '@angular/core';
2 | import { FormControl, Validators } from '@angular/forms';
3 | import {
4 | DirectiveSuperclass,
5 | FormControlSuperclass,
6 | InjectableSuperclass,
7 | provideValueAccessor,
8 | WrappedFormControlSuperclass,
9 | } from 's-ng-utils';
10 |
11 | // used in a stackoverflow answer: https://stackoverflow.com/a/55091023/1836506
12 |
13 | @Component({
14 | selector: 'app-root',
15 | templateUrl: './app.component.html',
16 | styleUrls: ['./app.component.css'],
17 | })
18 | export class AppComponent {
19 | title = 'failure';
20 | location = { city: 'Portage', country: 'Michigan' };
21 | requiredFormControl = new FormControl('initial', Validators.required);
22 |
23 | constructor(injector: Injector) {
24 | // use each function once just to show in can be imported
25 | // tslint:disable:no-unused-expression
26 | new (class extends DirectiveSuperclass {})(injector);
27 | new (class extends InjectableSuperclass {})();
28 | provideValueAccessor(AppComponent);
29 | new (class extends FormControlSuperclass {
30 | handleIncomingValue(): void {}
31 | })(injector);
32 | new (class extends WrappedFormControlSuperclass {})(injector);
33 |
34 | this.title = 's-ng-utils-platform';
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { FormsModule, ReactiveFormsModule } from "@angular/forms";
2 | import { BrowserModule } from "@angular/platform-browser";
3 | import { NgModule } from "@angular/core";
4 | import { AppComponent } from "./app.component";
5 | import { LocationComponent } from "./location.component";
6 | import { ErrorDisplayingInputComponent } from "./error-displaying-input.component";
7 |
8 | @NgModule({
9 | declarations: [
10 | AppComponent,
11 | ErrorDisplayingInputComponent,
12 | LocationComponent,
13 | ],
14 | imports: [BrowserModule, FormsModule, ReactiveFormsModule],
15 | providers: [],
16 | bootstrap: [AppComponent],
17 | })
18 | export class AppModule {}
19 |
--------------------------------------------------------------------------------
/src/app/error-displaying-input.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Injector, Self } from "@angular/core";
2 | import { NgControl } from "@angular/forms";
3 | import { WrappedFormControlSuperclass } from "s-ng-utils";
4 |
5 | // used to answer https://github.com/simontonsoftware/s-ng-utils/issues/4#issuecomment-484048187
6 |
7 | @Component({
8 | selector: "app-error-displaying-input",
9 | template: `
10 |
11 | I'm required!
12 | `,
13 | })
14 | export class ErrorDisplayingInputComponent extends WrappedFormControlSuperclass<
15 | string
16 | > {
17 | constructor(@Self() public control: NgControl, injector: Injector) {
18 | super(injector);
19 | control.valueAccessor = this;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/app/location.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Injector } from '@angular/core';
2 | import { FormControlSuperclass, provideValueAccessor } from 's-ng-utils';
3 |
4 | // used in a stackoverflow answer: https://stackoverflow.com/a/55091023/1836506
5 |
6 | interface Location {
7 | city: string;
8 | country: string;
9 | }
10 |
11 | @Component({
12 | selector: 'app-location',
13 | template: `
14 | City:
15 |
19 | Country:
20 |
24 | `,
25 | providers: [provideValueAccessor(LocationComponent)],
26 | })
27 | export class LocationComponent extends FormControlSuperclass {
28 | location!: Location;
29 |
30 | constructor(injector: Injector) {
31 | super(injector);
32 | }
33 |
34 | handleIncomingValue(value: Location): void {
35 | this.location = value;
36 | }
37 |
38 | modifyLocation(field: K, value: Location[K]): void {
39 | this.location = { ...this.location, [field]: value };
40 | this.emitOutgoingValue(this.location);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "the-project",
3 | "private": true,
4 | "description_1": "This is a special package.json file that is not used by package managers.",
5 | "description_2": "It is used to tell the tools and bundlers whether the code under this directory is free of code with non-local side-effect. Any code that does have non-local side-effects can't be well optimized (tree-shaken) and will result in unnecessary increased payload size.",
6 | "description_3": "It should be safe to set this option to 'false' for new applications, but existing code bases could be broken when built with the production config if the application code does contain non-local side-effects that the application depends on.",
7 | "description_4": "To learn more about this file see: https://angular.io/config/app-package-json.",
8 | "sideEffects": false
9 | }
10 |
--------------------------------------------------------------------------------
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simontonsoftware/s-ng-utils/cbae499f6e832701193e773b776488a12663fcb2/src/assets/.gitkeep
--------------------------------------------------------------------------------
/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true,
3 | };
4 |
--------------------------------------------------------------------------------
/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // This file can be replaced during build by using the `fileReplacements` array.
2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
3 | // The list of file replacements can be found in `angular.json`.
4 |
5 | export const environment = {
6 | production: false,
7 | };
8 |
9 | /*
10 | * For easier debugging in development mode, you can import the following file
11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
12 | *
13 | * This import should be commented out in production mode because it will have a negative impact
14 | * on performance if an error is thrown.
15 | */
16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI.
17 |
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simontonsoftware/s-ng-utils/cbae499f6e832701193e773b776488a12663fcb2/src/favicon.ico
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SNgUtilsPlatform
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from "@angular/core";
2 | import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
3 |
4 | import { AppModule } from "./app/app.module";
5 | import { environment } from "./environments/environment";
6 |
7 | if (environment.production) {
8 | enableProdMode();
9 | }
10 |
11 | platformBrowserDynamic()
12 | .bootstrapModule(AppModule)
13 | .catch((err) => console.error(err));
14 |
--------------------------------------------------------------------------------
/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/guide/browser-support
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */
22 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
23 |
24 | /**
25 | * Web Animations `@angular/platform-browser/animations`
26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
28 | */
29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
30 |
31 | /**
32 | * By default, zone.js will patch all possible macroTask and DomEvents
33 | * user can disable parts of macroTask/DomEvents patch by setting following flags
34 | * because those flags need to be set before `zone.js` being loaded, and webpack
35 | * will put import in the top of bundle, so user need to create a separate file
36 | * in this directory (for example: zone-flags.ts), and put the following flags
37 | * into that file, and then add the following code before importing zone.js.
38 | * import './zone-flags';
39 | *
40 | * The flags allowed in zone-flags.ts are listed here.
41 | *
42 | * The following flags will work for all browsers.
43 | *
44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
47 | *
48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
50 | *
51 | * (window as any).__Zone_enable_cross_context_check = true;
52 | *
53 | */
54 |
55 | /***************************************************************************************************
56 | * Zone JS is required by default for Angular itself.
57 | */
58 | import 'zone.js/dist/zone'; // Included with Angular CLI.
59 |
60 | /***************************************************************************************************
61 | * APPLICATION IMPORTS
62 | */
63 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 |
--------------------------------------------------------------------------------
/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import "zone.js/dist/zone-testing";
4 | import { getTestBed } from "@angular/core/testing";
5 | import {
6 | BrowserDynamicTestingModule,
7 | platformBrowserDynamicTesting,
8 | } from "@angular/platform-browser-dynamic/testing";
9 |
10 | declare const require: {
11 | context(
12 | path: string,
13 | deep?: boolean,
14 | filter?: RegExp,
15 | ): {
16 | keys(): string[];
17 | (id: string): T;
18 | };
19 | };
20 |
21 | // First, initialize the Angular testing environment.
22 | getTestBed().initTestEnvironment(
23 | BrowserDynamicTestingModule,
24 | platformBrowserDynamicTesting(),
25 | );
26 | // Then we find all the tests.
27 | const context = require.context("./", true, /\.spec\.ts$/);
28 | // And load the modules.
29 | context.keys().map(context);
30 |
--------------------------------------------------------------------------------
/standard-version-postbump.ts:
--------------------------------------------------------------------------------
1 | import { writeFileSync } from "fs";
2 | import { format } from "prettier";
3 |
4 | const projectName = "s-ng-utils";
5 |
6 | const packageJson = require("./package.json");
7 | const libPackageJson = require(`./projects/${projectName}/package.json`);
8 | writeFileSync(
9 | `./projects/${projectName}/package.json`,
10 | format(JSON.stringify({ ...libPackageJson, version: packageJson.version }), {
11 | parser: "json",
12 | }),
13 | );
14 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "./tsconfig.base.json",
4 | "compilerOptions": {
5 | "outDir": "./out-tsc/app",
6 | "types": []
7 | },
8 | "files": ["src/main.ts", "src/polyfills.ts"],
9 | "include": ["src/**/*.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | //
5 | // from the Angular CLI
6 | //
7 | "baseUrl": "./",
8 | "outDir": "./dist/out-tsc",
9 | "forceConsistentCasingInFileNames": true,
10 | "strict": true,
11 | "noImplicitReturns": true,
12 | "noFallthroughCasesInSwitch": true,
13 | "sourceMap": true,
14 | "declaration": false,
15 | "downlevelIteration": true,
16 | "experimentalDecorators": true,
17 | "moduleResolution": "node",
18 | "importHelpers": true,
19 | "target": "es2015",
20 | "module": "es2020",
21 | "lib": ["es2018", "dom"],
22 | "paths": {
23 | "s-ng-utils": ["dist/s-ng-utils/s-ng-utils", "dist/s-ng-utils"]
24 | },
25 |
26 | //
27 | // Simonton Software additions
28 | //
29 | "noUnusedLocals": true,
30 | "noUnusedParameters": true
31 | },
32 | "angularCompilerOptions": {
33 | "strictInjectionParameters": true,
34 | "strictTemplates": true
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | /*
2 | This is a "Solution Style" tsconfig.json file, and is used by editors and TypeScript’s language server to improve development experience.
3 | It is not intended to be used to perform a compilation.
4 | To learn more about this file see: https://angular.io/config/solution-tsconfig.
5 | */
6 | {
7 | "files": [],
8 | "references": [
9 | { "path": "./tsconfig.app.json" },
10 | { "path": "./tsconfig.spec.json" },
11 | { "path": "./e2e/tsconfig.json" },
12 | { "path": "./projects/s-ng-utils/tsconfig.lib.json" },
13 | { "path": "./projects/s-ng-utils/tsconfig.spec.json" }
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "./tsconfig.base.json",
4 | "compilerOptions": {
5 | "outDir": "./out-tsc/spec",
6 | "types": ["jasmine"]
7 | },
8 | "files": ["src/test.ts", "src/polyfills.ts"],
9 | "include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "s-ng-dev-utils/tslint"
3 | }
4 |
--------------------------------------------------------------------------------