diff --git a/e2e/repeater.e2e-spec.ts b/e2e/repeater.e2e-spec.ts index 0bcebc06..c40e60c9 100644 --- a/e2e/repeater.e2e-spec.ts +++ b/e2e/repeater.e2e-spec.ts @@ -4,15 +4,6 @@ import { } from '@skyux-sdk/e2e'; describe('Repeater', () => { - it('should match the baseline sort screenshot', (done) => { - SkyHostBrowser.get('visual/sort'); - SkyHostBrowser.setWindowBreakpoint('lg'); - SkyHostBrowser.scrollTo('#screenshot-sort-full'); - expect('#screenshot-sort-full').toMatchBaselineScreenshot(done, { - screenshotName: 'sort' - }); - }); - it('should match previous repeater screenshot', (done) => { SkyHostBrowser.get('visual/repeater'); SkyHostBrowser.setWindowBreakpoint('lg'); @@ -22,6 +13,15 @@ describe('Repeater', () => { }); }); + it('should match previous repeater screenshot when an item is active', (done) => { + SkyHostBrowser.get('visual/repeater'); + SkyHostBrowser.setWindowBreakpoint('lg'); + SkyHostBrowser.scrollTo('#screenshot-repeater-with-active-item'); + expect('#screenshot-repeater-with-active-item').toMatchBaselineScreenshot(done, { + screenshotName: 'repeater-with-active-item' + }); + }); + it('should match previous repeater screenshot when all are collapsed', (done) => { SkyHostBrowser.get('visual/repeater'); SkyHostBrowser.setWindowBreakpoint('lg'); diff --git a/src/app/public/modules/repeater/fixtures/repeater.component.fixture.html b/src/app/public/modules/repeater/fixtures/repeater.component.fixture.html index cdf1749f..df1f1b65 100644 --- a/src/app/public/modules/repeater/fixtures/repeater.component.fixture.html +++ b/src/app/public/modules/repeater/fixtures/repeater.component.fixture.html @@ -1,4 +1,7 @@ - + Title 2 Content 2 - + a diff --git a/src/app/public/modules/repeater/fixtures/repeater.component.fixture.ts b/src/app/public/modules/repeater/fixtures/repeater.component.fixture.ts index 33fc98f4..dd62a364 100644 --- a/src/app/public/modules/repeater/fixtures/repeater.component.fixture.ts +++ b/src/app/public/modules/repeater/fixtures/repeater.component.fixture.ts @@ -12,12 +12,8 @@ import { templateUrl: './repeater.component.fixture.html' }) export class RepeaterTestComponent { - @ViewChild(SkyRepeaterComponent) - public repeater: SkyRepeaterComponent; - - public showContextMenu: boolean; - public removeLastItem: boolean; + public activeIndex: number = undefined; public expandMode = 'single'; @@ -25,6 +21,13 @@ export class RepeaterTestComponent { public lastItemSelected = false; + public removeLastItem: boolean; + + public showContextMenu: boolean; + + @ViewChild(SkyRepeaterComponent) + public repeater: SkyRepeaterComponent; + public onCollapse(): void {} public onExpand(): void {} diff --git a/src/app/public/modules/repeater/repeater-item.component.html b/src/app/public/modules/repeater/repeater-item.component.html index e5b6915b..3bae3cc3 100644 --- a/src/app/public/modules/repeater/repeater-item.component.html +++ b/src/app/public/modules/repeater/repeater-item.component.html @@ -1,6 +1,7 @@
; @Input() public set isExpanded(value: boolean) { this.updateForExpanded(value, true); } - public get isSelected(): boolean { - return this._isSelected; + public get isExpanded(): boolean { + return this._isExpanded; } @Input() @@ -58,20 +64,15 @@ export class SkyRepeaterItemComponent implements OnDestroy { this._isSelected = value; } - @Input() - public showInlineForm: boolean = false; - - @Input() - public inlineFormConfig: SkyInlineFormConfig; + public get isSelected(): boolean { + return this._isSelected; + } @Input() - public inlineFormTemplate: TemplateRef; - - @Output() - public inlineFormClose = new EventEmitter(); + public selectable: boolean = false; @Input() - public selectable: boolean = false; + public showInlineForm: boolean = false; @Output() public collapse = new EventEmitter(); @@ -79,11 +80,13 @@ export class SkyRepeaterItemComponent implements OnDestroy { @Output() public expand = new EventEmitter(); - public slideDirection: string; + @Output() + public inlineFormClose = new EventEmitter(); + + public contentId: string = `sky-radio-content-${++nextContentId}`; + + public isActive: boolean = false; - public get isCollapsible(): boolean { - return this._isCollapsible; - } public set isCollapsible(value: boolean) { if (this.isCollapsible !== value) { this._isCollapsible = value; @@ -95,6 +98,14 @@ export class SkyRepeaterItemComponent implements OnDestroy { } } + public get isCollapsible(): boolean { + return this._isCollapsible; + } + + public slideDirection: string; + + private ngUnsubscribe = new Subject(); + private _isCollapsible = true; private _isExpanded = true; @@ -109,23 +120,40 @@ export class SkyRepeaterItemComponent implements OnDestroy { this.slideForExpanded(false); } + public ngOnInit(): void { + setTimeout(() => { + this.repeaterService.registerItem(this); + this.repeaterService.activeItemChange + .takeUntil(this.ngUnsubscribe) + .subscribe((item: SkyRepeaterItemComponent) => { + this.isActive = this === item; + this.changeDetector.markForCheck(); + }); + }); + } + public ngOnDestroy(): void { this.collapse.complete(); this.expand.complete(); this.inlineFormClose.complete(); + + this.ngUnsubscribe.next(); + this.ngUnsubscribe.complete(); + + this.repeaterService.unregisterItem(this); } - public headerClick() { + public headerClick(): void { if (this.isCollapsible) { this.updateForExpanded(!this.isExpanded, true); } } - public chevronDirectionChange(direction: string) { + public chevronDirectionChange(direction: string): void { this.updateForExpanded(direction === 'up', true); } - public updateForExpanded(value: boolean, animate: boolean) { + public updateForExpanded(value: boolean, animate: boolean): void { if (this.isCollapsible === false && value === false) { this.logService.warn( `Setting isExpanded to false when the repeater item is not collapsible @@ -146,7 +174,7 @@ export class SkyRepeaterItemComponent implements OnDestroy { } } - public updateIsSelected(value: SkyCheckboxChange) { + public updateIsSelected(value: SkyCheckboxChange): void { this._isSelected = value.checked; } @@ -154,7 +182,7 @@ export class SkyRepeaterItemComponent implements OnDestroy { this.inlineFormClose.emit(inlineFormCloseArgs); } - private slideForExpanded(animate: boolean) { + private slideForExpanded(animate: boolean): void { this.slideDirection = this.isExpanded ? 'down' : 'up'; } } diff --git a/src/app/public/modules/repeater/repeater.component.spec.ts b/src/app/public/modules/repeater/repeater.component.spec.ts index ab4ad1cd..d5f9d551 100644 --- a/src/app/public/modules/repeater/repeater.component.spec.ts +++ b/src/app/public/modules/repeater/repeater.component.spec.ts @@ -536,6 +536,57 @@ describe('Repeater item component', () => { })); }); + describe('with activeIndex', () => { + let fixture: ComponentFixture; + let cmp: RepeaterTestComponent; + let el: any; + + beforeEach(() => { + fixture = TestBed.createComponent(RepeaterTestComponent); + cmp = fixture.componentInstance; + el = fixture.nativeElement; + }); + + function getItems(): HTMLElement[] { + return el.querySelectorAll('.sky-repeater-item'); + } + + it('should show active item if activeIndex is set on init', fakeAsync(() => { + cmp.activeIndex = 0; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + tick(); + + let activeRepeaterItem = el.querySelectorAll('.sky-repeater-item-active'); + expect(activeRepeaterItem.length).toBe(1); + })); + + it('should add and remove active css class when activeIndex value changes', fakeAsync(() => { + fixture.detectChanges(); + tick(); + + let activeRepeaterItem = el.querySelectorAll('.sky-repeater-item-active'); + expect(activeRepeaterItem.length).toBe(0); + + cmp.activeIndex = 0; + fixture.detectChanges(); + tick(); + + activeRepeaterItem = el.querySelectorAll('.sky-repeater-item-active'); + expect(activeRepeaterItem.length).toBe(1); + const items = getItems(); + expect(items[0]).toHaveCssClass('sky-repeater-item-active'); + + cmp.activeIndex = undefined; + fixture.detectChanges(); + tick(); + + activeRepeaterItem = el.querySelectorAll('.sky-repeater-item-active'); + expect(activeRepeaterItem.length).toBe(0); + })); + }); + describe('with inline-form', () => { let fixture: ComponentFixture; let el: HTMLElement; diff --git a/src/app/public/modules/repeater/repeater.component.ts b/src/app/public/modules/repeater/repeater.component.ts index 8dbbc611..65155766 100644 --- a/src/app/public/modules/repeater/repeater.component.ts +++ b/src/app/public/modules/repeater/repeater.component.ts @@ -3,9 +3,16 @@ import { Component, ContentChildren, Input, - QueryList + QueryList, + OnChanges, + OnDestroy, + SimpleChanges } from '@angular/core'; +import { + Subject +} from 'rxjs/Subject'; + import { SkyRepeaterItemComponent } from './repeater-item.component'; @@ -17,9 +24,14 @@ import { @Component({ selector: 'sky-repeater', styleUrls: ['./repeater.component.scss'], - templateUrl: './repeater.component.html' + templateUrl: './repeater.component.html', + providers: [SkyRepeaterService] }) -export class SkyRepeaterComponent implements AfterContentInit { +export class SkyRepeaterComponent implements AfterContentInit, OnChanges, OnDestroy { + + @Input() + public activeIndex: number; + @Input() public set expandMode(value: string) { this._expandMode = value; @@ -33,36 +45,62 @@ export class SkyRepeaterComponent implements AfterContentInit { @ContentChildren(SkyRepeaterItemComponent) public items: QueryList; + private ngUnsubscribe = new Subject(); + private _expandMode = 'none'; - constructor(private repeaterService: SkyRepeaterService) { - this.repeaterService.itemCollapseStateChange.subscribe((item: SkyRepeaterItemComponent) => { - if (this.expandMode === 'single' && item.isExpanded) { - this.items.forEach((otherItem) => { - if (otherItem !== item && otherItem.isExpanded) { - otherItem.isExpanded = false; - } - }); - } - }); + constructor( + private repeaterService: SkyRepeaterService + ) { + this.repeaterService.itemCollapseStateChange + .takeUntil(this.ngUnsubscribe) + .subscribe((item: SkyRepeaterItemComponent) => { + if (this.expandMode === 'single' && item.isExpanded) { + this.items.forEach((otherItem) => { + if (otherItem !== item && otherItem.isExpanded) { + otherItem.isExpanded = false; + } + }); + } + }); this.updateForExpandMode(); } public ngAfterContentInit() { + // If activeIndex has been set on init, call service to activate the appropriate item. + setTimeout(() => { + if (this.activeIndex || this.activeIndex === 0) { + this.repeaterService.activateItemByIndex(this.activeIndex); + } + }); + // HACK: Not updating for expand mode in a timeout causes an error. // https://github.com/angular/angular/issues/6005 - this.items.changes.subscribe(() => { - setTimeout(() => { - this.updateForExpandMode(this.items.last); - }, 0); - }); + this.items.changes + .takeUntil(this.ngUnsubscribe) + .subscribe(() => { + setTimeout(() => { + this.updateForExpandMode(this.items.last); + }, 0); + }); setTimeout(() => { this.updateForExpandMode(); }, 0); } + public ngOnChanges(changes: SimpleChanges): void { + if (changes['activeIndex'] && changes['activeIndex'].currentValue !== changes['activeIndex'].previousValue) { + this.repeaterService.activateItemByIndex(this.activeIndex); + } + } + + public ngOnDestroy(): void { + this.ngUnsubscribe.next(); + this.ngUnsubscribe.complete(); + } + private updateForExpandMode(itemAdded?: SkyRepeaterItemComponent) { if (this.items) { let foundExpanded = false; diff --git a/src/app/public/modules/repeater/repeater.service.ts b/src/app/public/modules/repeater/repeater.service.ts index e8e37cfd..3b3f677c 100644 --- a/src/app/public/modules/repeater/repeater.service.ts +++ b/src/app/public/modules/repeater/repeater.service.ts @@ -1,17 +1,54 @@ import { EventEmitter, - Injectable + Injectable, + OnDestroy } from '@angular/core'; +import { + BehaviorSubject +} from 'rxjs/BehaviorSubject'; + import { SkyRepeaterItemComponent } from './repeater-item.component'; @Injectable() -export class SkyRepeaterService { +export class SkyRepeaterService implements OnDestroy { + + public activeItemChange = new BehaviorSubject(undefined); + public itemCollapseStateChange = new EventEmitter(); - public onItemCollapseStateChange(item: SkyRepeaterItemComponent) { + public items: SkyRepeaterItemComponent[] = []; + + public ngOnDestroy(): void { + this.activeItemChange.complete(); + this.itemCollapseStateChange.complete(); + } + + public activateItemByIndex(index: number): void { + if (index === undefined) { + this.activeItemChange.next(undefined); + } else { + const activeItem = this.items[index]; + if (activeItem) { + this.activeItemChange.next(activeItem); + } + } + } + + public registerItem(item: SkyRepeaterItemComponent): void { + this.items.push(item); + } + + public unregisterItem(item: SkyRepeaterItemComponent): void { + const indexOfDestroyedItem = this.items.indexOf(item); + if (indexOfDestroyedItem > -1) { + this.items.splice(indexOfDestroyedItem, 1); + } + } + + public onItemCollapseStateChange(item: SkyRepeaterItemComponent): void { this.itemCollapseStateChange.emit(item); } } diff --git a/src/app/visual/repeater/repeater-visual.component.html b/src/app/visual/repeater/repeater-visual.component.html index c8014a66..68b43864 100644 --- a/src/app/visual/repeater/repeater-visual.component.html +++ b/src/app/visual/repeater/repeater-visual.component.html @@ -20,6 +20,45 @@

Basic repeater

+
+

Repeater with activeIndex set on click

+ + + + {{ item.title }} + + + {{ item.note }} + + + + + + +
+
{ + if (this.items[index]) { + this.activeIndex = index; + } else if (this.items[index - 1]) { + this.activeIndex = index - 1; + } + }); + } + + } + + public addItem(): void { + const newItem = { + id: nextItemId++, + title: 'New record ' + nextItemId, + note: 'This is a new record', + fund: 'New fund' + }; + this.items.push(newItem); + } public onCollapse(): void { console.log('Collapsed.'); @@ -62,4 +105,8 @@ export class RepeaterVisualComponent { // Form handling would go here } + + public onItemClick(index: number): void { + this.activeIndex = index; + } }