2 years of Angular: lessons learned
Over the past two years, we have been working at Malmberg to build an amazing new education platform: Bingel. In this blog I like to talk about our adventures, mainly with Angular, and the lessons we learned.
Bingel is a new platform for digital primary education in the Netherlands. It will go live in September 2019. It consists of multiple apps and it supports teachers and pupils in the classroom with adaptive learning.
The apps contain a lot of interactive features. For example, pupils have different ways to solve exercises and quizzes: by connecting choices, dragging the hands of a clock, dragging choices in their corresponding gaps, and so on.
One of the reasons we prefer Angular as the framework to build the frontend apps.
Migration of AngularJS components
For this project we first needed to migrate some existing AngularJS components to Angular 2+. Generic components used in multiple of the projects. We did this migration by hand.
First, we tried to stay as close as possible to the original AngularJS code, but this didn’t always work out. So in the end we tried to do more things the Angular way, for example by using a more reactive approach with RxJS.
We work with multiple Angular apps for different types of end users. We moved shared code to several separated libraries. All of these apps and libraries live in the same repo. This is called a monorepo approach.
By this time, Angular CLI already supported multiple apps. It added library support later on.
We also used Nx workspaces: a collection of tools that enables a more well-structured monorepo and provides a more efficiënt build process.
What we learned:
- Consider a monorepo if that fits your project. This makes version management and release management a lot easier. Besides that, you can refactor more easily, add new libraries, or move code between libraries.
- Think in different separated packages and their responsibilities. This forces you to think about a good structure.
The routing in our apps can be quite complex sometimes. We make use of the default Angular Router. We did encounter some bugs and limitations, but they all have been fixed after we reported some issues on Github.
What we learned:
- Use route guards and use them wisely. Guards enable you to add additional routing logic in a clean way, without polluting your existing components. Try to understand the mechanics of the different type of guards. A ResolveGuard can fetch data on routing activation and provide this data to all underlying routed components. The disadvantage is that this process is blocking: a component will be rendered after the data has been fetched. The advantage is that you can intervene (e.g. navigate away), while in components, you are often too late. You can benefit from this in a CanActivateGuard as well.
- Get used to the (reactive) API of the Router. Inside a component you can “listen” for changes in the activated route, for example whenever a parameter has been changed. You can do this by using the reactive API of the router. This automatically brings you in contact with RxJS. It is important to understand the principles of reactive programming so that your code won’t get messy when it becomes more complex. Also note that the router also contains a non-reactive (static) API in route snapshots. For some cases, it is perfectly valid to use that instead.
Our Angular codebase consists of many different components. To keep the structure and responsibilities of these components as pure as possible, we decided to distinguish between different types of components:
- Page components: also known as routed components. These components are bound to a specific route. Their responsibility is to fetch data (using services) and to pass it on to underlying components. Page components are not allowed to have any presentation logic but have to delegate it to other components.
- Feature components: these components are meant for specific presentation logic. They belong to a feature module and may be coupled to a domain model.
- UI components: these components are meant for generic presentation logic. They belong to a generic UI module and may not be coupled to any domain model.
We learned that it is very useful to have a UI library that you can use for any feature. However, this requires them to be generic.
For pages it was better to be specific though. This prevents them to grow too big, serving multiple purposes and becoming fuzzy.
So, even when two pages look similar, but have too much differences, it may be wise to split them into different components and reuse underlying UI components.
When you start with Angular, you become familiar with style encapsulation. This is a concept from Web Components and means that each component is only allowed to style its own piece of DOM (“shadow DOM“).
Angular has this feature enabled by default.
In the beginning you have to get used to it, but it is very elegant and prevents unwanted side-effects in your styling. It also removes the need for naming conventions.
What we learned:
- Prefer component styling over global styling.
- Prevent using “cheats” like ::ng-deep. It’s a code smell! Use other concepts instead, like :host-context.
- Don’t forget to style the :host element. Don’t use a single root element in your template, when you can use your host element instead. The host element though, can also be styled from its parent component (we call this contextual styling).
- Go for more robust default styling of a component, and less contextual styling.
- Make use of the CSS inherit keyword.
- Use em/rem units instead of pixels.
We all agree that unit tests are important. However, we don’t always like to write them.
This was also the case in our project. Partly, this had to do with the tooling that Angular provides out of the box: Karma as test runner, Jasmine as test framework and Angulars TestBed API to setup tests for components.
We found that the TestBed API from Angular can be quite verbose. As a result, the test code became long and hard to read.
Unit testing became a lot more fun when we discovered Spectator. This open-source library, originally written by Netanel Basal, makes (component) testing in Angular a lot easier!
Its API is easy to learn. It reduces a lot of boilerplate code and it provides a lot of handy shortcut methods for change detection, querying elements and dispatching events. Read more about Spectator on the Github page.
We also started using Jest. Jest, originating from the React ecosystem, is becoming more and more popular for Angular projects. For us, it was quite an improvement, due to its fast and interactive CLI runner.
What we learned:
- Consider using Spectator instead of the TestBed API of Angular.
Consider using Jest instead of Karma/Jasmine. Migration is quite easy.
- Make unit testing fun and try to write easy-to-read test code.
- Prefer functional component testing over technical class testing. Think in user events instead of methods. Test from the DOM, not from the class.
- Pure unit tests are isolated. If you want to mock components, you may want to use ng-mocks.
- However, there is nothing wrong with a bit of integration. Discuss with your team how you implement a test pyramid.
- Organize your test data in one place.
Care about your code, you write it for your co-workers and yourself. A book doesn’t read well with an ugly font-face. Besides that, you want to discover mistakes as soon as possible.
- Make your TypeScript compiler as strict as possible. This makes your code less error-prone. Always make use of strict typing.
- Make your tslint configuration as strict as possible.
- Consider using Prettier. We currently do not use it, but we intend to.
Building complex (interactive) components, go for a well-designed architecture.
- Don’t reinvent the wheel. Try to solve complex problems using Angular CDK. It provides many useful (low-level) utilities for scrolling, drag/drop, overlays, accessibility, and so on.
- When you build something yourself, get inspiration by looking to other libraries like Angular Material. They teach you how to do things “the Angular way”.
We use open-source stuff every day. We should care about it!
- Report your issues. Describe your issue as good as possible and spend time to make it reproducible.
- Try to solve things yourself and make that PR! Open-source projects belong to us all. Don’t be afraid to be part of it.
- Know what’s going on. Read blogs, explore changelogs, browse issues.
We did learn a lot of other things as well. Want to know more? Need some help? Attend a workshop? Contact us.
PS there are some slides that go with this talk about our Angular experience, you can download these slides on SlideShare.