Responsive Tables

It’s not unusual to have data which suits being displayed in a tabular format. In the web world we have for this the table element, a classical feature of HTML, and once upon a time also a misused staple of layout. Today we have far better tools for layout, but there remains a challenge with tables: how to display them as the screen-width shrinks, or to use the current parlance, how to make them responsive.

The internet includes a number of options on how best to handle narrow tables, including hiding columns and using horizontal scroll. However I’m operating under the assumption that all the table data must still be displayed, and it should remain grouped by its data rows (which may or may not be rendered horizontally).

I considered three general approaches to a responsive tabular layout: what I will call div-flex; CSS grids; and tables.

In div-flex, each data row is rendered as a div with display: flex, and each data element is a block element within. As the screen-width reduces, the flex layout will wrap the elements onto subsequent lines. The big drawback with this approach is that all the block elements need to be sized consistently, and as it wraps it loses the context of any header row.

In CSS grids each data element is a block element, but unlike div-flex, all those elements are direct children of some container, i.e. there is no element representing the data row. As the screen-width decreases, media selectors are used to change the shape of the grid so it gradually moves from, say, 6 columns to 1 column as the page width shrinks. The biggest drawback of CSS grids is that they have no row grouping – no way of indicating various blocks belong together as the size changes. They simply aren’t designed to have a notion of row-association.

Finally we return to the table. It is semantically the preferred choice and there is a good solution out there which changes the table into a card layout when the media-width is reached. However the drawback with that solution is the need to specify column headers in CSS, something that at best separates content from layout (HTML is for content, not CSS) and, if the headers are dynamically set, is impractical.

My change is to create a small amount of content duplication as a penalty: given a table with headers, we add the header information into a hidden field in each td that is only visible once the screen width is suitably reduced. This is a lot of duplication, but most of us are using HTML generated by templates which support looping, so it’s very little extra effort on our part. The resulting HTML will look something like this (using Angular syntax):

<table class="table-reponsive">
  <thead>
    <tr>
      <th>Name</th>
      <th>Color</th>
      <th>Size</th>
    </tr>
  </thead>
  <tr *ngFor="let shirt of shirts">
    <td><span class="hidden-label">Name</span>{{shirt.name}}</td>
	<td><span class="hidden-label">Color</span>{{shirt.color}}</td>
	<td><span class="hidden-label">Size</span>{{shirt.size}}</td>
  </tr>
</table>

And the CSS:

table.table-reponsive {

    td > span.hidden-label { display: none; } /* Hide labels */

    /* based on https://css-tricks.com/responsive-data-tables/ */
    @media (max-width: 768px) {

        table, thead, tbody, th, td, tr { display: block; }
        thead tr { position: absolute; top: -9999px; left: -9999px; } /* Hide table headers */       
        tr { border: 1px solid #ccc }
        td { border: 0 }
        td > span.hidden-label { display: inline-block; width: 7em; }
    }
}

The result is that the table layout will change to a card layout at width 768px, and display each data-row as a card with the header and the data value.

Angular, Jest, and async NgOnInit

It is common to fetch data in Angular’s ngOnInit method with the expectation that the page will update when the data is returned and bound. In Typescript it is nice to use async methods to improve readability leading to components that have async ngOnInit() methods.

This presents a challenge when working with a testing framework, as we need to wait for promises to be resolved before our component is set up and tests can continue. To handle this, Angular’s TestBed includes async support, and in particular the whenStable() method for waiting for promises to resolve. This is useful, but means having a whenStable()/detectChanges() pair in every test method. Instead I found that whenStable() runs fine in the beforeEach() method where the fixture is created (via TestBed.createComponent()). I’m not clear on why this works when it’s not in an Angular async() (as opposed to a TypeScript async) block, with my best guess being that the TestBed.createComponent automatically creates the NgZone that whenStable() needs.

As the beforeEach is calling whenStable() it now needs to be (TypeScript) async and Jest will wait for the resulting promise to finish before it considers beforeEach to be done. With this approach the unit tests no longer need to be wrapped in an (Angular test) async.

The following code illustrates the full pattern, and also uses a mocking library, ts-jest. The code is all in TypeScript and uses (TypeScript) async for handling promises. It is organized so each inner describe block (e.g. ‘with specificMockDataset’) covers a specific test data set.

// --- MyComponent.Component.ts ---
// … usual imports …
export class MyComponent implements OnInit {
    async ngOnInit() {
        this.thing = await this.myService.getFromServer();
    }
}

// --- MyComponent.Component.test.ts ---
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { mocked } from 'ts-jest/utils';
import { MyComponent } from 'app/components/my.component';
import { MyService } from 'app/services/my.service';
import { AppModule } from 'app/app.module';

jest.mock('app/services/my.service');
const mockService = mocked(MyService, true);

describe('MyComponent component', () => {
    let fixture: ComponentFixture;
    beforeEach(async () => {
        await TestBed.configureTestingModule({
            declarations: [MyComponent],
            providers: [mockService],
            imports: [AppModule]
        }).compileComponents();
    });

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

        beforeEach(async () => {
            mockService.mockClear();
            mockService.prototype.getFromServer.mockImplementation(() => { return Promise.resolve({ field: "value" }); });

            fixture = TestBed.createComponent(MyComponent);
            fixture.detectChanges(); // calls ngOnInit()
            await fixture.whenStable(); // waits for promises to complete
            fixture.detectChanges(); // detect changes made due to field changes made during ngOnInit
        });

        it('should have options', () => {
            expect(fixture.componentInstance).toBeDefined();
            const selectClient: HTMLSelectElement = fixture.debugElement.query(By.css('#someEle')).nativeElement;
            expect(selectClient.options.length).toBe(2);
        });
    });
});

Edit 23-Feb-2021:

The above approach, which worked with Angular 5, no longer seems to work with Angular 10. Now whenStable() never returns causing a timeout in the beforeEach. My solution has been to replace the whenStable with a timeout call which gives up the event loop, allowing the ngOnInit to run.
The beforeEach() now looks like this:

beforeEach((done) => {
    mockService.mockClear();
    mockService.prototype.getFromServer.mockImplementation(() => { return Promise.resolve({ field: "value" }); });fixture = TestBed.createComponent(MyComponent);

    fixture.detectChanges(); // calls ngOnInit()
    setTimeout(() => {
        fixture.detectChanges(); // detect changes made due to field changes made during ngOnInit
        done();
    }, 1); // let the event loop run
});