Skip to content

Commit

Permalink
Add support for timezone / local dates (#26)
Browse files Browse the repository at this point in the history
* Add DateLocalValueAccessor with timezone support

See: #13

* Add DateLocalValueAccessor with timezone support: use valueAsDate

Assign UTC Date to valueAsDate
Output Local Date

See: #13

* Create dedicated LocalDateValueAccessorModule, UTs

See: #13

* DateValueAccessor UT: check if date is in UTC time

See: #13

* LocalDateValueAccessor: Update UTs

See: #13

* LocalDateValueAccessor: public API

See: #13

* Add DateLocalValueAccessor with timezone support: use valueAsDate

Assign UTC Date to valueAsDate
Output Local Date

See: #13

* Update Demo

See: #13

* Update Demo: Fix typo

See: #13

* Update README
  • Loading branch information
spierala authored Dec 2, 2020
1 parent 1357c89 commit 8e5a2de
Show file tree
Hide file tree
Showing 12 changed files with 252 additions and 22 deletions.
44 changes: 42 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,23 @@ Download the package via NPM:
npm install --save angular-date-value-accessor
```

Then import the module via NgModule:
## UTC Time and Local Time
When working with Dates in Javascript you either operate in UTC or Local Time.

* UTC is has no timezone offset.
* Local Time depends on the host system time zone and offset.

Javascript Dates support both the UTC and the Local Time representation.
Depending on the requirements of your application you can choose the from these Value Accessors:
* [DateValueAccessor (UTC)](#datevalueaccessor-utc)
* [LocalDateValueAccessor (Local Time)](#localdatevalueaccessor-local-time)


## DateValueAccessor (UTC)
The DateValueAccessor operates in UTC (Coordinated Universal Time).
The HTML date input will read the UTC representation of the Date Object. When you select a date it will output an UTC date with the time set to 00:00 (UTC).

Import the module via NgModule:

```js
// app.module.ts
Expand All @@ -65,8 +81,32 @@ import { DateValueAccessorModule } from 'angular-date-value-accessor';
export class AppModule { }
```

Now you can apply the "useValueAsDate" to your date input controls.
Now you can apply the `useValueAsDate` to your date input controls.

## LocalDateValueAccessor (Local Time)

If you prefer to work with Local Dates then you can use the `LocalDateValueAccessorModule`.

The HTML date input will read the Local Time representation of the Date Object. When you select a date it will output a Local Date with the time set to 00:00 (Local Time).

Most UI component libraries like Angular Material, Kendo Angular, PrimeNG implement their DatePickers operating in Local Time.

Also the Angular Date Pipe uses the Local Time representation of the Date Object by default.

```js
// app.module.ts

import { LocalDateValueAccessorModule } from 'angular-date-value-accessor';

@NgModule({
imports: [
LocalDateValueAccessorModule
]
})
export class AppModule { }
```

Now you can apply the `useValueAsLocalDate` to your date input controls.


[npm-url]: https://npmjs.org/package/angular-date-value-accessor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,25 @@ import { FormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';

import { DateValueAccessor } from './date-value-accessor';
import { dispatchInputEvent } from './spec-utils';

@Component({
template: `
<form>
<input type="text" name="test0" [(ngModel)]="test">
<input type="date" name="normalInput" [(ngModel)]="testDate1">
<input type="date" name="fixedInput" [(ngModel)]="testDate2" useValueAsDate>
</form>`
})
export class TestFormComponent {
test: string;
testDate1: any;
testDate1: Date | string;
testDate2: Date;

constructor() {
this.test = 'Hello Angular';
this.testDate1 = new Date('2019-01-01');
this.testDate2 = new Date('2020-01-01');
this.testDate1 = new Date('2019-01-01'); // Create UTC Date
this.testDate2 = new Date('2020-01-01'); // Create UTC Date
}
}

function dispatchInputEvent(inputElement: HTMLInputElement, fixture: ComponentFixture<TestFormComponent>, text: string): void {
inputElement.value = text;
inputElement.dispatchEvent(new Event('input'));
}

describe('DateValueAccessor', () => {

let fixture: ComponentFixture<TestFormComponent>;
Expand Down Expand Up @@ -62,7 +55,7 @@ describe('DateValueAccessor', () => {
});

it('should populate simple strings on change', waitForAsync(() => {
dispatchInputEvent(normalInput.nativeElement, fixture, '1984-09-30');
dispatchInputEvent(normalInput.nativeElement, '1984-09-30');
expect(fixture.componentInstance.testDate1).toEqual('1984-09-30');
}));
});
Expand All @@ -76,10 +69,15 @@ describe('DateValueAccessor', () => {
expect(fixedInput.nativeElement.value).toBe('2020-01-01');
}));

it('should also populate dates (instead of strings) on change', waitForAsync(() => {
dispatchInputEvent(fixedInput.nativeElement, fixture, '2020-12-31');
it('should populate UTC dates (instead of strings) on change', waitForAsync(() => {
dispatchInputEvent(fixedInput.nativeElement, '2020-12-31');
expect(fixture.componentInstance.testDate2).toEqual(jasmine.any(Date));
expect(fixture.componentInstance.testDate2).toEqual(new Date('2020-12-31'));
expect(fixture.componentInstance.testDate2.getUTCDate()).toBe(31);
expect(fixture.componentInstance.testDate2.getUTCMonth()).toBe(11);
expect(fixture.componentInstance.testDate2.getUTCFullYear()).toBe(2020);
expect(fixture.componentInstance.testDate2.getUTCHours()).toBe(0);
expect(fixture.componentInstance.testDate2.getUTCMinutes()).toBe(0);
}));
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { NgModule } from '@angular/core';
import { LocalDateValueAccessor } from './local-date-value-accessor';
@NgModule({
declarations: [LocalDateValueAccessor],
exports: [LocalDateValueAccessor]
})
export class LocalDateValueAccessorModule { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Component, DebugElement } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';

import { dispatchInputEvent } from './spec-utils';
import { LocalDateValueAccessor } from './local-date-value-accessor';

@Component({
template: `
<form>
<input type="date" name="fixedInput" [(ngModel)]="testDate" useValueAsLocalDate>
</form>`
})
export class TestFormComponent {
testDate: Date = new Date(2020, 11, 8); // Create LOCAL Date
}

describe('LocalDateValueAccessor', () => {

let fixture: ComponentFixture<TestFormComponent>;

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [TestFormComponent, LocalDateValueAccessor],
imports: [FormsModule]
})
.compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(TestFormComponent);
});

beforeEach(waitForAsync(() => {
// https://stackoverflow.com/questions/39582707/updating-input-html-field-from-within-an-angular-2-test
fixture.detectChanges();
fixture.whenStable();
}));

describe('with the "useValueAsDateLocal" attribute', () => {

let fixedInput: DebugElement;
beforeEach(() => fixedInput = fixture.debugElement.query(By.css('input[name=fixedInput]')));

it('should fix date input controls to bind on dates', waitForAsync(() => {
expect(fixedInput.nativeElement.value).toBe('2020-12-08');
}));

it('should populate LOCAL dates (instead of strings) on change', waitForAsync(() => {
dispatchInputEvent(fixedInput.nativeElement, '2020-12-31');
expect(fixture.componentInstance.testDate).toEqual(jasmine.any(Date));
expect(fixture.componentInstance.testDate).toEqual(new Date(2020, 11, 31));
expect(fixture.componentInstance.testDate.getDate()).toBe(31);
expect(fixture.componentInstance.testDate.getMonth()).toBe(11);
expect(fixture.componentInstance.testDate.getFullYear()).toBe(2020);
expect(fixture.componentInstance.testDate.getHours()).toBe(0);
expect(fixture.componentInstance.testDate.getMinutes()).toBe(0);
}));
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Directive, ElementRef, HostListener, Renderer2, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

export const LOCAL_DATE_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => LocalDateValueAccessor),
multi: true
};

/**
* The accessor for writing a value and listening to changes on a date input element
*
* ### Example
* `<input type="date" name="myBirthday" ngModel useValueAsLocalDate>`
*/
@Directive({
selector: '[useValueAsLocalDate]',
providers: [LOCAL_DATE_VALUE_ACCESSOR]
})
// tslint:disable-next-line: directive-class-suffix
export class LocalDateValueAccessor implements ControlValueAccessor {

onChange: any = () => {};

@HostListener('input', ['$event.target.valueAsDate']) onInput = (date: Date) => {
let selectedDate: Date | null = null;
if (date) {
// Create LOCAL Date, time is set to 00:00 in LOCAL time
selectedDate = new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());;
}
this.onChange(selectedDate);
}
@HostListener('blur', []) onTouched = () => { };

constructor(private renderer: Renderer2, private elementRef: ElementRef) { }

writeValue(date: Date): void {
// Create UTC Date, time is set to 00:00 in UTC time
const utcDate: Date = date ?
new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())) :
null;
this.renderer.setProperty(this.elementRef.nativeElement, 'valueAsDate', utcDate);
}

registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
registerOnTouched(fn: () => void): void { this.onTouched = fn; }

setDisabledState(isDisabled: boolean): void {
this.renderer.setProperty(this.elementRef.nativeElement, 'disabled', isDisabled);
}
}

// Use Local Dates with html input type date
// https://stackoverflow.com/questions/53032953/what-is-the-correct-way-to-set-and-get-htmlinputelement-valueasdate-using-local
4 changes: 4 additions & 0 deletions workspace/projects/date-value-accessor/src/lib/spec-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export function dispatchInputEvent(inputElement: HTMLInputElement, text: string): void {
inputElement.value = text;
inputElement.dispatchEvent(new Event('input'));
}
2 changes: 2 additions & 0 deletions workspace/projects/date-value-accessor/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@

export * from './lib/date-value-accessor';
export * from './lib/date-value-accessor.module';
export * from './lib/local-date-value-accessor';
export * from './lib/local-date-value-accessor.module';
5 changes: 3 additions & 2 deletions workspace/projects/demo/src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { DateValueAccessorModule } from 'projects/date-value-accessor/src/public-api';
import { DateValueAccessorModule, LocalDateValueAccessorModule } from 'projects/date-value-accessor/src/public-api';

import { AppComponent } from './app.component';
import { ReactiveFormComponent } from './reactive-form/reactive-form.component';
Expand All @@ -17,7 +17,8 @@ import { TemplateDrivenFormComponent } from './template-driven-form/template-dri
BrowserModule,
FormsModule,
ReactiveFormsModule,
DateValueAccessorModule
DateValueAccessorModule,
LocalDateValueAccessorModule
],
bootstrap: [AppComponent]
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ <h2>DateValueAccessor</h2>

<form [formGroup]="myForm1">
<label>Version:</label> <input type="text" formControlName="version">
<label>Relase date:</label> <input type="date" formControlName="releaseDate" useValueAsDate>
<label>Release date:</label> <input type="date" formControlName="releaseDate" useValueAsDate>
</form>

<pre>
Expand All @@ -25,12 +25,37 @@ <h2>DateValueAccessor</h2>

<hr>

<div class="good">
<h2>LocalDateValueAccessor</h2>

<form [formGroup]="myForm3">
<label>Version:</label> <input type="text" formControlName="version">
<label>Release date:</label> <input type="date" formControlName="releaseDate" useValueAsLocalDate>
</form>

<pre>
&lt;input type=&quot;text&quot;
formControlName=&quot;version&quot;&gt;

&lt;input type=&quot;date&quot;
formControlName=&quot;releaseDate&quot;
<b>useValueAsLocalDate</b>&gt;
</pre>

<pre>
DEBUG:
{{ release3 | json }}
</pre>
</div>

<hr>

<div class="bad">
<h2>DefaultValueAccessor<br><small>(does not work)</small></h2>

<form [formGroup]="myForm2">
<label>Version:</label> <input type="text" formControlName="version">
<label>Relase date:</label> <input type="date" formControlName="releaseDate">
<label>Release date:</label> <input type="date" formControlName="releaseDate">
</form>

<pre>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ export class ReactiveFormComponent implements OnInit {

release1: Release;
release2: Release;
release3: Release;

myForm1: FormGroup;
myForm2: FormGroup;
myForm3: FormGroup;

constructor(private fb: FormBuilder) {
this.release1 = new Release('2.0.0', new Date('2020-01-01'));
this.release2 = new Release('1.5.8', new Date('2016-07-22'));
this.release3 = new Release('3.0.0', new Date(2020, 0, 1));
}

ngOnInit() {
Expand All @@ -30,7 +33,13 @@ export class ReactiveFormComponent implements OnInit {
releaseDate: [this.release2.releaseDate]
});

this.myForm3 = this.fb.group({
version: [this.release3.version],
releaseDate: [this.release3.releaseDate]
});

this.myForm1.valueChanges.subscribe(values => this.release1 = new Release(values.version, values.releaseDate));
this.myForm2.valueChanges.subscribe(values => this.release2 = new Release(values.version, values.releaseDate));
this.myForm3.valueChanges.subscribe(values => this.release3 = new Release(values.version, values.releaseDate));
}
}
Loading

0 comments on commit 8e5a2de

Please sign in to comment.