2

So I'm working on a very small web component to feature as part of a much larger design system.

I'm a bit new to use of Web Components, but I know this particular web-component could be used many, many times in a single layout.

This web component controls how much vertical space to put around any child components that are passed into it.

The anatomy of the component is pretty simple:

import { LitElement, html, css, unsafeCSS, property, customElement } from 'lit-element';

export const spacingAmounts = {
  'x-small': css`4px`,
  'small': css`8px`,
  'medium': css`12px`,
  'large': css`16px`,
  'x-large': css`20px`,
  '2x-large': css`30px`,
  '3x-large': css`40px`,
  '4x-large': css`60px`,
  '5x-large': css`90px`,
  '6x-large': css`120px`,
};

const createSpacingStyleRules = (direction: 'top' | 'bottom') => {
  return Object.keys(spacingAmounts).map(s => {
    const amount = spacingAmounts[s];
    return css`
      :host([${unsafeCSS(direction)}="${unsafeCSS(s)}"]) {
        margin-${unsafeCSS(direction)}: ${unsafeCSS(amount)};
      }
    `;
  });
};

@customElement('gu-vertical-space')
export class GuVerticalSpace extends LitElement {
  @property() top: string;
  @property() bottom: string;

  static get styles() {
    const styles = [
      css`
        :host {
          display: block;
        }
      `,
      // ----------------------------------------------------------
      // @TODO:
      // test if it's better performance wise to either:
      //
      // 1 -  generate a verbose list of static styles for
      //      each instance of gu-vertical-space
      //  or
      // 2 -  generate a tiny amount of styles on the fly, 
      //      based on property inputs...
      // ----------------------------------------------------------
      // ...createSpacingStyleRules('top'),
      // ...createSpacingStyleRules('bottom'),
    ];
    return styles;
  }

  render() {
    const styles = [];
    if (this.top) {
      styles.push(css`
        :host([top="${unsafeCSS(this.top)}"]) {
          margin-top: ${spacingAmounts[this.top]}
        }
      `);
    }
    if (this.bottom) {
      styles.push(css`
        :host([bottom="${unsafeCSS(this.bottom)}"]) {
          margin-bottom: ${spacingAmounts[this.bottom]}
        }
      `);
    }
    return html`
      <style>${styles}</style>
      <slot></slot>
    `;
  }
}


There are 2 approaches here for mapping pre-defined margin amounts to either the top or bottom of the web component's host.

The currently active approach is simply to dynamically generate a <style> block inside the render function, which contains any margin amounts, as decided by the "top" or "bottom" input properties.

The other approach that I'm contemplating (is commented out atm though) - is to staticly generate a massive list of style rules - thus avoiding the need to generate any dynamic style stuff inside the render() function, which could be an anti-pattern - if i'm understanding the lit-element docs correctly.

Perhaps there's a more elegant approach that I'm missing? I'm leaning toward the current approach, just because it feels simpler to understand - but curious as to what others think!

glo
  • 687
  • 1
  • 8
  • 20
  • 3
    I feel like you could simplify this a lot by using css variables, basically your idea is to add a top or bottom margin depending on the value of a property in your component? – Alan Dávalos Jan 08 '20 at 04:44
  • yup! so i did think about trying to use css vars. I just couldn't make them fit to the use case (affecting the :host el's margins - based on dynamic inputs...). A simple solve for this ofcourse is to wrap the tag in a container div that has a styleMap - but given how often i plan on using this component, this felt a bit heavy handed... – glo Jan 08 '20 at 05:47
  • 3
    you can set the css vars just using javascript like `this.style.setProperty('--my-variable', value)` and that would simplify everything imo – Alan Dávalos Jan 08 '20 at 06:03
  • Oh man - you’re right - that’s heaps easier! Thank you, Alan! :) – glo Jan 08 '20 at 09:59
  • And allow a global setting: ``margin: var( --GuVerticalSpace-margin, var( --my-variable , 12px ) )`` – Danny '365CSI' Engelman Jan 08 '20 at 10:09

1 Answers1

6

As suggested by Alan, a much simpler solution to this issue is to use css-variables.

Basically, just map the input margin amounts as a css-variable at the connect() life-cycle event (or render if you imagine the props will ever change after the initial render) - and call it a day!

static get styles() {
  return css`
    :host {
      display: block;
      margin-top: var(--margin-top);
      margin-bottom: var(--margin-bottom);
    }
  `;
}

connectedCallback() {
  super.connectedCallback();
  this.style.setProperty(
    '--margin-top',
    this.top
      ? spacingAmounts[this.top]
      : 'unset'
  );
  this.style.setProperty(
    '--margin-bottom',
    this.bottom
      ? spacingAmounts[this.bottom]
      : 'unset'
  );
}
glo
  • 687
  • 1
  • 8
  • 20
  • upvoted--please note the values should be trusted though, otherwise be sure to sanitize somehow, perhaps cast as `Number(value)+'px'` and similar – jimmont Nov 04 '21 at 22:08