Skip to content

Commit

Permalink
fix(workbench/view): do not error when initializing view in ngOnInit
Browse files Browse the repository at this point in the history
  • Loading branch information
danielwiehl committed Jun 13, 2024
1 parent 33f4a07 commit 92f0bec
Show file tree
Hide file tree
Showing 2 changed files with 231 additions and 18 deletions.
45 changes: 29 additions & 16 deletions projects/scion/workbench/src/lib/view/view.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {ViewDragService} from '../view-dnd/view-drag.service';
import {GLASS_PANE_BLOCKABLE, GLASS_PANE_OPTIONS, GlassPaneDirective, GlassPaneOptions} from '../glass-pane/glass-pane.directive';
import {WorkbenchView} from './workbench-view.model';
import {NgClass} from '@angular/common';

/**
* Renders the workbench view, using a router-outlet to display view content.
Expand All @@ -36,6 +37,7 @@ import {WorkbenchView} from './workbench-view.model';
],
hostDirectives: [
GlassPaneDirective,
NgClass,
],
providers: [
configureViewGlassPane(),
Expand All @@ -58,30 +60,18 @@ export class ViewComponent implements OnDestroy {
return this._view.id;
}

@HostBinding('attr.class')
public get cssClasses(): string {
return this._view.classList.value.join(' ');
}

@HostBinding('class.view-drag')
public get isViewDragActive(): boolean {
return this._viewDragService.viewDragData !== null;
}

constructor(private _view: ɵWorkbenchView,
private _logger: Logger,
private _host: ElementRef<HTMLElement>,
private _viewDragService: ViewDragService,
viewContextMenuService: ViewMenuService) {
private _logger: Logger) {
this._logger.debug(() => `Constructing ViewComponent. [viewId=${this.viewId}]`, LoggerNames.LIFECYCLE);

viewContextMenuService.installMenuItemAccelerators$(this._host, this._view)
.pipe(takeUntilDestroyed())
.subscribe();

combineLatest([this._view.active$, this._viewport$])
.pipe(takeUntilDestroyed())
.subscribe(([active, viewport]) => active ? this.onActivateView(viewport) : this.onDeactivateView(viewport));
this.installMenuItemAccelerators();
this.subscribeForViewActivation();
this.addViewClassesToHost();
}

private onActivateView(viewport: SciViewportComponent): void {
Expand All @@ -95,6 +85,29 @@ export class ViewComponent implements OnDestroy {
this._view.scrollLeft = viewport.scrollLeft;
}

private installMenuItemAccelerators(): void {
inject(ViewMenuService).installMenuItemAccelerators$(inject(ElementRef<HTMLElement>), this._view)
.pipe(takeUntilDestroyed())
.subscribe();
}

private subscribeForViewActivation(): void {
combineLatest([this._view.active$, this._viewport$])
.pipe(takeUntilDestroyed())
.subscribe(([active, viewport]) => {
active ? this.onActivateView(viewport) : this.onDeactivateView(viewport);
});
}

private addViewClassesToHost(): void {
const ngClass = inject(NgClass);
this._view.classList.value$
.pipe(takeUntilDestroyed())
.subscribe(cssClasses => {
ngClass.ngClass = cssClasses;
});
}

public ngOnDestroy(): void {
this._logger.debug(() => `Destroying ViewComponent [viewId=${this.viewId}]'`, LoggerNames.LIFECYCLE);
}
Expand Down
204 changes: 202 additions & 2 deletions projects/scion/workbench/src/lib/view/view.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
*/

import {ComponentFixture, TestBed} from '@angular/core/testing';
import {Component, inject, Injector, OnDestroy, Type} from '@angular/core';
import {Component, inject, Injector, OnDestroy, OnInit, Type} from '@angular/core';
import {ActivatedRoute, provideRouter} from '@angular/router';
import {WorkbenchViewRegistry} from './workbench-view.registry';
import {WorkbenchRouter} from '../routing/workbench-router.service';
Expand Down Expand Up @@ -1080,7 +1080,7 @@ describe('View', () => {
spyOn(console, 'error').and.callThrough().and.callFake(args => errors.push(...args));

// Open view.
TestBed.inject(WorkbenchRouter).navigate(['path/to/view'], {target: 'view.100'}).then();
await TestBed.inject(WorkbenchRouter).navigate(['path/to/view'], {target: 'view.100'});
await waitUntilStable();

// Expect dialog to show.
Expand All @@ -1102,6 +1102,206 @@ describe('View', () => {
expect(errors).not.toContain(jasmine.stringMatching(`ExpressionChangedAfterItHasBeenCheckedError`));
});

it('should not throw "ExpressionChangedAfterItHasBeenCheckedError" if setting view properties in constructor (navigate new view)', async () => {
@Component({
selector: 'spec-view',
template: 'View',
standalone: true,
})
class SpecViewComponent {
constructor(view: WorkbenchView) {
view.title = 'Title';
view.heading = 'Heading';
view.cssClass = ['class-1', 'class-2'];
}
}

TestBed.configureTestingModule({
providers: [
provideWorkbenchForTest(),
provideRouter([
{path: 'path/to/view', component: SpecViewComponent},
]),
],
});

const fixture = styleFixture(TestBed.createComponent(WorkbenchComponent));
await waitForInitialWorkbenchLayout();

// Spy console.
const errors = new Array<any>();
spyOn(console, 'error').and.callThrough().and.callFake(args => errors.push(...args));

// Open view.
await TestBed.inject(WorkbenchRouter).navigate(['path/to/view'], {target: 'view.100'});
await waitUntilStable();

// Expect view to show.
expect(fixture).toShow(SpecViewComponent);

// Expect view properties.
expect(getViewTitle(fixture, 'view.100')).toEqual('Title');
expect(getViewHeading(fixture, 'view.100')).toEqual('Heading');
expect(getViewCssClass(fixture, 'view.100')).toEqual(jasmine.arrayContaining(['class-1', 'class-2']));

// Expect not to throw `ExpressionChangedAfterItHasBeenCheckedError`.
expect(errors).not.toContain(jasmine.stringMatching(`ExpressionChangedAfterItHasBeenCheckedError`));
});

it('should not throw "ExpressionChangedAfterItHasBeenCheckedError" if setting view properties in "ngOnInit" (navigate new view)', async () => {
@Component({
selector: 'spec-view',
template: 'View',
standalone: true,
})
class SpecViewComponent implements OnInit {

constructor(private _view: WorkbenchView) {
}

public ngOnInit(): void {
this._view.title = 'Title';
this._view.heading = 'Heading';
this._view.cssClass = ['class-1', 'class-2'];
}
}

TestBed.configureTestingModule({
providers: [
provideWorkbenchForTest(),
provideRouter([
{path: 'path/to/view', component: SpecViewComponent},
]),
],
});

const fixture = styleFixture(TestBed.createComponent(WorkbenchComponent));
await waitForInitialWorkbenchLayout();

// Spy console.
const errors = new Array<any>();
spyOn(console, 'error').and.callThrough().and.callFake(args => errors.push(...args));

// Open view.
await TestBed.inject(WorkbenchRouter).navigate(['path/to/view'], {target: 'view.100'});
await waitUntilStable();

// Expect view to show.
expect(fixture).toShow(SpecViewComponent);

// Expect view properties.
expect(getViewTitle(fixture, 'view.100')).toEqual('Title');
expect(getViewHeading(fixture, 'view.100')).toEqual('Heading');
expect(getViewCssClass(fixture, 'view.100')).toEqual(jasmine.arrayContaining(['class-1', 'class-2']));

// Expect not to throw `ExpressionChangedAfterItHasBeenCheckedError`.
expect(errors).not.toContain(jasmine.stringMatching(`ExpressionChangedAfterItHasBeenCheckedError`));
});

it('should not throw "ExpressionChangedAfterItHasBeenCheckedError" if setting view properties in constructor (navigate existing view)', async () => {
@Component({
selector: 'spec-view',
template: 'View',
standalone: true,
})
class SpecViewComponent {
constructor(view: WorkbenchView) {
view.title = 'Title';
view.heading = 'Heading';
view.cssClass = ['class-1', 'class-2'];
}
}

TestBed.configureTestingModule({
providers: [
provideWorkbenchForTest(),
provideRouter([
{path: 'path/to/view', component: SpecViewComponent},
]),
],
});

const fixture = styleFixture(TestBed.createComponent(WorkbenchComponent));
await waitForInitialWorkbenchLayout();

// Spy console.
const errors = new Array<any>();
spyOn(console, 'error').and.callThrough().and.callFake(args => errors.push(...args));

// Open view.
await TestBed.inject(ɵWorkbenchRouter).navigate(layout => layout.addView('view.100', {partId: layout.mainAreaGrid!.activePartId!}));
await waitUntilStable();

// Navigate view.
await TestBed.inject(WorkbenchRouter).navigate(['path/to/view'], {target: 'view.100'});
await waitUntilStable();

// Expect view to show.
expect(fixture).toShow(SpecViewComponent);

// Expect view properties.
expect(getViewTitle(fixture, 'view.100')).toEqual('Title');
expect(getViewHeading(fixture, 'view.100')).toEqual('Heading');
expect(getViewCssClass(fixture, 'view.100')).toEqual(jasmine.arrayContaining(['class-1', 'class-2']));

// Expect not to throw `ExpressionChangedAfterItHasBeenCheckedError`.
expect(errors).not.toContain(jasmine.stringMatching(`ExpressionChangedAfterItHasBeenCheckedError`));
});

it('should not throw "ExpressionChangedAfterItHasBeenCheckedError" if setting view properties in "ngOnInit" (navigate existing view)', async () => {
@Component({
selector: 'spec-view',
template: 'View',
standalone: true,
})
class SpecViewComponent implements OnInit {

constructor(private _view: WorkbenchView) {
}

public ngOnInit(): void {
this._view.title = 'Title';
this._view.heading = 'Heading';
this._view.cssClass = ['class-1', 'class-2'];
}
}

TestBed.configureTestingModule({
providers: [
provideWorkbenchForTest(),
provideRouter([
{path: 'path/to/view', component: SpecViewComponent},
]),
],
});

const fixture = styleFixture(TestBed.createComponent(WorkbenchComponent));
await waitForInitialWorkbenchLayout();

// Spy console.
const errors = new Array<any>();
spyOn(console, 'error').and.callThrough().and.callFake(args => errors.push(...args));

// Open view.
await TestBed.inject(ɵWorkbenchRouter).navigate(layout => layout.addView('view.100', {partId: layout.mainAreaGrid!.activePartId!}));
await waitUntilStable();

// Navigate view.
await TestBed.inject(WorkbenchRouter).navigate(['path/to/view'], {target: 'view.100'});
await waitUntilStable();

// Expect view to show.
expect(fixture).toShow(SpecViewComponent);

// Expect view properties.
expect(getViewTitle(fixture, 'view.100')).toEqual('Title');
expect(getViewHeading(fixture, 'view.100')).toEqual('Heading');
expect(getViewCssClass(fixture, 'view.100')).toEqual(jasmine.arrayContaining(['class-1', 'class-2']));

// Expect not to throw `ExpressionChangedAfterItHasBeenCheckedError`.
expect(errors).not.toContain(jasmine.stringMatching(`ExpressionChangedAfterItHasBeenCheckedError`));
});

describe('Activated Route', () => {

it('should set title and heading from route', async () => {
Expand Down

0 comments on commit 92f0bec

Please sign in to comment.