diff --git a/.editorconfig b/.editorconfig index d24a75e0..77668e83 100644 --- a/.editorconfig +++ b/.editorconfig @@ -16,7 +16,8 @@ insert_final_newline = false [*.md] max_line_length = off trim_trailing_whitespace = false +indent_style = space [*.{ts,js,html}] indent_size = 4 -indent_style = tab \ No newline at end of file +indent_style = tab diff --git a/README.md b/README.md index 6f57af04..33e2736d 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,9 @@ This repo contains general usage libraries for shd Angular projects. Those libra - images (`@studiohyperdrive/ngx-images`): - progressive image loading +- forms (`@studiohyperdrive/ngx-forms`): + - custom validators + You can find detailed explanations in their respective README’s. It is build with: diff --git a/angular.json b/angular.json index 1a704d62..684d2623 100644 --- a/angular.json +++ b/angular.json @@ -1,5 +1,8 @@ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "cli": { + "analytics": false + }, "version": 1, "newProjectRoot": "projects", "projects": { @@ -82,6 +85,46 @@ } } } + }, + "forms": { + "projectType": "library", + "root": "projects/forms", + "sourceRoot": "projects/forms/src", + "prefix": "lib", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:ng-packagr", + "options": { + "tsConfig": "projects/forms/tsconfig.lib.json", + "project": "projects/forms/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "projects/forms/tsconfig.lib.prod.json" + } + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "projects/forms/src/test.ts", + "tsConfig": "projects/forms/tsconfig.spec.json", + "karmaConfig": "projects/forms/karma.conf.js" + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "projects/forms/tsconfig.lib.json", + "projects/forms/tsconfig.spec.json" + ], + "exclude": [ + "**/node_modules/**" + ] + } + } + } } }, "defaultProject": "utils" diff --git a/package-lock.json b/package-lock.json index 83fd560e..699d9f3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2598,6 +2598,16 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "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" + } + }, "bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -5647,6 +5657,13 @@ } } }, + "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": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -8238,6 +8255,13 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "nan": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "dev": true, + "optional": true + }, "nanoid": { "version": "3.1.20", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz", @@ -15340,7 +15364,11 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, - "optional": true + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } }, "glob-parent": { "version": "3.1.0", @@ -16210,7 +16238,11 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, - "optional": true + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } }, "glob-parent": { "version": "3.1.0", diff --git a/package.json b/package.json index 3d48c109..878c6f79 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "ng": "ng", "start": "ng serve", "build": "ng build", - "test": "ng test", + "test": "ng test --code-coverage --watch false", + "test:watch": "ng test", "lint": "ng lint --fix" }, "private": true, diff --git a/projects/forms/README.md b/projects/forms/README.md new file mode 100644 index 00000000..b58433dc --- /dev/null +++ b/projects/forms/README.md @@ -0,0 +1,14 @@ +# Angular Tools: forms (`@studiohyperdrive/ngx-forms`) + +Install the package first: +```shell +npm install @studiohyperdrive/ngx-forms +``` + +## 1. Validators + +A set of extra custom validators compatible with the default Angular validators and reactive forms. + +| Validator | Description | +|----------------|------------------------------------------------------------------------------------------------------| +| extendedEmail | Extends the default e-mail validator with a required period in the tld part of te email. | diff --git a/projects/forms/karma.conf.js b/projects/forms/karma.conf.js new file mode 100644 index 00000000..6d61396c --- /dev/null +++ b/projects/forms/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, '../../coverage/forms'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/projects/forms/ng-package.json b/projects/forms/ng-package.json new file mode 100644 index 00000000..c2565def --- /dev/null +++ b/projects/forms/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../dist/forms", + "lib": { + "entryFile": "src/public-api.ts" + } +} \ No newline at end of file diff --git a/projects/forms/package.json b/projects/forms/package.json new file mode 100644 index 00000000..9921aaed --- /dev/null +++ b/projects/forms/package.json @@ -0,0 +1,11 @@ +{ + "name": "@studiohyperdrive/ngx-utils", + "version": "0.0.1", + "peerDependencies": { + "@angular/common": "^11.2.1", + "@angular/core": "^11.2.1" + }, + "dependencies": { + "tslib": "^2.0.0" + } +} \ No newline at end of file diff --git a/projects/forms/src/lib/validators/extended-email.validator.spec.ts b/projects/forms/src/lib/validators/extended-email.validator.spec.ts new file mode 100644 index 00000000..e432119c --- /dev/null +++ b/projects/forms/src/lib/validators/extended-email.validator.spec.ts @@ -0,0 +1,34 @@ +import { FormControl, FormGroup } from '@angular/forms'; + +import { Validators } from './validators'; + +describe('Extended Email Validator', () => { + it('should throw an error if e-mail is not valid', () => { + const control1 = new FormControl('test'); + const control2 = new FormControl('test@test'); + + expect(Validators.extendedEmail(control1)).toEqual({ extendedEmail: true }); + expect(Validators.extendedEmail(control2)).toEqual({ extendedEmail: true }); + }); + + it('should not throw an error if e-mail is valid', () => { + const control1 = new FormControl(''); // Should be valid, use Validators.required for this + const control2 = new FormControl('test@test.be'); + + expect(Validators.extendedEmail(control1)).toBeNull(); + expect(Validators.extendedEmail(control2)).toBeNull(); + }); + + it('should work as validator in a reactive form', () => { + const form = new FormGroup({ + email1: new FormControl('', [Validators.extendedEmail]), + email2: new FormControl('', [Validators.extendedEmail]), + }); + + form.get('email1').setValue('test@test'); + form.get('email2').setValue('test@test.be'); + + expect(form.get('email1').errors).toEqual({ extendedEmail: true }); + expect(form.get('email2').errors).toBeNull(); + }); +}); diff --git a/projects/forms/src/lib/validators/validators.ts b/projects/forms/src/lib/validators/validators.ts new file mode 100644 index 00000000..11122cbf --- /dev/null +++ b/projects/forms/src/lib/validators/validators.ts @@ -0,0 +1,34 @@ +import { AbstractControl, ValidationErrors } from '@angular/forms'; + +/** + * Helpers + * + * Don't add to class because we don't want them to be exposed as static prop + */ + +function isEmptyInputValue(value: any): boolean { + // we don't check for string here so it also works with arrays + return value == null || value.length === 0; +} + +export function extendedEmailValidator(control: AbstractControl): ValidationErrors|null { + if (isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + + // Validates more strictly than the default email validator. Requires a period in the tld part. + return /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/.test(control.value) ? null : { extendedEmail : true}; +} + + +/** + * Exported Class + */ + +export class Validators { + static extendedEmail(control: AbstractControl): ValidationErrors|null { + return extendedEmailValidator(control); + } + + // Add other custom validators :-) +} diff --git a/projects/forms/src/public-api.ts b/projects/forms/src/public-api.ts new file mode 100644 index 00000000..926749ab --- /dev/null +++ b/projects/forms/src/public-api.ts @@ -0,0 +1,5 @@ +/* + * Public API Surface of forms + */ + +export { Validators } from '../../forms/src/lib/validators/validators'; diff --git a/projects/forms/src/test.ts b/projects/forms/src/test.ts new file mode 100644 index 00000000..81ef7e7b --- /dev/null +++ b/projects/forms/src/test.ts @@ -0,0 +1,26 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting, +} from '@angular/platform-browser-dynamic/testing'; +import 'zone.js/dist/zone'; +import 'zone.js/dist/zone-testing'; + +declare const require: { + context(path: string, deep?: boolean, filter?: RegExp): { + keys(): string[]; + (id: string): T; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() +); +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); diff --git a/projects/forms/tsconfig.lib.json b/projects/forms/tsconfig.lib.json new file mode 100644 index 00000000..6e06ad54 --- /dev/null +++ b/projects/forms/tsconfig.lib.json @@ -0,0 +1,25 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/lib", + "target": "es2015", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [], + "lib": [ + "dom", + "es2018" + ] + }, + "angularCompilerOptions": { + "skipTemplateCodegen": true, + "strictMetadataEmit": true, + "enableResourceInlining": true + }, + "exclude": [ + "src/test.ts", + "**/*.spec.ts" + ] +} diff --git a/projects/forms/tsconfig.lib.prod.json b/projects/forms/tsconfig.lib.prod.json new file mode 100644 index 00000000..5615c27d --- /dev/null +++ b/projects/forms/tsconfig.lib.prod.json @@ -0,0 +1,10 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + }, + "angularCompilerOptions": { + "enableIvy": false + } +} diff --git a/projects/forms/tsconfig.spec.json b/projects/forms/tsconfig.spec.json new file mode 100644 index 00000000..715dd0a5 --- /dev/null +++ b/projects/forms/tsconfig.spec.json @@ -0,0 +1,17 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "src/test.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/projects/forms/tslint.json b/projects/forms/tslint.json new file mode 100644 index 00000000..124133f8 --- /dev/null +++ b/projects/forms/tslint.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tslint.json", + "rules": { + "directive-selector": [ + true, + "attribute", + "lib", + "camelCase" + ], + "component-selector": [ + true, + "element", + "lib", + "kebab-case" + ] + } +} diff --git a/projects/utils/src/public-api.ts b/projects/utils/src/public-api.ts index 123c860f..4c10b838 100644 --- a/projects/utils/src/public-api.ts +++ b/projects/utils/src/public-api.ts @@ -6,4 +6,3 @@ export { windowMock, windowServiceMock } from './lib/window-service/window.servi export { SubscriptionService } from './lib/subscription-service/subscription.service'; export * from './lib/utils.module'; - diff --git a/tsconfig.json b/tsconfig.json index d81baf8e..897c3d10 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,6 +20,10 @@ "dom" ], "paths": { + "forms": [ + "dist/forms/forms", + "dist/forms" + ], "images": [ "dist/images/images", "dist/images"