Creating a Card Component in PatternLab and Mapping to Drupal the "right" way

Yes, I know, there's more than one way to integrate PatternLab with Drupal. Here's how I create a card component and map it to Drupal.

Here's the task - create a card component with fields for:

  1. Card Image
  2. Card Title
  3. Card Body
  4. Card Link URL
  5. Card Style (sets a colour for a border-top on the card)
  6. Card Size (sets the width of the card)

In PatternLab, here's what our Twig file might look like (with explanations after it):

{%
set classes = [
  "card",
  card_image ? 'card--has-image',
  card_style ? 'card--' ~ card_style,
  card_size ? 'card--' ~ card_size,
]
%}

{% if card_link_url %}
  {% set element = 'a' %}
  {% else %}
    {% set element = 'div' %}
{% endif %}

<{{element}}{{ attributes.addClass(classes) }}{% if card_link_url %} href="{{ card_link_url}}"{% endif %}>

  {% if card_image %}
    <div class="card__image">
      {{ card_image }}
    </div>
  {% endif %}

  <div class="card__content">

    {% if card_title %}
      <div class="card__title h1">
        {{ card_title }}
      </div>
    {% endif %}

    {% if card_text %}
      <div class="card__text">
        {{ card_text }}
      </div>
    {% endif %}

  </div>

</{{element}}>

{% block content_variable %}
  {#
    This allows the cache_context to bubble up for us, without having to
    individually list every field in
    {{ content|without('field_name', 'field_other_field', 'field_etc') }}
  #}
  {% set catch_cache = content|render %}
{% endblock %}

The classes array at the top allows us to set variations for our card, depending on values chosen by the editor. So, if there is an image, we add a class of .card--has-image; if a style is chosen, we add a class of that style, for example: .card--medium (I create options for small, medium, large, and full - with 'small' being the default - corresponding on large screens to a width within their container of 33%, 50% 66% and 100% respectively).

Next, we set our {{ element }}. This allows us to have the card wrapped in an a tag or a div tag. We check to see if the link field has been filled in and, if so, we use the a element, but if not, we use the div element instead. This will render HTML like one of the following:

<a class="card" href="#">
  CARD STUFF GOES HERE
</a>
<div class="card">
  CARD STUFF GOES HERE
</div>

Following this, we check if there is an image and, if so, we render our image div. Checking first allows us to have nice bem-style classes, but also means we don't end up rendering emtpy divs. Although, when it comes to Drupal, what's another div!

We then do the same for the title and body.

The funny looking part at the end about cache was inspired by an article about Drupal block cache bubbling by PreviousNext. The specific code came from this Drupal.org issue. The PreviousNext article says to render the {{ content }} variable with our fields set to 'without', because without the {{ content }} variable rendering, caching is not working properly (I don't know enough about caching to explain more). However, on a content type with loads of fields, it's very cumbersome to add every field in with {{ content|without('field_image', 'field_tags', 'field_other', etc) }}. Instead, I put that {{ catch_cache = content|render }} at the bottom of each of my content patterns - node, block, paragraphs, etc, then don't need to add it later in Drupal.

The SCSS for this looks like this:

// Theming individual cards

.card {
  width: 100%;
  margin-bottom: $base-line-height;
  border-top: 0.5rem solid $c-primary;
  background-color: $c-grey--lighter;
}

a.card {
  text-decoration: none;
  &:focus,
  &:hover {
    background-color: $c-primary;
    background-image: linear-gradient($c-primary, darken($c-primary, 15%));
  }
}

.card--has-image {
  background-color: $c-white;
}

.card--small {
  @include breakpoint($bp--medium) {
    width: calc(33% - 2rem);
  }
}
.card--medium {
  @include breakpoint($bp--medium) {
    width: calc(50% - 2rem);
  }
}
.card--large {
  @include breakpoint($bp--medium) {
    width: calc(66% - 2rem);
  }
}
.card--full {
  @include breakpoint($bp--medium) {
    width: calc(100% - 2rem);
  }
}

.card--primary {
  border-top-color: $c-primary;
}
.card--secondary {
  border-top-color: $c-secondary;
}
.card--tertiary {
  border-top-color: $c-tertiary;
}
.card--quaternary {
  border-top-color: $c-quaternary;
}
.card--quinary {
  border-top-color: $c-quinary;
}
a.card--primary {
  &:focus,
  &:hover {
    background-color: $c-primary;
    background-image: linear-gradient($c-primary, darken($c-primary, 15%));
  }
}
a.card--secondary {
  &:focus,
  &:hover {
    background-color: $c-secondary;
    background-image: linear-gradient($c-secondary, darken($c-secondary, 15%));
    .card__text {
      color: $c-white;
    }
  }
}
a.card--tertiary {
  &:focus,
  &:hover {
    background-color: $c-tertiary;
    background-image: linear-gradient($c-tertiary, darken($c-tertiary, 15%));
    .card__title,
    .card__text {
      color: $c-white;
    }
  }
}
a.card--quaternary {
  &:focus,
  &:hover {
    background-color: $c-quaternary;
    background-image: linear-gradient($c-quaternary, darken($c-quaternary, 15%));
    .card__title,
    .card__text {
      color: $c-white;
    }
  }
}
a.card--quinary {
  &:focus,
  &:hover {
    background-color: $c-quinary;
    background-image: linear-gradient($c-quinary, darken($c-quinary, 15%));
    .card__text {
      color: $c-white;
    }
  }
}

.card__content {
  padding: $base-line-height;
}

.card__title {
  color: $c-grey--darker;
  font-family: $ff--alternate;
}

.card__image img {
  width: 100%;
  height: auto;
}

.card__text {
  color: $c-grey--dark;
  p:last-of-type {
    margin-bottom: 0;
  }
}

We can do the site building very easily with the paragraphs module. Create a paragraph of type card, add the fields

  1. Card Image - media image
  2. Card Title - text (plain)
  3. Card Body - text (long, formatted)
  4. Card Link URL - link
  5. Card Style (sets a colour for a border-top on the card) - text (list)
  6. Card Size (sets the width of the card) - text (list)

Then, in our paragraph--card.html.twig file, we write the following code:

{% if paragraph.field_p_card_style.value %}
  {% set card_style = paragraph.field_p_card_style.value %}
{% endif %}

{% if paragraph.field_p_card_size.value %}
  {% set card_size = paragraph.field_p_card_size.value %}
{% endif %}

{% if paragraph.field_p_card_link.value %}
  {% set card_link_url = content.field_p_card_link.0['#url'] %}
{% endif %}

{% if paragraph.field_p_card_image.value %}
  {% set card_image = content.field_p_card_image %}
{% endif %}

{% if paragraph.field_p_card_title.value %}
  {% set card_title = content.field_p_card_title %}
{% endif %}

{% if paragraph.field_p_card_text.value %}
  {% set card_text = content.field_p_card_text %}
{% endif %}

{% include "@building-blocks/card-layout/_card.twig" %}

What the above does is checks if the card paragraph has values in its fields and then sets variables if it does. This means we don't render empty divs.

You will also notice that I render each field's full content for image, title, and body. This is to keep all the Drupal goodness we have in the attributes object - for accessibility and to make sure things like contextual links/quick edit still work.

You will often see the same template written like this:

{% include "@building-blocks/card-layout/_card.twig"
  with {
    card_style = paragraph.field_p_card_style.value,
    card_size = paragraph.field_p_card_size.value,
    card_link_url = content.field_p_card_link.0['#url'],
    card_image = content.field_p_card_image,
    card_title = content.field_p_card_title,
    card_text = content.field_p_card_text
  }
%}

I find doing that leads to fields such as {{ content.field_image }} always returning true because of Drupal's rendering system. So, even if we don't have an image, we'll still have an image div, whereas doing an explicit check before we {% include %} our variable seems much safer.

That's it - PatternLab + Drupal integrated beautifully (I think) the "right" way (according to me - you might differ).

===

You can see a sample of this in action on this site's PatternLab.
Note - I call cards in my PatternLab 'tiles' and make them part of a wrapper component called 'Tiled Layout', which allows me lots of flexibility for cool layouts with little effort.

Filed Under:

  1. Drupal
  2. Drupal Planet
  3. PatternLab
  4. Design in Browser
  5. Frontend Development