Skip to content

Commit 11425cb

Browse files
committed
[angular-material] ListWithDetail improvements
* Implement default selection behavior * Fixed with of master sidenav * Delete buttons only shown on hover
1 parent 483c00c commit 11425cb

File tree

2 files changed

+281
-13
lines changed

2 files changed

+281
-13
lines changed

packages/angular-material/src/other/master-detail/master.ts

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,23 +27,30 @@ export const removeSchemaKeywords = (path: string) => {
2727
<mat-sidenav-container class="container" [fxHide]="hidden">
2828
<mat-sidenav mode="side" opened>
2929
<mat-nav-list>
30+
<mat-list-item *ngIf="masterItems.length === 0">No items</mat-list-item>
3031
<mat-list-item
31-
*ngFor="let item of masterItems; trackBy: trackElement"
32+
*ngFor="let item of masterItems; let i = index; trackBy: trackElement"
3233
[class.selected]="item === selectedItem"
33-
(click)="onSelect(item)"
34+
(click)="onSelect(item, i)"
35+
(mouseover)="onListItemHover(i)"
36+
(mouseout)="onListItemHover(undefined)"
3437
>
3538
<a matLine>{{item.label}}</a>
36-
<button mat-icon-button class="button" (click)="onDeleteClick(item)">
39+
<button mat-icon-button
40+
class="button hide"
41+
(click)="onDeleteClick(item, i)"
42+
[ngClass]="{'show': highlightedIdx == i}"
43+
>
3744
<mat-icon mat-list-icon>delete</mat-icon>
3845
</button>
3946
</mat-list-item>
4047
</mat-nav-list>
41-
<button mat-fab color="primary" class="button" (click)="onAddClick()">
48+
<button mat-fab color="primary" class="add-button" (click)="onAddClick()" >
4249
<mat-icon aria-label="Add item to list">add</mat-icon>
4350
</button>
4451
</mat-sidenav>
4552
<mat-sidenav-content class="content">
46-
<jsonforms-detail [item]="selectedItem"></jsonforms-detail>
53+
<jsonforms-detail *ngIf="selectedItem" [item]="selectedItem"></jsonforms-detail>
4754
</mat-sidenav-content>
4855
</mat-sidenav-container>`,
4956
styles: [`
@@ -53,24 +60,44 @@ export const removeSchemaKeywords = (path: string) => {
5360
.content {
5461
padding: 15px;
5562
}
63+
.add-button {
64+
float: right;
65+
margin-top: 0.5em;
66+
margin-right: 0.25em;
67+
}
5668
.button {
5769
float: right;
5870
margin-right: 0.25em;
5971
}
72+
.hide {
73+
display: none;
74+
}
75+
.show {
76+
display: inline-block;
77+
}
78+
mat-sidenav {
79+
width: 20%;
80+
}
6081
`]
6182
})
6283
export class MasterListComponent extends JsonFormsControl {
6384

6485
masterItems: any[];
6586
selectedItem: any;
87+
selectedItemIdx: number;
6688
addItem: (path: string) => () => void;
6789
removeItems: (path: string, toDelete: any[]) => () => void;
6890
propsPath: string;
91+
highlightedIdx: number;
6992

7093
constructor(ngRedux: NgRedux<JsonFormsState>) {
7194
super(ngRedux);
7295
}
7396

97+
onListItemHover(idx: number) {
98+
this.highlightedIdx = idx;
99+
}
100+
74101
trackElement(_index: number, element: any) {
75102
return element ? element.label : null;
76103
}
@@ -106,17 +133,45 @@ export class MasterListComponent extends JsonFormsControl {
106133
return masterItem;
107134
});
108135
this.masterItems = masterItems;
136+
137+
// pre-select 1st entry
138+
if (this.masterItems.length > 0 && this.selectedItem === undefined) {
139+
this.selectedItem = this.masterItems[0];
140+
this.selectedItemIdx = 0;
141+
}
109142
}
110143

111-
onSelect(item: any): void {
144+
onSelect(item: any, idx: number): void {
112145
this.selectedItem = item;
146+
this.selectedItemIdx = idx;
113147
}
114148

115149
onAddClick() {
116150
this.addItem(this.propsPath)();
117151
}
118152

119-
onDeleteClick(item: any) {
153+
onDeleteClick(item: any, idx: number) {
154+
this.deleteItem(item);
155+
if (this.selectedItem.path === item.path && this.masterItems[idx]) {
156+
// select next element, if possible
157+
this.selectedItem = this.masterItems[idx];
158+
this.selectedItemIdx = idx;
159+
} else if (this.selectedItem.path === item.path && this.masterItems.length > 0) {
160+
// select 1st entry, if no next element available
161+
this.selectedItem = this.masterItems[0];
162+
this.selectedItemIdx = 0;
163+
} else if (this.selectedItemIdx >= idx) {
164+
// selected index has changed
165+
this.selectedItemIdx -= 1;
166+
this.selectedItem = this.masterItems[this.selectedItemIdx];
167+
} else if (this.masterItems.length === 0) {
168+
// unset select if no elements anymore
169+
this.selectedItemIdx = -1;
170+
this.selectedItem = undefined;
171+
}
172+
}
173+
174+
deleteItem(item: any) {
120175
this.removeItems(this.propsPath, [item.data])();
121176
}
122177
}

packages/angular-material/test/master-detail.spec.ts

Lines changed: 219 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2323
THE SOFTWARE.
2424
*/
25-
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
25+
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
2626
import { By } from '@angular/platform-browser';
2727
import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing';
2828
import {
@@ -36,14 +36,14 @@ import { NgRedux } from '@angular-redux/store';
3636
import { MockNgRedux } from '@angular-redux/store/testing';
3737
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
3838
import { JsonFormsOutlet, UnknownRenderer } from '@jsonforms/angular';
39-
import { MasterListComponent } from '../src/other/master-detail/master';
40-
import { JsonFormsDetailComponent } from '../src/other/master-detail/detail';
4139
import { FlexLayoutModule } from '@angular/flex-layout';
4240
import { DebugElement } from '@angular/core';
41+
import { MasterListComponent } from '../src/other/master-detail/master';
42+
import { JsonFormsDetailComponent } from '../src/other/master-detail/detail';
4343

4444
describe('Master detail', () => {
4545

46-
let fixture: ComponentFixture<any>;
46+
let fixture: ComponentFixture<MasterListComponent>;
4747
let component: any;
4848

4949
const data = {
@@ -181,7 +181,7 @@ describe('Master detail', () => {
181181
});
182182
}));
183183

184-
it('remove a master item', async(() => {
184+
it('remove an item', async(() => {
185185
const mockSubStore = MockNgRedux.getSelectorStub();
186186
component.uischema = uischema;
187187

@@ -208,6 +208,218 @@ describe('Master detail', () => {
208208
});
209209
}));
210210

211+
it('remove an item with index < selected index', fakeAsync(() => {
212+
const moreData = {
213+
orders: [
214+
{
215+
customer: { name: 'Carrot Chipmunk' },
216+
title: 'Carrots'
217+
},
218+
{
219+
customer: { name: 'Banana Joe' },
220+
title: 'Bananas'
221+
},
222+
{
223+
customer: { name: 'Fry' },
224+
title: 'Slurm'
225+
}
226+
]
227+
};
228+
const mockSubStore = MockNgRedux.getSelectorStub();
229+
component.uischema = uischema;
230+
231+
mockSubStore.next({
232+
jsonforms: {
233+
core: {
234+
data: moreData,
235+
schema,
236+
}
237+
}
238+
});
239+
component.ngOnInit();
240+
fixture.detectChanges();
241+
tick();
242+
243+
// select last element
244+
const listItems: DebugElement[] = fixture.debugElement.queryAll(By.directive(MatListItem));
245+
listItems[2].nativeElement.click();
246+
fixture.detectChanges();
247+
tick();
248+
249+
// delete 1st item
250+
spyOn(component, 'deleteItem').and.callFake(() => {
251+
mockSubStore.next({
252+
jsonforms: {
253+
core: {
254+
data: { orders: moreData.orders.slice(1) },
255+
schema,
256+
}
257+
}
258+
});
259+
mockSubStore.complete();
260+
fixture.detectChanges();
261+
tick();
262+
});
263+
const buttons: DebugElement[] = fixture.debugElement.queryAll(By.css('button'));
264+
buttons[0].nativeElement.click();
265+
266+
expect(component.selectedItemIdx).toBe(1);
267+
expect(component.selectedItem.data.title).toBe('Slurm');
268+
}));
269+
270+
it('remove an item with index > selected index', fakeAsync(() => {
271+
const moreData = {
272+
orders: [
273+
{
274+
customer: { name: 'Carrot Chipmunk' },
275+
title: 'Carrots'
276+
},
277+
{
278+
customer: { name: 'Banana Joe' },
279+
title: 'Bananas'
280+
},
281+
{
282+
customer: { name: 'Fry' },
283+
title: 'Slurm'
284+
}
285+
]
286+
};
287+
const mockSubStore = MockNgRedux.getSelectorStub();
288+
component.uischema = uischema;
289+
290+
mockSubStore.next({
291+
jsonforms: {
292+
core: {
293+
data: moreData,
294+
schema,
295+
}
296+
}
297+
});
298+
component.ngOnInit();
299+
fixture.detectChanges();
300+
tick();
301+
302+
// delete 2nd item
303+
spyOn(component, 'deleteItem').and.callFake(() => {
304+
const copy = moreData.orders.slice();
305+
copy.splice(1, 1);
306+
mockSubStore.next({
307+
jsonforms: {
308+
core: {
309+
data: { orders: copy },
310+
schema,
311+
}
312+
}
313+
});
314+
mockSubStore.complete();
315+
fixture.detectChanges();
316+
tick();
317+
});
318+
const buttons: DebugElement[] = fixture.debugElement.queryAll(By.css('button'));
319+
buttons[1].nativeElement.click();
320+
321+
expect(component.selectedItemIdx).toBe(0);
322+
expect(component.selectedItem.data.title).toBe('Carrots');
323+
}));
324+
325+
it('remove an item with index == selected index', fakeAsync(() => {
326+
const moreData = {
327+
orders: [
328+
{
329+
customer: { name: 'Carrot Chipmunk' },
330+
title: 'Carrots'
331+
},
332+
{
333+
customer: { name: 'Banana Joe' },
334+
title: 'Bananas'
335+
},
336+
{
337+
customer: { name: 'Fry' },
338+
title: 'Slurm'
339+
}
340+
]
341+
};
342+
const mockSubStore = MockNgRedux.getSelectorStub();
343+
component.uischema = uischema;
344+
345+
mockSubStore.next({
346+
jsonforms: {
347+
core: {
348+
data: moreData,
349+
schema,
350+
}
351+
}
352+
});
353+
component.ngOnInit();
354+
fixture.detectChanges();
355+
tick();
356+
357+
// delete 1st item
358+
spyOn(component, 'deleteItem').and.callFake(() => {
359+
mockSubStore.next({
360+
jsonforms: {
361+
core: {
362+
data: { orders: moreData.orders.slice(1) },
363+
schema,
364+
}
365+
}
366+
});
367+
mockSubStore.complete();
368+
fixture.detectChanges();
369+
tick();
370+
});
371+
const buttons: DebugElement[] = fixture.debugElement.queryAll(By.css('button'));
372+
buttons[0].nativeElement.click();
373+
374+
expect(component.selectedItemIdx).toBe(0);
375+
expect(component.selectedItem.data.title).toBe('Bananas');
376+
}));
377+
378+
it('remove last item', fakeAsync(() => {
379+
const moreData = {
380+
orders: [
381+
{
382+
customer: { name: 'Carrot Chipmunk' },
383+
title: 'Carrots'
384+
}
385+
]
386+
};
387+
const mockSubStore = MockNgRedux.getSelectorStub();
388+
component.uischema = uischema;
389+
390+
mockSubStore.next({
391+
jsonforms: {
392+
core: {
393+
data: moreData,
394+
schema,
395+
}
396+
}
397+
});
398+
component.ngOnInit();
399+
fixture.detectChanges();
400+
tick();
401+
402+
// delete item
403+
spyOn(component, 'deleteItem').and.callFake(() => {
404+
mockSubStore.next({
405+
jsonforms: {
406+
core: {
407+
data: { orders: [] },
408+
schema,
409+
}
410+
}
411+
});
412+
mockSubStore.complete();
413+
fixture.detectChanges();
414+
tick();
415+
});
416+
const buttons: DebugElement[] = fixture.debugElement.queryAll(By.css('button'));
417+
buttons[0].nativeElement.click();
418+
419+
expect(component.selectedItemIdx).toBe(-1);
420+
expect(component.selectedItem).toBe(undefined);
421+
}));
422+
211423
it('setting detail on click', async(() => {
212424
const mockSubStore = MockNgRedux.getSelectorStub();
213425
component.uischema = uischema;
@@ -251,7 +463,8 @@ describe('Master detail', () => {
251463
scope: '#/properties/customer/properties/name'
252464
}]
253465
}
254-
}
466+
},
467+
0
255468
);
256469
});
257470
});

0 commit comments

Comments
 (0)