Adjusting Dates By Adding Date / Time Parts In Angular 11.0.0
Over the past week, I've had fun exploring dates in Angular. First, when I stumbled upon the formatDate()
function that ships with Angular core; and then after, when I demonstrated how to recreate the Moment.js
.fromNow()
functionality in Angular. To round this adventure out, I wanted to take a quick look at how you can easily adjust dates by adding date and/or time "parts" in Angular 11.0.0.
Run this demo in my JavaScript Demos project on GitHub.
View this code in my JavaScript Demos project on GitHub.
I'm sure that all server-side languages have ways to add and remove time from a given date. For example, in the Lucee CFML / ColdFusion world, I can easily add 48-hours to a given date with the dateAdd()
function:
result = dateAdd( "h", 48, now() );
These types of functions work just as well negative deltas. So, I can easily subtract 48-hours from a given date with the same function:
result = dateAdd( "h", -48, now() );
JavaScript has the same exact functionality; only, it's not presented quite as cleanly as it is in some other languages. In JavaScript, this functionality is manifest in the Date
object's native "overflow behavior". As I demonstrated a few years ago, when you add relative values to a Date
, all date fields are updated. Meaning, we can reproduce the above 48-hour example in JavaScript using .setHours()
:
timestamp.setHours( timestamp.getHours() + 48 );
... and, to subtract time, it's the same exactly thing:
timestamp.setHours( timestamp.getHours() - 48 );
There are only 24-hours in a day. However, we can add and subtract more than 24-hours and the "overflow" will automatically get distributed to the rest of the date. Meaning, by setting the hours to +48 on this JavaScript Date
, we're actually adding 2-days.
This same approach works for all of the Date
parts:
date.setFullYear( date.getFullYear() + delta )
date.setMonth( date.getMonth() + delta )
date.setDate( date.getDate() + delta )
date.setHours( date.getHours() + delta )
date.setMinutes( date.getMinutes() + delta )
date.setSeconds( date.getSeconds() + delta )
date.setMilliseconds( date.getMilliseconds() + delta )
Of course, as I said above, other languages make this behavior a bit more palatable with some sort of dateAdd()
function. Well, we can do the same thing in Angular. To wrap this date add/subtract up, I've crated a DateHelper
class that exposes an .add()
function:
.add( part: string, delta: number: input: Date ) : Date
The "part" denotes which "field" within the input Date is going to be used, and then applies the relevant .setXYZ()
and .getXYZ()
. But, unlike the native JavaScript behavior, I'm going to make a copy of the original Date so that we don't mutate the input:
// Import the core angular services.
import { formatDate as ngFormatDate } from "@angular/common";
import { Inject } from "@angular/core";
import { Injectable } from "@angular/core";
import { LOCALE_ID } from "@angular/core";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// CAUTION: Numbers are implicitly assumed to be milliseconds since epoch and strings are
// implicitly assumed to be valid for the Date() constructor.
export type DateInput = Date | number | string;
// The single-character values here are meant to match the mask placeholders used in the
// native formatDate() function.
export type DatePart =
| "y" | "year" // Year
| "M" | "month" // Month
| "d" | "day" // Day
| "h" | "hour" // Hour
| "m" | "minute" // Minute
| "s" | "second" // Second
| "S" | "millisecond" // Fractional second (millisecond)
;
@Injectable({
providedIn: "root"
})
export class DateHelper {
private localID: string;
// I initialize the date-helper with the given localization token.
constructor( @Inject( LOCALE_ID ) localID: string ) {
this.localID = localID;
}
// ---
// PUBLIC METHODS.
// ---
// I add the given date/time delta to the given date. A new date is returned.
public add( part: DatePart, delta: number, input: DateInput ) : Date {
var result = new Date( input );
switch ( part ) {
case "year":
case "y":
result.setFullYear( result.getFullYear() + delta );
break;
case "month":
case "M":
result.setMonth( result.getMonth() + delta );
break;
case "day":
case "d":
result.setDate( result.getDate() + delta );
break;
case "hour":
case "h":
result.setHours( result.getHours() + delta );
break;
case "minute":
case "m":
result.setMinutes( result.getMinutes() + delta );
break;
case "second":
case "s":
result.setSeconds( result.getSeconds() + delta );
break;
case "millisecond":
case "S":
result.setMilliseconds( result.getMilliseconds() + delta );
break;
}
return( result );
}
// I proxy the native formatDate() function with a partial application of the
// LOCALE_ID that is being used in the application.
public format( value: DateInput, mask: string ) : string {
return( ngFormatDate( value, mask, this.localID ) );
}
}
As you can see, there's almost no logic here - all we're doing is translating the given "part" into the right date-mutation call. And, of course, to format dates, we're just going to lean on Angular's native formatDate()
functionality which does so much heavy lifting for us.
To see this in action, I've created a simple App component that exposes a range-input for each possible date-part, with each range going from -100
to 100
:
<div class="result">
<span class="result__content">
{{ formattedDate }}
</span>
</div>
<div class="slider">
<div class="slider__label">
Year
</div>
<input
#yearRef
type="range"
min="-100"
max="100"
[value]="yearDelta"
(input)="( yearDelta = +yearRef.value )"
class="slider__input"
/>
<div class="slider__value">
{{ yearDelta }}
</div>
</div>
<div class="slider">
<div class="slider__label">
Month
</div>
<input
#monthRef
type="range"
min="-100"
max="100"
[value]="monthDelta"
(input)="( monthDelta = +monthRef.value )"
class="slider__input"
/>
<div class="slider__value">
{{ monthDelta }}
</div>
</div>
<div class="slider">
<div class="slider__label">
Day
</div>
<input
#dayRef
type="range"
min="-100"
max="100"
[value]="dayDelta"
(input)="( dayDelta = +dayRef.value )"
class="slider__input"
/>
<div class="slider__value">
{{ dayDelta }}
</div>
</div>
<div class="slider">
<div class="slider__label">
Hour
</div>
<input
#hourRef
type="range"
min="-100"
max="100"
[value]="hourDelta"
(input)="( hourDelta = +hourRef.value )"
class="slider__input"
/>
<div class="slider__value">
{{ hourDelta }}
</div>
</div>
<div class="slider">
<div class="slider__label">
Minute
</div>
<input
#minuteRef
type="range"
min="-100"
max="100"
[value]="minuteDelta"
(input)="( minuteDelta = +minuteRef.value )"
class="slider__input"
/>
<div class="slider__value">
{{ minuteDelta }}
</div>
</div>
<div class="slider">
<div class="slider__label">
Second
</div>
<input
#secondRef
type="range"
min="-100"
max="100"
[value]="secondDelta"
(input)="( secondDelta = +secondRef.value )"
class="slider__input"
/>
<div class="slider__value">
{{ secondDelta }}
</div>
</div>
<div class="slider">
<div class="slider__label">
Millis
</div>
<input
#millisecondRef
type="range"
min="-100"
max="100"
[value]="millisecondDelta"
(input)="( millisecondDelta = +millisecondRef.value )"
class="slider__input"
/>
<div class="slider__value">
{{ millisecondDelta }}
</div>
</div>
As you can see, each (input)
event just stores the given range's value back into the view-model (since I'm not bothering to include the FormsModule
or ngModel
for this demo). Each (input)
event will trigger a change-detection digest - Angular's magic sauce - and, I'm going to take all of the "deltas" defined by these ranges and apply them to a Date
, which is then formatted and output at the top of the template:
// Import the core angular services.
import { Component } from "@angular/core";
// Import the application components and services.
import { DateHelper } from "./date-helper";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
@Component({
selector: "app-root",
styleUrls: [ "./app.component.less" ],
templateUrl: "./app.component.html"
})
export class AppComponent {
public baseDate: Date;
public dayDelta: number;
public formattedDate: string;
public hourDelta: number;
public millisecondDelta: number;
public minuteDelta: number;
public monthDelta: number;
public secondDelta: number;
public yearDelta: number;
private dateHelper: DateHelper;
private dateMask: string;
// I initialize the app component.
constructor( dateHelper: DateHelper ) {
this.dateHelper = dateHelper;
this.baseDate = new Date();
this.dayDelta = 0;
this.hourDelta = 0;
this.millisecondDelta = 0;
this.minuteDelta = 0;
this.monthDelta = 0;
this.secondDelta = 0;
this.yearDelta = 0;
this.dateMask = "yyyy-MM-dd HH:mm:ss.SSS";
this.formattedDate = "";
}
// ---
// PUBLIC METHODS.
// ---
// I get called on every digest.
// --
// NOTE: Rather than have an explicit function that has to get called every time a
// date-delta is adjusted, we're just going to hook into the digest since we know
// that a new digest will be triggered on every (input) event.
public ngDoCheck() : void {
var result = this.baseDate;
// The .add() function returns a NEW date each time, so we have to keep saving
// and reusing the result of each call.
result = this.dateHelper.add( "year", this.yearDelta, result );
result = this.dateHelper.add( "month", this.monthDelta, result );
result = this.dateHelper.add( "day", this.dayDelta, result );
result = this.dateHelper.add( "hour", this.hourDelta, result );
result = this.dateHelper.add( "minute", this.minuteDelta, result );
result = this.dateHelper.add( "second", this.secondDelta, result );
result = this.dateHelper.add( "millisecond", this.millisecondDelta, result );
this.formattedDate = this.dateHelper.format( result, this.dateMask );
}
}
As you can see, in my ngDoCheck()
life-cycle method, I'm just taking all of the current deltas and I'm applying them to the base-date, which is whatever time it was when the demo is loaded. Then, as I drag each range input around, we can see how it affects the .add()
function and the resultant date value:
As you can see, each input range adjusts a single "part" within the base date. However, as we "overflow" the capacity of each part, that overflow naturally flows into the other fields of the Date
object. How cool is that! JavaScript is so freaking sexy!
The Date
object is definitely an under-appreciated part of the JavaScript ecosystem. It does some serious heavy-lifting for us, including the ability to quickly and easily add and subtract time deltas. But, its API is always the nicest. In Angular, we can make it nice by wrapping it up in a small date-helper class.
Want to use code from this post? Check out the license.
Reader Comments
@All,
As a final (probably) follow-up, I've aggregated a few of these smaller posts into 300 lines-of-code that will replace all of the external date libraries in my Angular applications:
www.bennadel.com/blog/3926-replacing-all-external-date-libraries-with-300-lines-of-code-in-angular-11-0-0.htm
I love me some Angular!
ave textbox1 which display server date as 27-Nov-2010 Textbox2 will display time as 08:00:00 AM.
I want if the departure of the plane is Date 27-Nov-2010 and Time 08:00:00 AM then if he/she wants to cancel ticket, it can cancel their ticket till Date 27-Nov-2010 and Time 04:00:00 AM.
Anybody can cancel their ticket before 4 hour of departure date/time ...
I want cancel plane ticket before 4 hours of departure time if dep time is 08:00:00 AM then Passengers can cancel ticket till 04:00:00 AM?