Skip to content

Commit

Permalink
feat: added dbxNavbar
Browse files Browse the repository at this point in the history
  • Loading branch information
dereekb committed Feb 1, 2022
1 parent 0b65ac1 commit 8f7d087
Show file tree
Hide file tree
Showing 26 changed files with 393 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,8 @@ export class DbxAngularRouterService implements DbxRouterService, DbxRouterTrans
}
}

isActive(segueRef: SegueRef<any>): boolean {
return false; // TODO!
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,12 @@ export class DbxUIRouterService implements DbxRouterService, DbxRouterTransition
return this.state.go(segueRef.ref, params, segueRef.refOptions).then(_ => true).catch(_ => false);
}

isActive(segueRef: SegueRef): boolean {
const { ref, refParams } = segueRef;

const targetRef = (ref.startsWith('.') ? `^${ref}` : ref);
const active = this.state.includes(targetRef, refParams);
return active;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,11 @@ export abstract class DbxRouterService {
*/
abstract go(segueRef: SegueRef): Promise<boolean>;

/**
* Returns true if the input segue ref is considered active.
*
* @param segueRef
*/
abstract isActive(segueRef: SegueRef): boolean;

}
2 changes: 2 additions & 0 deletions packages/dbx-core/src/lib/router/router/transition/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from './transition';
export * from './transition.rxjs';
export * from './transition.directive';
export * from './transition.watcher.directive';
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Observable, startWith } from 'rxjs';
import { Directive, NgZone } from '@angular/core';
import { DbxRouterTransitionService } from '../service/router.transition.service';
import { successTransition } from './transition.rxjs';

/**
* Abstract directive that listens to onSuccess transition events and runs a function.
*/
@Directive()
export abstract class AbstractTransitionDirective {

readonly transitionSuccess$ = successTransition(this.dbNgxRouterTransitionService.transitions$);
readonly initAndUpdateOnTransitionSuccess$: Observable<void> = this.transitionSuccess$.pipe(startWith(undefined)) as Observable<void>;

constructor(protected readonly dbNgxRouterTransitionService: DbxRouterTransitionService) { }

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { filter, MonoTypeOperatorFunction, Observable } from "rxjs";
import { DbxRouterTransitionEvent, DbxRouterTransitionEventType } from "./transition";

/**
* Convenience function for filtering success from the input observable.
*
* @param obs
* @returns
*/
export function successTransition(obs: Observable<DbxRouterTransitionEvent>): Observable<DbxRouterTransitionEvent> {
return obs.pipe(filterTransitionSuccess());
}

export function filterTransitionSuccess(): MonoTypeOperatorFunction<DbxRouterTransitionEvent> {
return filterTransitionEvent(DbxRouterTransitionEventType.SUCCESS);
}

export function filterTransitionEvent(type: DbxRouterTransitionEventType): MonoTypeOperatorFunction<DbxRouterTransitionEvent> {
return filter(x => x.type === type);
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
import { Directive, NgZone, OnDestroy, OnInit } from '@angular/core';
import { filter } from 'rxjs/operators';
import { DbxRouterTransitionEventType } from './transition';
import { DbxRouterTransitionService } from '../service/router.transition.service';
import { SubscriptionObject } from '@dereekb/rxjs';
import { DbxRouterTransitionService } from '../service/router.transition.service';
import { AbstractTransitionDirective } from './transition.directive';

/**
* Abstract directive that listens to onSuccess transition events and runs a function.
*/
@Directive()
export abstract class AbstractTransitionWatcherDirective implements OnInit, OnDestroy {
export abstract class AbstractTransitionWatcherDirective extends AbstractTransitionDirective implements OnInit, OnDestroy {

private _transitionSub = new SubscriptionObject();

constructor(protected readonly dbNgxRouterTransitionService: DbxRouterTransitionService, protected readonly ngZone: NgZone) { }

ngOnInit(): void {
this._transitionSub.subscription = this.dbNgxRouterTransitionService.transitions$.pipe(
filter(x => x.type === DbxRouterTransitionEventType.SUCCESS)
).subscribe(() => {
this._transitionSub.subscription = this.transitionSuccess$.subscribe(() => {
this.updateForSuccessfulTransition();
});
}
Expand All @@ -26,6 +21,10 @@ export abstract class AbstractTransitionWatcherDirective implements OnInit, OnDe
this._transitionSub.destroy();
}

constructor(dbNgxRouterTransitionService: DbxRouterTransitionService, protected readonly ngZone: NgZone) {
super(dbNgxRouterTransitionService);
}

// MARK: Action
protected zoneUpdateForSuccessfulTransition(): void {
this.ngZone.run(() => this.updateForSuccessfulTransition());
Expand Down
12 changes: 12 additions & 0 deletions packages/dbx-core/src/lib/util/view.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import { MonoTypeOperatorFunction, tap } from 'rxjs';
import { ChangeDetectorRef, ViewRef, ElementRef } from "@angular/core";
import { Maybe } from "@dereekb/util";

/**
* Convenience function used within observables for views that need to detect changes after a value changes.
*
* @param cdRef
* @param timeout
* @returns
*/
export function tapDetectChanges<T>(cdRef: ChangeDetectorRef, timeout = 0): MonoTypeOperatorFunction<T> {
return tap(() => setTimeout(() => safeDetectChanges(cdRef), timeout));
}

/**
* Triggers a detection change on the input view as long as the view has not been destroyed.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,17 @@
import { Component, Input } from '@angular/core';

export enum DbxContentContainerPadding {
NONE = 'none',
MIN = 'min',
SMALL = 'small',
NORMAL = 'normal'
}
export type DbxContentContainerPadding = 'none' | 'min' | 'small' | 'normal';

export enum DbxContentContainerWidth {
SMALL = 'small',
MEDIUM = 'medium',
WIDE = 'wide',
FULL = 'full'
}
export type DbxContentContainerWidth = 'small' | 'medium' | 'wide' | 'full';

export enum DbxContentContainerType {
/**
* Full/unrestricted height content.
*/
NORMAL = 'normal',
/**
* Content that has a header above it and should take up the rest of the height of the page.
*/
CONTENT = 'content'
}
/**
* DbxContentContainer type.
*
* Two values:
* - normal: Full/unrestricted height content.
* - content: Content that has a header above it and should take up the rest of the height of the page.
*/
export type DbxContentContainerType = 'normal' | 'content';

/**
* Component that limits the max-width of the content.
Expand All @@ -40,13 +28,13 @@ export enum DbxContentContainerType {
export class DbxContentContainerComponent {

@Input()
type = DbxContentContainerType.NORMAL;
type = 'normal';

@Input()
width = DbxContentContainerWidth.WIDE;
width = 'wide';

@Input()
padding = DbxContentContainerPadding.NORMAL;
padding = 'normal';

@Input()
scrollingContent = false;
Expand Down
1 change: 1 addition & 0 deletions packages/dbx-web/src/lib/layout/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './bar';
export * from './block';
export * from './card';
export * from './column';
Expand Down
1 change: 1 addition & 0 deletions packages/dbx-web/src/lib/router/layout/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './anchor';
export * from './sidenav';
export * from './layout.module';
6 changes: 5 additions & 1 deletion packages/dbx-web/src/lib/router/layout/layout.module.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { NgModule } from '@angular/core';
import { DbxAnchorModule } from './anchor/anchor.module';
import { DbxNavbarModule } from './navbar/navbar.module';
import { DbxSidenavModule } from './sidenav/sidenav.module';

@NgModule({
exports: [
DbxAnchorModule
DbxAnchorModule,
DbxNavbarModule,
DbxSidenavModule
]
})
export class DbxRouterLayoutModule { }
2 changes: 2 additions & 0 deletions packages/dbx-web/src/lib/router/layout/navbar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './navbar.component';
export * from './navbar.module';
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<ng-container [ngSwitch]="mode$ | async">
<!-- Button -->
<ng-container *ngSwitchCase="'button'">
<button mat-icon-button [matMenuTriggerFor]="menu" class="nav-menu-button" aria-label="open navigation">
<mat-icon>menu</mat-icon>
</button>
<mat-menu #menu>
<dbx-anchor *ngFor="let x of anchors$ | async" [anchor]="x.anchor">
<button mat-menu-item [ngClass]="(x.selected) ? 'active' : ''">{{ x.anchor.title }}</button>
</dbx-anchor>
</mat-menu>
</ng-container>
<!-- Bar -->
<nav *ngSwitchDefault mat-tab-nav-bar [attr.mat-align-tabs]="navAlign">
<dbx-anchor *ngFor="let x of anchors$ | async" [anchor]="x.anchor">
<a mat-tab-link [active]="x.selected">{{ x.anchor.title }}</a>
</dbx-anchor>
</nav>
</ng-container>
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, Input } from '@angular/core';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { UIRouterModule } from '@uirouter/angular';
import { DbxNavbarModule } from './navbar.module';
import { ClickableAnchorLink } from '@dereekb/dbx-core';

describe('NavbarComponent', () => {

beforeEach(async () => {
TestBed.configureTestingModule({
imports: [
NoopAnimationsModule,
DbxNavbarModule,
UIRouterModule.forRoot()
],
declarations: [TestViewComponent]
}).compileComponents();
});

let testComponent: TestViewComponent;
let fixture: ComponentFixture<TestViewComponent>;

beforeEach(() => {
fixture = TestBed.createComponent(TestViewComponent);
testComponent = fixture.componentInstance;
fixture.detectChanges();
});

describe('with links', () => {

beforeEach(async () => {
testComponent.links = [{
title: 'Test'
}];

fixture.detectChanges();
});

it('should render', () => {
expect(true).toBe(true);
});

});

});

@Component({
template: `
<app-nav-bar [links]="links"></app-nav-bar>
`
})
class TestViewComponent {

@Input()
public links?: ClickableAnchorLink[];

}
95 changes: 95 additions & 0 deletions packages/dbx-web/src/lib/router/layout/navbar/navbar.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Observable } from 'rxjs';
import { ScreenMediaWidthType, screenMediaWidthTypeIsActive } from './../../../screen/screen';
import { DbxScreenMediaService } from '../../../screen/screen.service';
import { Maybe } from '@dereekb/util';
import { Input, Component, NgZone, OnDestroy, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { ClickableAnchorLink, AbstractTransitionWatcherDirective, DbxRouterService, DbxRouterTransitionService, AbstractTransitionDirective, tapDetectChanges } from '@dereekb/dbx-core';
import { BehaviorSubject, combineLatest, map, shareReplay, distinctUntilChanged, startWith, tap } from 'rxjs';

interface NavAnchorLink {
selected: boolean;
anchor: ClickableAnchorLink;
}

export type NavBarContentAlign = 'center' | 'left' | 'right';
export type NavbarMode = 'bar' | 'button';

/**
* Component that displays a navbar.
*/
@Component({
selector: 'dbx-navbar',
templateUrl: './navbar.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DbxNavbarComponent extends AbstractTransitionDirective implements OnDestroy {

@Input()
navAlign = 'center';

private _inputMode = new BehaviorSubject<Maybe<NavbarMode>>(undefined);
private _breakpoint = new BehaviorSubject<ScreenMediaWidthType>('large');
private _anchors = new BehaviorSubject<ClickableAnchorLink[]>([]);

readonly isBreakpointActive$ = combineLatest([this._dbxScreenMediaService.widthType$, this._breakpoint]).pipe(
map(([current, breakpoint]) => screenMediaWidthTypeIsActive(current, breakpoint)),
distinctUntilChanged(),
shareReplay(1)
);

readonly mode$ = combineLatest([this._inputMode, this.isBreakpointActive$]).pipe(
map(([inputMode, breakpointActive]) => {
return (breakpointActive) ? (inputMode ?? 'bar') : 'button';
}),
distinctUntilChanged(),
tapDetectChanges(this.cdr),
shareReplay(1)
);

readonly anchors$: Observable<NavAnchorLink[]> = combineLatest([this._anchors, this.initAndUpdateOnTransitionSuccess$]).pipe(
map(([anchors]) => {
return anchors.map((anchor) => {
let selected = this._dbxRouterService.isActive(anchor);

return {
selected,
anchor
};
});
}),
tapDetectChanges(this.cdr),
shareReplay(1)
);

constructor(
dbxRouterTransitionService: DbxRouterTransitionService,
private cdr: ChangeDetectorRef,
private readonly _dbxScreenMediaService: DbxScreenMediaService,
private readonly _dbxRouterService: DbxRouterService
) {
super(dbxRouterTransitionService);
}

ngOnDestroy(): void {
this._inputMode.complete();
this._breakpoint.complete();
this._anchors.complete();
}

// MARK: Accessors
@Input()
public set anchors(anchors: Maybe<ClickableAnchorLink[]>) {
this._anchors.next(anchors ?? []);
}

@Input()
public set mode(mode: Maybe<NavbarMode>) {
this._inputMode.next(mode);
}

@Input()
public set breakpoint(breakpoint: ScreenMediaWidthType) {
this._breakpoint.next(breakpoint);
}

}
Loading

0 comments on commit 8f7d087

Please sign in to comment.