Skip to content

Instantly share code, notes, and snippets.

@esperancaJS
Last active August 21, 2017 11:58
Show Gist options
  • Save esperancaJS/f5f4008426334e68d8b45ce5fa1293fb to your computer and use it in GitHub Desktop.
Save esperancaJS/f5f4008426334e68d8b45ce5fa1293fb to your computer and use it in GitHub Desktop.
Using Angular CLI in a way that scales

Angular CLI - Lessons Learned

Why Angular 2 and angular CLI ?

The one difference we can all agree on between developing in Angular or in React is the level of predictability.

Jumping from an Angular project to another should be seamless if it follows the same principles described in the style guide.

And Angular CLI helps this level of predictability even further by giving us a standard and encapsulated way of managing our environments, generating new elements and keeping up to date with the best way of doing all this.

When and when not to use Angular CLI

General websites of any level of complexity but with no need for custom webpack configurations are a very good case and account for probably 75%+ of use cases.

So for example: if you want to develop an app with nativecript or electron maybe using angular cli will not help you. For this you can refer to angular-seed-advanced.


Who is this for ?

This blog post is not a tutorial, it's a collection of bits of information that can be useful to someone looking for ways on how to organize their angular cli applications.

So if you haven't tried out Angular and Angular CLI I suggest you do that first, you don't need to master it, just to have a basic understanding of what you want to achieve with it and some ideas on how to do it.


SASS/SCSS structure

With Angular's component based architecture our global mixins and variables must be imported into each .sass file individually (same with other css processors)

So that we can do things like:

@import "~bootstrap/scss/variables"
@import "custom"  // <-- bootstrap configuration goes here
@import "~bootstrap/scss/mixins"

@include media-breakpoint-down(md) 
	//conditions for small medium and under screens here

and

@import "our-color-variables.scss"


.some-button
	color: $our-picton-blue

You can start realizing the problem here, having import these files everywhere becomes a nuisance.

So we can join them all in a single file file called _all_variables_and_mixins.scss like so:

// bootstrap
@import "~bootstrap/scss/variables";
@import "custom";  // <-- bootstrap configuration goes here
@import "~bootstrap/scss/mixins";

//our
@import "our-color-variables.scss";
@import "our-button-mixins.scss";
@import "our-animation-mixins.scss";

that we then import pretty much in every .sass file like so:

@import "all-variables-and-mixins"

Notes:

  • only import variables and mixins into _all_variables_and_mixins.scss since these don't increase your CSS bundle, importing css classes on the other hand does and you would start seeing repeated classes.

  • to import .scss files into others without resorting to ../../../../.. in the @import declarations you must first configure global paths for folders where you keep files that you want to make accessible from anywhere in the app structure on .angular-cli.json like so:

    //...
    "apps": [
      {
        //...
        "stylePreprocessorOptions": {
          "includePaths": [
            "assets/styles" // <-- globally accessible files via @import
          ]
        },
        //...
      }
    ]
    //...

Building

Tools for making sure the build is as small as possible and our pages as fast as possible to compile are built in to Angular CLI, all that is needed is to make use of them.

The command to achieve the smallest possible and fastest to render build is:

ng build --aot --env=prod --prod
  • --prod minify hash, and gzip.
  • --env=prod use your prod environment constants file.
  • --aot compile angular templates via AOT

And to analyze our build files we can use the flag:


Generating Components in an App that can scale

Encapsulating components with their own modules makes it much easier to understand what dependencies they are using.

So instead of doing:

ng generate component component-name

Which generates a folder structure like such:

├── home/
│   ├── home.module.ts
│   ├── home.component.ts|html|css|spec.ts
│   └── shared/
│     ├── shared.module.ts
│     ├── bar/
│     │  └── bar.component.ts|html|css|spec.ts
│     ├── footer/
│     │  └── footer.component.ts|html|css|spec.ts
│     ├── top-banner/
│     │  └── top-banner.component.ts|html|css|spec.ts

And importing all the dependencies for all the shared components in home/shared/shared.module.ts

We can instead do:

ng generate module component-name
ng generate component component-name

and then manually declare the components as exports in component-name.module.ts like so:

@NgModule({
	...
	exports: [ComponentNameComponent]
	...
})

Which generates a folder structure like such:

├── home/
│   ├── home.module.ts
│   ├── home.component.ts|html|css|spec.ts
│   └── shared/
│     ├── shared.module.ts
│     ├── bar/
│     │  ├── bar.module.ts
│     │  └── bar.component.ts|html|css|spec.ts
│     ├── footer/
│     │  ├── footer.module.ts
│     │  └── footer.component.ts|html|css|spec.ts
│     ├── top-banner/
│     │  ├── top-banner.module.ts
│     │  └── top-banner.component.ts|html|css|spec.ts

This way our components are wrapped in their own module where we can include dependencies only to the component making it easier to understand what components are using what.


Injecting scripts in index.html

Including scripts in the index.html from third party services is a very common use case that can be achieved by injecting them into src/main.ts instead like so:

if (environment.production) {

  document.write(
    `
		<!-- Google Analytics -->
		<script>
		window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
		ga('create', '${environment.keys.googleanalyticsKey}', 'auto');
		ga('send', 'pageview');
		</script>
		<script async src='https://www.google-analytics.com/analytics.js'></script>
		<!-- End Google Analytics -->
    `
  );

  enableProdMode();
}

Note ${environment.keys.googleanalyticsKey} where we set the key according to the environment.


Bonus: Routing that scales

This last one is maybe more of an opinion that I feel like sharing since most of the examples on how to use angular routing have various approaches and this one is a combination of many of those.

Firstly maybe it's better to have a view of our app structure

├── app.module.ts
├── app.routes.ts
├── app.component.ts|html|css|spec.ts
├── home/
│   ├── home.module.ts
│   ├── home.routes.ts
│   ├── home.component.ts|html|css|spec.ts
│   │  ├─index/
│   │  ├─about/
│   │  ├─team/
│   ...
├── blog/                       <-- lazy loaded
│   ...
│   ├── blog.routes.ts
│   ...
├── style-guide/                <-- lazy loaded
│   ...
│   ├── style-guide.routes.ts
│   ...

So for any component, lazy or not, that has it's own set of child page components will have it's own [name].routes.ts with it's respective routes which looks something like:

import { ModuleWithProviders } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { HomeComponent } from './home.component';
import { IndexComponent } from './index/index.component';
import { AboutComponent } from './about/about.component';
import { TeamComponent } from './team/team.component';

export const homeRoutes: Routes = [
  {
    path: '',
    component: HomeComponent,
    children: [
      {
        path: '',
        component: IndexComponent
      },
      {
        path: 'about',
        component: AboutComponent
      },
      {
        path: 'team',
        component: TeamComponent
      }
    ]
  }
];

export const routing: ModuleWithProviders = RouterModule.forChild(homeRoutes);

and that must be imported in it's component's module like so:

import { routing } from './home.routes';
...
@NgModule({
  imports: [
    ...
    routing
  ],
  ...
})

Then in app.module.ts and app.routes.ts we decide if the component is lazy or not.

So app.routes.ts :

import { ModuleWithProviders } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const appRoutes: Routes = [

    // { path: '', component: HomeComponent }, <- in the home module

    // lazy loaded routes
    { path: 'blog', loadChildren: './blog/blog.module#BlogModule' },
    { path: 'style-guide', loadChildren: './style-guide/style-guide.module#StyleGuideModule' },

    // any other route
    { path: '**', redirectTo: '' }

];

export const appRoutingProviders: any[] = [];

export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes);

And app.module.ts:

...
import { routing, appRoutingProviders } from './app.routes';
import { HomeModule } from './home/home.module';      // <-- non lazy loaded component
...

@NgModule({
  ...
  imports: [
    ...
    routing,
    HomeModule,                                       // <-- non lazy loaded component
    ...
  ],
  providers: [appRoutingProviders],
  ...
})
export class AppModule { }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment