27

Angular 2 Barrels

In Angular 2, I'm trying to get barrels to work as described in the documentation.

The official Angular 2 style guide talks about using barrels to aggregate and shorten import statements.

I'm finding out that for some barrels, I have to specify the index JavaScript file name on the import when I shouldn't have to.

Barrel Example

(modify the app/app.component.ts file on line 12)

After have encountered this in my actual project (running under ASP.NET) I have created a Plunker to demonstrate the problem where I modified the Tour of Heroes to use barrels.

In app/app.component, the basic way to import is like this:

import { HeroService } from './hero.service';
import { HeroesComponent } from './heroes.component';
import { HeroDetailComponent } from './hero-detail.component';

But, to use a barrel instead, the import definition would look like this:

import {
  HeroService,
  HeroesComponent,
  HeroDetailComponent
} from '../app';

The from '../app'; line indicates a file with the name of index.ts that contain the exported/imported components:

// app/index.ts
export * from './hero-detail.component';
export * from './hero.service';
export * from './heroes.component';

But this doesn't work for me in all cases. The only way I've gotten this to work correctly is by explicitly including the index file name:

import {
  HeroService,
  HeroesComponent,
  HeroDetailComponent
} from '../app/index'; // have to indicate 'index'

How can I get this to work where the index.js file name is implied?

Brad Rem
  • 6,036
  • 2
  • 25
  • 50
  • Looks like a bug or maybe the docs are wrong. Would be nice to import it only with the folder name as in ES6 – Tiberiu Popescu May 07 '16 at 00:54
  • My understanding is that NodeJS will look for an *index.js* file if you only reference a directory for the "require" or in this case "import" call. I am not sure if Plunkr can mimick this behavior. – Mdd May 10 '16 at 21:05
  • @Mdd, that's a good point about Plunker. Although, I'm using ASP.NET in my actual project where I first encountered this problem, so it is failing in a real environment. – Brad Rem May 11 '16 at 13:41
  • AFAIK, `app` can't work as a `barrelName` because you've already specified the default for it `{ main: 'main.ts', defaultExtension: 'ts' }`, and that's what it's all about to configure a barrel, see my answer for that – Ankit Singh May 13 '16 at 10:22

3 Answers3

8

AFAIK SystemJS doesn't understand barrels by itself but Webpack does. BTW, After digging up how Angular does it for it's modules, I've found a solution


In system.config.js you'll need to do the following things

Note: the parent directory of a index.ts is the barrel, ICYDK

  • Add the paths of your barrels

// map tells the System loader where to look for things
  var map = {
    'app':                        'app', // 'dist',
    'rxjs':                       'node_modules/rxjs',
    'angular2-in-memory-web-api': 'node_modules/angular2-in-memory-web-api',
    '@angular':                   'node_modules/@angular',

    'barrel':                 'path/to/your/barrel'
  };
  • Don't add barrels to packages (explained later)##

// packages tells the System loader how to load when no filename and/or no extension
  var packages = {
    'app':                        { main: 'app/boot.js',  defaultExtension: 'js' },
    'rxjs':                       { defaultExtension: 'js' },
    'angular2-in-memory-web-api': { defaultExtension: 'js' }
  };
  • Add them to packageNames, just like angular

var packageNames = [
    '@angular/common',
    ...
    '@angular/upgrade',

    'barrel'
  ];

And you're done.


##

Why we used packageNames instead of packages is because you'll have to repeat the { main: 'index.js', defaultExtension: 'js' } (filename and extension) for every barrel, but angular is already doing it by looping with this.

packageNames.forEach(function(pkgName) {
    packages[pkgName] = { main: 'index.js', defaultExtension: 'js' };
});

Which is ultimately adding them to packages.


Usage

import {something} from '../../barrel'; // relative path to directory of barrel

and

import {something} from 'barrel'; // name of barrel

Both work, but the later one fails to provide intellisense and shows an error saying cannot find module 'barrel'. Which I don't have a solution for. But I'll add it when I do.

Ankit Singh
  • 24,525
  • 11
  • 66
  • 89
  • 2
    Wouldn't the new [typescript 2 module resolution enhancements](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#module-resolution-enhancements-baseurl-path-mapping-rootdirs-and-tracing) help with the `import {something} from 'barrel'` problem? – Aides Jan 19 '17 at 13:47
6

This is all seems overly complicated.

There's no need to add anything to map, since app, which should be containing everything is already in there. We can just create an array with the subpackages. In my case that was:

var subPackageNames = [
        '+heroes',
        '+heroes/shared',
        '+heroes/hero-list',
        '+heroes/hero-detail',
        '+heroes/hero-dashboard'
    ];

and then modify the provided packIndex function to take a second argument

function packIndex(pkgName, baseName) {
    packages[baseName+pkgName] = { main: 'index.js', defaultExtension: 'js' };
}

now we can add our sub packages to the packages object just like angular does.

ngPackageNames.forEach(name => setPackageConfig(name, '@angular/'));
subPackageNames.forEach(name => packIndex(name, 'app/'));
MrLoh
  • 456
  • 1
  • 5
  • 12
0

I followed A_Singh's idea, but had to adapt it a little since my systemjs.config.js is different, based on the tour-of-heroes tutorial as of right now. (June 3, 2016 - RC1).

This modification works, and under VisualStudio Code, Intellisense worked well for me.

I have also, been playing with directory structure to keep things modular, and here is where the barrels made sense.

This is how I have modified the tour-of-heroes project

app
|- main.ts
|- app.component.ts
|- app.component.css
|-+ components
| |-+ dashboard
|   |- dashboard.component.css
|   |- dashboard.component.html
|   |- dashboard.component.ts
|-+ modules
| |-+ hero
|   |- index.ts   <-- //The barrel for the Hero module
|   |-+ components 
|   | |-+ detail
|   | | |- hero-detail.component.css
|   | | |- hero-detail.component.html
|   | | |- hero-detail.component.ts
|   | |-+ list
|   |   |- hero-list.component.css
|   |   |- hero-list.component.html
|   |   |- hero-list.component.ts
|   |-+ models
|   | |- hero.model.ts
|   |-+ services
|     |- hero.service.ts
|     |- mock-heroes.ts

And here the updated systemjs.config.js

(function(global) {
  // map tells the System loader where to look for things
  var map = {
    'app':                        'app', // 'dist',
    '@angular':                   'node_modules/@angular',
    'angular2-in-memory-web-api': 'node_modules/angular2-in-memory-web-api',
    'rxjs':                       'node_modules/rxjs',

    // The added barrel map
    'hero':                       'app/modules/hero'
  };
  var packages = {
    'app':                        { main: 'main.js',  defaultExtension: 'js' },
    'rxjs':                       { defaultExtension: 'js' },
    'angular2-in-memory-web-api': { defaultExtension: 'js' },

    // The package definition (notice I had to declare index.js)
    'hero':                       { main: 'index.js',  defaultExtension: 'js' }
  };
  var ngPackageNames = [
    'common',
    'compiler',
    'core',
    'http',
    'platform-browser',
    'platform-browser-dynamic',
    'router',
    'router-deprecated',
    'upgrade',
  ];
  ngPackageNames.forEach(function(pkgName) {
    packages['@angular/'+pkgName] = { main: pkgName + '.umd.js', defaultExtension: 'js' };
  });
  var config = {
    map: map,
    packages: packages
  }
  System.config(config);
})(this);

And finally, here is how the app.component.ts now looks like

import { Component } from '@angular/core';
import { RouteConfig, ROUTER_DIRECTIVES, ROUTER_PROVIDERS } from '@angular/router-deprecated';

import { DashboardComponent } from './components/dashboard/dashboard.component';
import { HeroListComponent, HeroDetailComponent, HeroService } from './modules/hero';

@Component({
    selector:   'my-app',
    directives: [ROUTER_DIRECTIVES],
    providers:  [
        ROUTER_PROVIDERS,
        HeroService
    ],
    template:   `
<h1>{{title}}</h1>
<nav>
    <a [routerLink]="['Dashboard']">Dashboard</a>
    <a [routerLink]="['HeroList']">Hero List</a>
</nav>
<router-outlet></router-outlet>
`,
    styleUrls: ['app/app.component.css']
})
@RouteConfig([
    {
        path: '/dashboard',
        name: 'Dashboard',
        component: DashboardComponent,
        useAsDefault: true
    },
    {
        path: '/hero-list',
        name: 'HeroList',
        component: HeroListComponent
    },
    {
        path: '/detail/:id',
        name: 'HeroDetail',
        component: HeroDetailComponent
    }
])
export class AppComponent {
    title: string = 'Tour of Heroes';
}

One final note, you could do the import as

import { HeroListComponent, HeroDetailComponent, HeroService } from 'hero';

And it will work, however, since it is not technically a NodeJS imported module, VS Code complains that it cannot find it. So, my personal choice is to leave the explicit ./modules/hero, it helps me know that it is one of my modules and not an imported one and I'm happy to not see red lines.

AlphaZygma
  • 228
  • 2
  • 9