Making it reusable

Posted by James on February 8, 2021

My app now has a component to connect to the Contentful API and manipulate the response objects so that I can use them with our components, plus I've actually made quite a lot of components.

In separate but related news, my friend Henry is just about to publish a book, and he asked me if I could sort out his website for him. I thought it might be a good opportunity to try out my Serverless CMS code. But I didn't really want to be maintaining it in two places. I figured that now would be a good time to make it into a library.

The CLI makes it quite easy to create libraries. First, go to the root of your project and do:

ng generate library contentful-connector

You should get a new directory in your root called projects. Projects should have an src dirctory inside that, and a a lib library inside that. There's also a public-api.ts file in our src directory, in which you specify what the library needs to export.

As it happens, everything I've written that connects to Contentful is a service. I've deleted the autogenerated content of the lib directory (except for the module file, which I'm going to need), and copied in my services from my app, then added them top the public-api.ts file, like this:

/*
 * Public API Surface of contentful-connector
 */

export * from './lib/content-flattener.service';
export * from './lib/contentful.service';
export * from './lib/includes-finder.service';

However, if I do ng-build contentful-connector now, I get some big red errors saying that there's no environment file. Quite right too - we had one in my app, but not in the library.

Using forRoot

The forRoot pattern allows you to pass a configuration object from outside your library. I said above that you'd need the module file - here is why you'll need it. In your module file, inside the class declaration, you'll need to add the following:

export class ContentfulConnectorModule { 

  public static forRoot(environment: any): ModuleWithProviders { 
    return { 
      ngModule: ContentfulConnectorModule,
      providers: [
        ContentfulService,
        {
          provide: 'env',
          useValue: environment 
        }
      ]
    }
  }
}

Now, in the services, you can remove the references to the environment files, and inject the environment configuration in the constructor...

  constructor(
    private http: HttpClient,
    @Inject('env') private environment
  ) {
  }

...plus you'll need to update the references in your code to the environment variable, which is now a propery on the service's this object.

Finally you'll need to import the module into your app using the forRoot method to pass in the environment variable. So add the following to the imports block in your app.module.ts.

ContentfulConnectorModule.forRoot(environment)

You should now be able to run ng build contentful-connector. Once it's built, you can change the references to the services from the filepaths within your app to the name of the library, and run ng build.

Testing forRoot

If you've written tests for the component that's using the service, you'll probably spot that they fail. You'll need to include the module that you created in the test module configuration, and pass it some dummy configuration, like this:

    await TestBed.configureTestingModule({
      declarations: [ ContentPageComponent ],
      imports: [ 
        RouterTestingModule,
        HttpClientTestingModule,
        ContentfulConnectorModule.forRoot({'foo': 'foo'})
      ]
    })

Publishing to NPM

Turns out this is actually the easy part. Log into NPM in your terminal, then do npm publish. And here it is:

Now I can install contentful-connector in my blog application and switch out the filepath references to ContentfulService in my blog application, and replace them with references to the library itself.