Skip to content

Commit

Permalink
refactor: replace ngx-bootstrap w/ ng-bootstrap
Browse files Browse the repository at this point in the history
ng-bootstrap seems to be better maintained, uses standalone components, and better supports dark mode in Bootstrap 5.
  • Loading branch information
jrassa committed Jul 16, 2024
1 parent 8583774 commit f100afa
Show file tree
Hide file tree
Showing 48 changed files with 660 additions and 392 deletions.
491 changes: 247 additions & 244 deletions package-lock.json

Large diffs are not rendered by default.

25 changes: 13 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,38 +18,39 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^17.3.4",
"@angular/animations": "^17.3.11",
"@angular/cdk": "^16.2.12",
"@angular/common": "^17.3.4",
"@angular/compiler": "^17.3.4",
"@angular/core": "^17.3.4",
"@angular/forms": "^17.3.4",
"@angular/platform-browser": "^17.3.4",
"@angular/platform-browser-dynamic": "^17.3.4",
"@angular/router": "^17.3.4",
"@angular/common": "^17.3.11",
"@angular/compiler": "^17.3.11",
"@angular/core": "^17.3.11",
"@angular/forms": "^17.3.11",
"@angular/platform-browser": "^17.3.11",
"@angular/platform-browser-dynamic": "^17.3.11",
"@angular/router": "^17.3.11",
"@fortawesome/fontawesome-free": "^6.2.1",
"@ng-bootstrap/ng-bootstrap": "~16.0.0",
"@ng-select/ng-select": "~12.0.7",
"@ng-web-apis/common": "^3.0.6",
"@ng-web-apis/storage": "^3.0.6",
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.3",
"lodash": "~4.17.21",
"luxon": "~3.2.1",
"ngx-bootstrap": "~12.0.0",
"roboto-fontface": "0.10",
"rxjs": "~7.8.1",
"socket.io-client": "^2.2.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.3.4",
"@angular-devkit/build-angular": "^17.3.8",
"@angular-eslint/builder": "17.3.0",
"@angular-eslint/eslint-plugin": "17.3.0",
"@angular-eslint/eslint-plugin-template": "17.3.0",
"@angular-eslint/schematics": "17.3.0",
"@angular-eslint/template-parser": "17.3.0",
"@angular/cli": "~17.3.4",
"@angular/compiler-cli": "^17.3.4",
"@angular/cli": "~17.3.8",
"@angular/compiler-cli": "^17.3.11",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/jasmine": "~4.3.0",
"@types/lodash": "4.14",
Expand Down
4 changes: 0 additions & 4 deletions src/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ import {
withInMemoryScrolling
} from '@angular/router';

import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
import { TooltipModule } from 'ngx-bootstrap/tooltip';

import { provideCdkDialog } from './common/dialog';
import { provideAdminFeature } from './core/admin';
import { provideAuditFeature } from './core/audit';
Expand All @@ -35,7 +32,6 @@ const disableAnimations: boolean = window.matchMedia('(prefers-reduced-motion: r

export const appConfig: ApplicationConfig = {
providers: [
importProvidersFrom(BsDatepickerModule.forRoot(), TooltipModule.forRoot()),
!disableAnimations ? provideAnimations() : provideNoopAnimations(),
provideHttpClient(
withInterceptors([
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<div class="dp-hidden position-absolute">
<input
class="form-control"
name="datepicker"
autoClose="outside"
ngbDatepicker
outsideDays="hidden"
tabindex="-1"
#datepicker="ngbDatepicker"
[dayTemplate]="t"
[displayMonths]="2"
[startDate]="fromDate()!"
(dateSelect)="onDateSelection($event)"
/>
<ng-template let-date let-focused="focused" #t>
<span
class="custom-day"
[class.faded]="isHovered(date) || isInside(date)"
[class.focused]="focused"
[class.range]="isRange(date)"
(mouseenter)="hoveredDate.set(date)"
(mouseleave)="hoveredDate.set(null)"
>
{{ date.day }}
</span>
</ng-template>
</div>
<div class="input-group">
<input
class="form-control"
name="dpFromDate"
[value]="fromDateString()"
autocomplete="off"
placeholder="yyyy-mm-dd"
#dpFromDate
[disabled]="disabled()"
(input)="fromDate.set(validateInput(fromDate(), dpFromDate.value))"
/>
<span class="input-group-text">-</span>
<input
class="form-control"
name="dpToDate"
[value]="toDateString()"
autocomplete="off"
placeholder="yyyy-mm-dd"
#dpToDate
[disabled]="disabled()"
(input)="toDate.set(validateInput(toDate(), dpToDate.value))"
/>
<button
class="btn btn-outline-secondary"
type="button"
[disabled]="disabled()"
(click)="datepicker.toggle()"
>
<span class="fa-solid fa-calendar-days"></span>
</button>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
.dp-hidden {
width: 0;
margin: 0;
border: none;
padding: 0;
}

.custom-day {
text-align: center;
padding: 0.25rem;
display: inline-block;
height: 2rem;
width: 2rem;
}

.custom-day.focused {
background-color: var(--bs-primary);
color: white;
}

.custom-day.range,
.custom-day:hover {
background-color: var(--bs-primary);
color: white;
}

.custom-day.faded {
background-color: var(--bs-primary-bg-subtle);
color: var(--bs-body-color);
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { DatepickerRangePopupComponent } from './datepicker-range-popup.component';

describe('DatepickerRangePopupComponent', () => {
let component: DatepickerRangePopupComponent;
let fixture: ComponentFixture<DatepickerRangePopupComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DatepickerRangePopupComponent]
}).compileComponents();

fixture = TestBed.createComponent(DatepickerRangePopupComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { JsonPipe } from '@angular/common';
import {
Component,
computed,
effect,
forwardRef,
inject,
input,
signal,
viewChild
} from '@angular/core';
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';

import {
NgbCalendar,
NgbDate,
NgbDateAdapter,
NgbDateParserFormatter,
NgbDatepickerModule,
NgbInputDatepicker
} from '@ng-bootstrap/ng-bootstrap';

type DateRange = [Date | null, Date | null];

@Component({
selector: 'app-datepicker-range-popup',
standalone: true,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DatepickerRangePopupComponent),
multi: true
}
],
imports: [NgbDatepickerModule, FormsModule, JsonPipe],
templateUrl: './datepicker-range-popup.component.html',
styleUrl: './datepicker-range-popup.component.scss'
})
export class DatepickerRangePopupComponent implements ControlValueAccessor {
readonly #calendar = inject(NgbCalendar);
readonly formatter = inject(NgbDateParserFormatter);
readonly #adapter = inject(NgbDateAdapter);

readonly #changed = new Array<(value: DateRange) => void>();
readonly #touched = new Array<() => void>();

readonly datepicker = viewChild.required(NgbInputDatepicker);

readonly disabled = input(false);

readonly fromDate = signal<NgbDate | null>(null);
readonly toDate = signal<NgbDate | null>(null);
readonly hoveredDate = signal<NgbDate | null>(null);

readonly fromDateString = computed(() => this.formatter.format(this.fromDate()));
readonly toDateString = computed(() => this.formatter.format(this.toDate()));

readonly asNativeDateTuple = computed<DateRange>(() => [
this.#adapter.toModel(this.fromDate()),
this.#adapter.toModel(this.toDate())
]);

constructor() {
effect(() => {
this.propagateChange();
});
}

writeValue(value: DateRange) {
this.fromDate.set(NgbDate.from(this.#adapter.fromModel(value?.[0])));
this.toDate.set(NgbDate.from(this.#adapter.fromModel(value?.[1])));
}

registerOnChange(fn: (value: DateRange) => void) {
this.#changed.push(fn);
}

registerOnTouched(fn: () => void) {
this.#touched.push(fn);
}

propagateChange() {
this.#changed.forEach((f) => f(this.asNativeDateTuple()));
}

onDateSelection(date: NgbDate) {
if (!this.fromDate() && !this.toDate()) {
this.fromDate.set(date);
} else if (this.fromDate() && !this.toDate() && date && date.after(this.fromDate())) {
this.toDate.set(date);
this.datepicker().close();
} else {
this.toDate.set(null);
this.fromDate.set(date);
}
}

isHovered(date: NgbDate) {
return (
this.fromDate() &&
!this.toDate() &&
this.hoveredDate() &&
date.after(this.fromDate()) &&
date.before(this.hoveredDate())
);
}

isInside(date: NgbDate) {
return this.toDate() && date.after(this.fromDate()) && date.before(this.toDate());
}

isRange(date: NgbDate) {
return (
date.equals(this.fromDate()) ||
(this.toDate() && date.equals(this.toDate())) ||
this.isInside(date) ||
this.isHovered(date)
);
}

validateInput(currentValue: NgbDate | null, input: string): NgbDate | null {
const parsed = this.formatter.parse(input);
return parsed && this.#calendar.isValid(NgbDate.from(parsed))
? NgbDate.from(parsed)
: currentValue;
}
}
1 change: 1 addition & 0 deletions src/app/common/datepicker/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './public-api';
43 changes: 43 additions & 0 deletions src/app/common/datepicker/ngb-date-custom-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Injectable } from '@angular/core';

import { NgbDateAdapter, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap';
import isInteger from 'lodash/isInteger';

/**
* [`NgbDateAdapter`](#/components/datepicker/api#NgbDateAdapter) implementation that uses
* native javascript dates as a user date model.
*/

// eslint-disable-next-line @angular-eslint/use-injectable-provided-in
@Injectable()
export class NgbDateCustomAdapter extends NgbDateAdapter<Date> {
/**
* Converts a native `Date` to a `NgbDateStruct`.
*/
fromModel(date: number | Date | string | null): NgbDateStruct | null {
if (date && !(date instanceof Date)) {
date = new Date(date);
}
return date instanceof Date && !isNaN(date.getTime()) ? this._fromNativeDate(date) : null;
}

/**
* Converts a `NgbDateStruct` to a native `Date`.
*/
toModel(date: NgbDateStruct | null): Date | null {
return date && isInteger(date.year) && isInteger(date.month) && isInteger(date.day)
? this._toNativeDate(date)
: null;
}

protected _fromNativeDate(date: Date): NgbDateStruct {
return { year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate() };
}

protected _toNativeDate(date: NgbDateStruct): Date {
const jsDate = new Date(date.year, date.month - 1, date.day, 12);
// avoid 30 -> 1930 conversion
jsDate.setFullYear(date.year);
return jsDate;
}
}
14 changes: 14 additions & 0 deletions src/app/common/datepicker/provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { makeEnvironmentProviders } from '@angular/core';

import { NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';

import { NgbDateCustomAdapter } from './ngb-date-custom-adapter';

export function provideNgbDateAdapter() {
return makeEnvironmentProviders([
{
provide: NgbDateAdapter,
useClass: NgbDateCustomAdapter
}
]);
}
3 changes: 3 additions & 0 deletions src/app/common/datepicker/public-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './provider';
export * from './ngb-date-custom-adapter';
export * from './datepicker-range-popup/datepicker-range-popup.component';
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
/>
</th>
<td class="text-nowrap" cdk-cell *cdkCellDef="let obj">
<div class="text-nowrap" container="body" tooltip="{{ obj[name] | utcDate: format() }}">
<div class="text-nowrap" container="body" ngbTooltip="{{ obj[name] | utcDate: format() }}">
{{ obj[name] | agoDate: hideAgo() }}
</div>
</td>
Expand Down
Loading

0 comments on commit f100afa

Please sign in to comment.