Oombla DocsMenuoombla.com

React

The base template for exporting assets as React components

The React template appears convoluted at first glance, but if the code is reduced to its core logic then it is much easier to get a good grasp of what it’s doing. The base template is set up to not have any third-party dependencies (e.g. CSS-in-JS libraries). It’s only dependency is React. However, if you know what dependencies you will have in your project you can simplify the template to take advantage of them. An example of how the template could be constructed if your project used styled components is given at the end of this article.

The base template is very long, but let’s reduce it to just the logic so it can be easier to follow:

import React from 'react'

/* Section 1 */
<% if (variantSvgs) { %>
const renderSvgs = ({
  [passed_props]
}) => {
  <% _.each(variantSvgs, (variantSvg, i) => { %>
  [if_else_if_variant_logic] {
    return (
      <svg
        [variant_svg_and_props]
      >
        [title_and_desc]
        <%= variantSvg.contents %>
      </svg>
    )
  }
  <% }) %>
  else {
    return (
      <svg
        [svg_and_props]
      >
        [title_and_desc]
        <%= svg.contents %>
      </svg>
    )
  }
}
<% } %>

/* Section 2 */
const <%= name %> = ({
  [props]
}) => {
  [css_var]

  return (
    [react_fragment_conditional]
      [style_css_var]

      <% if (variantSvgs) { %>
      {renderSvgs({
        [pass_props]
      })}
      <% } else { %>
      <svg
        [svg_and_props]
      >
        [title_and_desc]
        <%= svg.contents %>
      </svg>
      <% } %>
    [/react_fragment_conditional]
  )
}

export default <%= name %>

Section 1: if (variantSvgs)

At the beginning of the template, React is imported and then in the first section is a single conditional that only renders if variantSvgs is present (that is, not empty). A function named renderSvgs() is output to match the function of the same name that is called in Section 2 if variants are present. This renderSvgs() is the render function for the component if variants are present and the props from the component are passed to it. For now, we will just cover the logic and come back later to how the props are rendered.

Here is Section 1 again:

/* Section 1 */
<% if (variantSvgs) { %>
const renderSvgs = ({
  [passed_props]
}) => {
  <% _.each(variantSvgs, (variantSvg, i) => { %>
  [if_else_if_variant_logic] {
    return (
      <svg
        [variant_svg_and_props]
      >
        [title_and_desc]
        <%= variantSvg.contents %>
      </svg>
    )
  }
  <% }) %>
  else {
    return (
      <svg
        [svg_and_props]
      >
        [title_and_desc]
        <%= svg.contents %>
      </svg>
    )
  }
}
<% } %>

Inside of the function two things happen:

  1. First, all of the variant objects in variantSvgs are output with a conditional wrapper that is setup to match their variant props. There is a logic operator to construct a series of if and else if statements to chain these variant conditionals.
  2. Lastly, an else statement is constructed to complete the if and else if chain and the svg object is used to render the default (else) state.

So for example if there were three assets (objects) in variantSvgs and each asset had two variants the final (abridged) output for this internal section would look like this:

if (variant1 === 'asset1_variant1_value' && variant2 === 'asset1_variant2_value') {
  [asset1 from variantSvgs rendered]
}
else if (variant1 === 'asset2_variant1_value' && variant2 === 'asset2_variant2_value') {
  [asset2 from variantSvgs rendered]
}
else if (variant1 === 'asset3_variant1_value' && variant2 === 'asset3_variant2_value') {
  [asset3 from variantSvgs rendered]
}
else {
  [default asset from svg rendered]
}

Section 2: Main export

The second section contains the main component export function. The logic for this section is simpler than Section 1. First the component name is rendered with <%= name %> and then the props are passed to the component (we will go over how the props are rendered after this section). Then in the return method there is a conditional that calls the aforementioned renderSvgs() function if variants are present and if not renders the asset in svg.

/* Section 2 */
const <%= name %> = ({
  [props]
}) => {
  [css_var]

  return (
    [react_fragment_conditional]
      [style_css_var]

      <% if (variantSvgs) { %>
      {renderSvgs({
        [pass_props]
      })}
      <% } else { %>
      <svg
        [svg_and_props]
      >
        [title_and_desc]
        <%= svg.contents %>
      </svg>
      <% } %>
    [/react_fragment_conditional]
  )
}

export default <%= name %>

Now let’s cover the bracketed sections. Note that these bracketed section labels have been added only for this explanation article as a way to make the overarching logic easier to parse. They are placeholder markers for the code that is in their place in the actual template. Below as we go through each placeholder, the code that replaces the placeholder will follow its explanation.

[title_and_desc]

This occurs in several places and is a conditional render for the title and description accessibility props if they exist. Accessible SVGs is a good place to start if you want to read more about SVGs and accessibility.

{!!title && <title>{title}</title>}
{!!desc && <desc>{desc}</desc>}

[props]

The component props can include anything you would like to have available as a prop for the component. In the base template there are static props that have no dependency on the assets being exported like className and title and desc, props that are rendered conditionally like the color props, and then props that are rendered dynamically like the variant props.

The four color props are only rendered if either the defaultFill property on the svg object is present or if the component has variants (i.e. variantSvgs is not empty). The reason for the latter case is there could be scenarios where the default asset does not have a defaultFill value but one or more of its variants does have it.

The variant props are extracted from the variants object. Note how only the keys are passed as props.

<% if (svg.defaultFill || variantSvgs) { %>activeColor,<% } %>
className,
<% if (svg.defaultFill || variantSvgs) { %>color,<% } %>
desc,
<% if (svg.defaultFill || variantSvgs) { %>focusColor,<% } %>
height,
<% if (svg.defaultFill || variantSvgs) { %>hoverColor,<% } %>
title,
width,
<% _.each(variants, (variantValues, variantKey) => { %>
<%= variantKey %>,
<% }) %>

[pass_props]

The props defined for the component are passed to the renderSvgs() function. The static and conditional props are just copied since the conditional props were all partially based on the existence of variantSvgs and if renderSvgs() is called it means that variantSvgs exists. Then the variant props are rendered with the same function as in the main props area.

activeColor,
className,
desc,
focusColor,
height,
hoverColor,
color,
title,
width,
<% _.each(variants, (variantValues, variantKey) => { %>
<%= variantKey %>,
<% }) %>

[svg_and_props]

The props that are passed to the asset if there are no variants are mingled with the attributes stored in svg. If the defaultFill property on svg is present, the hover, focus and active color props are passed to data- attributes which are used as style targets in the CSS and the color prop is added as the value for the fill attribute with the defaultFill value as the fallback (in case the prop is not passed). If svg.defaultFill does not exist, the original fill value from the asset is assigned to the fill attribute if it existed.

The width and height attributes are a bit confusing. The way the component is set up is to set the size of the SVG via the width property using the viewBox attribute to ensure proper scaling. If the width prop is defined, the height attribute is marked as undefined so that the SVG uses the viewBox to scale the SVG appropriately. However, there may be times when it’s the height attribute that needs to drive the scaling, in that case, if no corresponding width prop is defined, the width attribute is set to undefined. If neither width nor height props are present it leaves the height attribute as undefined and looks for the asset’s original width attribute value to use for the width attribute; if this can’t be found then width is set as undefined as well (though this value should usually be present).

The xmlns attribute is assigned with the normal value set as the fallback. And then if there are any other attributes that were extracted from the original asset they are output with <%= svg.attributes.other %>.

{...{ className }}
<% if (svg.defaultFill) { %>
data-hover={hoverColor}
data-focus={focusColor}
data-active={activeColor}
<% } %>
width={width ? width : height ? undefined : <%= svg.attributes.width ? '"' + svg.attributes.width + '"' : 'undefined' %>}
height={height ? height : undefined}
<% if (svg.attributes.viewBox) { %>viewBox="<%= svg.attributes.viewBox %>" <% } %>
<% if (svg.defaultFill) { %>
fill={color || "<%= svg.defaultFill %>"}
<% } else { %>
<% if (svg.attributes.fill) { %>fill="<%= svg.attributes.fill %>"<% } %>
<% } %>
xmlns="<%= svg.attributes.xmlns ? svg.attributes.xmlns : 'http://www.w3.org/2000/svg' %>"
<%= svg.attributes.other %>

[css_var]

If there is a default fill property for the svg object or variants present, a css variable is created with basic CSS styles to enable the hoverColor prop on the hover state and likewise for the focus and active states. The variable is set up to where in the component if these props are empty the style rules are not rendered. This is set up this way so that the base template does not force any third-party dependencies.

<% if (svg.defaultFill || variantSvgs) { %>
const css = `
  ${hoverColor ? `svg[data-hover="${hoverColor}"]:hover {
    fill: ${hoverColor};
  }` : ''}
  ${focusColor ? `svg[data-focus="${focusColor}"]:focus {
    fill: ${focusColor};
  }` : ''}
  ${activeColor ? `svg[data-active="${activeColor}"]:active {
    fill: ${activeColor};
  }` : ''}
`
<% } %>

[style_css_var]

Using the same conditional as the css_var block this portion of the template inserts the CSS between <style> tags in the return of the component.

<% if (svg.defaultFill || variantSvgs) { %>
<style>
  {css}
</style>
<% } %>

[react_fragment_conditional]

This section is present because if the <style> tag is rendered then we need to wrap both it and the <svg> tag in a <React.Fragment>. It has the same conditional as the css_var and style_css_var blocks.

<% if (svg.defaultFill || variantSvgs) { %><React.Fragment><% } %>
...
<% if (svg.defaultFill || variantSvgs) { %></React.Fragment><% } %>

[passed_props]

Now moving on to Section 1, the props in the pass_props block where the renderSvgs() function is called are also duplicated here in the same way according to the same logic.

activeColor,
className,
desc,
focusColor,
height,
hoverColor,
color,
title,
width,
<% _.each(variants, (variantValues, variantKey) => { %>
<%= variantKey %>,
<% }) %>

[variant_svg_and_props]

This block has the same attributes and props assignment and logic as the svg_and_props block; the only difference is the values are coming from each object in variantSvgs as opposed to the svg object.

{...{ className }}
<% if (variantSvg.defaultFill) { %>
data-hover={hoverColor}
data-focus={focusColor}
data-active={activeColor}
<% } %>
width={width ? width : height ? undefined : <%= variantSvg.attributes.width ? '"' + variantSvg.attributes.width + '"' : 'undefined' %>}
height={height ? height : undefined}
<% if (variantSvg.attributes.viewBox) { %>viewBox="<%= variantSvg.attributes.viewBox %>" <% } %>
<% if (variantSvg.defaultFill) { %>
fill={color || "<%= variantSvg.defaultFill %>"}
<% } else { %>
<% if (variantSvg.attributes.fill) { %>fill="<%= variantSvg.attributes.fill %>"<% } %>
<% } %>
xmlns="<%= variantSvg.attributes.xmlns ? variantSvg.attributes.xmlns : 'http://www.w3.org/2000/svg'%>"
<%= variantSvg.attributes.other %>

[if_else_if_variant_logic]

This last section is probably the hardest to follow. It’s written on a single line like it is so that when the final component code is rendered the if and else statements look like they would if a person has coded them. In the sample below though I have added line breaks to make it easier to read.

<%= (i === 0) ? 'if (' : 'else if (' %>
  <% variantSvg.variants.map((variant, j) => { %>
    <%= variant.key %> === <%= typeof(variant.value) === 'boolean' ? "" : "'" %><%= variant.value %><%= typeof(variant.value) === 'boolean' ? "" : "'" %>
    <%= (j < variantSvg.variants.length - 1) ? ' && ' : '' %>
  <% }) %>
) {
  • line1: The i index is being keyed off the <% _.each(variantSvgs, (variantSvg, i) => { %> function that was called just above it. If it is the first object in variantSvgs it gets the if ( statement. Otherwise it gets else if (. Keep in mind that the closing ) is in line6 and the final else is handled in a separate block with the svg object.
  • line2: For the variantSvg (of variantSvgs), go through each variant combination where variant is an object with a single key (prop) and value (prop value). The index is assigned to the variable j so as not be confused with the already present i.
  • line3: Output the prop and value items as [prop] === [value]. What makes this line extra long is a typeof ternary that is performed on either side of the prop value to determine if it should be output as a string or boolean (e.g. direction === 'right' or right === true).
  • line4: If there are still items left in the variants array, output && to continue chaining the conditions needed to render this variant. If not, if this is the last item in the array, output nothing.
  • line5: Close the map function from line2.
  • line6: Add the closing parenthesis and opening bracket for the if or else if statement. (The bracket is closed later after the asset has been rendered.)

Styled Components

This example uses Emotion but the syntax should be similar for styled-components.

import React from 'react'
import styled from '@emotion/styled'

const StyledSvg = styled.svg`
  ${props => `
    ${!!props.hoverColor && `
      &:hover {
        fill: ${props.hoverColor};
      }
    `}
    ${!!props.focusColor && `
      &:focus {
        fill: ${props.focusColor};
      }
    `}
    ${!!props.activeColor && `
      &:active {
        fill: ${props.activeColor};
      }
    `}
  `}
`

<% if (variantSvgs) { %>
const renderSvgs = ({
  activeColor,
  className,
  desc,
  focusColor,
  height,
  hoverColor,
  color,
  title,
  width,
  <% _.each(variants, (variantValues, variantKey) => { %>
  <%= variantKey %>,
  <% }) %>
}) => {
  <% _.each(variantSvgs, (variantSvg, i) => { %>
  <%= (i === 0) ? 'if (' : 'else if (' %><% variantSvg.variants.map((variant, j) => { %><%= variant.key %> === '<%= variant.value %>'<%= (j < variantSvg.variants.length - 1) ? ' && ' : '' %><% }) %>) {
    return (
      <StyledSvg
        {...{ className<% if (variantSvg.defaultFill) { %>, activeColor, focusColor, hoverColor<% } %> }}
        width={width ? width : height ? undefined : <%= variantSvg.attributes.width ? '"' + variantSvg.attributes.width + '"' : 'undefined' %>}
        height={height ? height : undefined}
        <% if (variantSvg.attributes.viewBox) { %>viewBox="<%= variantSvg.attributes.viewBox %>" <% } %>
        <% if (variantSvg.defaultFill) { %>
        fill={color || "<%= variantSvg.defaultFill %>"}
        <% } else { %>
        <% if (variantSvg.attributes.fill) { %>fill="<%= variantSvg.attributes.fill %>"<% } %>
        <% } %>
        xmlns="<%= variantSvg.attributes.xmlns ? variantSvg.attributes.xmlns : 'http://www.w3.org/2000/svg'%>"
        <%= variantSvg.attributes.other %>
      >
        {!!title && <title>{title}</title>}
        {!!desc && <desc>{desc}</desc>}

        <%= variantSvg.contents %>
      </StyledSvg>
    )
  }
  <% }) %>
  else {
    return (
      <StyledSvg
        {...{ className<% if (svg.defaultFill) { %>, activeColor, focusColor, hoverColor<% } %> }}
        width={width ? width : height ? undefined : <%= svg.attributes.width ? '"' + svg.attributes.width + '"' : 'undefined' %>}
        height={height ? height : undefined}
        <% if (svg.attributes.viewBox) { %>viewBox="<%= svg.attributes.viewBox %>" <% } %>
        <% if (svg.defaultFill) { %>
        fill={color || "<%= svg.defaultFill %>"}
        <% } else { %>
        <% if (svg.attributes.fill) { %>fill="<%= svg.attributes.fill %>"<% } %>
        <% } %>
        xmlns="<%= svg.attributes.xmlns ? svg.attributes.xmlns : 'http://www.w3.org/2000/svg' %>"
        <%= svg.attributes.other %>
      >
        {!!title && <title>{title}</title>}
        {!!desc && <desc>{desc}</desc>}

        <%= svg.contents %>
      </StyledSvg>
    )
  }
}
<% } %>
const <%= name %> = ({
  <% if (svg.defaultFill || variantSvgs) { %>activeColor,<% } %>
  className,
  <% if (svg.defaultFill || variantSvgs) { %>color,<% } %>
  desc,
  <% if (svg.defaultFill || variantSvgs) { %>focusColor,<% } %>
  height,
  <% if (svg.defaultFill || variantSvgs) { %>hoverColor,<% } %>
  title,
  width,
  <% _.each(variants, (variantValues, variantKey) => { %>
  <%= variantKey %>,
  <% }) %>
}) => (
  <% if (variantSvgs) { %>
  renderSvgs({
    activeColor,
    className,
    desc,
    focusColor,
    height,
    hoverColor,
    color,
    title,
    width,
    <% _.each(variants, (variantValues, variantKey) => { %>
    <%= variantKey %>,
    <% }) %>
  })
  <% } else { %>
  <StyledSvg
    {...{ className<% if (svg.defaultFill) { %>, activeColor, focusColor, hoverColor<% } %> }}
    width={width ? width : height ? undefined : <%= svg.attributes.width ? '"' + svg.attributes.width + '"' : 'undefined' %>}
    height={height ? height : undefined}
    <% if (svg.attributes.viewBox) { %>viewBox="<%= svg.attributes.viewBox %>" <% } %>
    <% if (svg.defaultFill) { %>
    fill={color || "<%= svg.defaultFill %>"}
    <% } else { %>
    <% if (svg.attributes.fill) { %>fill="<%= svg.attributes.fill %>"<% } %>
    <% } %>
    xmlns="<%= svg.attributes.xmlns ? svg.attributes.xmlns : 'http://www.w3.org/2000/svg' %>"
    <%= svg.attributes.other %>
  >
    {!!title && <title>{title}</title>}
    {!!desc && <desc>{desc}</desc>}

    <%= svg.contents %>
  </StyledSvg>
  <% } %>
)

export default <%= name %>