{"community_link":"https://github.com/indepth-dev/content/discussions/5"}

Learn Angular Component Design Patterns — Creating a Drawer Component

I keep telling people that Angular is way more powerful than what youtube teaches. Learning design patterns will help to create powerful components. Let's learn to create an Angular component, iterate over it to make it reusable, scalable, and powerful.

Learn Angular Component Design Patterns — Creating a Drawer Component
“Repetition is the root of all software evil” — Martin Fowler.

I keep telling people that Angular is way more powerful than what youtube teaches. Angular, being a full fledged framework has strong opinions on many aspects of the development cycle and the framework enforces set of best practices. There are powerful patterns and tools Angular provides to create highly scalable, structured, and maintainable web applications.

It is easy to miss these patterns when learning as most of the tutorials out there seem to omit advanced concepts.

This article will  create a simple drawer component, make it more flexible and reusable, version by version. The goal is not to create a perfect component but, to learn patterns that can be!

Angular component design patterns

A while ago, I had written an article on Angular component design patterns from material components, where I discussed the component design decisions that were taken in the material library, and how we could get inspired to do the same in our projects. This article will be an implementation of the same where we create a custom side drawer component and use multiple design pattern solutions to polish the implementation which will give us neat and clean solutions.

Our final result should look like something, that medium pops in to show responses to an article.

Medium comments drawer

Now that we know what we need, let’s jump right in!

How it started — v1

For the initial implementation, let’s follow this approach

  1. Create a component for the drawer(comment-drawer.component).
  2. Use @Input to control  isOpen state.
  3. Dynamically update the left CSS property to open/close the drawer.
  4. Use an @Output to notify when the drawer is closed.

As simple as it is!

<div class="drawer-container" [style.right.px]="isOpen ? 0 : -400">
  <button class="close" (click)="close()">X</button>
  <div class="header">
    <h5>Responses</h5>
  </div>
  <div class="body">
    <p>
      Loved the article
    </p>
  </div>
</div>
v1.drawer.component.html

We are using style binding to control the right property of the element. When open (isOpen == true), the element is positioned at right:0 otherwise at right:-400px (width of the element).

Let’s make it work with the help of some CSS.

.drawer-container {
  position: absolute;
  top: 0;
  right: 0;
  width: 400px;
  transition: all 300ms;
}
v1.drawer.component.css

This is a pretty classic way of implementing a sidebar or a drawer.

Our component class can then simply be have,

export class DrawerComponent {
  @Input() isOpen = false;
  @Output() closed = new EventEmitter();
  close() {
    this.closed.emit();
  }
}
v1.drawer.component.ts

Then in the parent component, we can go ahead and use our drawer like,

<app-drawer
    [isOpen]="isDrawerOpen"
    (closed)="isDrawerOpen = false"
  ></app-drawer>
v1.parent.component.html

Though this solution is a bit naive, it works well as far as the application is concerned.

v1 output

Woo, that looks good enough! Or, does it?


Taking it to the next level — v2

Now, imagine we have to implement the drawer like logic in some other parts of the application, to display user data, let’s say. With the current implementation in place, how would you go about it? Also, what if you need another drawer that opens from the left?

There currently exists a rigid and business logic oriented implementation which makes it mostly, non reusable. So to implement the drawer logic elsewhere, we will have to create a new component, copy everything to that component and bootstrap it.

In practice, we will be repeating everything we just did. And we all know copying and pasting the same code is a big NO!

We have two high level goals for the v2.

  1. Make this component reusable across the application
  2. Give more control to the user(parent) such as the position of the drawer(left or right)
  3. Give the ab

Let’s now come up with another approach where we refactor the above component to be more flexible and reusable.

We are going to follow the following approach.

  1. Create a wrapper component for the drawer (drawer.component)
  2. Move the logic of opening, closing to the wrapper component
  3. Make comment-drawer.component to use drawer.component internally and pass content (comments) to it.

Time to write some code! Our drawer component will simply project the template that was passed to it (using content projection). It will also have an icon to close the pane which triggers close() so that the parent can listen to and perform actions if needed.

<div class="drawer-container">
  <button class="close" (click)="close()">X</button>
  <ng-content></ng-content>
</div>
v2-drawer.component.html

The component takes a couple of inputs and emits the drawerClosed() event.

export class DrawerComponent {
  @Input() isOpen: false;
  @Input() width: number = 400;
  @Input() position: 'left' | 'right' = 'right';
  @Output() drawerClosed = new EventEmitter();

  close() {
    this.drawerClosed.emit();
  }
}
v2-drawer.component.ts

The position property above is referring to which side the drawer is getting opened from. By default, it opens from the right end like in the above demo video.

We can now make use of the inputs and pass styles to .drawer-container like,

<div class="side-pane-drawer-container" [ngStyle]="drawerStyles">
...
</div>
v2-drawer.component.html

The getter  drawerStyles, conditionally pass styles to the template like so,

// ...
get drawerStyles() {
    const commonStyles = { width: `${this.width}px` };
    if (this.position == 'right') {
      return {
        ...commonStyles,
        right: `${this.isOpen ? 0 : -1 * this.width}px`,
      };
    } else {
      return {
        ...commonStyles,
        left: `${this.isOpen ? 0 : -1 * this.width}px`,
      };
    }
  }
// ...
v2-drawer.component.ts

Our wrapper component (drawer-component) is now ready. Let’s now alter the comment-drawer.component to make use of this wrapper.

<app-drawer [isOpen]="isOpen" (drawerClosed)="close()">
  <div>
    <h5>Responses</h5>
  </div>
  <div>
    <p>
      Loved the article!
    </p>
  </div>
</app-drawer>
v2-comment-drawer.component.html

The v2 is now ready to roll!

You can verify that the refactoring made no difference to the screen reader but made it possible for the developer to plug the drawer anywhere in the application in future and make it work in a snap.

💡 You probably want to move your open/close logic to a service with observables. You can head to my dark mode toggle article to take inspiration.

Woo! That now is already a big step!

Multiple content projection for better control - v3

Let’s not stop just yet!

We already are making use of the content projection. Let’s take this still forward. Let’s implement multiple content projections. This wouldn't be the best case scenario for implementing multiple content projections but, why not learn it!

Multiple content projection, just like it says, is the technique of projecting more than one template from the user(parent) to the component. Basically, instead of projecting everything in between <app-drawer></app-drawer> at once, we project the content piece by piece. In our case, we will have two pieces; one for the drawer header and one for the drawer body.

<div class="drawer-container" [ngStyle]="drawerStyles">
  <button class="close" (click)="close()">X</button>
  <div class="header">
    <ng-content select="[header]"></ng-content>
  </div>
  <div class="body">
    <ng-content select="[body]"></ng-content>
  </div>
</div>
v3-drawer.component.html

The select attribute is used to pick the right content to the right placeholder. We essentially have the projected content split to header and body.

Now, in the user component,

<app-drawer [isOpen]="isOpen" (drawerClosed)="close()">
  <div header class=”drawer-header”>
    <h5>Responses</h5>
  </div>
  <div body>
    <p>
      Loved the article!
    </p>
  </div>
</app-drawer>
v3-comment-drawer.component.html
💡 The selector above refers to the CSS selector. Any valid CSS selector is valid here. As you might have already noticed, in the above example, we are using attribute selector with the square bracket syntax. That would mean that you could select and project the same header using select=”.drawer-header”  in place of select=”[header]”

So, why did we do this?

One down side of v2 was the fact that any content could be projected to the drawer. This freedom can lead to less control of the content. In v3, we clearly say that the drawer can have a header and a body and can even provide basic stylings so that the user will not end up messing it up.

'exportAs’ for flexibility — v4

One major limitation of our current implementation is the fact that there is no way we can provide the close button from the outside. But in some parts of the application, we might need to project the close button along with the header. Or might need to style the close button differently.

How do we do it?

One way to go about is to use the @ViewChild in the user (comment-drawer.component) and call close() explicitly. This can lead to more boilerplate code.

This is where exportAs can help.

Angular component metadata has a property named exportAs. The exportAs property takes the name under which the component instance is exported in a template.  This is particularly useful when you want to expose public functions such as close() in our case

You can imagine this as a variable that references the component in the template.

// ...
@Component({
  selector: 'app-drawer',
  ...
  exportAs: 'drawer',
})
// ...
v4.drawer.component.ts


We can now get rid of the rigid close button from drawer.component as pass it from user like,

<app-drawer
  [isOpen]="isOpen"
  #commentDrawer="drawer">
  <div header>
    // ---
    <button class="close" (click)="commentDrawer.close()">X</button>
  </div>
  <div body>
   // ---
  </div>
</app-drawer>
v4.comment-drawer.component.html

Beautiful, don’t you think?

The magic of CSS variables — v5

This might catch you by surprise. Did you know we can bind a CSS property (CSS variable) of an element using style binding?

Okay, stay with me.

The way we are applying styles now is via the getter in the drawer component.

get drawerStyles() {
    if (this.position == 'right') {
      // return style object for right position
    } else {
      // return return style object for left position
    }
  }

This works. But there is a lot of code in the controller just to get the styles to apply conditionally. Is there a better, more intuitive way?

Turns out, there is! Here’s the plan.

We will pass our width property via property binding(css variables) to the drawer component. Then we use class binding to determine:

  1. When the drawer is open, using isOpen property
  2. If open, determine whether to open from left or right, using position property

This approach would also mean that we are no longer required to pass width as an input property.

<!-- Use class binding instead of the getter -->
<div
  class="drawer-container"
  [class.is-open]="isOpen"
  [class.position-right]="position === 'right'"
  [class.position-left]="position === 'left'"
>
  <div class="header">
    <ng-content select="[header]"></ng-content>
  </div>
  <div class="body">
    <ng-content select="[body]"></ng-content>
  </div>
</div>
v5.drawer.component.html

We are binding classes is-open , position-left , position-right to the container. Note that we do not have ngStyle binding anymore, the conditional getter will also go away with it.

The only part that is remaining is to implement those classes we used above.

:host {
  --drawer-width: 400px;
}
.drawer-container {
  // ---
  width: var(--drawer-width);
  // ---
  &.position-right {
    right: calc(-1 * var(--drawer-width));
    &.is-open {
      right: 0;
    }
  }
  &.position-left {
    left: calc(-1 * var(--drawer-width));
    &.is-open {
      left: 0;
    }
  }
}
v5.drawer.component.scss

We defined --drawer-width using the :host selector to make it globally available in the component stylesheet.  Here comes the magic of CSS. You can simply override the --drawer-width property from the parent which uses this component(comment-drawer in our case) like,

<app-drawer [style.--drawer-width]="'500px'">
    <!-- ... -->
</app-drawer>
v5.comment-drawer.component.html

Damn, it works like a charm!

Now, the element having position-right class in which case, we need to set the right property to the right-end so that the drawer stays invisible(This is important as we are using transition). When the drawer is in isOpen state, let’s make the right property to 0 so that the drawer is now visible and will have a smooth animated entry.

The above explanation will hold good for the element being placed at the left with position-left class.

Lazy load for performance — v6

Every time you create a component that is intended for reuse (every component must be intended for reuse, by the way), you should ask these questions to your application

“Is it imperative to render this component at the initial load of the page? Is the information in it vital to be rendered in the initial load?”

If your answer is "no" or "maybe", you should consider lazy loading the component. Because otherwise, we are giving overhead to the browser and making our application slower and grippy.

You might be thinking about what overhead an innocent text in the body can cause. Now, imagine your drawer is making an API request to the server to fetch all the comments. That extra API call will trigger on initial load causing the page to load slow but providing no value to the screen reader!

Yes, this is a really good use case of a lazy loading component. We need to basically tell Angular to not load any content until the drawer is opened.

What are your initial thoughts on implementation here?

One simple solution to implement would be to use isOpen in combination with *ngIf and rendering it using TemplateRef in the drawer.component  make sure the body is not rendered until the drawer is opened.

But what if you need more advanced conditional lazy loadings? Maybe parts on the component? The above approach will easily get messy there.

I have discussed lazy loading components in detail in this article, the same technique can be applied here to make your drawer component even more performant!


Wrapping it up

Let’s wrap our marathon refactoring here for now. Throughout this article we have learned how we could use efficient tools Angular provides to create high quality components. We learned that techniques such as multiple content projection, lazy loading, exportAs etc can be really handy when creating highly efficient, reusable components. Mastering these concepts/techniques can really make your development more fun and productive.

You can find the final code and result here at this stackblitz.

There must still be more, elegant iterations we could do over this component and I hope this article inspired you to start doing so, because simple refactoring(s) wouldn’t hurt anybody!