Component inheritance in Angular is a feature that doesn’t seem to get a lot of love. Maybe it’s the tie to object-oriented programming which is losing a little love to the functional programming trend. However, I’ve found component inheritance to be just the right fit for a number of use-cases.
Here’s 4 reasons why you should fall in love with Angular Component Inheritance:
- Share Inputs
- Share Lifecycle Methods
- Reuse Methods
- Breakup Layout
What is Inheritance?
“Inheritance” in object-oriented programming describes the relationship between a parent class and one or more children. A “child” object “extends” its parent and “inherits” its features. The child can do everything the parent can do while also declaring functions/properties of its own. Children can use functions from the parent as-is, or override them to tweak features according to their needs.
A classic example of inheritance is an “Animal” class.
As seen above, our Animal has an age, and can walk and run. Our “dogs” and “cats” can also do these things, but declare their own properties and methods: our cat has “lives'' and can “meow”, our dog can “bark”.
This lets us write commonly used code once while isolating unique functions. We’ve successfully avoided duplicate code while creating an easy-to-follow relationship between our furry friends.
Inheritance with Angular Components
This same concept can be applied to Angular components. We can create a “parent” component with common properties/functions, followed by a child component that “extends” the parent. The child will inherit the parent’s properties and functions but will have its own template, stylesheet and test file.
Building a Coffee Ordering App Using Component Inheritance
For this example I’ve setup a simple coffee ordering app. We'll be walking through the code, and you can check out the whole example on Stackblitz.
Here's a preview of the finished example:
In the app we have a coffee beverage (a latte), and are now looking to add options like milk or flavor. If you aren’t a coffee drinker, a latte will have “milk” and “foam” by default.
We'll be using inheritance to build out the "additions" available for our latte order. The details of each addition may change, but there is a lot of shared logic between them.
We'll add the additions to our latte, which is then added to a "coffee order" that reflects our total price. Our coffee order is managed by a "coffee-order-service" which generates the order and contains functions for saving our changes. In a real app this would be connected to a web API, but we're faking it client-side to keep things simple.
Let's get started! Here is our base “CoffeeAddition” component.
We’ll walk through this code in more detail, but let’s call out a few key pieces:
- Inputs: two pieces of data are passed as inputs, a “CoffeeOrder” and an “Addition” (we’ll explain these in a bit).
- A “constructor” function injecting a service called “orderService”. We’ll use this to update our CoffeeOrder.
- An “ngOnInit” function. This runs the very first time your component loads (more on Angular lifecycle hooks).
- A few additional functions which define basic features.
Next we have a "FoamAddition" component which extends our "CoffeeAddition" component.
How to implement Angular component inheritance:
- We’re using the “extends” keyword and extending our “CoffeeAdditionComponent”.
- We’re calling “super()” in our constructor. This does the actual work of extending the component when our code is compiled. You’ll notice we’re passing in a service called “coffeeService”. This service is required by our parent CoffeeAddition, therefore it is also required in our extended Foam component. More about Angular dependency injection.
This is a basic but highly impactful feature of inheritance, allowing us to share code when it makes sense while keeping unique functions cleanly separated from other “CoffeeAddition” components.
You’ll notice two new functions towards the bottom: “saveSelection()” and “suggestCappuccino()”. We’ll get into the details later, but it's important to note that these functions will only be available to the “Foam” component. This is a good thing! Just like not all of our animals needed to “bark()”, not all of our additions will need to “suggestCappuccino()”
Reason # 1: Share Inputs
The ability to share inputs is a simple but highly useful feature of component inheritance. Let’s look at an example.
Here are two models: CoffeeOrder and CoffeeAddition, plus a few extras that we'll get to later. Not sure how TypeScript interfaces work? Learn more about them here.
Next we have two inputs on our “CoffeeAddition” component, sensibly named “coffeeOrder” and “addition”. Each uses one of the models listed above.
This gives us a jumping off point to display data from our Addition object, as well as a “CoffeeOrder” object which we’ll eventually use to save our additions.
Now that we’ve added inputs to the parent CoffeeAddition component, let’s look at the changes we need to make to the Foam component.
Notice anything? There aren’t any changes!
Since the Foam component extends CoffeeAddition, it inherits the inputs added to its parent.
This is an extremely useful layer of meaning that we can convey with component inheritance. Our FoamComponent knows that it’s a “CoffeeAddition” but it doesn’t need to worry about what that means. It gets all of its “CoffeeAddition” functionality from its parent, the only thing it needs to care about are things unique to “foam”. This keeps our logic cleanly separated, and nudges us towards generic components (more on that in a bit).
Reason #2: Share Lifecycle Methods
Now that we have data, we can add some smarts to our components. Suppose our coffee already has milk added to it, we’ll want to “pre-select” this existing milk option when the component loads.
Let’s revisit our CoffeeAddition component:
You'll notice we have boolean property called “selected”. The “public” keyword is important here, private members are not inherited by child components.
Next is a function called “additionSelected”. Don’t sweat the details, just know that it returns true or false if our component’s addition is attached to the coffee object.
Finally we’ll use this “additionSelected” function to set the value of the “selected” property when our component is initialized.
One problem: the Angular CLI will generate our Foam component with its own “ngOnInit”, which overrides the ngOnInit from the parent. But isn’t the whole point of this section to share functionality using component inheritance? How do we solve this?
Easy! We just call “super.ngOnInit()”. This calls our parent’s “ngOnInit” function, which takes care of pre-selecting the addition.
This is a simple example, but let’s consider the alternative: if we skip the “super.ngOnInit()” call and set the “selected” property in the Foam component’s “ngOnInit”, we end up with two identical blocks of code in two different components. What if we had five extended components instead of one? That’s a lot of duplicate code, and we’ve only set one property.
Instead, anytime we need a new addition we just extend our parent component, call its ngOnInit, and voila! We have a functional coffee addition pre-selected and ready to go.
This feature works for other lifecycle functions (ngOnChanges, ngAfterViewInit, etc), which you can hook into in the same way as ngOnInit.
Note: Removing the “ngOnInit” function but keeping the “implements OnInit” in your child component will also make it fall back to the parent’s “ngOnInit. However, this requires removing default code generated by the Angular CLI, and I personally find it harder to understand.
More on sharing lifecycle methods
Let’s make our Foam component even smarter. Suppose our user adds extra foam to their drink: they might not know it, but what they probably want is a cappuccino. This type of nudging is common in ordering platforms, let’s see how to implement it.
We’re going to leverage two enums for checking our Foam addition’s value and the type of drink we’re working with: CoffeeProducts and DairyFoam. Fall in love with enums here.
Let's revisit our FoamComponent.
This component has a boolean for showing our cappuccino suggestion and are setting its value in ngOnInit. Like “selected”, this property is fed by a function; in this case we’re checking for “extra foam” on a drink that isn’t already a cappuccino.
You’ll notice we’re still calling “super.ngOnInit()”. Sharing component lifecycle is flexible: you can call the parent’s lifecycle function, override it entirely, or call the parent followed by new code that’s specific to your child component.
Angular won’t chain you to your parent component’s lifecycle. This “ngOnInit” lets your FoamComponent flex its dairy smarts while still leveraging all the logic inherited from its CoffeeAddition parent.
Reason #3: Reuse Methods
Next we need the ability to add our additions to our coffee. Odds are most, if not all, of our additions can be added in the same way. If our API doesn’t care if we’re adding milk, flavor or sugar, then why should our front-end?
Let’s go back to our “CoffeeAddition” component.
Notice the last two functions: a “saveSelection” for passing our current coffee and addition to the “coffeeService”, and a “clearSelection” for removing the addition from our drink order.
This is yet another big time saver: our Foam component doesn’t need to worry about how to save itself, it’s parent already knows.
Like “ngOnInit”, the Foam component could override this function and add its own foam-specific logic. However, leveraging the parent component’s function removes the need to write (and test) another “save” function. The benefits of this shared code become greater as your codebase grows in size. Leverage shared code whenever you can!
Reason #4. Breakup Layout
This might be my favorite use for component inheritance, and is the use-case that originally sent me down the path of exploring it.
Let’s look at our SyrupComponent. So far all of our additions have only supported one selection at a time: no way to have both “light” and “extra” foam. However, we definitely want to support multiple syrup options, so our “select” UI doesn’t really make sense.
But we’ve already written all this coffee addition logic, surely we can keep taking advantage of it?
Voila! Our SyrupComponent extends CoffeeAddition, but switches up the layout in the template. This is another simple but highly effective use for component inheritance. We can render the same data in whatever UI we need while still leveraging all of our existing code for selecting additions, saving additions, etc.
I use this all the time for splitting up desktop and mobile layouts. Let’s say we wanted to ditch the “select” boxes for Foam and render the options out in a list: with component inheritance, we just extend the existing “FoamComponent” and whip up a new template!
BONUS REASON: Write Generic Components
Take a look at our “Sugar” and “Dairy” elements. Notice anything? We’re using our base “CoffeeAddition” component! These additions don’t have any unique logic or features, so they don't need their own component.
I’ve found that determining which properties/features can be shared often reveals that most of my child items don’t have any unique properties at all. Starting from a generic “baseline” component that covers the majority of your scenarios can cut down on code, development time and testing while avoiding messy nested if-statements.
Imagine rubber stamping new CoffeeAdditions with zero new code or unit tests? Pure joy, my friends.
Component inheritance is an extremely powerful tool for abstracting logic, reducing code and keeping your front-end tidy. By extending components you create a semantically meaningful relationship between UI elements, making items which seem like they’re related actually be related.
Share code, keep your layouts clean and you too will fall in love with component inheritance.