cover of story

Creating and styling components in Angular correctly

A few simple examples to help you avoid over-complex code, redundant component styles, and non-obvious traps

Maksim Dolgikh
Published in
7 min readJan 24, 2024

--

Introduction

In Angular, a component is the basic building block for creating a user interface. Components in Angular are independent, reusable blocks of code that combine a template (HTML markup), styles (CSS), and component logic (TypeScript).

You can always realize that you are looking at an Angular application. Just open DevTools and analyze DOM. Besides the usual div and span elements, there are alternative app-*, ng-*, mat-* and others — these are Angular components.

Because the components you create are all DOM elements. They are not containers, which call render() and pass an exact template instead of a selector “behind the scenes”. Because of this omission, you might think that Angular creates elements with display:contents, but no, components have display:inline style by default, hence the element has weight.

Lightweight and customizable components

Some developers miss this point and create a wrapper element to work with the component elements. That's why, you can’t style the component to control its content. For wrapper styles are lower down the DOM tree and you have to resort to ways to access the component elements via ViewEncapsulation.None or ::ng-deep.

Your component is already a wrapper, it just doesn’t have styles. Do not create <div></div> wrappers inside the component for grouping and arranging elements. It is enough to add :host with necessary settings in the component’s style file.

:host {
// default as div
display: block;
// -- block settings --


// flex-container
display: flex;
// -- flex settings --


// grid-container
display: grid;
// -- grid settings --
}

It also applies to data-* settings. Declare all of your component style states in the :host

:host {
// other styles
&[data-status="success"]{
background: green;
}
&[data-status="warn"]{
background: yellow;
}
&[data-status="error"]{
background: red;
}
}

And control them via input

@HostBinding('[attr.data-status]')
@Input()
public status: 'success' | 'warn' | 'error' | 'default' = 'default'

What does it do?

  1. DOM will become lighter, due to the absence of unnecessary elements
  2. No need to create wrapper-classes
  3. Parent components will be able to style your component without resorting to ViewEncapsulation.None or :ng-deep, because they work directly with the component element

Don’t recreate, better inherit

For example, you create a <table> and you want to use the <tr> row of the table as a component.

What is the default way a developer would make this component also support the table element? — to create a component with styles <tr> .

:host {
display: table-row;
}

But this is not correct. Why?

The component selector is a parameter that searches for a match among the templates to create a component in place of the element. You can write tags, attributes, id, CSS classes, basically anything allowed when working with the native querySelector() function to the selector;

So if you want to create an element replicating some native behavior, instead of creating it with the usual tag, you can specify tr[my-table-row] like this.

In the template, apply it as follows

<table>
<tr *ngFor="let row of rows" my-table-row [data]="row"></tr>
</table>

By doing this, you are able to preserve the semantics, but also ensure that your table is decomposed into separate parts. The component, as an element, already has <tr> properties, so you don’t have to write them in :host.

This method can be applied to a cell via td[my-table-cell] too.

Host properties

Knowing that a component is an element, you may want to apply some CSS-classes or attributes to it from inside the component. This can be as inheriting from global CSS-classes or creating an element identity with its own unique value.

In all of these cases, you would probably do this

@HostBinding('class.flex')
private readonly _classFlex = true


@HostBinding('attr.id')
private readonly _id = window.crypto.randomUUID()

But these values are needed only when creating an element, they do not change during redrawing and we do not interact with them in any way.

They can be abandoned from their declaration in the component class, in favor of declaring these static bindings in the host, which is in @Component

@Component({
// ...settings
host: {
'[class.flex]': 'true',
'[attr.id]': 'id'
}
})
export class SomeComponent {
public readonly id = window.crypto.randomUUID();
}

These are the same bindings as via HostBinding, but our class code is now cleaner. We don’t need additional imports to maintain this approach.

In the same way, you can bind animations to your component element. Which will let you avoid creating unnecessary elements inside the component

@Component({
// ...settings
host: {
'[@fade]': '' // without props,
'[@expand]': 'props'
}
})

Advanced animation in Angular

2 stories
cover of topic
cover of story

Mixins

Suppose your codebase has some global mixins for text

@mixin text-m(){
font-size: 14px;
}


@mixin text-h2(){
font-size: 16px;
font-weight: bold;
}

You are developing a component where you want to use common styling mixins

<p class="title">
Title
</p>
<p class="description">
Description
</p>
@use '../../styles/mixins';

.title {
@include mixins.text-h2();
}

.description {
@include mixins.text-m();
}

You open your browser and see a beautiful picture where everything is working and the styles have been applied

Elements are rendered with styles in the browser
Elements are rendered with styles in the browser

What’s wrong? — You’ve created unnecessary duplicate styles.

Ideally, if you have global mixins, you should also have global CSS-selectors .text-m and .text-h2 that use these mixins.

If you don’t have them — make them

Therefore, it is correct not to create styles for each of your selectors, which repeat the global ones, but to apply them to the element.

- <p class="title">
+ <p class="title text-h2">
Title
</p>
- <p class="description">
+ <p class="description text-m">
Description
</p>

Use the new selectors for other styles that reflect the specificity of these elements or don’t use them at all

- @use '../../styles/mixins';

.title {
- @include mixins.text-h2()
+ line-height: 21px;
}

.description {
- @include mixins.text-h2()
+ color: grey
}

Styling mixins should only be used when you can’t apply styles to an element via the class attribute. Most of your styles from the design kit should be centralized and reused to the best of your ability

Shared styles

Suppose, at runtime, you discover that several CSS-classes and properties are repeated in two components

Analyze <style> tags
Analyze <style> tags

You understand that this is code duplication, that it increases the size and this needs to be optimized.

What will you do as a developer? — Take out those styles to the generic CSS-styles file in <domain>/shared/styles/text.scss, and then reuse it in the components you need.

  • shared/styles/text.scss
.title {
line-height: 21px;
}

.description {
color: grey;
}
  • Component 1
@Component({
- styleUrl: './some-1.component.scss',
+ styleUrls: ['./some-1.component.scss', '../shared/styles/text.scss'],
})
  • Component 2
@Component({
- styleUrl: './some-2.component.scss',
+ styleUrls: ['./some-2.component.scss', '../shared/styles/text.scss'],
})

You open your browser and see that you have benefited and affected the current components in no way. Well done!

But the reality is that you haven’t changed anything. Everything’s still where it was. As there was duplication of styles, so it remains.

Simply now Angular creates for each file in styleUrls its own separate <style> tag and nothing more.

The usage of shared file doesn’t have an effect
The usage of shared file doesn’t have an effect

If you want to reduce the amount of style repetition in components, your main goal is to make styles available within the application and reuse them.

This can be done in 3 ways:

  • If CSS-classes do not overlap with the rest of the application and their naming is abstract for reuse — import them in styles.scss
  • If CSS-classes do not overlap with the rest of the application, but their naming is specific, but you want to use them globally — change the name of the CSS-class and import it in styles.scss
  • If styles overlap with the rest of the application and you need them only in a certain part — put them in a common parent for these elements with ViewEncapsulation.None.

Consider the option that duplicate styles are not a problem, but a way to control the use of CSS. If you remove one of these components, the duplicate code will go away with it, and the styles will remain as a single instance.

Conclusion

As you can see, creating components in Angular isn’t always the easy and optimized way. Some practices that seem good to you at first sight can be deceptive, and harm more than help.

Links

Stackademic

Thank you for reading until the end. Before you go:

--

--