Skip to content

Latest commit

 

History

History
300 lines (239 loc) · 10.3 KB

app-shell.md

File metadata and controls

300 lines (239 loc) · 10.3 KB

App Shell

This guide assumes that the CLI setup guide has been completed.

Let's build the App Shell to our app. App Shell is an architecture that focuses on providing an instant-loading experience to users by rendering a "shell" of an application immediately when the first document is returned from the server, before any other assets have loaded. Read more about it on developers.google.com.

By creating a new project with Angular CLI and the --mobile flag, the application already has the build step configured to generate an App Shell from the app's root component. It's up to you, the developer, to evolve the App Shell as your app evolves.

In this guide, we'll use the main app component that Angular CLI created for us to easily generate an App Shell. Our app shell will just have an Angular Material toolbar with the name of our app.

Let's install the Angular Material toolbar and Angular Material core packages from npm.

$ npm install --save @angular2-material/toolbar @angular2-material/core

(Note: this recipe was written as of Angular Material 2 alpha.4)

There are a couple of steps to let the CLI know how to serve the Angular Material toolbar assets. First, open src/system-config.ts, and add a SystemJS config entry for the toolbar in the existing config object:

src/system-config.ts:

/***********************************************************************************************
 * User Configuration.
 **********************************************************************************************/
/** Map relative paths to URLs. */
const map: any = {
  '@angular2-material': 'vendor/@angular2-material'
};

/** User packages configuration. */
const packages: any = {
  '@angular2-material/toolbar': {
    defaultExtension: 'js',
    main: 'toolbar.js'
  }
};

This tells the SystemJS JavaScript loader, which Angular CLI uses to load and bundle JS files, how to load assets when a module imports @angular2-material/toolbar.

The second step to make the package available is to open angular-cli-build.js in the project's root directory, and add an entry to the vendorNpmFiles array:

angular-cli-build.js:

var Angular2App = require('angular-cli/lib/broccoli/angular2-app');
module.exports = function(defaults) {
  return new Angular2App(defaults, {
    vendorNpmFiles: [
      // ...leave default files intact
      '@angular2-material/**/*.+(js|css|map)'
    ]
  });
};

This will cause the CLI to copy the toolbar, and any other Angular Material components added later, into a "vendor" directory at build time.

Note: when making changes to angular-cli-build.js, it's necessary to kill the ng serve process and start again for changes to take effect.

Now let's add the toolbar to our component. Open src/app/hello-mobile.component.ts in your editor. We first need to import the directives from the toolbar package like so:

src/app/hello-mobile.component.ts:

import { Component } from '@angular/core';
import { APP_SHELL_DIRECTIVES } from '@angular/app-shell';
import { MdToolbar } from '@angular2-material/toolbar';

And then we need to register the MdToolbar directive with our component by adding it to the @Component decorator's directives list so it can be available in the component's view. Here is what the @Component decorator and class should look like:

src/app/hello-mobile.component.ts:

@Component({
  moduleId: module.id,
  selector: 'hello-mobile-app',
  template: `
  <h1>
    {{title}}
  </h1>
  `,
  styles: [],
  directives: [APP_SHELL_DIRECTIVES, MdToolbar]
})
export class HelloMobileAppComponent {
  title = 'hello-mobile works!';
}

Now let's add the toolbar to our template, and give it a better title. So our final component should look like this:

src/app/hello-mobile.component.ts:

import { Component } from '@angular/core';
import { APP_SHELL_DIRECTIVES } from '@angular/app-shell';
import { MdToolbar } from '@angular2-material/toolbar';

@Component({
  moduleId: module.id,
  selector: 'hello-mobile-app',
  template: `
    <md-toolbar>
      {{title}}
    </md-toolbar>
  `,
  styles: [],
  directives: [APP_SHELL_DIRECTIVES, MdToolbar]
})
export class HelloMobileAppComponent {
  title = 'Hello Mobile';
}

(Note about templates: Currently the App Shell build tool only supports inline templates and CSS for components that are included in the pre-rendered shell. Support for templateUrl and styleUrls is planned. See Angular CLI Issue #810.

If you're still running ng serve, kill it and build the app with ng serve --prod, and examine the generated index.html in dist/index.html. You should see something like this:

dist/index.html

<hello-mobile-app>
  <md-toolbar _nghost-fnt-2="">
    <div _ngcontent-fnt-2="" class="md-toolbar-layout">
      <md-toolbar-row _ngcontent-fnt-2="">
        Hello Mobile
      </md-toolbar-row>
    </div>
  </md-toolbar>
</hello-mobile-app>

Our App Shell plugin has automatically pre-rendered the HelloMobileAppComponent in place of the hello-mobile-app element. The index also includes inlined CSS styles to give styling to the toolbar so that it can be rendered immediately, without requesting additional resources over the network. This is what the app shell should look like before the rest of the application is loaded and bootstrapped:

Screen shot of App Shell

Note: the app shell itself is continually re-compiled during development, but running ng serve --prod or ng build --prod will also bundle all of the app's JavaScript into a single JS file that gets loaded asynchronously after the App Shell is rendered. Since there is more bundling and minification logic with the production build, it takes significantly longer, and is therefore not recommended during active development.

dist/index.html:

<script src="/app-concat.js" async=""></script>

When the application is ready, it will seamlessly replace the app shell component with the dynamic root component.

Providers

As we continue building our app, we'll likely add more Dependency Injection providers. If any of the directives included in the App Shell require these providers, it will be necessary to add the providers inside of main-app-shell.ts. Since our App Shell plugin is using Angular Universal to prerender our App Shell, it's okay to use providers that come with Universal, such as the Http providers. If your App Shell depends on providers that aren't supported in NodeJS, then it's necessary to create fake, or "Mock", implementations of the providers in a way that will work in Node.

Sharing the Root Component

Ideally, we'd like to use the same root component for the pre-compiled App Shell and dynamic runtime versions of our application. This makes it easier to make sure the App Shell transitions seamlessly to the dynamic application, and saves the headache of maintaining two copies of the same component.

However, there are usually components that you'd like to have in your App Shell but not your app at runtime, and vice versa. For example, we might want to have a router outlet in our dynamic application, and a progress indicator in its place in our App Shell. Fortunately, our app component already has a couple of directives available that make this simple, *shellRender and *shellNoRender. These directives are already included in the HelloMobileAppComponent via the APP_SHELL_DIRECTIVES added to the directives list.

Let's add a progress indicator to our app shell to see put these directives to use. Of course, we'll use the progress indicator provided by Angular Material.

$ npm install --save @angular2-material/progress-circle

We need to add an entry for the progress-circle to system-config.ts so SystemJS knows how to load it:

src/system-config.ts:

/** User packages configuration. */
const packages: any = {
  '@angular2-material/toolbar': {
    defaultExtension: 'js',
    main: 'toolbar.js'
  },
  '@angular2-material/progress-circle': {
    defaultExtension: 'js',
    main: 'progress-circle.js'
  }
};

Then we'll import the MdSpinner directive to HelloMobileAppComponent, add it to our component's directives, and add it to our template. We'll also a CSS rule to center and add some space around the loading indicator.

src/app/hello-mobile.component.ts:

import { Component } from '@angular/core';
import { APP_SHELL_DIRECTIVES } from '@angular/app-shell';
import { MdToolbar } from '@angular2-material/toolbar';
import { MdSpinner } from '@angular2-material/progress-circle';

@Component({
  moduleId: module.id,
  selector: 'hello-mobile-app',
  template: `
    <md-toolbar>
      {{title}}
    </md-toolbar>
    <md-spinner></md-spinner>
  `,
  styles: [`
    md-spinner {
      margin: 24px auto 0;
    }
  `],
  directives: [APP_SHELL_DIRECTIVES, MdToolbar, MdSpinner]
})
export class HelloMobileAppComponent {
  title = 'Hello Mobile';
}

Now, as we run our app with ng serve, we should see this for both our App Shell and the dynamic, runtime version of our app:

Screen shot of App Shell with Spinner

But we don't want the spinner to stick around. Let's put the *shellRender directive to use, and specify that the spinner should only show in the App Shell.

<md-spinner *shellRender></md-spinner>

Now when the app is fully loaded and Angular takes over, the spinner will disappear. Inversely, we can use the *shellNoRender directive to only render components at runtime and not in the App Shell. For example, if we add a route to our app, we can prevent the <router-outlet> component from rendering any routes. Let's illustrate with a simple element that should only be shown when the page is fully rendered.

src/hello-mobile.component.ts:

<md-toolbar>
  {{title}}
</md-toolbar>
<md-spinner *shellRender></md-spinner>
<h1 *shellNoRender>App is Fully Rendered</h1>

Now if we navigate to localhost:4200 in our browser, the spinner should render when the app shell is displayed, and the "App is Fully Rendered" text should render when Angular bootstraps the app.