Develop a web application with Angular2

Angular2 promises many improvements over the previous version.

It’s simpler to learn thanks to easier and more concise concepts like component-based architecture.

The Angular2 module system makes it easier to develop complex and larger projects.

Angular2 is faster than its ancestor thanks to completely rewritten data binding and change detection.

Router module is improved by providing new features like sibling views and nested states.

The aim of this post is to discover some new Angular2 features by developing a web project that uses apart Angular2 other projects like Express, TypeScript and MongoDb.

The source code is provided on GitHub here. You can run the project at this link.

Project structure

The Angular2 sample project has this structure.

    |   +-app 
    |   +-assets (Directoy conatins images and css)
    |   +-index.html
    |   +-tsconfig.json
    |   \-typings.json
    |   +-server.ts
    |   +-tsconfig.json
    |   \-typings.json
    +-dist (Directory generated by gulp)

You can explore the project structure on Github here

Project dependencies

The package.json file identifies npm package dependencies for the project.

  "name": "ng2-project",
  "version": "1.0.0",
  "main": "server/index.js",
  "license": "ISC",
  "scripts": {
    "start": "concurrently \"npm run tsc:w\" \"npm run lite\" ",
    "tsc": "tsc",
    "tsc:w": "tsc -w",
    "lite": "lite-server",
    "postinstall": "gulp"
  "dependencies": {
    "@angular/core": "2.0.0",
    "@angular/common": "2.0.0",
    "@angular/compiler": "2.0.0",
    "@angular/http": "2.0.0",
    "@angular/platform-browser": "2.0.0",
    "@angular/platform-browser-dynamic": "2.0.0",
    "@angular/router": "3.0.0",
    "@angular/upgrade": "2.0.0",
    "systemjs": "0.19.27",
    "es6-shim": "^0.35.0",
    "reflect-metadata": "^0.1.3",
    "rxjs": "5.0.0-beta.12",
    "zone.js": "^0.6.23",
    "angular2-in-memory-web-api": "0.0.20",
    "material-design-lite": "1.1.2",
    "express": "4.13.4",
    "mongodb": "2.1.18",
  "devDependencies": {
    "concurrently": "^2.0.0",
    "lite-server": "^2.1.0",
    "typescript": "1.8.10",
    "del": "2.2.0",
    "gulp": "3.9.1",
    "gulp-concat": "2.6.0",
    "gulp-sourcemaps": "1.6.0",
    "gulp-typescript": "2.13.4",
    "gulp-tsd": "0.1.1",
    "gulp-typings": "1.3.6",
    "run-sequence": "1.1.5"

1- Server Side

We start with the server side implementation, and then we can test the rest API before using it in the client side code.

1.1 Add tsconfig.json file

The tsconfig.json file provides a TypeScript compiler configuration.

  "version": "1.8.10",
  "compilerOptions": {
    "target": "ES5",
    "module": "commonjs",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": false
  "files": [
  "exclude": [

1.1 Add typings file

TypeScript compiler doesn’t recognize natively some JavaScript libraries features and syntax.
Some of the included files that provides required description for TypeScript. Others don’t provide it and they will require to be configured in typings.js file.

  "version": false,
  "ambientDependencies": {
    "node": "github:DefinitelyTyped/DefinitelyTyped/node/node.d.ts#a44529fcb1e1cdc0355e71c42a6048de99b57c34",
    "express-serve-static-core": "github:DefinitelyTyped/DefinitelyTyped/express-serve-static-core/express-serve-static-core.d.ts#a44529fcb1e1cdc0355e71c42a6048de99b57c34",
    "express": "github:DefinitelyTyped/DefinitelyTyped/express/express.d.ts#a44529fcb1e1cdc0355e71c42a6048de99b57c34",
    "serve-static": "github:DefinitelyTyped/DefinitelyTyped/serve-static/serve-static.d.ts#a44529fcb1e1cdc0355e71c42a6048de99b57c34",
    "mime": "github:DefinitelyTyped/DefinitelyTyped/mime/mime.d.ts#a44529fcb1e1cdc0355e71c42a6048de99b57c34"

1.2 – Install and create database

To install MongoDB you can download and install it by following instructions in this link
Next we will need to initialize database data. you can use the code bellow:

use mydata

{"id" : "1" , "name" : "Steve" , "mail" : "" , "phone" : "+33 0120255555" , "adress" : "280 Central Park West New York, NY 10024"},
{"id" : "2" , "name" : "Narco" , "mail" : "" , "phone" : "+33 0120255555" , "adress" : "280 Central Park West New York, NY 10024"},
{"id" : "3" , "name" : "Bombasto" , "mail" : "" , "phone" : "+33 0120255555" , "adress" : "280 Central Park West New York, NY 10024"},
{"id" : "4" , "name" : "Celeritas" , "mail" : "" , "phone" : "+33 0120255555" , "adress" : "280 Central Park West New York, NY 10024"},
{"id" : "5" , "name" : "Magneta" , "mail" : "" , "phone" : "+33 0120255555" , "adress" : "280 Central Park West New York, NY 10024"},
{"id" : "6" , "name" : "RubberMan" , "mail" : "" , "phone" : "+33 0120255555" , "adress" : "280 Central Park West New York, NY 10024"}

You can use MongoDb as a service provided by mLab. All what you have to do is to sign up, create a database and add a user to access the database by using a simple web user interface.

1.3 – Implement the Rest API

In this project we use Express.js to implement the server side API.

Express.js is a web application framework for Node.js, It is designed for building web applications and APIs.

import express = require('express');
import path = require('path');
var port: number = process.env.PORT || 3000;
var app = express();
var MongoClient = require('mongodb').MongoClient;
var database;

app.use('/assets', express.static(path.resolve(__dirname, 'assets')));
app.use('/app', express.static(path.resolve(__dirname, 'app')));
app.use('/libs', express.static(path.resolve(__dirname, 'libs')));

var server = app.listen(port, function() {
    var port = server.address().port;
    console.log("This express app is listening on port " + port);

    function(err, db) {
    if (err) {
        throw err;
    database = db;

app.get("/api/users", (req, res) => {
    // Get all registrations
    database.collection('users').find().toArray(function(err, result) {
        if (err) {
            console.log("Can't get users from database : " + err);
            throw err;
        console.log("/api/users result size = " + result.length);

app.get("/api/users/:id", (req, res) => {
    database.collection('users').findOne( {id :} , function(err, item) {
        if (err) {
            console.log("Can't get users from database : " + err);
            throw err;
        console.log("found user :" + JSON.stringify(item));

var renderIndex = (req: express.Request, res: express.Response) => {
    res.sendFile(path.resolve(__dirname, 'index.html'));

app.get('/*', renderIndex);

1.4 Build and test server side code

To build a project we use Gulp that install typings and compiles TypeScript files.

Bellow the source code of gulpfile.js file

var gulp = require('gulp');
var sourcemaps = require('gulp-sourcemaps');
var ts = require('gulp-typescript');
var runSequence = require('run-sequence');
var gulpTypings = require("gulp-typings");

gulp.task("installTypings", function () {
    var stream = gulp.src(["./server/typings.json"])
    return stream;

gulp.task('buildServer', function () {
    var tsProject = ts.createProject('server/tsconfig.json');
    var tsResult = gulp.src('server/**/*.ts')
    return tsResult.js

gulp.task('build', function (callback) {
    runSequence('installTypings', 'buildServer', callback);

gulp.task('default', ['build']);

To build the project, Open a terminal window and enter this command:

npm install

Next, run the node server:

node dist/server.js

Then test the API in your browseron this url:

If everything is well done, the browser display an array of users in JSON format.

2- Client side

2.1 Add typings file

Bellow the source code of client typings.json

  "ambientDependencies": {
    "es6-shim": "registry:dt/es6-shim#0.31.2+20160317120654"

2.2 Add typescript configuration file

  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": false
  "exclude": [

2.3 Add SystemJs config file

SystemJs is a dynamic module loader. It loads ES6 modules, AMD, CommonJS and global scripts in the browser and NodeJS.
SystemJs will load libraries when they are required.

The configuration below tells SystemJs where to look for a module when component is imported.

(function(global) {

    // map tells the System loader where to look for things
    var map = {
        'app':      'app',        
        // angular bundles
        '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
        '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
        '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
        '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
        '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
        '@angular/http': 'npm:@angular/http/bundles/http.umd.js',
        '@angular/router': 'npm:@angular/router/bundles/router.umd.js',
        'rxjs':     'libs/rxjs',
        '@angular': 'libs/@angular'

    // packages tells the System loader how to load when no filename and/or no extension
    var packages = {
        'app':      { main: './main.js',  defaultExtension: 'js' },
        'rxjs':     { defaultExtension: 'js' },
        'angular2-in-memory-web-api': {defaultExtension: 'js'}

    var config = {
        paths: { // paths serve as alias
          'npm:': 'libs/'
        map: map,
        packages: packages


2.4 Add index.html page

Here, we import style sheets, load libraries and configure SystemJs.

We provide to SystemJS the root module (app) of the application. Then SytemJs will look for all dependencies referenced by the app module.

<html lang="en">
    <base href="/"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge">

    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href=",bold,italic,thin,light,bolditalic,black,medium&amp;lang=en">
    <link rel="stylesheet" href="">
    <link rel="stylesheet" href="">
    <link rel="stylesheet" href="assets/css/styles.css">

    <!-- 1. Load libraries -->
    <!-- IE required polyfills, in this exact order -->
    <script src="libs/es6-shim/es6-shim.min.js"></script>
    <script src="libs/zone.js/dist/zone.js"></script>
    <script src="libs/reflect-metadata/Reflect.js"></script>
    <script src="libs/systemjs/dist/system.src.js"></script>

    <!-- 2. Configure SystemJS -->
    <script src="app/systemjs.config.js"></script>
        System.import('app').catch(function (err) {

    <script src="libs/material-design-lite/dist/material.min.js"></script>


<!-- 3. Display the application -->
<body class="mdl-color--grey-100">

2.5 Add application module : app.module.ts

NgModule is a decorator function that describes the module. The most important properties are:

  • declarations – the view classes that belong to this module.
  • imports – other modules whose exported classes are needed by component templates declared in this module.
  • providers – creators of services that this module contributes to the global collection of services; they become accessible in all parts of the app.
  • bootstrap – the main application view, called the root component, that hosts all other app views. Only the root module should set this bootstrap property.

Here’s a root module of our application:

import { NgModule }             from '@angular/core';
import { BrowserModule }        from '@angular/platform-browser';
import { HttpModule }           from '@angular/http';
import { AppComponent }         from './app.component';
import { routing, appRoutingProviders }  from './app.routing';
import {HomeComponent}          from './home/home.comonent';
import {UserListComponent}      from './users/user-list.component';
import {UserDetailComponent}    from './users/user-detail.component';

  imports: [
  declarations: [
  providers: [
  bootstrap: [ AppComponent ]

export class AppModule { }

2.6 Add main.ts file

Here we launch the application by bootstrapping its root module.

/// <reference path="../typings/browser.d.ts" />
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app.module';

2.7 Configure router : app.routing.ts

Here we configure the router that looks for a corresponding Route from which it can determine the component to display.

In the following example, we configure our application with three route definitions.

import { ModuleWithProviders } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import {HomeComponent}         from './home/home.comonent';
import {HomeResolver}         from './home/home.resolver';
import {HomeService}         from './home/home.service';
import {UserListComponent}     from './users/user-list.component';
import {UserDetailComponent}   from './users/user-detail.component';
import {UserDetailResolver}   from './users/user-detail.resolver';
import {UserListResolver}   from './users/user-list.resolver';
import {UserService}   from './users/user.service';

export const appRoutes : Routes =[
    {path: '',  
     component: HomeComponent,
     resolve : {technologies: HomeResolver}
    {path: 'users', 
     component: UserListComponent,
     resolve : {users: UserListResolver}
    {path: 'user/:id', 
     component: UserDetailComponent, 
     resolve : {user: UserDetailResolver}

export const appRoutingProviders: any[] = [HomeResolver, UserListResolver, 
        UserDetailResolver, HomeService, UserService];
export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes);

To achieve a better user-experience when browsing between pages, we use resolver.

It avoids displaying pages before data finished loading.

It also makes the controller’s code much cleaner in contrast to fetching data inside the controller.

import {Injectable} from '@angular/core';
import {Resolve, ActivatedRouteSnapshot, RouterStateSnapshot} from '@angular/router';
import {Observable} from 'rxjs/Observable';
import { User, UserService } from './user.service';

export class UserListResolver implements Resolve<User[]> {
        private service: UserService
 ) {}
 resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<User[]> {
        return this.service.getUsers();

2.7 Add components

2.6.1 Add application main component

import {Component} from '@angular/core';
import {MDL} from './directives/MaterialDesignLite';
import './rxjs-operators';

    selector: 'my-app',
    templateUrl: 'app/app.component.html'

export class AppComponent { }

“rxjs-oppertors” references the “rxjs-oppertors.ts” file where we will imports all required rxjs statics and operators we need for this application.

// Statics
import 'rxjs/add/observable/throw';
// Operators
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/toPromise';

Next, we write the template html (app.component.html) code.

<div mdl class="mdl-layout mdl-js-layout mdl-layout--fixed-header">
  <header class="mdl-layout__header">
    <div class="mdl-layout__header-row">
      <!-- Title -->
      <span class="mdl-layout-title">Title</span>
      <!-- Add spacer, to align navigation to the right -->
      <div class="mdl-layout-spacer"></div>
      <!-- Navigation. We hide it in small screens. -->
      <nav class="mdl-navigation mdl-layout--large-screen-only">
        <a class="mdl-navigation__link" [routerLink]="['/']" >Home</a>
        <a class="mdl-navigation__link" [routerLink]="['users']">Users</a>
  <div class="mdl-layout__drawer">
    <span class="mdl-layout-title">Title</span>
    <nav class="mdl-navigation">
      <a class="mdl-navigation__link" [routerLink]="['/']" >Home</a>
      <a class="mdl-navigation__link" [routerLink]="['/users']">Users</a>
  <main class="mdl-layout__content">

2.6.2 Add users list component

This page calls a rest web service and display users list.

First, we develop service module.

import {Injectable} from '@angular/core';
import {Http, Headers, Response} from '@angular/http';
import {Observable}     from 'rxjs/Observable';

export class User {
    constructor(public id:number, public name:string, 
         public email:string, public phone:string, public adress:string) {

export class UserService {
    constructor (private http: Http) {}
    private _usersUrl = 'api/users';

    private extractData(res: Response) {
        if (res.status < 200 || res.status >= 300) {
            throw new Error('Bad response status: ' + res.status);
        return res.json();

    private handleError (error: any) {
        let errMsg = error.message || 'Server error';
        console.error(errMsg); // log to console instead
        return Observable.throw(errMsg);

    getUsers() : Observable<User[]> {
        return this.http.get(this._usersUrl)

    getUser(id:number | string) : Observable<User> {
        return this.http.get(this._usersUrl + "/" + id)

Next, we develop the UsersList component.

import {Component, OnInit}   from '@angular/core';
import {Router, ActivatedRoute} from '@angular/router';
import {User}   from './user.service';

    templateUrl: 'app/users/user-list.component.html'
export class UserListComponent implements OnInit {
    private selectedId:number;

    constructor(private route: ActivatedRoute,
                private router:Router) {

    ngOnInit() {
        this.users =['users'];

    isSelected(user:User) {
        return === this.selectedId;

    onSelect(user:User) {

The source code bellow presents the template html code

<div class="page-content mdl-grid">
    <div class="mdl-grid--no-spacing mdl-card mdl-shadow--4dp mdl-cell mdl-cell--12-col">
    <div class="users-card-wide">
        <div class="mdl-card__title mdl-color--purple-700">
            <h1 class="mdl-card__title-text">Users</h1>
        <div class="mdl-list">
          <div class="mdl-list__item mdl-list__item--two-line" *ngFor="let user of users"
            <span class="mdl-list__item-primary-content">
              <img class="user-list-avatar mdl-list__item-avatar" 
              <span class="mdl-list__item-sub-title">
            <a class="mdl-list__item-secondary-action" href="#">
                <i class="material-icons">star</i>

3. Build project

To build the client side and the server side code of our project, we update gulpfile.js.

var gulp = require('gulp');
var path = require('path');
var sourcemaps = require('gulp-sourcemaps');
var ts = require('gulp-typescript');
var del = require('del');
var runSequence = require('run-sequence');
var gulpTypings = require("gulp-typings");

gulp.task('clean', function () {
    return del('dist')

gulp.task("installTypings", function () {
    var stream = gulp.src(["./client/typings.json", "./server/typings.json"])
    return stream;

gulp.task("deleteDupTypings", function () {
    return del(["./client/typings/main.d.ts",

gulp.task('buildServer', function () {
    // code provided previded in the previous section

 jsNPMDependencies, sometimes order matters here! so becareful!
var jsNPMDependencies = [

gulp.task('buildIndex', function () {
    var mappedPaths = (file) {
        return path.resolve('node_modules', file)

    //Let's copy our head dependencies into a dist/libs
    var copyJsNPMDependencies = gulp.src(mappedPaths, {base: 'node_modules'})

    //Let's copy html and js to dist
    var copyIndex = gulp.src(['client/**/*.html', 'client/**/*.js'])

    var copyAsserts = gulp.src('client/assets/**/*')

    return [copyJsNPMDependencies, copyIndex, copyAsserts];

gulp.task('buildClient', function () {
    var tsProject = ts.createProject('client/tsconfig.json');
    var tsResult = gulp.src('client/**/*.ts')
    return tsResult.js

gulp.task('watch', ['buildServer'], function () {'server/*.ts', ['buildServer']);'client/**/*.ts', ['buildClient']);

gulp.task('build', function (callback) {
    runSequence('clean', 'installTypings', 'deleteDupTypings', 'buildServer', 
                  'buildIndex', 'buildClient', callback);

gulp.task('default', ['build']);

Next, Open a terminal window and enter this command:

npm install

4. Run the project

Finally, we launch the node server.

node dist/server.js

Open your browser and test the application at this URL http://localhost:8080.



First impression after developing this sample project, Angular2 needs more investment than its ancestor from developer to start developing application. It requires learning Typescript and understanding other features like SystemJs and Typings.

Angular2 have the advantage to be modular and more structured than its previous version that make it a good choice for large applications development.


Develop a web application with AngularJS and Spring MVC

Nowadays customers have a rich web experience with Gmail, Google Apps, Facebook and Twitter. A basic server side rendered web pages doesn’t fit anymore to the new HMI (human-machine interface) needs.

AngularJs is one of the most successful front-end frameworks for RIA (Rich Internet Application Development) development.

AngularJs encourages the use of the Model-View-Controller (MVC) design pattern to decouple the code and to separate concerns.


Denver Art Museum / Daniel Libeskind

Despite the Javascript language specificities, a JEE developer will appreciate having some best practice and design patterns that he’s familiar with.

This article illustrate how to integrate AngularJs to a Spring MVC project. I hope this article will help developers to take the plunge and start using AngularJs.

The sample project source code is available on this link
The sample IHM Demo is available here.

1. Add AngularJs librery

I adopted Webjars as a way to manage static web resources via the Maven dependency management mechanism. This could be done by front package manager like Bower.


Add ResourceHandler for webjars to your Spring configuration file if it isn’t already done.

@ComponentScan(basePackages = { "com.mycompany.myproject.web.controller" })
public class MvcConfig extends WebMvcConfigurerAdapter {

    public void addResourceHandlers(ResourceHandlerRegistry registry) {
    // Non shown code

2. Develop the business logic

In this sample project, we have a simple business logic. The front-end get a list from a rest web service and display it.
The code bellow presents our business logic implementation.

Add a new javasvript file app.js.

    .module('myApp', ['ngResource'])
    .service('UsersService', function ($log, $resource) {
        return {
            getAll: function () {
                var userResource = $resource('users', {}, {
                    query: {method: 'GET', params: {}, isArray: true}
                return userResource.query();
    .controller('UsersController', function ($scope, $log, UsersService) {
        $scope.users = UsersService.getAll();

myApp : root module for the project.
UsersService : service for executing the http request
UsersController : controller executed when the page is loaded.

3. Develop the view part

AngularJs implements MVC pattern. The html page is the view part, the controller is developed client side by java script and the model is Json object to get from the server side.

Import AngularJs library to your Html code.

<!DOCTYPE html ng-app="myApp">
<html ng-controller="UsersController">
<!-- header code -->

<div ng-repeat="user in users">{{user.firstName}} {{user.familyName}}</div>

<script type="text/javascript" src="webjars/jquery/2.1.1/jquery.js"></script>
<script type="text/javascript" src="webjars/angularjs/1.3.8/angular.min.js"></script>
<script type="text/javascript" src="webjars/angularjs/1.3.8/angular-resource.min.js"></script>

4. Deploy and run the application

Now we will deploy and run the application to verify the configuration.

5. Application enhancement

5.1 Design enhancement

To enhance the application design, we use twitter bootstrap librery with a material design theme.

  • first step, add dependency for jquery, bootstrap and bootstrap-material-design using webjars.
  • Next, import css and js files to the html page.
  • finally, modify html code for displaying the list.

5.2 Functional enhancement

AngularJS comes with many handy filters built-in. We use “filter:string” to add search function. All strings or objects with string properties in array that match this string will be returned.

<!DOCTYPE html>
<html ng-app="myApp">
<head lang="en">
    <meta charset="UTF-8">
    <link rel="stylesheet" href="webjars/bootstrap/3.2.0/css/bootstrap.css">
    <link rel="stylesheet" href="webjars/bootstrap-material-design/0.2.1/css/material.css">
<body ng-controller="UsersController">

<div class="row">
    <div class="container">
        <div id="userList" class="col-sm-offset-1 col-sm-10">
            <div class="input-group">
                <input class="form-control" id="search" name="search" placeholder="Search for" ng-model="query"
              <span class="input-group-btn">
                  <button type="submit" class="btn btn-default">
                      <i class="glyphicon glyphicon-search"></i>
            <div class="list-group">
                <div class="list-group-item">
                    <div ng-repeat="user in users | filter:query" class="list-group-item" style="margin-top:16px">
                        <div class="row-picture">
                            <img class="circle"
                                 src="{{user.firstName | lowercase}}-{{user.familyName | lowercase}}_50x50.jpg"
                        <div class="row-content">
                            <h4 class="list-group-item-heading">{{user.firstName}} {{user.familyName}}</h4>

                            <p class="list-group-item-text"><i class="glyphicon glyphicon-envelope"></i> {{}}
<script type="text/javascript" src="webjars/jquery/2.1.1/jquery.js"></script>
<script type="text/javascript" src="webjars/angularjs/1.3.8/angular.min.js"></script>
<script type="text/javascript" src="webjars/angularjs/1.3.8/angular-resource.min.js"></script>
<script type="text/javascript" src="webjars/bootstrap-material-design/0.2.1/js/material.js"></script>
<script type="text/javascript" src="resources/js/app.js"></script>

The picture bellow presents a print screen for the application.

I think AngularJs, is one of the best front-end framework. It has many great features like data binding, dependency injection, MVC pattern implementation and page templates.

The major new version 2.0 of AngularJS has a significant deference from version 1.X, but it doesn’t seem to have migration path. AngularJs developers will encounter a drastically different looking framework and will need to learn a new architecture.

Even though there is no compatibility with the version 2.0, actual version has many good features and it’s still a good choice as a front-end framework.