All Components

Master-Detail Grids

Some projects require you to display table data in a hierarchical-like fashion so that the relations between parent and child records are easily visualized.

In such cases, you can use the detail template feature of the master Grid to load the detail component that contains the corresponding child Grid records filtered by the parent key field value.

import { Component, ViewChild } from '@angular/core';
import { Observable } from 'rxjs/Rx';

import {
    GridComponent,
    GridDataResult,
    DataStateChangeEvent
} from '@progress/kendo-angular-grid';

import { SortDescriptor } from '@progress/kendo-data-query';

import { CategoriesService } from './northwind.service';

@Component({
    providers: [CategoriesService],
    selector: 'my-app',
    template: `
      <kendo-grid
          [data]="view | async"
          [pageSize]="pageSize"
          [skip]="skip"
          [sortable]="true"
          [sort]="sort"
          [pageable]="true"
          [scrollable]="'none'"
          (dataStateChange)="dataStateChange($event)"
        >
        <kendo-grid-column field="CategoryID" width="100"></kendo-grid-column>
        <kendo-grid-column field="CategoryName" width="200" title="Category Name"></kendo-grid-column>
        <kendo-grid-column field="Description" [sortable]="false">
        </kendo-grid-column>
        <div *kendoGridDetailTemplate="let dataItem">
            <category-details [category]="dataItem"></category-details>
        </div>
      </kendo-grid>
  `
})
export class AppComponent {

    public view: Observable<GridDataResult>;
    public sort: Array<SortDescriptor> = [];
    public pageSize: number = 10;
    public skip: number = 0;

    @ViewChild(GridComponent) grid: GridComponent;

    constructor(private service: CategoriesService) { }

    public ngOnInit(): void {
        this.view = this.service; /*we bind directly to the service as it is a Subject and will return the data*/

        this.loadData(); /*fetch the data with the initial state*/
    }

    public dataStateChange({ skip, take, sort }: DataStateChangeEvent): void {
        /*save the current state of the Grid component*/
        this.skip = skip;
        this.pageSize = take;
        this.sort = sort;

        this.loadData(); /*re-load the data with the new state*/
    }

    public ngAfterViewInit(): void {
        this.grid.expandRow(0); // expand the first row initially
    }

    private loadData(): void {
        this.service.query({ skip: this.skip, take: this.pageSize, sort: this.sort });
    }
}
import { Component, ViewChild, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Rx';
import { GridDataResult, GridComponent, PageChangeEvent } from '@progress/kendo-angular-grid';

import { ProductsService } from './northwind.service';

@Component({
    selector: 'category-details',
    providers: [ProductsService],
    template: `
      <kendo-grid
          [data]="view | async"
          [pageSize]="5"
          [skip]="skip"
          [pageable]="true"
          [scrollable]="'none'"
          (pageChange)="pageChange($event)"
        >
      <kendo-grid-column field="ProductID" title="Product ID" width="120">
      </kendo-grid-column>
      <kendo-grid-column field="ProductName" title="Product Name">
      </kendo-grid-column>
      <kendo-grid-column field="UnitPrice" title="Unit Price" format="{0:c}">
      </kendo-grid-column>
      </kendo-grid>
  `
})
export class CategoryDetailComponent implements OnInit {

    /**
     * The category for which details are displayed
     */
    @Input() public category: Object;

    private view: Observable<GridDataResult>;
    private skip: number = 0;

    constructor(private service: ProductsService) { }

    public ngOnInit(): void {
        this.view = this.service;

        /*load products for the given category*/
        this.service.queryForCategory(this.category, { skip: this.skip, take: 5 });
    }

    protected pageChange({ skip, take }: PageChangeEvent): void {
        this.skip = skip;
        this.service.queryForCategory(this.category, { skip, take });
    }
}
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { GridDataResult } from '@progress/kendo-angular-grid';
import { toODataString } from '@progress/kendo-data-query';
import { Observable } from 'rxjs/Observable';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';

import 'rxjs/add/operator/map';

export abstract class NorthwindService extends BehaviorSubject<GridDataResult> {
    private BASE_URL: string = 'http://services.odata.org/V4/Northwind/Northwind.svc/';

    constructor(private http: Http, private tableName: string) {
        super(null);
    }

    public query(state: any): void {
        this.fetch(this.tableName, state)
            .subscribe(x => super.next(x));
    }

    private filterToString({ filter }: { filter?: string }): string {
        return filter ? `&$filter=${filter}` : '';
    }

    private fetch(tableName: string, state: any): Observable<GridDataResult> {
        const queryStr = `${toODataString(state)}&$count=true${this.filterToString(state)}`;

        return this.http
            .get(`${this.BASE_URL}${tableName}?${queryStr}`)
            .map(response => response.json())
            .map(response => (<GridDataResult>{
                data: response.value,
                total: parseInt(response["@odata.count"], 10)
            }));
    }
}

@Injectable()
export class ProductsService extends NorthwindService {
    constructor(http: Http) { super(http, "Products"); }

    public queryForCategory({ CategoryID }: { CategoryID: number }, state?: any): void {
        this.query(Object.assign({}, state, { "filter": `CategoryID eq ${CategoryID}` }));
    }

    public queryForProductName(ProductName: string, state?: any): void {
        this.query(Object.assign({}, state, { "filter": `contains(ProductName, '${ProductName}')` }));
    }

}

@Injectable()
export class CategoriesService extends NorthwindService {
    constructor(http: Http) { super(http, "Categories"); }
}
import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpModule } from '@angular/http';

import { GridModule } from '@progress/kendo-angular-grid';

import { AppComponent }   from './app.component';

import { CategoryDetailComponent } from './category-details.component';

@NgModule({
  imports: [
    BrowserModule,
    HttpModule,
    GridModule
  ],
  declarations: [
    AppComponent,
    CategoryDetailComponent
  ],
  bootstrap: [
    AppComponent
  ]
})
export class AppModule { }

import { AppModule } from './ng.module';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

const platform = platformBrowserDynamic();
platform.bootstrapModule(AppModule);

In this article