Smart table for Angular — part 2

Laurent Renard
ITNEXT
Published in
6 min readMay 31, 2018

--

In the previous part we have succeeded in creating a smart table component in a fully declarative way. In this part we are going to explore how to use the factories together with Angular dependency injection in order to complete different requirements.

Most of the smart-table-ng attribute directives rely on an injected smart table instance. Luckily for us, once provided in our components hierarchy, Angular takes care of injecting the instance for us everywhere needed.

In our previous tutorial the instance was created from a static array and provided in the users table container component thanks to the “of” factory.

import { Component } from ‘@angular/core’;
import { SmartTable, of, SortDirection } from ‘smart-table-ng’;
import { users, User } from ‘./users’;
const providers = [
{provide: SmartTable, useValue: of<User>(users)}
];
@Component({
selector: ‘user-list’,
templateUrl: ‘./user-list.component.html’,
providers
})export class UserListComponent {
}

Fetch data asynchronously

As a developer, I want to load my data asynchronously from a server before I plug it into a smart table.

The data won’t often be available before run time and you’ll more likely delegate the responsibility of fetching the data from the server to a dedicated service. Consider the following service.

import { Injectable } from ‘@angular/core’;
import { users, User } from ‘./users’;
@Injectable({
providedIn: ‘root’
})
export class UsersService {

fetchUsers(): Promise<User[]> {
return new Promise(resolve => {
setTimeout(() => resolve(users), 2000);
});
}
}

This service exposes a fetchUsers which returns a Promise that will eventually resolve with the list of the users. The implementation is a detail here, but fakes the round trip to the server by using a timeout of two seconds.

The good thing is that we don’t need to change the component at all, we simply have to change the way the Smart Table is provided.

import { Component } from ‘@angular/core’;
import { SmartTable, from } from ‘smart-table-ng’;
import { UsersService } from ‘./users.service’;
const providers = [{
provide: SmartTable,
useFactory: (Users: UsersService) => from(Users.fetchUsers()),
deps: [UsersService]
}];
@Component({
selector: ‘user-list’,
templateUrl: ‘./user-list.component.html’,
providers
})
export class UserListComponent {
}

We now use the “from” smart table factory. We use the angular FactoryProvider pattern so we can ask the injector to inject an available UsersService to the function in charge of creating the smart table instance.

But my service relies on an HttpClient which uses an API based on Observables

Angular framework relies a lot on the Observable abstraction. Observables are very handy when it comes to produce and consume multiple values produced over time but it can also be used to deal with asynchronous code. That’s what the HttpClient does.

Let’s change our service to make it use an Observable based API and fake interaction with a back end.

import { Injectable } from ‘@angular/core’;
import { users, User } from ‘./users’;
import { of, Observable } from ‘rxjs/index’;
import { delay } from ‘rxjs/operators’;
@Injectable({
providedIn: ‘root’
})
export class UsersService {

fetchUsers(): Observable<User[]> {
return of(users)
.pipe(delay(2000));
}
}

Again the implementation is a detail here and just help us to fake interaction with a server.

The Smart Table from factory works with Promises but also works with Observables so we don’t have anything to change.

Use default/initial values for our directives

I want my users to be sorted by the balance property by default. I also want to filter out all the users whose balance is higher than 2000$ so I can focus on users with low balance in priority.

In some cases you will want to apply default settings to your Smart Table or restore a previous state from a storage system (like locale storage).

Both of and from factories take as a second argument an initial table state.

Let’s change our provider to fulfill the requirements of our user story.

import { Component } from ‘@angular/core’;
import { SmartTable, from } from ‘smart-table-ng’;
import { UsersService } from ‘./users.service’;
const providers = [{
provide: SmartTable,
useFactory: (Users: UsersService) => from(Users.fetchUsers(),{
search: {},
slice: { page: 1, size: 10 },
filter: {
balance: [{ operator: ‘lt’, type: ‘number’, value: 2000 }]
},
sort: {
pointer: ‘balance’,
direction: ‘asc’
}}),
deps: [UsersService]
}];
@Component({
selector: ‘user-list’,
templateUrl: ‘./user-list.component.html’,
providers
})
export class UserListComponent {
}

Attentive readers will have noticed we use a hard coded initial table state. But we could require it as dependency to the injector or fetch it from a dedicated service. Something like:

const providers = [{
provide: SmartTable,
useFactory: (Users: UsersService, config: SmartTableConfigService) => from(Users.fetchUsers(),config.getFor('users')),
deps: [UsersService, SmartTableConfigService]
}];

You can see a similar approach in the following stackblitz:

That’s the beauty of dependency injection, we can swap provided entities very easily and keep our components/services loosely coupled.

Move the whole logic to the server

Fine but our data set has grown significantly and it does not make sense anymore to load all of it in the user’s browser. Our back end team has just finished with the development of a flexible endpoint which handle filter, search, sort and pagination. I guess, I’ll have to change my component.

And the answer is NO !

If you have read the previous tutorial or the smart-table-core documentation you already know that smart-table-core instances can be easily extended and there are already quite a few extensions available. Luckily for us, we can use these extensions (or build our own) within smart-table-ng. For our use case we may need smart-table-server extension.

Note: smart-table-server extension was more a proof of concept but you could develop your own server interface based on this model.

So let’s review our component file.

import { Component } from '@angular/core';
import { SmartTable, of, TableState } from 'smart-table-ng';
import { UsersService } from './users.service';
import { DefaultSettingsService } from './default-settings.service';
import server from 'smart-table-server';
const providers = [{
provide: SmartTable,
useFactory: (Users: UsersService, settings: TableState) => of([], settings, server({
query: (tableState) => Users.queryUsers(tableState)})),
deps: [UsersService, DefaultSettingsService]
}];
@Component({
selector: 'user-list',
templateUrl: './user-list.component.html',
providers
})
export class UserListComponent {
}

Focusing on the providers part.

snippet of the providers section

We use the “of” Smart Table factory with an empty array (indeed we don’t have any data set as everything will be done on the server side). The second argument remains a default table state. You can then pass a list of Smart Table extensions in the same way you would do with smart-table-core package.

In our case we use the smart-table-server extension which requires as argument an object with a query function. This query function rely on the injected UsersService. The implementation of the queryUsers function is not relevant for our tutorial but in practice it should transform a table state into a query the server can understand and parse the response to transform the data in a format smart table can understand.

you can see the result in the following stackblitz:

Conclusion

In this tutorial we have built on top of the component we had developed in the previous tutorial. Using smart table factories and dependency injection we were able to fulfill a wide range of different requirements … without changing a single line of code in our component.

In the next episode we are going to play with Observables to explore even more advanced use cases.

The development of smart-table-ng is still on going and you might see few breaking changes in the near future. I would also like to thank spudsoftware for sponsoring me during the development.

spudsoftware logo

--

--