Responsive Tables in Angular 1.5

The question remains; how does we make tabular data on mobile devices look good? Native HTML tables work fine on large screens but you end up scrolling horizontally on smaller screens, something that developers generally want to avoid. I will be going through the steps to make a responsive table using Angular 1.5 components to fix this issue and as a way to highlight some of the things we can do with components.

The component pattern in JavaScript is currently en vogue with both React and Angular 2 relying on it as a core concept. Angular 1.5 has introduced the component paradigm to make the transition up to Angular 2 less painful. However even if you don’t plan to migrate your apps, developing using the component pattern is really enjoyable.

The Table Components

We’re actually going to be creating 3 components that will work together. The components will work like so:

<hz-table>
<hz-table-row>
<hz-table-data>Apple</hz-table-data>
<hz-table-data>Green</hz-table-data>
<hz-table-data>$1.00</hz-table-data>
</hz-table-row>
<hz-table-row>
<hz-table-data>Banana</hz-table-data>
<hz-table-data>Yellow</hz-table-data>
<hz-table-data>$0.50</hz-table-data>
</hz-table-row>
</hz-table>

Each component has a similar structure

  • a template file (hz-table.html)
  • a controller (hz-table.controller.js)
  • a component definition (hz-table.component.js)
  • a style file (hz-table.scss)

For the hz-table component the component definition looks like

import template from './hzTable.html';
import controller from './hzTable.controller';
import './hzTable.scss';

let hzTableComponent = {
restrict: 'E',
bindings: {},
template,
controller,
transclude: true
};

export default hzTableComponent;

hz-table-row and hz-table-data components are very similiar

import template from './hzTableRow.html';
import controller from './hzTableRow.controller';
import './hzTableRow.scss';

let hzTableRowComponent = {
restrict: 'E',
bindings: {},
template,
controller,
transclude: true
};

export default hzTableRowComponent;
import template from './hzTableData.html';
import controller from './hzTableData.controller';
import './hzTableData.scss';

let hzTableDataComponent = {
restrict: 'E',
bindings: {},
template,
controller,
transclude: true
};

export default hzTableDataComponent;

Transclusion

The only important thing to note so far is that we have enabled transclusion in all 3 components. We do this in the hz-table-data component because we want to be able to pull the values that are inside the tags into the component template.

<hz-table-data>Apple</hz-table-data>

The value will be printed inside the hz-table-data template wherever ng-transclude is placed:

<div class="hz-table-data" ng-transclude>

</div>

When rendered this will give us something the following output:

<div class="hz-table-data">
Apple
</div>

The other 2 components, hz-table and hz-table-row will be doing something similar by pulling in the template of the child component.

<div ng-transclude class="hz-table">
</div>
<div ng-transclude class="hz-table-row">
</div>

Bindings

To set the table heading values we pass in an array of text values to the table component. In the desktop view of the table we will just show them along the top as in a normal native table. However in the mobile view we will print the heading beside the value in each data cell. So the data is entered at the topmost component, hz-table, and is available in the child components using bindings.

<hz-table headings="['Fruit', 'Color', 'Cost']">
<hz-table-row>
<hz-table-data>Apple</hz-table-data>
<hz-table-data>Green</hz-table-data>
<hz-table-data>$1.00</hz-table-data>
</hz-table-row>
<hz-table-row>
<hz-table-data>Banana</hz-table-data>
<hz-table-data>Yellow</hz-table-data>
<hz-table-data>$0.50</hz-table-data>
</hz-table-row>
</hz-table>

We can get these values into the component by adding ‘headings’ to the bindings object in the table component definition.

let hzTableComponent = {
restrict: 'E',
bindings: {
headings: '<'
},
template,
controller,
transclude: true
};

The ‘<’ means that the values coming in will be a one-way data flow into the component, i.e you can only read the values and can’t update them on the parent. We can now print the values in the table component.

<div class="hz-table">
<div class="hz-table__headings">
<div class="hz-table__heading"
ng-repeat="heading in $ctrl.headings">
{{ heading }}
</div>
</div>

<div ng-transclude></div>
</div>

Notice I’ve moved the transclude from the wrapping div to a div that is a sibling of the headings.

To make headings look more like a table heading we can add some styles

.hz-table {

&__heading {
display: inline-block;
padding: 10px 20px;
background-color: #333;
color: #fff;
}
}

That flattens the headings into one line. We do something similar for the table data elements.

.hz-table-data {

display: inline-block;
padding: 10px 20px;
background-color: #eee;
color: #333;
}

We’re left with something resembling a table but still needs some work.

We need to set the width to 100% and each cell needs to be a percent of the full 100%. For now lets assume we only ever have 3 columns (we’ll make this more generic later).

.hz-table-data {

display: inline-block;
padding: 10px 20px;
background-color: #eee;
color: #333;
float: left;

&--col-3 {
width: 33.33333%;
}
}
<div class="hz-table-data hz-table-data--col-3">
<ng-transclude></ng-transclude>
</div>

And the headings also

.hz-table {

&__heading {
display: inline-block;
padding: 10px 20px;
background-color: #333;
color: #fff;
float: left;

&--col-3 {
width: 33.33333%;
}
}
}
<div class="hz-table">
<div class="hz-table__headings">
<div class="hz-table__heading hz-table__heading--col-3" ng-repeat="heading in $ctrl.headings">{{ heading }}</div>
</div>

<div ng-transclude></div>
</div>

Better, but how do we make it more generic so we can add different numbers of columns?

  • Add styles for different column numbers for both headings and data components.
  • Figure out how many columns we have and apply the correct column class, e.g if there are 4 columns add class xxx — col-4.
&--col-2 {
width: 50%;
}
&--col-3 {
width: 33.33333%;
}
&--col-4 {
width: 25%;
}
/* keep going for as many columns as you need */

To figure out how many columns there are we just count the number of items in the array we passed into the hz-table component.

class HzTableController {
$onInit() {
this.dynamicClass = 'hz-table__heading--col-' + this.headings.length;
}
}
export default HzTableController;

And in the view we apply the dynamic class using ng-class

<div class="hz-table">
<div class="hz-table__headings">
<div class="hz-table__heading"
ng-class="$ctrl.dynamicClass"
ng-repeat="heading in $ctrl.headings">{{ heading }}</div>
</div>

<div ng-transclude></div>
</div>

The hz-table-data component also needs to know how may columns there are so it can apply the same styling logic but it doesn’t have access to the headings array, yet!

We can share data from the parent to the child or grandchild components by ‘requiring’ the parent in the component definitions of hz-table-row and hz-table-data.

In hz-table-row we make the hz-table controller available for use in the hz-table-row controller as “parent” like so:

require: {
parent: '^hzTable'
},

And the same goes for the hz-table-data controller definition.

require: {
parent: '^hzTableRow'
},

Functions on the parent controller can be called using

this.parent.[functionName]

The 3 component controllers now look like:

// hz-table component
class
HzTableController {
$onInit() {
this.dynamicClass = 'hz-table__heading--col-' + this.headings.length;
}
getHeadings() {
return this.headings;
}
}
export default HzTableController;

// hz-table-row component
class
HzTableRowController {
$onInit() {
this.headings = this.parent.getHeadings();
}
getHeadings() {
return this.headings;
}
}
export default HzTableRowController;

// hz-table-data component
class
HzTableDataController {
$onInit() {
this.headings = this.parent.getHeadings();
this.dynamicClass = 'hz-table-data--col-' + this.headings.length;
}
}
export default HzTableDataController;

This leaves us at a point where we can add as many columns as we want (provided we have a style for whatever column count it is). So by just adding an ID column the components should know how many columns to split the table into.

<hz-table headings="['ID', 'Fruit', 'Color', 'Cost']">
<hz-table-row>
<hz-table-data>1</hz-table-data>
<hz-table-data>Apple</hz-table-data>
<hz-table-data>Green</hz-table-data>
<hz-table-data>$1.00</hz-table-data>
</hz-table-row>
<hz-table-row>
<hz-table-data>2</hz-table-data>
<hz-table-data>Banana</hz-table-data>
<hz-table-data>Yellow</hz-table-data>
<hz-table-data>$0.50</hz-table-data>
</hz-table-row>
</hz-table>

Making it responsive

The meat of this section is styling so I’ll just include some styles for mobile views and show what the result is. We basically hide the headings area and stack up the table data cells.

.hz-table-data {

display: block;
padding: 10px 20px;
background-color: #eee;
color: #333;
}

@media screen and (min-width: 780px) {
.hz-table-data {
display: inline-block;
float: left;

&--col-3 {
width: 33.33333%;
}
&--col-4 {
width: 25%;
}
}
}
.hz-table {

&__heading {
display: none;
}
}

@media screen and (min-width: 780px) {
.hz-table {
&__heading {
display: inline-block;
padding: 10px 20px;
background-color: #333;
color: #fff;
float: left;

&--col-2 {
width: 50%;
}
&--col-3 {
width: 33.33333%;
}
&--col-4 {
width: 25%;
}
}
}
}

The result of this is the data cells stack up and the rows remain stacked.

The final part is to get the headings to display beside each data item. We already have access to the headings array in the hz-table-data component but each individual cell doesn’t know which heading corresponds to itself. We’re going to use the hz-table-row component to let its children hz-table-data components know where in the list they they come. Each table-data will register itself with its parent and the parent will return the place it comes in the list.

class HzTableRowController {
$onInit() {
this.headings = this.parent.getHeadings();
this.count = 0;
}
getHeadings() {
return this.headings;
}
getPlaceInList() {
return this.count++;
}
}
export default HzTableRowController;
class HzTableDataController {
$onInit() {
this.headings = this.parent.getHeadings();
this.dynamicClass = 'hz-table-data--col-' + this.headings.length;

this.placeInList = this.parent.getPlaceInList();
}
}
export default HzTableDataController;

And in the table-data template view we can grab the column name using $ctrl.headings[$ctrl.placeInList]

<div class="hz-table-data" ng-class="$ctrl.dynamicClass">
<span class="hz-table-data__col-name">{{$ctrl.headings[$ctrl.placeInList]}} : </span>
<ng-transclude></ng-transclude>
</div>

And that’s about it. You can get the code of the 3 components from my github. I’m hoping to add some pagination features to this in the coming weeks. Stay tuned.