Skip to main content
Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.

Adjusting Dates By Adding Date / Time Parts In Angular 11.0.0

By Ben Nadel on

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:

A date being updated by year, month, day, hour, minute, second, and millisecond input ranges.

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.



Reader Comments

Post A Comment

You — Get Out Of My Dreams, Get Into My Blog
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.