├── .github └── workflows │ └── nodejs.yml ├── LICENSE ├── README.md ├── old └── article.md └── workspace ├── .editorconfig ├── .gitignore ├── angular.json ├── package-lock.json ├── package.json ├── projects ├── date-value-accessor │ ├── karma.conf.js │ ├── ng-package.json │ ├── package.json │ ├── src │ │ ├── lib │ │ │ ├── date-value-accessor.module.ts │ │ │ ├── date-value-accessor.reactive.spec.ts │ │ │ ├── date-value-accessor.template.spec.ts │ │ │ ├── date-value-accessor.ts │ │ │ ├── default-value-accessor.template.spec.ts │ │ │ ├── iso-date-value-accessor.module.ts │ │ │ ├── iso-date-value-accessor.reactive.spec.ts │ │ │ ├── iso-date-value-accessor.template.spec.ts │ │ │ ├── iso-date-value-accessor.ts │ │ │ ├── local-date-value-accessor.module.ts │ │ │ ├── local-date-value-accessor.reactive.spec.ts │ │ │ ├── local-date-value-accessor.template.spec.ts │ │ │ ├── local-date-value-accessor.ts │ │ │ ├── local-iso-date-value-accessor.module.ts │ │ │ ├── local-iso-date-value-accessor.reactive.spec.ts │ │ │ ├── local-iso-date-value-accessor.template.spec.ts │ │ │ ├── local-iso-date-value-accessor.ts │ │ │ └── spec-utils.ts │ │ ├── public-api.ts │ │ └── test.ts │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.spec.json │ └── tslint.json └── demo │ ├── .browserslistrc │ ├── karma.conf.js │ ├── src │ ├── app │ │ ├── app.component.css │ │ ├── app.component.html │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── demo-with-dates │ │ │ ├── reactive-form │ │ │ │ ├── reactive-form.component.html │ │ │ │ └── reactive-form.component.ts │ │ │ └── template-driven-form │ │ │ │ ├── template-driven-form.component.html │ │ │ │ └── template-driven-form.component.ts │ │ ├── demo-with-strings │ │ │ ├── reactive-form-iso │ │ │ │ ├── reactive-form-iso.component.html │ │ │ │ └── reactive-form-iso.component.ts │ │ │ └── template-driven-form-iso │ │ │ │ ├── template-driven-form-iso.component.html │ │ │ │ └── template-driven-form-iso.component.ts │ │ └── shared │ │ │ ├── explanation-date-value-accessor.component.ts │ │ │ ├── explanation-default-value-accessor.component.ts │ │ │ ├── explanation-iso-date-value-accessor.component.ts │ │ │ ├── explanation-local-date-value-accessor.component.ts │ │ │ ├── explanation-local-iso-date-value-accessor.component.ts │ │ │ ├── release-with-iso-string.ts │ │ │ └── release.ts │ ├── assets │ │ ├── .gitkeep │ │ ├── ie11.png │ │ ├── reactive-does-not-work.gif │ │ ├── reactive-works-local.gif │ │ ├── reactive-works.gif │ │ ├── screenshot.png │ │ └── screenshot_simple.png │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── styles.css │ └── test.ts │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ └── tslint.json ├── tsconfig.json └── tslint.json /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Tests 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [14.x, 16.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: cd workspace && npm ci 28 | - run: cd workspace && npm run ng -- test --project date-value-accessor --watch=false --browsers=ChromeHeadless 29 | env: 30 | CI: true 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2021 Johannes Hoppe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DateValueAccessor for Angular 2 | [![NPM version][npm-image]][npm-url] 3 | [![Tests][tests-image]][tests-url] 4 | 5 | A set of `ControlValueAccessor`s for Angular to work with Browser's native date input elements. 6 | Now you can use `` directly with two-way data bindings (`ngModel`) as well as with reactive forms (`formControlName`/`formControl`). Choose freely between date objects and ISO-formatted strings. 7 | 8 | ## Releases 9 | 10 | The latest version is v3.0.0. Please refer to the [releases page](https://github.com/JohannesHoppe/angular-date-value-accessor/releases) to see which version of the package is compatible with which version of Angular and to find out what new features were rolled out at which point in time. 11 | 12 | ## Demo 13 | 14 | Here you can see the `DateValueAccessor` - the binding works! 15 | Changes to the input field are propagated to the model. 16 | 17 | ![Example: works](https://johanneshoppe.github.io/angular-date-value-accessor/assets/reactive-works.gif) 18 | 19 | And here you can see the `LocalDateValueAccessor` ⭐️. 20 | Please notice how the date is adjusted due to the German time zone (UTC+1) and how the time offset works. 21 | 22 | ![Example: works](https://johanneshoppe.github.io/angular-date-value-accessor/assets/reactive-works-local.gif) 23 | 24 | And this shows a not working form field (the default behaviour). 25 | Changes in the input field are propagated to the model, but unfortunately the date becomes a string which is not very useful for any further processing. 26 | 27 | ![Example: does not work](https://johanneshoppe.github.io/angular-date-value-accessor/assets/reactive-does-not-work.gif) 28 | 29 | **You can try out a full demo at the following page:** 30 | **→ http://johanneshoppe.github.io/angular-date-value-accessor/** 31 | 32 | 33 | ## Installation 34 | 35 | Download the package via NPM: 36 | 37 | ```bash 38 | npm install angular-date-value-accessor 39 | ``` 40 | 41 | 42 | ## UTC Time or Local Time 43 | When working with Dates in Javascript you either operate in UTC or Local Time. 44 | 45 | * UTC has no timezone offset. 46 | * Local Time depends on the host system time zone and offset. 47 | 48 | Javascript Dates support both the UTC and the Local Time representation. 49 | Depending on the requirements of your application you can choose from these Value Accessors: 50 | * [DateValueAccessor (UTC)](#datevalueaccessor-utc) 51 | * [LocalDateValueAccessor (Local Time) ⭐️](#localdatevalueaccessor-local-time) 52 | * [IsoDateValueAccessor (UTC as ISO 8601 string)](#isodatevalueaccessor-utc-as-iso-8601-string) 53 | * [LocalIsoDateValueAccessor (Local Time as ISO 8601 string)](#localisodatevalueaccessor-local-time-as-iso-8601-string) 54 | 55 | 56 | > **ℹ️ Hint:** Most UI component libraries like Angular Material, Kendo Angular, PrimeNG implement their DatePickers operating in Local Time. The Angular Date Pipe uses the Local Time representation of the Date Object by default, too. 57 | 58 | ## Installation & Usage 59 | 60 | You have to explicitly opt-in by adding one of these attribute directives to a HTML date input control: `useValueAsDate`, `useValueAsLocalDate`, `useValueAsIso` or `useValueAsLocalIso`. 61 | 62 | 63 | ### DateValueAccessor (UTC) 64 | 65 | The original `DateValueAccessor` operates in UTC (Coordinated Universal Time). 66 | The HTML date input will use the UTC representation of a given Date Object. 67 | When you select a date it will output an UTC date with the time set to 00:00 (UTC). 68 | 69 | **If you are unsure what to use, use the `LocalDateValueAccessor` and not the `DateValueAccessor`.** 70 | **Most users will expect the input field to correlate to their local clock.** 71 | 72 | Import the standalone directive *or* the NgModule: 73 | 74 | ```js 75 | // app.module.ts 76 | 77 | import { DateValueAccessor, DateValueAccessorModule } from 'angular-date-value-accessor'; 78 | 79 | @NgModule({ 80 | imports: [ 81 | DateValueAccessor 82 | // OR 83 | DateValueAccessorModule 84 | ] 85 | }) 86 | export class AppModule {} 87 | ``` 88 | 89 | Now you can apply the `useValueAsDate` to your date input controls: 90 | 91 | ```html 92 | 93 | 94 | 98 | 99 | OR 100 | 101 | 104 | ``` 105 | 106 | 107 | ### LocalDateValueAccessor (Local Time) ⭐️ 108 | 109 | The improved `LocalDateValueAccessor` operates in your Local Time. 110 | The HTML date input will use the Local Time representation of a given the Date Object. 111 | When you select a date it will output a Local Date with the time set to 00:00 (Local Time). 112 | 113 | Import the standalone directive *or* the NgModule: 114 | 115 | ```js 116 | // app.module.ts 117 | 118 | import { LocalDateValueAccessor, LocalDateValueAccessorModule } from 'angular-date-value-accessor'; 119 | 120 | @NgModule({ 121 | imports: [ 122 | LocalDateValueAccessor 123 | // OR 124 | LocalDateValueAccessorModule 125 | ] 126 | }) 127 | export class AppModule {} 128 | ``` 129 | 130 | Now you can apply the `useValueAsLocalDate` to your date input controls: 131 | 132 | ```html 133 | 134 | 135 | 139 | 140 | OR 141 | 142 | 145 | ``` 146 | 147 | 148 | ### IsoDateValueAccessor (UTC as ISO 8601 string) 149 | 150 | This directive gets and sets ISO 8601 formatted date strings in HTML date inputs. 151 | The handling of the dates is the same as for the `DateValueAccessor`. 152 | 153 | The `IsoDateValueAccessor` operates in UTC (Coordinated Universal Time). 154 | The HTML date input will use the UTC representation of a given ISO 8601 formatted date string. 155 | When you select a date it will output an ISO-formatted string with the time set to 00:00 (UTC). 156 | 157 | Import the standalone directive *or* the NgModule: 158 | 159 | ```js 160 | // app.module.ts 161 | import { IsoDateValueAccessor, IsoDateValueAccessorModule } from 'angular-date-value-accessor'; 162 | 163 | @NgModule({ 164 | imports: [ 165 | IsoDateValueAccessor 166 | // OR 167 | IsoDateValueAccessorModule 168 | ] 169 | }) 170 | export class AppModule { } 171 | ``` 172 | 173 | Now you can apply the `useValueAsIso` to your date input controls: 174 | 175 | ```html 176 | 177 | 178 | 182 | 183 | OR 184 | 185 | 188 | ``` 189 | 190 | 191 | ### LocalIsoDateValueAccessor (Local Time as ISO 8601 string) 192 | 193 | This directive gets and sets ISO 8601 formatted date strings in HTML date inputs. 194 | The handling of the dates is the same as for the `LocalDateValueAccessor`. 195 | 196 | The `LocalIsoDateValueAccessor` operates in your Local Time. 197 | The HTML date input will use the Local Time representation of a given ISO 8601 formatted date string. 198 | When you select a date it will output an ISO-formatted string with a time that equals to 00:00 (Local Time).
199 |
200 | Note: The timezone of the outputted string is always zero UTC offset, as denoted by the suffix "Z". 201 | 202 | Import the standalone directive *or* the NgModule: 203 | 204 | ```js 205 | // app.module.ts 206 | import { LocalIsoDateValueAccessor, LocalIsoDateValueAccessorModule } from 'angular-date-value-accessor'; 207 | 208 | @NgModule({ 209 | imports: [ 210 | LocalIsoDateValueAccessor 211 | // OR 212 | LocalIsoDateValueAccessorModule 213 | ] 214 | }) 215 | export class AppModule { } 216 | ``` 217 | 218 | Now you can apply the `useValueAsLocalIso` to your date input controls: 219 | 220 | ```html 221 | 222 | 223 | 227 | 228 | OR 229 | 230 | 233 | ``` 234 | 235 | 236 | ## License 237 | 238 | This code is published under the [MIT license](LICENSE). 239 | 240 | 241 | 242 | [npm-url]: https://npmjs.org/package/angular-date-value-accessor 243 | [npm-image]: https://badge.fury.io/js/angular-date-value-accessor.svg 244 | [tests-url]: https://github.com/JohannesHoppe/angular-date-value-accessor/actions?query=workflow%3ATests 245 | [tests-image]: https://github.com/JohannesHoppe/angular-date-value-accessor/workflows/Tests/badge.svg 246 | -------------------------------------------------------------------------------- /old/article.md: -------------------------------------------------------------------------------- 1 | # Angular 2: How to use date input controls with Angular Forms 2 | [![NPM version][npm-image]][npm-url] 3 | 4 | [![Screenshot](https://johanneshoppe.github.io/angular-date-value-accessor/assets/screenshot.png)](http://johanneshoppe.github.io/angular-date-value-accessor/) 5 | 6 | 7 | ## The problem 8 | 9 | Working with forms is pretty easy in Angular 2. 10 | You just need to decide between Template-Driven and Reactive Forms and you are ready to start with some bindings and validation. The following code shows a two-way data binding with `ngModel` against a property of type `string`: 11 | 12 | ``` 13 | 14 | ``` 15 | 16 | But there is one problem to tackle: models of type `Date`! 17 | You might wonder, because HTML5 date input controls are not working as expected: 18 | 19 | ``` 20 | 21 | ``` 22 | 23 | Even if `myBirtday` contains a valid date, the date input control is not rendering the value at all. 24 | In fact, we are supposed to set a string that is representing a full-date as defined in [RFC 3339](https://www.w3.org/TR/html-markup/references.html#refsRFC3339). The same string is written back to the model, when changes have been made, e.g. "2016-10-13". This behavior is specified in the [W3C HTML language reference for `inputEl.value`](https://www.w3.org/TR/html-markup/input.date.html#input.date.attrs.value). According to the specification, date input controls are based on strings. So what can we do to keep the Date type? 25 | 26 | ## A solution 27 | 28 | Let's review the possible solutions: 29 | 30 | 1. We could create a [custom form control](http://blog.thoughtram.io/angular/2016/07/27/custom-form-controls-in-angular-2.html). This would be a clean and extendable solution, but it might lead to more code than required for the given use-case. 31 | 32 | 2. We could convert the strings directly in our `@Component` as described [here](http://stackoverflow.com/a/37055451). But do we really want to bloat our "business code layer" with boilerplate code? 33 | 34 | 3. We could create a __custom value accessor__. The following article discusses this option. 35 | 36 | It turns out, that date input control has another, not that well-known property: [`inputEl.valueAsDate`](https://www.w3.org/TR/2012/WD-html5-20121025/common-input-element-apis.html#dom-input-valueasdate)! The `inputEl.valueAsDate` attribute represents the value (still a string) of the element, interpreted as a date. This is exactly what we need. Now we only need to convince Angular to use this property, instead of `inputEl.value`. 37 | 38 | Fortunately Angular 2 is very expansible here. FormControls (both template-driven and reactive) subscribe for values and write values via Directives that implement `ControlValueAccessor`. Take a look at the relevant method [selectValueAccessor](https://github.com/angular/angular/blob/2.1.0-beta.0/modules/%40angular/forms/src/directives/shared.ts#L140), which is used in all necessary directives. Normal input controls (e.g. ``) or textareas are handled by the [DefaultValueAccessor](https://github.com/angular/angular/blob/2.1.0-beta.0/modules/%40angular/forms/src/directives/default_value_accessor.ts). Another example is the [CheckboxValueAccessor](https://github.com/angular/angular/blob/2.1.0-beta.0/modules/%40angular/forms/src/directives/checkbox_value_accessor.ts) which is applied to checkbox input controls. 39 | 40 | The job isn't complicated at all. We just need to implement a new value accessor for date input controls. 41 | `DateValueAccessor` is a nice name: 42 | 43 | ```js 44 | // date-value-accessor.ts 45 | 46 | import { Directive, ElementRef, HostListener, Renderer, forwardRef } from '@angular/core'; 47 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; 48 | 49 | export const DATE_VALUE_ACCESSOR: any = { 50 | provide: NG_VALUE_ACCESSOR, 51 | useExisting: forwardRef(() => DateValueAccessor), 52 | multi: true 53 | }; 54 | 55 | /** 56 | * The accessor for writing a value and listening to changes on a date input element 57 | * 58 | * ### Example 59 | * `` 60 | */ 61 | @Directive({ 62 | selector: '[useValueAsDate]', 63 | providers: [DATE_VALUE_ACCESSOR] 64 | }) 65 | export class DateValueAccessor implements ControlValueAccessor { 66 | 67 | @HostListener('input', ['$event.target.valueAsDate']) onChange = (_: any) => { }; 68 | @HostListener('blur', []) onTouched = () => { }; 69 | 70 | constructor(private _renderer: Renderer, private _elementRef: ElementRef) { } 71 | 72 | writeValue(value: Date): void { 73 | this._renderer.setElementProperty(this._elementRef.nativeElement, 'valueAsDate', value); 74 | } 75 | 76 | registerOnChange(fn: (_: any) => void): void { this.onChange = fn; } 77 | registerOnTouched(fn: () => void): void { this.onTouched = fn; } 78 | 79 | setDisabledState(isDisabled: boolean): void { 80 | this._renderer.setElementProperty(this._elementRef.nativeElement, 'disabled', isDisabled); 81 | } 82 | } 83 | 84 | ``` 85 | 86 | We attach the `DateValueAccessor` to the multi-provider `DATE_VALUE_ACCESSOR`, so that [selectValueAccessor](https://github.com/angular/angular/blob/2.1.0-beta.0/modules/%40angular/forms/src/directives/shared.ts#L140) can find it. 87 | 88 | The only question is, which selector should be used. I decided for an opt-in solution. Here the DateValueAccessor selects on the attribute "useValueAsDate". 89 | 90 | ```html 91 | 92 | 93 | OR 94 | 95 | 96 | 97 | OR 98 | 99 | 100 | ``` 101 | 102 | It is also possible to fix the default implementation. The following selector would activate the feature magically. 103 | 104 | ```js 105 | // this selector changes the previous behavior silently and might break existing code 106 | selector: 'input[type=date][formControlName],input[type=date][formControl],input[type=date][ngModel]' 107 | ``` 108 | 109 | But please be aware, that this might break existing implementations that rely of the old behaviour. So I would go for the opt-in version! 110 | 111 | ## It's all on NPM 112 | 113 | For your convenience, I created the project [`angular-data-value-accessor`](https://github.com/JohannesHoppe/angular-date-value-accessor) on Github. 114 | There is also a NPM package available: 115 | 116 | 117 | ```bash 118 | npm install --save angular-date-value-accessor 119 | ``` 120 | 121 | Then import the module via NgModule: 122 | 123 | ```js 124 | // app.module.ts 125 | 126 | import { DateValueAccessorModule } from 'angular-date-value-accessor'; 127 | 128 | @NgModule({ 129 | imports: [ 130 | DateValueAccessorModule 131 | ] 132 | }) 133 | export class AppModule { } 134 | ``` 135 | 136 | ## Demo 137 | 138 | Of course, there is a demo at: 139 | http://johanneshoppe.github.io/angular-date-value-accessor/ 140 | 141 | [npm-url]: https://npmjs.org/package/angular-date-value-accessor 142 | [npm-image]: https://badge.fury.io/js/angular-date-value-accessor.svg -------------------------------------------------------------------------------- /workspace/.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 | -------------------------------------------------------------------------------- /workspace/.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 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.angular/cache 36 | /.sass-cache 37 | /connect.lock 38 | /coverage 39 | /libpeerconnection.log 40 | npm-debug.log 41 | yarn-error.log 42 | testem.log 43 | /typings 44 | 45 | # System Files 46 | .DS_Store 47 | Thumbs.db 48 | 49 | projects/date-value-accessor/README.md 50 | -------------------------------------------------------------------------------- /workspace/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "date-value-accessor": { 7 | "projectType": "library", 8 | "root": "projects/date-value-accessor", 9 | "sourceRoot": "projects/date-value-accessor/src", 10 | "prefix": "lib", 11 | "architect": { 12 | "build": { 13 | "builder": "@angular-devkit/build-angular:ng-packagr", 14 | "options": { 15 | "tsConfig": "projects/date-value-accessor/tsconfig.lib.json", 16 | "project": "projects/date-value-accessor/ng-package.json" 17 | }, 18 | "configurations": { 19 | "production": { 20 | "tsConfig": "projects/date-value-accessor/tsconfig.lib.prod.json" 21 | } 22 | } 23 | }, 24 | "test": { 25 | "builder": "@angular-devkit/build-angular:karma", 26 | "options": { 27 | "main": "projects/date-value-accessor/src/test.ts", 28 | "tsConfig": "projects/date-value-accessor/tsconfig.spec.json", 29 | "karmaConfig": "projects/date-value-accessor/karma.conf.js" 30 | } 31 | } 32 | } 33 | }, 34 | "demo": { 35 | "projectType": "application", 36 | "schematics": {}, 37 | "root": "projects/demo", 38 | "sourceRoot": "projects/demo/src", 39 | "prefix": "app", 40 | "architect": { 41 | "build": { 42 | "builder": "@angular-devkit/build-angular:browser", 43 | "options": { 44 | "outputPath": "dist/demo", 45 | "index": "projects/demo/src/index.html", 46 | "main": "projects/demo/src/main.ts", 47 | "polyfills": "projects/demo/src/polyfills.ts", 48 | "tsConfig": "projects/demo/tsconfig.app.json", 49 | "assets": [ 50 | "projects/demo/src/favicon.ico", 51 | "projects/demo/src/assets" 52 | ], 53 | "styles": [ 54 | "projects/demo/src/styles.css" 55 | ], 56 | "scripts": [], 57 | "vendorChunk": true, 58 | "extractLicenses": false, 59 | "buildOptimizer": false, 60 | "sourceMap": true, 61 | "optimization": false, 62 | "namedChunks": true 63 | }, 64 | "configurations": { 65 | "production": { 66 | "fileReplacements": [ 67 | { 68 | "replace": "projects/demo/src/environments/environment.ts", 69 | "with": "projects/demo/src/environments/environment.prod.ts" 70 | } 71 | ], 72 | "optimization": true, 73 | "outputHashing": "all", 74 | "sourceMap": false, 75 | "namedChunks": false, 76 | "extractLicenses": true, 77 | "vendorChunk": false, 78 | "buildOptimizer": true, 79 | "budgets": [ 80 | { 81 | "type": "initial", 82 | "maximumWarning": "2mb", 83 | "maximumError": "5mb" 84 | }, 85 | { 86 | "type": "anyComponentStyle", 87 | "maximumWarning": "6kb", 88 | "maximumError": "10kb" 89 | } 90 | ] 91 | } 92 | }, 93 | "defaultConfiguration": "" 94 | }, 95 | "serve": { 96 | "builder": "@angular-devkit/build-angular:dev-server", 97 | "options": { 98 | "browserTarget": "demo:build" 99 | }, 100 | "configurations": { 101 | "production": { 102 | "browserTarget": "demo:build:production" 103 | } 104 | } 105 | } 106 | } 107 | }} 108 | } 109 | -------------------------------------------------------------------------------- /workspace/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workspace", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve --project demo", 7 | "prebuild": "cd .. && npx copyfiles README.md workspace/projects/date-value-accessor", 8 | "publish-library": "npm run build && cd dist/date-value-accessor && npm publish", 9 | "publish-demo": "ng build --configuration production --project demo --base-href /angular-date-value-accessor/ && npx angular-cli-ghpages --dir dist/demo", 10 | "build": "ng build --configuration production --project date-value-accessor", 11 | "test": "ng test --project date-value-accessor --watch=false", 12 | "lint": "ng lint", 13 | "e2e": "ng e2e" 14 | }, 15 | "private": true, 16 | "dependencies": { 17 | "@angular/animations": "^14.2.7", 18 | "@angular/common": "^14.2.7", 19 | "@angular/compiler": "^14.2.7", 20 | "@angular/core": "^14.2.7", 21 | "@angular/forms": "^14.2.7", 22 | "@angular/platform-browser": "^14.2.7", 23 | "@angular/platform-browser-dynamic": "^14.2.7", 24 | "@angular/router": "^14.2.7", 25 | "configurable-date-input-polyfill": "^2.7.2", 26 | "rxjs": "~6.6.0", 27 | "tslib": "^2.0.0", 28 | "zone.js": "~0.11.4" 29 | }, 30 | "devDependencies": { 31 | "@angular-devkit/build-angular": "^14.2.6", 32 | "@angular/cli": "^14.2.6", 33 | "@angular/compiler-cli": "^14.2.7", 34 | "@types/jasmine": "~3.6.0", 35 | "@types/jasminewd2": "~2.0.3", 36 | "@types/node": "^12.11.1", 37 | "codelyzer": "^6.0.0", 38 | "jasmine-core": "~3.6.0", 39 | "jasmine-spec-reporter": "~5.0.0", 40 | "karma": "~6.3.4", 41 | "karma-chrome-launcher": "~3.1.0", 42 | "karma-coverage-istanbul-reporter": "~3.0.2", 43 | "karma-jasmine": "~4.0.0", 44 | "karma-jasmine-html-reporter": "^1.5.0", 45 | "ng-packagr": "^14.2.1", 46 | "ts-node": "~8.3.0", 47 | "tslint": "~6.1.0", 48 | "typescript": "~4.7.4" 49 | } 50 | } -------------------------------------------------------------------------------- /workspace/projects/date-value-accessor/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/date-value-accessor'), 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 | -------------------------------------------------------------------------------- /workspace/projects/date-value-accessor/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/date-value-accessor", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } -------------------------------------------------------------------------------- /workspace/projects/date-value-accessor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-date-value-accessor", 3 | "version": "3.0.0", 4 | "description": "A set of ControlValueAccessors for Angular to work with Browser's native date input elements. Now you can use with template-driven and reactive forms!", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/johanneshoppe/angular-date-value-accessor.git" 8 | }, 9 | "keywords": [ 10 | "angular", 11 | "forms", 12 | "date", 13 | "ControlValueAccessor", 14 | "DateValueAccessor", 15 | "LocalDateValueAccessor", 16 | "LocalIsoDateValueAccessor", 17 | "UTC", 18 | "Local Time" 19 | ], 20 | "author": { 21 | "name": "Johannes Hoppe", 22 | "email": "johannes.hoppe@haushoppe-its.de" 23 | }, 24 | "contributors": [ 25 | { 26 | "name": "Ferdinand Malcher", 27 | "email": "ferdinand@malcher.media" 28 | } 29 | ], 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/johanneshoppe/angular-date-value-accessor/issues" 33 | }, 34 | "homepage": "https://github.com/johanneshoppe/angular-date-value-accessor", 35 | "peerDependencies": { 36 | "@angular/common": ">= 14.0.0", 37 | "@angular/core": ">= 14.0.0" 38 | }, 39 | "dependencies": { 40 | "tslib": "^2.0.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /workspace/projects/date-value-accessor/src/lib/date-value-accessor.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { DateValueAccessor } from './date-value-accessor'; 3 | 4 | @NgModule({ 5 | imports: [DateValueAccessor], 6 | exports: [DateValueAccessor] 7 | }) 8 | export class DateValueAccessorModule { } 9 | -------------------------------------------------------------------------------- /workspace/projects/date-value-accessor/src/lib/date-value-accessor.reactive.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { FormControl } from '@angular/forms'; 3 | 4 | import { DateValueAccessor } from './date-value-accessor'; 5 | import { Context, dispatchInputEvent, setupReactiveForms } from './spec-utils'; 6 | 7 | @Component({ 8 | template: ` 9 |
10 | 11 |
` 12 | }) 13 | export class TestFormComponent { 14 | testDate = new FormControl(new Date('2020-01-01'), { nonNullable: true }) // Create UTC Date 15 | } 16 | 17 | describe('DateValueAccessor (reactive forms)', () => { 18 | 19 | let c: Context = {}; 20 | setupReactiveForms(c, TestFormComponent, DateValueAccessor); 21 | 22 | it('should reflect changes from the model to the input after init', () => { 23 | expect(c.inputElement.value).toBe('2020-01-01'); 24 | }); 25 | 26 | it('should reflect changes from the model to the input', () => { 27 | c.fixture.componentInstance.testDate.setValue(new Date('2021-11-15')); 28 | c.fixture.detectChanges(); 29 | expect(c.inputElement.value).toBe('2021-11-15'); 30 | }); 31 | 32 | it('should reflect changes from the input to the model', () => { 33 | dispatchInputEvent(c.inputElement, '2020-12-31'); 34 | 35 | expect(c.fixture.componentInstance.testDate.value).toEqual(jasmine.any(Date)); 36 | expect(c.fixture.componentInstance.testDate.value).toEqual(new Date('2020-12-31')); 37 | expect(c.fixture.componentInstance.testDate.value.getUTCDate()).toBe(31); 38 | expect(c.fixture.componentInstance.testDate.value.getUTCMonth()).toBe(11); 39 | expect(c.fixture.componentInstance.testDate.value.getUTCFullYear()).toBe(2020); 40 | expect(c.fixture.componentInstance.testDate.value.getUTCHours()).toBe(0); 41 | expect(c.fixture.componentInstance.testDate.value.getUTCMinutes()).toBe(0); 42 | }); 43 | 44 | it('should populate NULL for invalid dates', () => { 45 | dispatchInputEvent(c.inputElement, "NOT A DATE"); 46 | expect(c.fixture.componentInstance.testDate.value).toBeNull(); 47 | expect(c.inputElement.value).toBe(''); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /workspace/projects/date-value-accessor/src/lib/date-value-accessor.template.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { DateValueAccessor } from './date-value-accessor'; 4 | import { Context, dispatchInputEvent, setupTemplateDrivenForms } from './spec-utils'; 5 | 6 | @Component({ 7 | template: ` 8 |
9 | 10 |
` 11 | }) 12 | export class TestFormComponent { 13 | testDate = new Date('2020-01-01'); // Create UTC Date 14 | } 15 | 16 | describe('DateValueAccessor (template-driven forms)', () => { 17 | 18 | let c: Context = {}; 19 | setupTemplateDrivenForms(c, TestFormComponent, DateValueAccessor); 20 | 21 | it('should reflect changes from the model to the input after init', () => { 22 | expect(c.inputElement.value).toBe('2020-01-01'); 23 | }); 24 | 25 | it('should reflect changes from the model to the input', done => { 26 | c.fixture.componentInstance.testDate = new Date('2021-11-15'); 27 | c.fixture.detectChanges(); 28 | c.fixture.whenStable().then(() => { 29 | expect(c.inputElement.value).toBe('2021-11-15'); 30 | done(); 31 | }) 32 | }); 33 | 34 | it('should reflect changes from the input to the model', () => { 35 | dispatchInputEvent(c.inputElement, '2020-12-31'); 36 | 37 | expect(c.fixture.componentInstance.testDate).toEqual(jasmine.any(Date)); 38 | expect(c.fixture.componentInstance.testDate).toEqual(new Date('2020-12-31')); 39 | expect(c.fixture.componentInstance.testDate.getUTCDate()).toBe(31); 40 | expect(c.fixture.componentInstance.testDate.getUTCMonth()).toBe(11); 41 | expect(c.fixture.componentInstance.testDate.getUTCFullYear()).toBe(2020); 42 | expect(c.fixture.componentInstance.testDate.getUTCHours()).toBe(0); 43 | expect(c.fixture.componentInstance.testDate.getUTCMinutes()).toBe(0); 44 | }); 45 | 46 | it('should populate NULL for invalid dates', () => { 47 | dispatchInputEvent(c.inputElement, "NOT A DATE"); 48 | expect(c.fixture.componentInstance.testDate).toBeNull(); 49 | expect(c.inputElement.value).toBe(''); 50 | }); 51 | }); 52 | 53 | 54 | -------------------------------------------------------------------------------- /workspace/projects/date-value-accessor/src/lib/date-value-accessor.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, forwardRef, HostListener, Renderer2 } from '@angular/core'; 2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; 3 | 4 | /** 5 | * The accessor for writing a date object value and listening to changes on a date input element. 6 | * 7 | * ### Example 8 | * `` 9 | */ 10 | @Directive({ 11 | selector: '[useValueAsDate]', 12 | providers: [ 13 | { 14 | provide: NG_VALUE_ACCESSOR, 15 | useExisting: forwardRef(() => DateValueAccessor), 16 | multi: true 17 | } 18 | ], 19 | standalone: true 20 | }) 21 | export class DateValueAccessor implements ControlValueAccessor { 22 | 23 | @HostListener('input', ['$event.target.valueAsDate']) onChange = (_: any) => { }; 24 | @HostListener('blur', []) onTouched = () => { }; 25 | 26 | constructor(private renderer: Renderer2, private elementRef: ElementRef) { } 27 | 28 | writeValue(date?: Date): void { 29 | this.renderer.setProperty(this.elementRef.nativeElement, 'valueAsDate', date); 30 | } 31 | 32 | registerOnChange(fn: (_: any) => void): void { 33 | this.onChange = fn; 34 | } 35 | 36 | registerOnTouched(fn: () => void): void { 37 | this.onTouched = fn; 38 | } 39 | 40 | setDisabledState(isDisabled: boolean): void { 41 | this.renderer.setProperty(this.elementRef.nativeElement, 'disabled', isDisabled); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /workspace/projects/date-value-accessor/src/lib/default-value-accessor.template.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { Context, dispatchInputEvent, setupTemplateDrivenForms } from './spec-utils'; 4 | 5 | @Component({ 6 | template: ` 7 |
8 | 9 |
` 10 | }) 11 | export class TestFormComponent { 12 | testDate: Date | string = new Date('2020-01-01'); // Create UTC Date 13 | } 14 | 15 | // just to describe the default behaviour af the inbuild DefaultValueAccessor 16 | describe('DefaultValueAccessor (template-driven forms)', () => { 17 | 18 | let c: Context = {}; 19 | setupTemplateDrivenForms(c, TestFormComponent); 20 | 21 | it('should NOT reflect changes from the model to the input after init (when using dates)', () => { 22 | expect(c.fixture.componentInstance.testDate).toEqual(new Date('2020-01-01')); // nothing happens 23 | expect(c.inputElement.value).toBe(''); 24 | }); 25 | 26 | it('should reflect changes from the input to the model, BUT returns simple strings', () => { 27 | dispatchInputEvent(c.inputElement, '2020-12-31'); 28 | expect(c.fixture.componentInstance.testDate).toEqual('2020-12-31'); 29 | }); 30 | 31 | // All our value accessors have a different behaviour here 32 | // by design, they return NULL instead of an empty string 33 | // Default implementation in Angular: 34 | // 35 | // const normalizedValue = value == null ? '' : value; 36 | // 37 | // see: https://github.com/angular/angular/blob/539d720fcde2ba5094224abc6e7380f9f6d4828f/packages/forms/src/directives/default_value_accessor.ts#L104 38 | // 39 | // Q: Why NULL and not UNDEFINED? 40 | // A: input.valueAsDate also returns NULL on broken input values 41 | it('IMPORTANT: should populate EMPTY STRING for invalid dates', () => { 42 | dispatchInputEvent(c.inputElement, "NOT A DATE"); 43 | expect(c.fixture.componentInstance.testDate).toBe(''); // <-- !!!! 44 | expect(c.inputElement.value).toBe(''); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /workspace/projects/date-value-accessor/src/lib/iso-date-value-accessor.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { IsoDateValueAccessor } from './iso-date-value-accessor'; 3 | 4 | @NgModule({ 5 | imports: [IsoDateValueAccessor], 6 | exports: [IsoDateValueAccessor] 7 | }) 8 | export class IsoDateValueAccessorModule { } 9 | -------------------------------------------------------------------------------- /workspace/projects/date-value-accessor/src/lib/iso-date-value-accessor.reactive.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { FormControl } from '@angular/forms'; 3 | 4 | import { IsoDateValueAccessor } from './iso-date-value-accessor'; 5 | import { Context, dispatchInputEvent, setupReactiveForms } from './spec-utils'; 6 | 7 | @Component({ 8 | template: ` 9 |
10 | 11 |
` 12 | }) 13 | export class TestFormComponent { 14 | testDate = new FormControl(new Date('2020-01-01').toISOString(), { nonNullable: true }); // Create UTC Date 15 | } 16 | 17 | describe('IsoDateValueAccessor (reactive forms)', () => { 18 | 19 | let c: Context = {}; 20 | setupReactiveForms(c, TestFormComponent, IsoDateValueAccessor); 21 | 22 | it('should reflect changes from the model to the input after init', () => { 23 | expect(c.inputElement.value).toBe('2020-01-01'); 24 | }); 25 | 26 | it('should reflect changes from the model to the input', () => { 27 | c.fixture.componentInstance.testDate.setValue(new Date('2021-11-15').toISOString()); 28 | c.fixture.detectChanges(); 29 | expect(c.inputElement.value).toBe('2021-11-15'); 30 | }); 31 | 32 | it('should reflect changes from the input to the model', () => { 33 | dispatchInputEvent(c.inputElement, '2020-12-31'); 34 | expect(c.fixture.componentInstance.testDate.value).toBe('2020-12-31T00:00:00.000Z'); 35 | }); 36 | 37 | it('should populate NULL for invalid dates', () => { 38 | dispatchInputEvent(c.inputElement, "NOT A DATE"); 39 | expect(c.fixture.componentInstance.testDate.value).toBeNull(); 40 | expect(c.inputElement.value).toBe(''); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /workspace/projects/date-value-accessor/src/lib/iso-date-value-accessor.template.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { IsoDateValueAccessor } from './iso-date-value-accessor'; 4 | import { Context, dispatchInputEvent, setupTemplateDrivenForms } from './spec-utils'; 5 | 6 | @Component({ 7 | template: ` 8 |
9 | 10 |
` 11 | }) 12 | export class TestFormComponent { 13 | testDate = new Date('2020-01-01').toISOString(); // Create UTC Date 14 | } 15 | 16 | describe('IsoDateValueAccessor (template-driven forms)', () => { 17 | 18 | let c: Context = {}; 19 | setupTemplateDrivenForms(c, TestFormComponent, IsoDateValueAccessor); 20 | 21 | it('should reflect changes from the model to the input after init', () => { 22 | expect(c.inputElement.value).toBe('2020-01-01'); 23 | }); 24 | 25 | it('should reflect changes from the model to the input', done => { 26 | c.fixture.componentInstance.testDate = new Date('2021-11-15').toISOString(); 27 | c.fixture.detectChanges(); 28 | c.fixture.whenStable().then(() => { 29 | expect(c.inputElement.value).toBe('2021-11-15'); 30 | done(); 31 | }) 32 | }); 33 | 34 | it('should reflect changes from the input to the model', () => { 35 | dispatchInputEvent(c.inputElement, '2020-12-31'); 36 | expect(c.fixture.componentInstance.testDate).toBe('2020-12-31T00:00:00.000Z'); 37 | }); 38 | 39 | it('should populate NULL for invalid dates', () => { 40 | dispatchInputEvent(c.inputElement, "NOT A DATE"); 41 | expect(c.fixture.componentInstance.testDate).toBeNull(); 42 | expect(c.inputElement.value).toBe(''); 43 | }); 44 | }); 45 | 46 | 47 | -------------------------------------------------------------------------------- /workspace/projects/date-value-accessor/src/lib/iso-date-value-accessor.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, forwardRef, HostBinding, HostListener, Renderer2 } from '@angular/core'; 2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; 3 | 4 | /** 5 | * The accessor for writing an iso-formatted string value and listening to changes on a date input element. 6 | * 7 | * ### Example 8 | * `` 9 | */ 10 | @Directive({ 11 | selector: '[useValueAsIso]', 12 | providers: [ 13 | { 14 | provide: NG_VALUE_ACCESSOR, 15 | useExisting: forwardRef(() => IsoDateValueAccessor), 16 | multi: true 17 | } 18 | ], 19 | standalone: true 20 | }) 21 | export class IsoDateValueAccessor implements ControlValueAccessor { 22 | 23 | @HostListener('input', ['$event.target.valueAsDate']) onInput = (date?: Date) => { 24 | const isoString = date ? date.toISOString() : null; 25 | this.onChange(isoString); 26 | } 27 | onChange: any = () => {}; 28 | 29 | @HostListener('blur', []) onTouched = () => { }; 30 | 31 | constructor(private renderer: Renderer2, private elementRef: ElementRef) { } 32 | 33 | writeValue(isoString?: string): void { 34 | const date = isoString ? new Date(isoString) : null; 35 | this.renderer.setProperty(this.elementRef.nativeElement, 'valueAsDate', date); 36 | } 37 | 38 | registerOnChange(fn: (_: any) => void): void { 39 | this.onChange = fn; 40 | } 41 | 42 | registerOnTouched(fn: any): void { 43 | this.onTouched = fn; 44 | } 45 | 46 | setDisabledState(isDisabled: boolean): void { 47 | this.renderer.setProperty(this.elementRef.nativeElement, 'disabled', isDisabled); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /workspace/projects/date-value-accessor/src/lib/local-date-value-accessor.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { LocalDateValueAccessor } from './local-date-value-accessor'; 3 | 4 | @NgModule({ 5 | imports: [LocalDateValueAccessor], 6 | exports: [LocalDateValueAccessor] 7 | }) 8 | export class LocalDateValueAccessorModule { } 9 | -------------------------------------------------------------------------------- /workspace/projects/date-value-accessor/src/lib/local-date-value-accessor.reactive.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { FormControl } from '@angular/forms'; 3 | 4 | import { LocalDateValueAccessor } from './local-date-value-accessor'; 5 | import { Context, dispatchInputEvent, setupReactiveForms } from './spec-utils'; 6 | 7 | @Component({ 8 | template: ` 9 |
10 | 11 |
` 12 | }) 13 | export class TestFormComponent { 14 | testDate = new FormControl(new Date(2020, 11, 8), { nonNullable: true }); // Create LOCAL Date, HINT: Everything except the day is 0 based! 15 | } 16 | 17 | describe('LocalDateValueAccessor (reactive forms)', () => { 18 | 19 | let c: Context = {}; 20 | setupReactiveForms(c, TestFormComponent, LocalDateValueAccessor); 21 | 22 | it('should reflect changes from the model to the input after init', () => { 23 | expect(c.inputElement.value).toBe('2020-12-08'); 24 | }); 25 | 26 | it('should reflect changes from the model to the input', () => { 27 | c.fixture.componentInstance.testDate.setValue(new Date(2021, 10, 15)); 28 | c.fixture.detectChanges(); 29 | expect(c.inputElement.value).toBe('2021-11-15'); 30 | }); 31 | 32 | it('should reflect changes from the input to the model', () => { 33 | dispatchInputEvent(c.inputElement, '2020-12-31'); 34 | 35 | expect(c.fixture.componentInstance.testDate.value).toEqual(jasmine.any(Date)); 36 | expect(c.fixture.componentInstance.testDate.value).toEqual(new Date(2020, 11, 31)); 37 | expect(c.fixture.componentInstance.testDate.value.getDate()).toBe(31); 38 | expect(c.fixture.componentInstance.testDate.value.getMonth()).toBe(11); 39 | expect(c.fixture.componentInstance.testDate.value.getFullYear()).toBe(2020); 40 | expect(c.fixture.componentInstance.testDate.value.getHours()).toBe(0); 41 | expect(c.fixture.componentInstance.testDate.value.getMinutes()).toBe(0); 42 | }); 43 | 44 | it('should populate NULL for invalid dates', () => { 45 | dispatchInputEvent(c.inputElement, "NOT A DATE"); 46 | expect(c.fixture.componentInstance.testDate.value).toBeNull(); 47 | expect(c.inputElement.value).toBe(''); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /workspace/projects/date-value-accessor/src/lib/local-date-value-accessor.template.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { waitForAsync } from '@angular/core/testing'; 3 | 4 | import { LocalDateValueAccessor } from './local-date-value-accessor'; 5 | import { Context, dispatchInputEvent, setupTemplateDrivenForms } from './spec-utils'; 6 | 7 | @Component({ 8 | template: ` 9 |
10 | 11 |
` 12 | }) 13 | export class TestFormComponent { 14 | testDate: Date = new Date(2020, 11, 8); // Create LOCAL Date, HINT: Everything except the day is 0 based! 15 | } 16 | 17 | describe('LocalDateValueAccessor (template-driven forms)', () => { 18 | 19 | let c: Context = {}; 20 | setupTemplateDrivenForms(c, TestFormComponent, LocalDateValueAccessor); 21 | 22 | it('should reflect changes from the model to the input after init', () => { 23 | expect(c.inputElement.value).toBe('2020-12-08'); 24 | }); 25 | 26 | it('should reflect changes from the model to the input', done => { 27 | c.fixture.componentInstance.testDate = new Date(2021, 10, 15); 28 | c.fixture.detectChanges(); 29 | c.fixture.whenStable().then(() => { 30 | expect(c.inputElement.value).toBe('2021-11-15'); 31 | done(); 32 | }) 33 | }); 34 | 35 | it('should reflect changes from the input to the model', () => { 36 | dispatchInputEvent(c.inputElement, '2020-12-31'); 37 | 38 | expect(c.fixture.componentInstance.testDate).toEqual(jasmine.any(Date)); 39 | expect(c.fixture.componentInstance.testDate).toEqual(new Date(2020, 11, 31)); 40 | expect(c.fixture.componentInstance.testDate.getDate()).toBe(31); 41 | expect(c.fixture.componentInstance.testDate.getMonth()).toBe(11); 42 | expect(c.fixture.componentInstance.testDate.getFullYear()).toBe(2020); 43 | expect(c.fixture.componentInstance.testDate.getHours()).toBe(0); 44 | expect(c.fixture.componentInstance.testDate.getMinutes()).toBe(0); 45 | }); 46 | 47 | it('should populate NULL for invalid dates', () => { 48 | dispatchInputEvent(c.inputElement, "NOT A DATE"); 49 | expect(c.fixture.componentInstance.testDate).toBeNull(); 50 | expect(c.inputElement.value).toBe(''); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /workspace/projects/date-value-accessor/src/lib/local-date-value-accessor.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, forwardRef, HostListener, Renderer2 } from '@angular/core'; 2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; 3 | 4 | /** 5 | * The accessor for writing a value and listening to changes on a date input element in local time. 6 | * 7 | * ### Example 8 | * `` 9 | * 10 | * See also: 11 | * What is the correct way to set and get HTMLInputElement.valueAsDate using local Dates? 12 | * https://stackoverflow.com/a/53033442 13 | */ 14 | @Directive({ 15 | selector: '[useValueAsLocalDate]', 16 | providers: [ 17 | { 18 | provide: NG_VALUE_ACCESSOR, 19 | useExisting: forwardRef(() => LocalDateValueAccessor), 20 | multi: true 21 | } 22 | ], 23 | standalone: true 24 | }) 25 | export class LocalDateValueAccessor implements ControlValueAccessor { 26 | 27 | @HostListener('input', ['$event.target.valueAsDate']) onInput = (date?: Date) => { 28 | // convert to LOCAL Date, time is set to 00:00 in LOCAL time 29 | const localDate = date ? new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()) : null; 30 | this.onChange(localDate); 31 | } 32 | onChange: any = () => {}; 33 | 34 | @HostListener('blur', []) onTouched = () => { }; 35 | 36 | constructor(private renderer: Renderer2, private elementRef: ElementRef) { } 37 | 38 | writeValue(date?: Date): void { 39 | // convert to UTC Date, time is set to 00:00 in UTC time 40 | const utcDate = date ? new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())) : null; 41 | this.renderer.setProperty(this.elementRef.nativeElement, 'valueAsDate', utcDate); 42 | } 43 | 44 | registerOnChange(fn: (_: any) => void): void { 45 | this.onChange = fn; 46 | } 47 | 48 | registerOnTouched(fn: () => void): void { 49 | this.onTouched = fn; 50 | } 51 | 52 | setDisabledState(isDisabled: boolean): void { 53 | this.renderer.setProperty(this.elementRef.nativeElement, 'disabled', isDisabled); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /workspace/projects/date-value-accessor/src/lib/local-iso-date-value-accessor.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { LocalIsoDateValueAccessor } from './local-iso-date-value-accessor'; 3 | 4 | @NgModule({ 5 | imports: [LocalIsoDateValueAccessor], 6 | exports: [LocalIsoDateValueAccessor] 7 | }) 8 | export class LocalIsoDateValueAccessorModule { } 9 | -------------------------------------------------------------------------------- /workspace/projects/date-value-accessor/src/lib/local-iso-date-value-accessor.reactive.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { FormControl } from '@angular/forms'; 3 | 4 | import { LocalIsoDateValueAccessor } from './local-iso-date-value-accessor'; 5 | import { Context, dispatchInputEvent, setupReactiveForms } from './spec-utils'; 6 | 7 | @Component({ 8 | template: ` 9 |
10 | 11 |
` 12 | }) 13 | export class TestFormComponent { 14 | testDate = new FormControl(new Date(2020, 11, 8).toISOString(), { nonNullable: true }); // Create LOCAL Date, HINT: Everything except the day is 0 based! 15 | } 16 | 17 | describe('LocalIsoDateValueAccessor (reactive forms)', () => { 18 | 19 | let c: Context = {}; 20 | setupReactiveForms(c, TestFormComponent, LocalIsoDateValueAccessor); 21 | 22 | it('should reflect changes from the model to the input after init', () => { 23 | expect(c.inputElement.value).toBe('2020-12-08'); 24 | }); 25 | 26 | it('should reflect changes from the model to the input', () => { 27 | c.fixture.componentInstance.testDate.setValue(new Date(2021, 10, 15).toISOString()); 28 | c.fixture.detectChanges(); 29 | expect(c.inputElement.value).toBe('2021-11-15'); 30 | }); 31 | 32 | it('should reflect changes from the input to the model', () => { 33 | dispatchInputEvent(c.inputElement, '2020-12-31'); 34 | expect(c.fixture.componentInstance.testDate.value).toBe(new Date(2020, 11, 31).toISOString()); 35 | }); 36 | 37 | it('should populate NULL for invalid dates', () => { 38 | dispatchInputEvent(c.inputElement, "NOT A DATE"); 39 | expect(c.fixture.componentInstance.testDate.value).toBeNull(); 40 | expect(c.inputElement.value).toBe(''); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /workspace/projects/date-value-accessor/src/lib/local-iso-date-value-accessor.template.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { LocalIsoDateValueAccessor } from './local-iso-date-value-accessor'; 4 | import { Context, dispatchInputEvent, setupTemplateDrivenForms } from './spec-utils'; 5 | 6 | @Component({ 7 | template: ` 8 |
9 | 10 |
` 11 | }) 12 | export class TestFormComponent { 13 | testDate = new Date(2020, 11, 8).toISOString(); // Create LOCAL Date, HINT: Everything except the day is 0 based! 14 | } 15 | 16 | describe('LocalIsoDateValueAccessor (template-driven forms)', () => { 17 | 18 | let c: Context = {}; 19 | setupTemplateDrivenForms(c, TestFormComponent, LocalIsoDateValueAccessor); 20 | 21 | it('should reflect changes from the model to the input after init', () => { 22 | expect(c.inputElement.value).toBe('2020-12-08'); 23 | }); 24 | 25 | it('should reflect changes from the model to the input', done => { 26 | c.fixture.componentInstance.testDate = new Date(2021, 10, 15).toISOString(); 27 | c.fixture.detectChanges(); 28 | c.fixture.whenStable().then(() => { 29 | expect(c.inputElement.value).toBe('2021-11-15'); 30 | done(); 31 | }) 32 | }); 33 | 34 | it('should reflect changes from the input to the model', () => { 35 | dispatchInputEvent(c.inputElement, '2020-12-31'); 36 | expect(c.fixture.componentInstance.testDate).toBe(new Date(2020, 11, 31).toISOString()); 37 | }); 38 | 39 | it('should populate NULL for invalid dates', () => { 40 | dispatchInputEvent(c.inputElement, "NOT A DATE"); 41 | expect(c.fixture.componentInstance.testDate).toBeNull(); 42 | expect(c.inputElement.value).toBe(''); 43 | }); 44 | }); 45 | 46 | 47 | -------------------------------------------------------------------------------- /workspace/projects/date-value-accessor/src/lib/local-iso-date-value-accessor.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, forwardRef, HostBinding, HostListener, Renderer2 } from '@angular/core'; 2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; 3 | 4 | /** 5 | * The accessor for writing an iso-formatted string and listening to changes on a date input element. 6 | * 7 | * ### Example 8 | * `` 9 | * 10 | * See also: 11 | * What is the correct way to set and get HTMLInputElement.valueAsDate using local Dates? 12 | * https://stackoverflow.com/a/53033442 13 | */ 14 | @Directive({ 15 | selector: '[useValueAsLocalIso]', 16 | providers: [ 17 | { 18 | provide: NG_VALUE_ACCESSOR, 19 | useExisting: forwardRef(() => LocalIsoDateValueAccessor), 20 | multi: true 21 | } 22 | ], 23 | standalone: true 24 | }) 25 | export class LocalIsoDateValueAccessor implements ControlValueAccessor { 26 | 27 | @HostListener('input', ['$event.target.valueAsDate']) onInput = (date?: Date) => { 28 | // convert to LOCAL Date, time is set to 00:00 in LOCAL time 29 | const localDate = date ? new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()) : null; 30 | const isoString = localDate ? localDate.toISOString() : null; 31 | this.onChange(isoString); 32 | } 33 | onChange: any = () => {}; 34 | 35 | @HostListener('blur', []) onTouched = () => { }; 36 | 37 | constructor(private renderer: Renderer2, private elementRef: ElementRef) { } 38 | 39 | writeValue(isoString?: string): void { 40 | const date = isoString ? new Date(isoString) : null; 41 | // convert to UTC Date, time is set to 00:00 in UTC time 42 | const utcDate = date ? new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())) : null; 43 | this.renderer.setProperty(this.elementRef.nativeElement, 'valueAsDate', utcDate); 44 | } 45 | 46 | registerOnChange(fn: (_: any) => void): void { 47 | this.onChange = fn; 48 | } 49 | 50 | registerOnTouched(fn: () => void): void { 51 | this.onTouched = fn; 52 | } 53 | 54 | setDisabledState(isDisabled: boolean): void { 55 | this.renderer.setProperty(this.elementRef.nativeElement, 'disabled', isDisabled); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /workspace/projects/date-value-accessor/src/lib/spec-utils.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@angular/core'; 2 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | 5 | 6 | export function dispatchInputEvent(inputElement: HTMLInputElement, text: string): void { 7 | inputElement.value = text; 8 | inputElement.dispatchEvent(new Event('input')); 9 | } 10 | 11 | export interface Context { 12 | fixture?: ComponentFixture; 13 | inputElement?: HTMLInputElement; 14 | } 15 | 16 | export function setupTemplateDrivenForms( 17 | context: Context, 18 | testComponent: Type, 19 | acessor?: TAcess) { 20 | 21 | beforeEach(waitForAsync(() => { 22 | TestBed.configureTestingModule({ 23 | declarations: [testComponent], 24 | imports: [ 25 | FormsModule, 26 | ...(acessor ? [acessor] : []) 27 | ], 28 | teardown: { destroyAfterEach: false } 29 | }) 30 | .compileComponents(); 31 | })); 32 | 33 | beforeEach(() => { 34 | context.fixture = TestBed.createComponent(testComponent); 35 | }); 36 | 37 | beforeEach(waitForAsync(() => { 38 | // https://stackoverflow.com/questions/39582707/updating-input-html-field-from-within-an-angular-2-test 39 | context.fixture.detectChanges(); 40 | context.fixture.whenStable(); 41 | })); 42 | 43 | beforeEach(() => context.inputElement = context.fixture.nativeElement.querySelector('input')); 44 | } 45 | 46 | export function setupReactiveForms( 47 | context: Context, 48 | testComponent: Type, 49 | acessor?: TAcess) { 50 | 51 | beforeEach(waitForAsync(() => { 52 | TestBed.configureTestingModule({ 53 | declarations: [testComponent], 54 | imports: [ 55 | ReactiveFormsModule, 56 | ...(acessor ? [acessor] : []) 57 | ], 58 | teardown: { destroyAfterEach: false } 59 | }) 60 | .compileComponents(); 61 | })); 62 | 63 | beforeEach(() => { 64 | context.fixture = TestBed.createComponent(testComponent); 65 | context.fixture.detectChanges(); 66 | }); 67 | 68 | beforeEach(() => context.inputElement = context.fixture.nativeElement.querySelector('input')); 69 | } 70 | -------------------------------------------------------------------------------- /workspace/projects/date-value-accessor/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of the date-value-accessor 3 | */ 4 | 5 | export * from './lib/date-value-accessor'; 6 | export * from './lib/date-value-accessor.module'; 7 | export * from './lib/local-date-value-accessor'; 8 | export * from './lib/local-date-value-accessor.module'; 9 | 10 | export * from './lib/iso-date-value-accessor'; 11 | export * from './lib/iso-date-value-accessor.module'; 12 | export * from './lib/local-iso-date-value-accessor'; 13 | export * from './lib/local-iso-date-value-accessor.module'; 14 | -------------------------------------------------------------------------------- /workspace/projects/date-value-accessor/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js'; 4 | import 'zone.js/testing'; 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { 7 | BrowserDynamicTestingModule, 8 | platformBrowserDynamicTesting 9 | } from '@angular/platform-browser-dynamic/testing'; 10 | 11 | declare const require: { 12 | context(path: string, deep?: boolean, filter?: RegExp): { 13 | keys(): string[]; 14 | (id: string): T; 15 | }; 16 | }; 17 | 18 | // First, initialize the Angular testing environment. 19 | getTestBed().initTestEnvironment( 20 | BrowserDynamicTestingModule, 21 | platformBrowserDynamicTesting() 22 | ); 23 | // Then we find all the tests. 24 | const context = require.context('./', true, /\.spec\.ts$/); 25 | // And load the modules. 26 | context.keys().map(context); 27 | -------------------------------------------------------------------------------- /workspace/projects/date-value-accessor/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/lib", 6 | "target": "es2020", 7 | "declaration": true, 8 | "declarationMap": true, 9 | "inlineSources": true, 10 | "types": [], 11 | "lib": [ 12 | "dom", 13 | "es2018" 14 | ] 15 | }, 16 | "angularCompilerOptions": { 17 | "skipTemplateCodegen": true, 18 | "strictMetadataEmit": true, 19 | "enableResourceInlining": true 20 | }, 21 | "exclude": [ 22 | "src/test.ts", 23 | "**/*.spec.ts" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /workspace/projects/date-value-accessor/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /workspace/projects/date-value-accessor/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /workspace/projects/date-value-accessor/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "lib", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "lib", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /workspace/projects/demo/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line. 18 | IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. 19 | -------------------------------------------------------------------------------- /workspace/projects/demo/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/demo'), 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 | -------------------------------------------------------------------------------- /workspace/projects/demo/src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohannesHoppe/angular-date-value-accessor/8a4caeb45bdacc45c3b523106f6ac42dc0389ca2/workspace/projects/demo/src/app/app.component.css -------------------------------------------------------------------------------- /workspace/projects/demo/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |

DateValueAccessor Demo

2 | 3 |

4 | This project knows a number of different value accessors. 5 |

6 | 7 |
    8 |
  1. If you want to use date objects, select the first option.
  2. 9 |
  3. If you want to work with ISO-formatted strings instead, select the second option.
  4. 10 |
11 | 12 |
13 | 1. Examples for working with date objects (click to show) 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 |
22 | 2. Examples for working with ISO-formatted strings (click to show) 23 | 24 | 25 | 26 | 27 |
28 | -------------------------------------------------------------------------------- /workspace/projects/demo/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(waitForAsync(() => { 6 | TestBed.configureTestingModule({ 7 | declarations: [ 8 | AppComponent 9 | ], 10 | }).compileComponents(); 11 | })); 12 | 13 | it('should create the app', () => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.componentInstance; 16 | expect(app).toBeTruthy(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /workspace/projects/demo/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html' 6 | }) 7 | export class AppComponent { 8 | 9 | 10 | } 11 | -------------------------------------------------------------------------------- /workspace/projects/demo/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { IsoDateValueAccessorModule } from 'projects/date-value-accessor/src/lib/iso-date-value-accessor.module'; 5 | import { 6 | DateValueAccessorModule, 7 | LocalDateValueAccessorModule, 8 | LocalIsoDateValueAccessorModule, 9 | } from 'projects/date-value-accessor/src/public-api'; 10 | 11 | import { AppComponent } from './app.component'; 12 | import { ReactiveFormComponent } from './demo-with-dates/reactive-form/reactive-form.component'; 13 | import { TemplateDrivenFormComponent } from './demo-with-dates/template-driven-form/template-driven-form.component'; 14 | import { ReactiveFormIsoComponent } from './demo-with-strings/reactive-form-iso/reactive-form-iso.component'; 15 | import { 16 | TemplateDrivenFormIsoComponent, 17 | } from './demo-with-strings/template-driven-form-iso/template-driven-form-iso.component'; 18 | import { ExplanationDateValueAccessorComponent } from './shared/explanation-date-value-accessor.component'; 19 | import { ExplanationDefaultValueAccessorComponent } from './shared/explanation-default-value-accessor.component'; 20 | import { ExplanationIsoDateValueAccessorComponent } from './shared/explanation-iso-date-value-accessor.component'; 21 | import { ExplanationLocalDateValueAccessorComponent } from './shared/explanation-local-date-value-accessor.component'; 22 | import { ExplanationLocalIsoDateValueAccessorComponent } from './shared/explanation-local-iso-date-value-accessor.component'; 23 | 24 | 25 | @NgModule({ 26 | declarations: [ 27 | AppComponent, 28 | TemplateDrivenFormComponent, 29 | ReactiveFormComponent, 30 | TemplateDrivenFormIsoComponent, 31 | ReactiveFormIsoComponent, 32 | ExplanationDateValueAccessorComponent, 33 | ExplanationLocalDateValueAccessorComponent, 34 | ExplanationDefaultValueAccessorComponent, 35 | ExplanationIsoDateValueAccessorComponent, 36 | ExplanationLocalIsoDateValueAccessorComponent 37 | ], 38 | imports: [ 39 | BrowserModule, 40 | FormsModule, 41 | ReactiveFormsModule, 42 | DateValueAccessorModule, 43 | LocalDateValueAccessorModule, 44 | IsoDateValueAccessorModule, 45 | LocalIsoDateValueAccessorModule 46 | ], 47 | bootstrap: [AppComponent] 48 | }) 49 | export class AppModule { } 50 | -------------------------------------------------------------------------------- /workspace/projects/demo/src/app/demo-with-dates/reactive-form/reactive-form.component.html: -------------------------------------------------------------------------------- 1 |

Reactive Forms

2 | 3 | 4 | 5 |
6 |

DateValueAccessor

7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 |
19 | <input type="text"
20 |   formControlName="version">
21 | 
22 | <input type="date"
23 |   formControlName="releaseDate"
24 |   useValueAsDate>
25 | 
26 | 27 |
28 | DEBUG:
29 | {{ demoDateValue | json }}
30 | typeof(releaseDate): {{ typeof(demoDateValue.releaseDate) }}
31 | 
32 |
33 | 34 |
35 | 36 | 37 | 38 |
39 |

LocalDateValueAccessor

40 | 41 |
42 | 43 | 44 | 45 | 46 | 47 |
48 | 49 | 50 | 51 |
52 | <input type="text"
53 |   formControlName="version">
54 | 
55 | <input type="date"
56 |   formControlName="releaseDate"
57 |   useValueAsLocalDate>
58 | 
59 | 60 |
61 | DEBUG:
62 | {{ demoLocalDateValue | json }}
63 | typeof(releaseDate): {{ typeof(demoLocalDateValue.releaseDate) }}
64 | 
65 |
66 | 67 |
68 | 69 | 70 | 71 |
72 |

DefaultValueAccessor
(does not work)

73 | 74 |
75 | 76 | 77 | 78 | 79 | 80 |
81 | 82 | 83 | 84 |
85 | <input type="text"
86 |   formControlName="version">
87 | 
88 | <input type="date"
89 |   formControlName="releaseDate">
90 | 
91 | 92 |
93 | DEBUG:
94 | {{ demoDefault | json }}
95 | typeof(releaseDate): {{ typeof(demoDefault.releaseDate) }}
96 | 
97 |
98 | -------------------------------------------------------------------------------- /workspace/projects/demo/src/app/demo-with-dates/reactive-form/reactive-form.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { FormControl, FormGroup } from '@angular/forms'; 3 | 4 | import { Release } from '../../shared/release'; 5 | 6 | @Component({ 7 | selector: 'app-reactive-form', 8 | templateUrl: './reactive-form.component.html' 9 | }) 10 | export class ReactiveFormComponent { 11 | 12 | demoDateValue = new Release('2.0.0', new Date('2020-01-01')); // UTC 13 | demoLocalDateValue = new Release('3.0.0', new Date(2020, 0, 1)); // with offset 14 | demoDefault = new Release('1.5.8', new Date('2016-07-22')); // UTC 15 | 16 | myFormDateValue = new FormGroup({ 17 | version: new FormControl(this.demoDateValue.version, { nonNullable: true }), 18 | releaseDate: new FormControl(this.demoDateValue.releaseDate, { nonNullable: true }) 19 | }); 20 | 21 | myFormLocalDateValue = new FormGroup({ 22 | version: new FormControl(this.demoLocalDateValue.version, { nonNullable: true }), 23 | releaseDate: new FormControl(this.demoLocalDateValue.releaseDate, { nonNullable: true }) 24 | }); 25 | 26 | myFormDefault = new FormGroup({ 27 | version: new FormControl(this.demoDefault.version, { nonNullable: true }), 28 | releaseDate: new FormControl(this.demoDefault.releaseDate, { nonNullable: true }) 29 | }); 30 | 31 | constructor() { 32 | this.myFormDateValue.valueChanges.subscribe(values => this.demoDateValue = new Release(values.version, values.releaseDate)); 33 | this.myFormLocalDateValue.valueChanges.subscribe(values => this.demoLocalDateValue = new Release(values.version, values.releaseDate)); 34 | this.myFormDefault.valueChanges.subscribe(values => this.demoDefault = new Release(values.version, values.releaseDate)); 35 | } 36 | 37 | toggle(formGroup: FormGroup) { 38 | const control = formGroup.get('releaseDate'); 39 | if (control.enabled) { 40 | control.disable(); 41 | } else { 42 | control.enable(); 43 | } 44 | } 45 | 46 | typeof(obj: any) { 47 | return typeof obj; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /workspace/projects/demo/src/app/demo-with-dates/template-driven-form/template-driven-form.component.html: -------------------------------------------------------------------------------- 1 |

Template-Driven Forms

2 | 3 | 4 | 5 |
6 |

DateValueAccessor

7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 |
 19 | <input type="text"
 20 |        name="version"
 21 |        [(ngModel)]="demoDateValue.version">
 22 | 
 23 | <input type="date"
 24 |        name="releaseDate"
 25 |        [(ngModel)]="demoDateValue.releaseDate"
 26 |        useValueAsDate>
 27 | 
28 | 29 |
 30 | DEBUG:
 31 | {{ demoDateValue | json }}
 32 | typeof(releaseDate): {{ typeof(demoDateValue.releaseDate) }}
 33 | 
34 |
35 | 36 |
37 | 38 | 39 | 40 |
41 |

LocalDateValueAccessor

42 | 43 |
44 | 45 | 46 | 47 | 48 | 49 |
50 | 51 | 52 | 53 |
 54 | <input type="text"
 55 |        name="version"
 56 |        [(ngModel)]="demoLocalDateValue.version">
 57 | 
 58 | <input type="date"
 59 |        name="releaseDate"
 60 |        [(ngModel)]="demoLocalDateValue.releaseDate"
 61 |        useValueAsLocalDate>
 62 | 
63 | 64 |
 65 | DEBUG:
 66 | {{ demoLocalDateValue | json }}
 67 | typeof(releaseDate): {{ typeof(demoLocalDateValue.releaseDate) }}
 68 | 
69 |
70 | 71 |
72 | 73 | 74 | 75 |
76 |

DefaultValueAccessor
(does not work)

77 | 78 |
79 | 80 | 81 | 82 | 83 | 84 |
85 | 86 | 87 | 88 |
 89 | <input type="text"
 90 |   name="version"
 91 |   [(ngModel)]="demoDefault.version">
 92 | 
 93 | <input type="date"
 94 |   name="releaseDate"
 95 |   [(ngModel)]="demoDefault.releaseDate">
 96 | 
97 | 98 |
 99 | DEBUG:
100 | {{ demoDefault | json }}
101 | typeof(releaseDate): {{ typeof(demoDefault.releaseDate) }}
102 | 
103 |
104 | 105 | -------------------------------------------------------------------------------- /workspace/projects/demo/src/app/demo-with-dates/template-driven-form/template-driven-form.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Release } from '../../shared/release'; 3 | 4 | @Component({ 5 | selector: 'app-template-driven-form', 6 | templateUrl: './template-driven-form.component.html' 7 | }) 8 | export class TemplateDrivenFormComponent { 9 | 10 | demoDateValue = new Release('2.0.0', new Date('2020-01-01')); // UTC 11 | demoLocalDateValue = new Release('3.0.0', new Date(2020, 0, 1)); // with offset 12 | demoDefault = new Release('1.5.8', new Date('2016-07-22')); // UTC 13 | 14 | demoDateValueDisabled = false; 15 | demoLocalDateValueDisabled = false; 16 | demoDefaultDisabled = false; 17 | 18 | typeof(obj: any) { 19 | return typeof obj; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /workspace/projects/demo/src/app/demo-with-strings/reactive-form-iso/reactive-form-iso.component.html: -------------------------------------------------------------------------------- 1 |

Reactive Forms

2 | 3 | 4 | 5 |
6 |

IsoDateValueAccessor

7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 |
19 | <input type="text"
20 |   formControlName="version">
21 | 
22 | <input type="date"
23 |   formControlName="releaseDate"
24 |   useValueAsIso>
25 | 
26 | 27 |
28 | DEBUG:
29 | {{ demoIsoDateValue | json }}
30 | typeof(releaseDate): {{ typeof(demoIsoDateValue.releaseDate) }}
31 | 
32 |
33 | 34 |
35 | 36 | 37 | 38 |
39 |

LocalIsoDateValueAccessor

40 | 41 |
42 | 43 | 44 | 45 | 46 | 47 |
48 | 49 | 50 | 51 |
52 | <input type="text"
53 |   formControlName="version">
54 | 
55 | <input type="date"
56 |   formControlName="releaseDate"
57 |   useValueAsLocalIso>
58 | 
59 | 60 |
61 | DEBUG:
62 | {{ demoLocalIsoDateValue | json }}
63 | typeof(releaseDate): {{ typeof(demoLocalIsoDateValue.releaseDate) }}
64 | 
65 |
66 | 67 |
68 | 69 |
70 |

DefaultValueAccessor
(does not work)

71 | 72 |
73 | 74 | 75 | 76 | 77 | 78 |
79 | 80 | 81 | 82 |
83 | <input type="text"
84 |   formControlName="version">
85 | 
86 | <input type="date"
87 |   formControlName="releaseDate">
88 | 
89 | 90 |
91 | DEBUG:
92 | {{ demoIsoDefault | json }}
93 | typeof(releaseDate): {{ typeof(demoIsoDefault.releaseDate) }}
94 | 
95 |
96 | -------------------------------------------------------------------------------- /workspace/projects/demo/src/app/demo-with-strings/reactive-form-iso/reactive-form-iso.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { FormControl, FormGroup } from '@angular/forms'; 3 | 4 | import { ReleaseWithIsoString } from '../../shared/release-with-iso-string'; 5 | 6 | @Component({ 7 | selector: 'app-reactive-form-iso', 8 | templateUrl: './reactive-form-iso.component.html' 9 | }) 10 | export class ReactiveFormIsoComponent { 11 | 12 | demoIsoDateValue = new ReleaseWithIsoString('2.0.0', new Date('2020-01-01').toISOString()); // UTC 13 | demoLocalIsoDateValue = new ReleaseWithIsoString('3.0.0', new Date(2020, 0, 1).toISOString()); // with offset 14 | demoIsoDefault = new ReleaseWithIsoString('1.5.8', new Date('2016-07-22').toISOString()); // UTC 15 | 16 | myFormDateValue = new FormGroup({ 17 | version: new FormControl(this.demoIsoDateValue.version, { nonNullable: true }), 18 | releaseDate: new FormControl(this.demoIsoDateValue.releaseDate, { nonNullable: true }) 19 | }); 20 | 21 | myFormLocalDateValue = new FormGroup({ 22 | version: new FormControl(this.demoLocalIsoDateValue.version, { nonNullable: true }), 23 | releaseDate: new FormControl(this.demoLocalIsoDateValue.releaseDate, { nonNullable: true }) 24 | }); 25 | 26 | myFormDefault = new FormGroup({ 27 | version: new FormControl(this.demoIsoDefault.version, { nonNullable: true }), 28 | releaseDate: new FormControl(this.demoIsoDefault.releaseDate, { nonNullable: true }) 29 | }); 30 | 31 | constructor() { 32 | this.myFormDateValue.valueChanges.subscribe(values => this.demoIsoDateValue = new ReleaseWithIsoString(values.version, values.releaseDate)); 33 | this.myFormLocalDateValue.valueChanges.subscribe(values => this.demoLocalIsoDateValue = new ReleaseWithIsoString(values.version, values.releaseDate)); 34 | this.myFormDefault.valueChanges.subscribe(values => this.demoIsoDefault = new ReleaseWithIsoString(values.version, values.releaseDate)); 35 | } 36 | 37 | toggle(formGroup: FormGroup) { 38 | const control = formGroup.get('releaseDate'); 39 | if (control.enabled) { 40 | control.disable(); 41 | } else { 42 | control.enable(); 43 | } 44 | } 45 | 46 | typeof(obj: any) { 47 | return typeof obj; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /workspace/projects/demo/src/app/demo-with-strings/template-driven-form-iso/template-driven-form-iso.component.html: -------------------------------------------------------------------------------- 1 |

Template-Driven Forms

2 | 3 | 4 | 5 |
6 |

IsoDateValueAccessor

7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 |
 19 | <input type="text"
 20 |        name="version"
 21 |        [(ngModel)]="demoIsoDateValue.version">
 22 | 
 23 | <input type="date"
 24 |        name="releaseDate"
 25 |        [(ngModel)]="demoIsoDateValue.releaseDate"
 26 |        useValueAsIso>
 27 | 
28 | 29 |
 30 | DEBUG:
 31 | {{ demoIsoDateValue | json }}
 32 | typeof(releaseDate): {{ typeof(demoIsoDateValue.releaseDate) }}
 33 | 
34 |
35 | 36 |
37 | 38 | 39 | 40 | 41 |
42 |

LocalIsoDateValueAccessor

43 | 44 |
45 | 46 | 47 | 48 | 49 | 50 |
51 | 52 | 53 | 54 |
 55 | <input type="text"
 56 |        name="version"
 57 |        [(ngModel)]="demoLocalIsoDateValue.version">
 58 | 
 59 | <input type="date"
 60 |        name="releaseDate"
 61 |        [(ngModel)]="demoLocalIsoDateValue.releaseDate"
 62 |        useValueAsLocalIso>
 63 | 
64 | 65 |
 66 | DEBUG:
 67 | {{ demoLocalIsoDateValue | json }}
 68 | typeof(releaseDate): {{ typeof(demoLocalIsoDateValue.releaseDate) }}
 69 | 
70 |
71 | 72 |
73 | 74 |
75 |

DefaultValueAccessor
(does not work)

76 | 77 |
78 | 79 | 80 | 81 | 82 | 83 |
84 | 85 | 86 | 87 |
 88 | <input type="text"
 89 |   name="version"
 90 |   [(ngModel)]="demoIsoDefault.version">
 91 | 
 92 | <input type="date"
 93 |   name="releaseDate"
 94 |   [(ngModel)]="demoIsoDefault.releaseDate">
 95 | 
96 | 97 |
 98 | DEBUG:
 99 | {{ demoIsoDefault | json }}
100 | typeof(releaseDate): {{ typeof(demoIsoDefault.releaseDate) }}
101 | 
102 |
103 | 104 | -------------------------------------------------------------------------------- /workspace/projects/demo/src/app/demo-with-strings/template-driven-form-iso/template-driven-form-iso.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ReleaseWithIsoString } from '../../shared/release-with-iso-string'; 3 | 4 | @Component({ 5 | selector: 'app-template-driven-form-iso', 6 | templateUrl: './template-driven-form-iso.component.html' 7 | }) 8 | export class TemplateDrivenFormIsoComponent { 9 | 10 | demoIsoDateValue: ReleaseWithIsoString; 11 | demoLocalIsoDateValue: ReleaseWithIsoString; 12 | demoIsoDefault: ReleaseWithIsoString; 13 | 14 | demoIsoDateValueDisabled = false; 15 | demoLocalIsoDateValueDisabled = false; 16 | demoIsoDefaultDisabled = false; 17 | 18 | constructor() { 19 | this.demoIsoDateValue = new ReleaseWithIsoString('2.0.0', new Date('2020-01-01').toISOString()); // UTC 20 | this.demoLocalIsoDateValue = new ReleaseWithIsoString('3.0.0', new Date(2020, 0, 1).toISOString()); // with offset 21 | this.demoIsoDefault = new ReleaseWithIsoString('1.5.8', new Date('2016-07-22').toISOString()); // UTC 22 | } 23 | 24 | typeof(obj: any) { 25 | return typeof obj; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /workspace/projects/demo/src/app/shared/explanation-date-value-accessor.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-explanation-date-value-accessor', 5 | template: ` 6 |

7 | The orginal DateValueAccessor operates in UTC (Coordinated Universal Time). 8 | The HTML date input will use the UTC representation of a given Date Object. 9 | When you select a date it will output an UTC date with the time set to 00:00 (UTC).
10 |
11 | 12 | If you are unsure what to use, use the LocalDateValueAccessor and not the DateValueAccessor. 13 | Most users will expect the input field to correlate to their local clock. 14 | 15 |

` 16 | }) 17 | export class ExplanationDateValueAccessorComponent { 18 | } 19 | -------------------------------------------------------------------------------- /workspace/projects/demo/src/app/shared/explanation-default-value-accessor.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-explanation-default-value-accessor', 5 | template: ` 6 |

7 | The DefaultValueAccessor from Angular will not work as expected. 8 | The HTML date input will not display the given Date Object – it's empty after load. 9 | When you select a date it will output as a simple string. Please note the typeof(releaseDate) after setting a value. 10 | This simple string contains only a date and no time. 11 |

` 12 | }) 13 | export class ExplanationDefaultValueAccessorComponent { 14 | } 15 | -------------------------------------------------------------------------------- /workspace/projects/demo/src/app/shared/explanation-iso-date-value-accessor.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-explanation-iso-date-value-accessor', 5 | template: ` 6 |

7 | This directive gets and sets ISO 8601 formatted date strings in HTML date inputs. 8 | The handling of the dates is the same as for the DateValueAccessor. 9 |
10 | The IsoDateValueAccessor operates in UTC (Coordinated Universal Time). 11 | The HTML date input will use the UTC representation of a given ISO 8601 formatted date string. 12 | When you select a date it will output an ISO-formatted string with the time set to 00:00 (UTC). 13 |

` 14 | }) 15 | export class ExplanationIsoDateValueAccessorComponent { 16 | } 17 | -------------------------------------------------------------------------------- /workspace/projects/demo/src/app/shared/explanation-local-date-value-accessor.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-explanation-local-date-value-accessor', 5 | template: ` 6 |

7 | The improved LocalDateValueAccessor operates in your Local Time. 8 | The HTML date input will use the Local Time representation of a given the Date Object. 9 | When you select a date it will output a Local Date with the time set to 00:00 (Local Time). 10 |

` 11 | }) 12 | export class ExplanationLocalDateValueAccessorComponent { 13 | } 14 | -------------------------------------------------------------------------------- /workspace/projects/demo/src/app/shared/explanation-local-iso-date-value-accessor.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-explanation-local-iso-date-value-accessor', 5 | template: ` 6 |

7 | This directive gets and sets ISO 8601 formatted date strings in HTML date inputs. 8 | The handling of the dates is the same as for the LocalDateValueAccessor. 9 |
10 | The LocalIsoDateValueAccessor operates in your Local Time. 11 | The HTML date input will use the Local Time representation of a given ISO 8601 formatted date string. 12 | When you select a date it will output an ISO-formatted string with a time that equals to 00:00 (Local Time).
13 |
14 | Note: The timezone of the outputted string is always zero UTC offset, as denoted by the suffix "Z". 15 |

` 16 | }) 17 | export class ExplanationLocalIsoDateValueAccessorComponent { 18 | } 19 | -------------------------------------------------------------------------------- /workspace/projects/demo/src/app/shared/release-with-iso-string.ts: -------------------------------------------------------------------------------- 1 | export class ReleaseWithIsoString { 2 | 3 | constructor( 4 | public version: string, 5 | public releaseDate: string) { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /workspace/projects/demo/src/app/shared/release.ts: -------------------------------------------------------------------------------- 1 | export class Release { 2 | 3 | constructor( 4 | public version: string, 5 | public releaseDate: Date) { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /workspace/projects/demo/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohannesHoppe/angular-date-value-accessor/8a4caeb45bdacc45c3b523106f6ac42dc0389ca2/workspace/projects/demo/src/assets/.gitkeep -------------------------------------------------------------------------------- /workspace/projects/demo/src/assets/ie11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohannesHoppe/angular-date-value-accessor/8a4caeb45bdacc45c3b523106f6ac42dc0389ca2/workspace/projects/demo/src/assets/ie11.png -------------------------------------------------------------------------------- /workspace/projects/demo/src/assets/reactive-does-not-work.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohannesHoppe/angular-date-value-accessor/8a4caeb45bdacc45c3b523106f6ac42dc0389ca2/workspace/projects/demo/src/assets/reactive-does-not-work.gif -------------------------------------------------------------------------------- /workspace/projects/demo/src/assets/reactive-works-local.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohannesHoppe/angular-date-value-accessor/8a4caeb45bdacc45c3b523106f6ac42dc0389ca2/workspace/projects/demo/src/assets/reactive-works-local.gif -------------------------------------------------------------------------------- /workspace/projects/demo/src/assets/reactive-works.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohannesHoppe/angular-date-value-accessor/8a4caeb45bdacc45c3b523106f6ac42dc0389ca2/workspace/projects/demo/src/assets/reactive-works.gif -------------------------------------------------------------------------------- /workspace/projects/demo/src/assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohannesHoppe/angular-date-value-accessor/8a4caeb45bdacc45c3b523106f6ac42dc0389ca2/workspace/projects/demo/src/assets/screenshot.png -------------------------------------------------------------------------------- /workspace/projects/demo/src/assets/screenshot_simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohannesHoppe/angular-date-value-accessor/8a4caeb45bdacc45c3b523106f6ac42dc0389ca2/workspace/projects/demo/src/assets/screenshot_simple.png -------------------------------------------------------------------------------- /workspace/projects/demo/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /workspace/projects/demo/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/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /workspace/projects/demo/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohannesHoppe/angular-date-value-accessor/8a4caeb45bdacc45c3b523106f6ac42dc0389ca2/workspace/projects/demo/src/favicon.ico -------------------------------------------------------------------------------- /workspace/projects/demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular: DateValueAccessor 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |

Loading...

19 | 20 | 21 | Fork me on GitHub 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /workspace/projects/demo/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /workspace/projects/demo/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 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | 51 | /*************************************************************************************************** 52 | * APPLICATION IMPORTS 53 | */ 54 | import 'configurable-date-input-polyfill'; 55 | -------------------------------------------------------------------------------- /workspace/projects/demo/src/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Helvetica, Arial, sans-serif; 3 | } 4 | 5 | svg { 6 | position: absolute; 7 | top: 10px; 8 | left: 10px; 9 | } 10 | 11 | label { 12 | float: left; 13 | width: 200px; 14 | } 15 | 16 | input { 17 | display: block; 18 | margin: 5px; 19 | width: 220px; 20 | } 21 | 22 | app-root { 23 | margin-left: 120px; 24 | display:block; 25 | } 26 | 27 | pre { 28 | background-color: #f7f7f7; 29 | width: 400px; 30 | padding: 12px; 31 | } 32 | 33 | app-template-driven-form, 34 | app-template-driven-form-iso { 35 | float: left; 36 | width: 520px; 37 | } 38 | 39 | app-reactive-form, 40 | app-reactive-form-iso { 41 | float: left; 42 | width: 520px; 43 | } 44 | 45 | summary { 46 | cursor: pointer; 47 | padding: 2px; 48 | } 49 | 50 | summary:hover { 51 | color:rgb(85, 85, 85); 52 | } 53 | 54 | h1 { 55 | margin-left: 20px; 56 | } 57 | 58 | p { 59 | margin: 20px; 60 | } 61 | 62 | p.small { 63 | font-size: 0.8em; 64 | color: rgb(85, 85, 85); 65 | } 66 | 67 | .good, 68 | .bad { 69 | border: 1px solid transparent; 70 | border-radius: 4px; 71 | padding: 10px 15px 10px 25px; 72 | margin: 20px; 73 | height: 450px; 74 | } 75 | 76 | .good { 77 | background-color: #dff0d8; 78 | border-color: #d6e9c6; 79 | color: #3c763d; 80 | } 81 | 82 | .bad { 83 | background-color: #f2dede; 84 | border-color: #ebccd1; 85 | color: #a94442; 86 | } 87 | 88 | small { 89 | font-weight: normal; 90 | font-size: 0.8em; 91 | } 92 | 93 | .clearfix::after { 94 | display: block; 95 | content: ""; 96 | clear: both; 97 | } 98 | 99 | /* fork on github badge */ 100 | 101 | #forkongithub a { 102 | background: #C3002F; 103 | color: #fff; 104 | text-decoration: none; 105 | font-family: arial,sans-serif; 106 | text-align: center; 107 | font-weight: bold; 108 | padding: 5px 40px; 109 | font-size: 1rem; 110 | line-height: 2rem; 111 | position: relative; 112 | transition: 0.5s; 113 | } 114 | 115 | #forkongithub a:hover { 116 | background: gray; 117 | color: #fff; 118 | } 119 | 120 | #forkongithub a::before, 121 | #forkongithub a::after { 122 | content: ""; 123 | width: 100%; 124 | display: block; 125 | position: absolute; 126 | top: 1px; 127 | left: 0; 128 | height: 1px; 129 | background: #fff; 130 | } 131 | 132 | #forkongithub a::after { 133 | bottom: 1px; 134 | top: auto; 135 | } 136 | 137 | @media screen and (min-width:800px) { 138 | #forkongithub { 139 | position: fixed; 140 | display: block; 141 | top: 0; 142 | right: 0; 143 | width: 200px; 144 | overflow: hidden; 145 | height: 200px; 146 | z-index: 9999; 147 | } 148 | 149 | #forkongithub a { 150 | width: 200px; 151 | position: absolute; 152 | top: 60px; 153 | right: -60px; 154 | transform: rotate(45deg); 155 | -webkit-transform: rotate(45deg); 156 | -ms-transform: rotate(45deg); 157 | -moz-transform: rotate(45deg); 158 | -o-transform: rotate(45deg); 159 | box-shadow: 4px 4px 10px rgba(0,0,0,0.8); 160 | } 161 | } 162 | 163 | -------------------------------------------------------------------------------- /workspace/projects/demo/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | keys(): string[]; 13 | (id: string): T; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting() 21 | ); 22 | // Then we find all the tests. 23 | const context = require.context('./', true, /\.spec\.ts$/); 24 | // And load the modules. 25 | context.keys().map(context); 26 | -------------------------------------------------------------------------------- /workspace/projects/demo/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/app", 6 | "types": [], 7 | "target": "es2020" 8 | }, 9 | "files": [ 10 | "src/main.ts", 11 | "src/polyfills.ts" 12 | ], 13 | "include": [ 14 | "src/**/*.d.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /workspace/projects/demo/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /workspace/projects/demo/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /workspace/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "target": "es2020", 13 | "module": "es2020", 14 | "lib": [ 15 | "es2018", 16 | "dom" 17 | ], 18 | "paths": { 19 | "date-value-accessor": [ 20 | "dist/date-value-accessor/date-value-accessor", 21 | "dist/date-value-accessor" 22 | ] 23 | } 24 | }, 25 | "angularCompilerOptions": { 26 | "strictTemplates": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /workspace/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "codelyzer" 5 | ], 6 | "rules": { 7 | "align": { 8 | "options": [ 9 | "parameters", 10 | "statements" 11 | ] 12 | }, 13 | "array-type": false, 14 | "arrow-return-shorthand": true, 15 | "curly": true, 16 | "deprecation": { 17 | "severity": "warning" 18 | }, 19 | "eofline": true, 20 | "import-blacklist": [ 21 | true, 22 | "rxjs/Rx" 23 | ], 24 | "import-spacing": true, 25 | "indent": { 26 | "options": [ 27 | "spaces" 28 | ] 29 | }, 30 | "max-classes-per-file": false, 31 | "max-line-length": [ 32 | true, 33 | 140 34 | ], 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-console": [ 47 | true, 48 | "debug", 49 | "info", 50 | "time", 51 | "timeEnd", 52 | "trace" 53 | ], 54 | "no-empty": false, 55 | "no-inferrable-types": [ 56 | true, 57 | "ignore-params" 58 | ], 59 | "no-non-null-assertion": true, 60 | "no-redundant-jsdoc": true, 61 | "no-switch-case-fall-through": true, 62 | "no-var-requires": false, 63 | "object-literal-key-quotes": [ 64 | true, 65 | "as-needed" 66 | ], 67 | "quotemark": [ 68 | true, 69 | "single" 70 | ], 71 | "semicolon": { 72 | "options": [ 73 | "always" 74 | ] 75 | }, 76 | "space-before-function-paren": { 77 | "options": { 78 | "anonymous": "never", 79 | "asyncArrow": "always", 80 | "constructor": "never", 81 | "method": "never", 82 | "named": "never" 83 | } 84 | }, 85 | "typedef": [ 86 | true, 87 | "call-signature" 88 | ], 89 | "typedef-whitespace": { 90 | "options": [ 91 | { 92 | "call-signature": "nospace", 93 | "index-signature": "nospace", 94 | "parameter": "nospace", 95 | "property-declaration": "nospace", 96 | "variable-declaration": "nospace" 97 | }, 98 | { 99 | "call-signature": "onespace", 100 | "index-signature": "onespace", 101 | "parameter": "onespace", 102 | "property-declaration": "onespace", 103 | "variable-declaration": "onespace" 104 | } 105 | ] 106 | }, 107 | "variable-name": { 108 | "options": [ 109 | "ban-keywords", 110 | "check-format", 111 | "allow-pascal-case" 112 | ] 113 | }, 114 | "whitespace": { 115 | "options": [ 116 | "check-branch", 117 | "check-decl", 118 | "check-operator", 119 | "check-separator", 120 | "check-type", 121 | "check-typecast" 122 | ] 123 | }, 124 | "component-class-suffix": true, 125 | "contextual-lifecycle": true, 126 | "directive-class-suffix": true, 127 | "no-conflicting-lifecycle": true, 128 | "no-host-metadata-property": true, 129 | "no-input-rename": true, 130 | "no-inputs-metadata-property": true, 131 | "no-output-native": true, 132 | "no-output-on-prefix": true, 133 | "no-output-rename": true, 134 | "no-outputs-metadata-property": true, 135 | "template-banana-in-box": true, 136 | "template-no-negated-async": true, 137 | "use-lifecycle-interface": true, 138 | "use-pipe-transform-interface": true 139 | } 140 | } 141 | --------------------------------------------------------------------------------