Composing Angular Components with Typescript Mixins

November 12, 2019 0 Comments

Composing Angular Components with Typescript Mixins

 

 

Learn how to maximize reusability in your Angular components by using composition instead of inheritance

Photo by Francisco Jacquier on Unsplash

This is a follow-up from my previous article about Component Composition with Angular where I listed 3 ways of composing Angular components:

  • Class Inheritance
  • Class Mixins
  • Component Composition

TLDR; my favorite way is to compose components in small units and use Inputs and Outputs to communicate between components. In order to share logic pieces between components, I like how Mixins can help us avoid some pitfalls from using class inheritance.

In this article, I want to focus more on the relationship between Class Inheritance and Class Mixins, how they differ, and some pitfalls from using Mixins for building components.

Tip: Use tools like Bit (Github) to increase code reuse by sharing and collaborating on Angular components across projects. Share your reusable building blocks to a collection on bit.dev, for future compositions.

Example: Shared Angular components in a Bit collection

The Pitfalls of Class Inheritance

You probably already know why using inheritance is at times very appealing: define some methods and properties once, then use them for every common subclass: wonderful!

On a superficial level, and in some cases, that is actually a good thing. Yet, there are some well-known and documented issues that come with class inheritance. The most important ones, from a component architect’s point of view, are the following:

  • fragile base class — when a change in your base class breaks the derived subclasses
  • it encourages early choices when designing the base class: it makes the design brittle and fragile
  • it breaks encapsulation

In fact, you may have heard the legendary quote from the Gang of Four book:

Favor composition over inheritance

There are several types of components where I see inheritance used pretty often:

  • form fields with common value accessors
  • route components that extend a base-route
  • modal, popups, etc. with common methods (show, hide, etc.)

This article focusses more on business logic rather than purely visual attributes (disabled, animated, etc.). I found that sharing logic between components to be a little bit complex and a mostly misunderstood topic, especially when the framework itself does not provide an official stance regarding the topic, as opposite for example to React.

Typescript Mixins

The concept of Mixins is pretty simple: imagine that instead of having a hierarchy of classes, you instead have a number of very small partial classes. These classes can be combined together and build larger classes with great flexibility.

The way Mixins are created with Typescript is simple: we define a function that takes as argument a class and extend the newly created class with the one passed as the argument.

First, we define the mixins pinMixin and closeMixin that define 1 method each:

function pinMixin(BaseClass) { return class extends BaseClass {

pin() {

// implementation } }

}

function closeMixin(BaseClass) { return class extends BaseClass {

close() {

// implementation } }

}

We create a Base class that is created by merging the mixins functions, and then we extend the implementation:

const BaseTabMixin = pinMixin(
closeMixin(class {})
);
class Tab extends BaseTabMixin {}// Tab now can use the methods close and pin

Scenario: A Social Media Aggregator App

As an example, I want to build a prototype of a Social Media aggregator application with a feed of posts from the main social media services.

This is a particular example that I faced many years ago as a Junior developer: Babel was released, and ES6 classes were the fancy new thing until they weren’t.

Junior me, a little naively, started creating base classes, extending left and right, and it was exciting. Look how much code I was able to share thanks to them! In the beginning, it’s something you don’t immediately realize: requirements are not fully fleshed-out, and as we all know, new details emerge continuously.

We’re going to see how to build posts components for social media like Facebook, Twitter, Youtube, and Reddit: first, we’ll be using the good old Inheritance.

Afterward, we’ll be refactoring using Composition.

Building a base post component with Inheritance

Let’s proceed and build a BasePost class that shares properties and methods that the derived subclasses will share. As you may already know, social media posts are fairly similar to each other, with subtle differences: they have an author, some content (be it text, a link, or an image), and allow some actions such as liking, sharing, editing, etc.

Our base class PostComponent will have an input (Post object) and will inject a service PostService to which we delegate our actions.

The only common action shared among all the Social posts is delete and therefore we add it to base class so that all the subclasses can inherit the method.

class PostComponent {
@Input() post: Post;
constructor(protected service: PostService) {}
delete() { this.post.delete(this.post.id); }

}

This is the bare minimum base class we can create. Now, we can proceed and add specific actions.

We know that both Facebook and Twitter allow posts to be liked, but not Twitter nor Youtube; therefore, we create a subclass called LikeablePost:

class LikeablePost extends PostComponent { get likes() { return this.post.likes;

}

like() { this.service.like(this.post.id); } unlike() { this.service.unlike(this.post.id); }

}

Both Youtube and Reddit allow posts to be upvoted and downvoted; it makes sense to create a subclass that allows performing such actions:

class VoteablePost extends PostComponent { downvote() { this.service.downvote(this.post.id); } upvote() { this.service.upvote(this.post.id); }

}

Facebook and Twitter also have another similarity: the concept of “sharing” as key metadata.

class ShareablePost extends LikeablePost { get shares() { return this.post.shares;

}

share() { this.service.share(this.post.id); }

}

A similarity shared among Youtube, Facebook and Reddit are that they all allow posts to be edited, unlike Twitter.

This is the first issue we encounter:

  • as the method is not shared by all classes, it would be a mistake to add it to the base class
  • we could implement the method edit for all the subclasses, but that’d be very repetitive

We proceed by implementing TwitterPostComponent

@Component({...})
class TwitterPostComponent extends ShareablePost {}

Let’s take a jump into the future, and Jack gives us horrible news: we can no longer delete tweets! Our class now needs to change, but wait: delete is defined in the base class.

  • if we remove the method from the base class, we will break the other classes
  • if we remove it only from TwitterBaseComponent we will end up breaking the Liskov substitution principle, that means TwitterBaseComponent and PostComponent should be able to be swapped without breaking anything

If it wasn’t clear enough by now, all this was a bad idea.

Enter Composition

Now, we’re going to rewrite all the previous by composing mini-classes instead and using Typescript mixins to create components made of many separate, small classes.

Let’s create the mixins required to create the component TwitterPostComponent: likeMixin, deleteMixin and shareMixin.

Base Class

First of all, we want the mixins to be generic enough to be applied to a variety of components, with one single dependency being the service injected to the component.

export interface PostComponent { post: Post; service: PostService;

}

likeMixin

// like
function
likeMixin<IBasePost extends Constructor<PostComponent>>( Base: IBasePost) {

return class extends BasePost implements CanLike {

get likes() { return this.post.likes;

}

like() {
return this.service.like(this.post.id); } unlike() {

return this.service.unlike(this.post.id);

} };

}

deleteMixin

function deleteMixin<IBasePost extends Constructor<PostComponent>>( BasePost: IBasePost) {

return class extends BasePost implements CanDelete {


delete() {
return this.service.delete(this.post.id); } };

}

shareMixin

export function shareMixin<IBasePost extends Constructor<PostComponent>>( BasePost: IBasePost) {

return class extends BasePost implements CanShare {


shares: number; share() {

return this.service.share(this.post.id);

} };

}

Creating the implementation component: TwitterPostComponent

Once created, we can apply them to the newly created TwitterPostComponent:

const TwitterBase = deleteMixin( likeMixin( shareMixin(PostComponent) )

);

If you prefer to use the applyMixins function described in Typescript’s own documentation, you can do the following:

class TwitterBase extends PostComponent {}

interface TwitterBase extends CanLike, CanDelete, CanShare {}

applyMixins(TwitterBase, [ shareMixin, likeMixin, deleteMixin

]);

Once created the base component composed with the mixins, we can extend the new component TwitterPostComponent:

@Component({ selector: 'twitter-post', template: &lt;div class=&quot;post&quot;&gt; &lt;div class=&quot;post-header&quot;&gt; {{ post.author }} &lt;/div&gt; &lt;div class=&quot;post-content&quot;&gt; {{ post.content }} &lt;/div&gt; &lt;div class=&quot;post-footer&quot;&gt; &lt;button (click)=&quot;like()&quot;&gt;Like&lt;/button&gt; &lt;button (click)=&quot;share()&quot;&gt;Share&lt;/button&gt; &lt;/div&gt; &lt;/div&gt;})

export class TwitterPostComponent extends TwitterBase {


}

In order to remove a delete functionality from the Tweets components, we don’t have to do much — we simply remove the deleteMixin mixin from our class:

const TwitterBase = likeMixin( shareMixin(PostComponent) )

);

Pitfalls of using Mixins

Mixins are great, but they’re not an infallible tool. While I would still prefer Mixins to multiple inheritance, it is important to understand the implications of using this technique.

This React blog post provides a great explanation on why Mixins are no longer considered a best practice in React:

  • Mixins create implicit dependencies: mixins that call methods on components, reference a property from the component, or components that need a mixin to work well, are all dependent on each other
  • Mixins start small but grow over time
  • Mixins lead to name clashes

Of course, because of the similarities, these also apply to Typescript mixins used with Angular components.

How to avoid these pitfalls?

  • Try not to apply too many mixins; if you have too many mixins, maybe you should split the component into several components and use component composition with inputs and outputs to communicate between each other
  • Strive to keep them as small as possible
  • Keeps dependencies between mixin/component to a minimum. For example, wherever possible, try not to call a component’s dependencies from the mixin
  • Combine the mixins technique with component composition. Together with the usage of small mixins, you can leverages both techniques to share code and maintain a healthy codebase

Resources

If you need any clarifications, or if you think something is unclear or wrong, do please leave a comment!


Tag cloud