Responsive Component Rendering from Screen Size
Tags: webcomponents, javascript, css, stencil
Published: 2019-04-05 15:03:29

---
title: Responsive Component Rendering from Screen Size
published: true
description: Beyond styling, how can we easily choose different components to render based on the changing size of the user's screen?
tags: webcomponents, javascript, css, stencil
cover_image: https://thepracticaldev.s3.amazonaws.com/i/5ce2sbf897c7e0acngnu.png
---

# The Problem

I'm pretty familiar with the general ideas of responsive design, but in building my web app, I wanted *more control* than just choosing different styles to be applied and deciding whether certain components should be positioned _horizontally_ or _vertically_, depending on the user's current screen size.

I wanted to be able to render *completely different UI structures*.

Here's a taste of what this article is about:

The same 'page' component on a small screen...

![alt text](https://thepracticaldev.s3.amazonaws.com/i/cu2hb1le14kykm8gi3fe.png "Small Screen Version")

...and on a larger screen...

![alt text](https://thepracticaldev.s3.amazonaws.com/i/dpq6n55z0mr6tu299yb0.png "Large Screen Version")

I'm sure some folks know how to make that sort of drastic change using just CSS, but it's much easier for me to design page layouts as I think about how they will appear on different screen sizes, and that will often include completely different UI components.

So...how to do that?

## Enter StencilJS and Web Components

![alt text](https://media.giphy.com/media/7K1OQmbCnKAa4/giphy.gif "Enter Stencil")

Stencil, for me, has been the opening of a door. I still sometimes walk into the door frame, but the door _is_ open. 

![alt text](https://media.giphy.com/media/FIx0no49Dnih2/giphy.gif "Walking into door")

Ok, weird metaphor... Bottom line: I feel like I can tackle more challenges using web components now that Stencil is on the scene.

If you're not familiar with Stencil, you should be. [CHECK IT OUT](https://stenciljs.com)

For this challenge, I decided to use `window.matchMedia()` provided by the JavaScript Web API to receive events when the screen size has changed and encapsulate that into a web component I can drop into my projects. With my new component, I could listen for a custom `sizeChanged` event based on my predetermined screen size breakpoints and define custom rendering logic around those events.

# ~~The~~ My Solution

Alright, I'm going to cut to the chase with the rest of this article and just show the code.

### My `viewport-size-publisher` web component:

```javascript
import { Component, Event, EventEmitter, Method, Prop } from "@stencil/core";

@Component({
  tag: 'viewport-size-publisher'
})
export class ViewportSizePublisher {

  @Event() sizeChanged: EventEmitter;

  @Prop() sizes: Object[] = [];

  private sizesList;

  componentWillLoad() {

    if (!this.sizes || this.sizes.length < 1) {

      // Default sizes, if none are provided as a Prop
      this.sizesList = [
        { name: 'xs', minWidth: '0', maxWidth: '319' },
        { name: 'sm', minWidth: '320', maxWidth: '511' },
        { name: 'md', minWidth: '512', maxWidth: '991' },
        { name: 'lg', minWidth: '992', maxWidth: '1199' },
        { name: 'xl', minWidth: '1200', maxWidth: '9999' }
      ];
    }
    else {

      this.sizesList = [...this.sizes];
    }
  }

  componentDidLoad() {

    // Add listeners for all sizes provided
    for (let i = 0; i < this.sizesList.length; i++) {

      window.matchMedia(`(min-width: ${this.sizesList[i].minWidth}px) 
                          and (max-width: ${this.sizesList[i].maxWidth}px)`)
        .addEventListener("change", this.handleMatchMediaChange.bind(this));
    }
  }

  componentDidUnload() {

    // Remove listeners for all sizes provided
    for (let i = 0; i < this.sizesList.length; i++) {

      window.matchMedia(`(min-width: ${this.sizesList[i].minWidth}px) 
                          and (max-width: ${this.sizesList[i].maxWidth}px)`)
        .removeEventListener("change", this.handleMatchMediaChange.bind(this));
    }
  }

  @Method()
  async getCurrentSize() {

    // Iterate over all given sizes and see which one matches
    for (let i = 0; i < this.sizesList.length; i++) {

      if (window.matchMedia(`(min-width: ${this.sizesList[i].minWidth}px) 
          and (max-width: ${this.sizesList[i].maxWidth}px)`).matches) {

        return this.sizesList[i].name;
      }
    }
  }

  handleMatchMediaChange(q) {

    if (q.matches) {
      
      // Find the name of the matching size and emit an event
      for (let i = 0; i < this.sizesList.length; i++) {

        if (q.media.indexOf(`min-width: ${this.sizesList[i].minWidth}px`) > -1) {

          this.sizeChanged.emit({ size: this.sizesList[i].name });
        }
      }
    }
  }

  render() {
    return [];
  }
}

```

In this component, I am taking in size definitions and _custom names_ for the different screen sizes or breakpoints. When the component loads, I add event listeners for all of the media queries generated from those screen sizes. When the component unloads, I remove those event listeners.

There's also a `@Method()` definition that allows other components to get the current size of the screen when they are first loading.

As the screen changes size, I `emit` a custom event called `sizeChanged`.



### Usage of my web component in my `app-root.tsx` component:

```html
<viewport-size-publisher sizes={[
          { name: ViewportSize.ExtraSmall, minWidth: '0', maxWidth: '319' },
          { name: ViewportSize.Small, minWidth: '320', maxWidth: '511' },
          { name: ViewportSize.Medium, minWidth: '512', maxWidth: '991' },
          { name: ViewportSize.Large, minWidth: '992', maxWidth: '1199' },
          { name: ViewportSize.ExtraLarge, minWidth: '1200', maxWidth: '9999' }
        ]} />
```

Here, you'll notice I'm using an `enum` to define and standardize the different screen size names, and I'm passing that into my new component.

### Implementation of Responsive Logic in Page Component

```javascript
import { Component, Prop, State, Listen } from "@stencil/core";
import { ViewportSize} from "../../../interfaces/interfaces";

@Component({
  tag: 'group-home-page'
})
export class GroupHomePage {
  ...
  @State() viewportSize: ViewportSize;
  
  async componentWillLoad() {
    ...
    // Get current viewport size, set this.viewportSize accordingly.
    let viewportSizePubElem = document.querySelector('viewport-size-publisher');
    this.viewportSize = await viewportSizePubElem.getCurrentSize();
  }

  @Listen('document:sizeChanged')
  handleViewportSizeChange(event: any) {
    ...
    this.viewportSize = event.detail.size;
  }

  renderForSmallerSizes() {
    return [
      ...
      //<ion-list> components without surrounding <ion-card>
      ...
    ];
  }

  renderForLargerSizes() {
    return [
      ...
      //<ion-list> components with surrounding <ion-card>
      ...
    ];
  }

  renderBasedOnSize() {

    switch (this.viewportSize) {
      case ViewportSize.ExtraSmall:
      case ViewportSize.Small:
      case ViewportSize.Medium: {
        return this.renderForSmallerSizes();
      }
      case ViewportSize.Large:
      case ViewportSize.ExtraLarge: {
        return this.renderForLargerSizes();
      }
    }
  }

  render() {
    return [
      <ion-header>
        <ion-toolbar color='secondary'>
          <ion-buttons slot='start'>
            <ion-menu-button></ion-menu-button>
          </ion-buttons>
          <ion-title>{ this.group ? this.group.name : '...' }</ion-title>
        </ion-toolbar>
      </ion-header>,
      <ion-content>
        
        {this.renderBasedOnSize()}

      </ion-content>
    ];
  }
}
```

Let's break down what's going on in this file a bit more.

`@State() viewportSize: ViewportSize;`

Any time this state variable is modified, it will cause the component to re-render.

When the page component is loading, I set the state variable with the current size, which I get by executing the `getCurrentSize` method:

`this.viewportSize = await viewportSizeElem.getCurrentSize();`

Thereafter, I decorated a function with a listener to handle any changes made to the screen size and update my state variable:

`@Listen('document:sizeChanged')`

As the component is rendering or re-rendering, a series of functions return the UI component structure I want for the different screen sizes. 

Although I'm combining several different screen sizes into just two main structures, I could easily created different `render...` functions for `Small` versus `ExtraSmall`.

# Conclusion

What are your thoughts on this approach? How have you done something similar in your project?