Skip to content

Instantly share code, notes, and snippets.

@tomblanchard
Last active April 24, 2017 04:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tomblanchard/77b7fa3af2d5acd4d0fba7145f23789b to your computer and use it in GitHub Desktop.
Save tomblanchard/77b7fa3af2d5acd4d0fba7145f23789b to your computer and use it in GitHub Desktop.
Shopify: Encapsulated components.

Concept

In React (JSX), you create a component like so:

function Button(props) {
  return (
    <button className={"button " + (props.color ? props.color : '')} type="button">
      {props.value}
    </button>
  );
};

This renders a button element with two properties - color and value, usage is like so:

<Button value="Submit" /> // "<button class='button'>Submit</button>"
<Button color="primary" value="Submit" /> // "<button class='button primary'>Submit</button>"
<Button color="secondary" value="Submit" /> // "<button class='button secondary'>Submit</button>"

These components are entirely encapsulated, meaning the properties won't leak out.

I want this within Liquid/Shopify themes.

Issue

We can achieve something like this like so:

// snippets/button.liquid

<button class="button {{ class }}" type="button">
  {{ value }}
</button>

Usage:

{% assign class = "primary" %}
{% assign value = "Submit" %}
{% include "button" %} // "<button class='button primary'>Submit</button>"

This works but the properties leak, for example:

{% assign class = "primary" %}
{% assign value = "Submit" %}
{% include "button" %} // "<button class='button primary'>Submit</button>"

// ...

{{ class }} // "primary"

We can get around this by using the inline include parameter syntax:

{% include "button",
   class: "primary",
   value: "Submit" %} // "<button class='button primary'>Submit</button>"

This fixes the leaking issue:

{% include "button",
   class: "primary",
   value: "Submit" %} // "<button class='button primary'>Submit</button>"

{{ class }} // nil

This brings a slightly different issue:

{% assign value = "Some higher up/global variable because `value` is a pretty generic variable name." %}

{% include "button",
   class: "primary",
   value: "Submit" %} // "<button class='button primary'>Submit</button>"

Let's say the first value variable is a global variable which should leak through into snippets/button.liquid, it won't work because the inline include parameter syntax overrides it; you could just rename one of the variables so they don't clash but at some point there will probably be a generic variable name which clashes with a component property.

Solution

What we need to do here is make every component property name unique to that component, we need to fake encapsulation and the only means we have at our disposal are naming conventions. I toyed with prefixing each component property name with the snippet file name, like:

{% include "button",
   button_class: "primary",
   button_value: "Submit" %} // "<button class='button primary'>Submit</button>"

This looks pretty good for this example, but it gets a little out of hand when our components have longer names such as snippets/product-thumbnail.liquid, an example of a component property for this is product_thumbnail_image_url, typing long variable names gets boring fast.

My solution is to associate a number to each component, so snippets/button.liquid would be component 1, so we'd prefix each component property name with c1_ like so (I've also added a DocBlock style header to document each component property):

// snippets/button.liquid

{% comment %}
  Component 1 - Button

  @param {String} c1_class
  @param {String} c1_value
{% endcomment %}

<button class="button {{ c1_class }}" type="button">
  {{ c1_value }}
</button>

Usage:

{% include "button",
   c1_class: "primary",
   c1_value: "Submit" %} // "<button class='button primary'>Submit</button>"

Now we don't have to worry about these variables ever clashing because variables beginning with c1_ will only ever exist when the snippets/button.liquid component is used.

Advanced Example

Buttons may no be the best example of a component here, most of the time buttons are kept pretty simple with a pretty flat DOM structure, we can usually just stick to using a class name as the style abstraction, there's not much difference between this:

{% include "button",
   c1_class: "primary",
   c1_value: "Submit" %} // "<button class='button primary'>Submit</button>"

...And this:

<button class="button primary">
  Submit
</button>

So, personally I wouldn't even add button as a component unless it got more complex. This concept of components shines when the DOM structure is more complex and there's a lot of class names, for example here's a component:

// snippets/faux-image.liquid

{% comment %}
  Component 2

  @param {String} c2_aspect_ratio
  @param {String} c2_src
  @param {String} c2_alt
{% endcomment %}

<div class="image-container box-placehold position-relative box-ratio--{{ c2_aspect_ratio }}">
  <div class="position-absolute position-full bg-full-center" data-src="{{ c2_src }}" data-no-resize>
    <img class="one-whole opacity-0 height-full" src="{{ c2_src }}" alt="{{ c2_alt }}" />
  </div>
</div>

This component renders a box with a proper ratio (4:3, 16:9, etc.) with an image which will stretch to fit the dimensions of the box without distorting. Usage is like so:

{% include "faux-image",
   c2_aspect_ratio: "16-9",
   c2_src: "//placehold.it/2000",
   c2_alt: "Awesome placeholder is awesome." %}

Which renders:

<div class="image-container box-placehold position-relative box-ratio--16-9">
  <div class="position-absolute position-full bg-full-center" data-src="//placehold.it/2000" data-no-resize>
    <img class="one-whole opacity-0 height-full" src="//placehold.it/2000" alt="Awesome placeholder is awesome." />
  </div>
</div>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment