Frontend Workflow and Drupal Content Delivery 

05/05/2018 - 14:30
Tassilo Gröper

DrupanCamp Transilvania
4-6 May 2018

www.wearewondrous.com

www.drupaltransylvania.ro

We are
WONDROUS

tassilogroeper

@pheadeaux

 

Full Stack Developer

WONDROUS Ltd

Swiss based
Digital Design Agency

www.wearewondrous.com

Tassilo Gröper

Overview

  1. What is the Road

  2. Fractal Styleguide Setup

  3. UI Patterns Setup

  4. Lessons learned

What is the Road?

GOal

Design,

develop

and deploy

independently

GOal and Catch

Faster implementation

vs.

more complex workflow

Goal

Create reusable components,

easy to customize

Goal

Low barrier

for new devs,

easy to adopt,

Easy to debug

... one more Thing

asset dependency management

 

Using Power of HTTP2

many small files,

rather than one big concat

Design Workflow

Zeplin

Zeplin using Variables

But Halt:

we Devs are still needed

Putting it together
is hard

E.g. Assembly ("marriage") of vehicles body and chassis

Fractal Styleguide Setup

Why Fractal?

  • Complete freedom of templating language

  • Integrate component library into whatever site

  • Component-focused workflow - living part of application

  • Preview data: hard coded,
    Faker generated or HTTP API calls

  • Local web server or static HTML

  • Customized theme or default theme

Compound components

├── components
│   └── blockquote
│   │   ├── blockquote.config.yml
│   │   ├── blockquote.twig
│   │   ├── fancy-quote.js
│   │   ├── README.md
│   │   └── styles.css

Configuration naming convention [component-name].config.{js|json|yml}

<blockquote>
    <p>{{text}}</p>
    <cite>{{citation}}</cite>
</blockquote>
title: "A simple blockquote component"
status: wip
context:
  text: "Blockquotes are the best!"
  citation: "Fractal Docs"
├── src
│   ├── components
│   │   └── alert.hbs
│   └── docs
│       └── index.md
├── fractal.js
└── package.json
// fractal.js
'use strict';

/* Create a new Fractal instance and export it for use elsewhere if required */
const fractal = module.exports = require('@frctl/fractal').create();

/* Set the title of the project */
fractal.set('project.title', 'FooCorp Component Library');

/* Tell Fractal where the components will live */
fractal.components.set('path', __dirname + '/src/components');

/* Tell Fractal where the documentation pages will live */
fractal.docs.set('path', __dirname + '/src/docs');
<!-- src/components/alert.hbs -->
<div class="alert">This is an alert!</div>
<!-- src/docs/index.md -->
---
title: FooCorp Components
---
This is the component library for FooCorp.
**Feel free to look around!**

Minimal

Fractal Setup

const pkg = require('./package.json');
const fractal = require('@frctl/fractal').create();
const twigAdapter = require('@wondrousllc/fractal-twig-drupal-adapter');
const twig = twigAdapter({
  handlePrefix: '@components/',
});

const paths = {
  build: `${__dirname}/docroot/tmp-build`, // exclude from git repo
  docs: `${__dirname}/docroot/themes/my_theme/docs`,
  components: `${__dirname}/docroot/themes/my_theme/components`,
  static: `${__dirname}/docroot/styleguide`, // optionally create via CI
};

fractal.set('project.title', pkg.name);
fractal.set('project.version', pkg.version);
fractal.set('project.author', pkg.author);

fractal.components.engine(twig);
fractal.components.set('default.preview', '@preview');
fractal.components.set('ext', '.twig');
fractal.components.set('path', paths.components);

fractal.docs.set('path', paths.docs);

fractal.web.set('static.path', paths.static);
fractal.web.set('builder.dest', paths.build);

module.exports = fractal;

FRACTAL.JS

Gulpfile.js
(1/2)

const fractal = require('./fractal.js');
const logger = fractal.cli.console; // keep a reference to the fractal CLI console utility
const gulp = require('gulp');

const paths = {
  theme: `${__dirname}/docroot/themes/my_theme`,
  modules: `${__dirname}/node_modules`,
  components: `${__dirname}/docroot/themes/my_theme/components`,
};

// Build static site
function build() {
  const builder = fractal.web.builder();
  builder.on('progress', (completed, total) => {
    logger.update(`Exported ${completed} of ${total} items`, 'info')
  });
  builder.on('error', err => logger.error(err.message));
  return builder.build().then(() => {
    logger.success('Fractal build completed!');
  });
}

// Serve dynamic site
function serve() {
  const server = fractal.web.server({sync: true});
  server.on('error', err => logger.error(err.message));
  return server.start().then(() => {
    logger.success(`Fractal server is now running at ${server.url}`);
  });
}
const sass_config = {
  includePaths: [
    `${paths.modules}`,    // node_modules for other libs like jQuery, Foundation, e.g
    `${paths.components}`,
  ],
};

function styles() {
  return gulp.src(`${paths.theme}/sass/*.scss`)
    .pipe(sourcemaps.init())
    .pipe(sass(sass_config).on('error', sass.logError))
    .pipe(sourcemaps.write('./'))
    .pipe(gulp.dest(`${paths.theme}/css`));
}

function watch() {
  serve();
  gulp.watch(`${paths.theme}/**/*.scss`, gulp.series(styles));
}

const compile = gulp.series(
  // icons,
  gulp.parallel(
    // scripts,
    styles,
  )
);

gulp.task('watch', gulp.series(compile, watch));
gulp.task('build', gulp.series(compile, build));
gulp.task('default', gulp.series(compile, serve));

Gulpfile.js
(2/2)

Example Compound

└── my_theme
    ├── components
    │   └── contact
    │       ├── contact.scss
    │       ├── contact.config.yml
    │       └── contact.twig
    ├── templates
    │   └── content
    │       └── node--contact.html.twig
    └── my_theme.info.yml

Fractal Twig template

{# themes/custom/my_theme/components/contact.twig #}
<div class="contact">
  {% if image is not empty %}
    <div class="contact__image">
      {{ image }}
    </div>
  {% endif %}
  <div class="contact__content">
    <h4 class="headline--semitransparent headline--uppercase">
      {{ name }}
    </h4>
    <p>
      {% for function in functions %}
        <strong>{{ function }}</strong><br>
      {% endfor %}
      {% for phone in phone %}
        <strong>{{ phone }}</strong><br>
      {% endfor %}
      {% for email in email %}
        <a href="mailto:{{ email }}">{{ email }}</a>
        {% if not loop.last %}<br>{% endif %}
      {% endfor %}
    </p>
  </div>
</div>
└── my_theme
    ├── components
    │   └── contact
    │       ├── contact.scss
    │       ├── contact.config.yml
    │       └── contact.twig
    ├── templates
    │   └── content
    │       └── node--contact.html.twig
    └── my_theme.info.yml
title: 'Contact Compound'
preview: '@preview'
status: 'wip'
context:
  tags:
    - 'sprint-1'
    - 'author:mark'
  name: 'Zechariah Boyle'
  image: '<img src=\https://nosrc.io/600x340/people4\>'
  functions: 
    - 'Spencer - Conroy'
    - 'Stokes LLC'
  phone: 
    - 214-579-6134
    - 1-348-685-3816 x3967
  email: 
    - Ralph_Dickinson35@gmail.com
    - Nina_Graham@hotmail.com
  

Integration

$ npm install --save @wondrousllc/fractal-twig-drupal-adapter
$ composer require drupal/components drupal/twig_field_value drupal/twig_extender
$ cd web
$ drush -y en components twig_field_value twig_extender

Component Library

# my_theme.info.yml
component-libraries:
  components:
    paths:
      - components
{# themes/custom/my_theme/templates/page.html.twig #}
{% include "@components/box/box.twig" with {
    'url': content.field_link_url
    'content': content.field_link_text
} %}
{# themes/custom/my_theme/components/box/box.twig #}
<a href="{{ url }}">
  {{ content }}
</a>
└── themes/custom
    └── my_theme
        ├── components
        │   └── box
        │       └── box.twig
        ├── templates
        │   └── page.html.twig
        └── my_theme.info.yml

Host Template Pattern

The Fallback inculde

Drupal include

{# themes/custom/my_theme/templates/content/node--contact.html.twig #}

{% include '@components/contact/contact.twig' with {
  'contact': {
    'name': label['#items'].getString,
    'functions': content.field_functions|field_value,
    'phone': content.field_phones|field_value,
    'email': content.field_emails|field_value,
    'image': content.field_image
  }
} %}
// contact.scss
.contact {
  border-bottom: calc-rem(1px) solid rgba($color-onyx, .2);
  display: flex;
  padding: 1em 0;
  .stripe--color--black & {
    border-bottom-color: rgba($color-snow, .2);
  }
}
.contact__image {
  flex-shrink: 0;
  width: 50%;
}
.contact__content {
  overflow: auto;
  overflow-wrap: break-word;
  .contact__image + & {
    margin-left: 1.25em;
  }
  p {
    font-size: .7em;
    line-height: 1.5;
  }
}
└── my_theme
    ├── components
    │   └── contact
    │       ├── contact.scss
    │       ├── contact.config.yml
    │       └── contact.twig
    ├── templates
    │   └── content
    │       └── node--contact.html.twig
    └── my_theme.info.yml
{ kint(name['#items'].getString) }}
{{ kint(name|field_value) }}
{% set catch_cache = name|render %}
{{ kint(catch_cache) }}

Rendering
values

Twig Extender 1/2

{# themes/custom/my_theme/templates/page/page.html.twig #}

{% include '@components/functional/top-bar/top-bar.twig' with {
  'logo': block_view('sitebranding'),
  'breadcrumbs': block_view('breadcrumbs'),
  'is_front': is_front
} only %}
└── my_theme
    ├── components
    │   └── functional
    │       └── top-bar
    │           ├── _top-bar.scss
    │           ├── top-bar.config.json
    │           └── top-bar.twig
    ├── templates
    │   └── page
    │       └── page.html.twig
    └── my_theme.info.yml

using blocks

{# themes/custom/my_theme/templates/fields/field--downloads.html.twig #}

{% extends '@components/functional/download/download--list.twig' %}
{% block content %}
  {% for item in items %}
    <li>
      {{ item.content }}
    </li>
  {% endfor %}
{% endblock %}
└── my_theme
    ├── components
    │   └── functional
    │       └── download
    │           ├── _download.scss
    │           ├── download.config.yml
    │           └── download.twig
    │           └── download--list.twig
    ├── templates
    │   └── fields
    │       └── field--downloads.html.twig
    └── my_theme.info.yml

Used in Field

Reminder:
Host template Pattern

Twig Extender 2/2

{# themes/custom/my_theme/templates/paragraphs/paragraph--downloads.html.twig #}

{% extends '@components/functional/download/download--list.twig' %}
{% block content %}
  {% for item in content.field_downloads|children %}
    <li>
      {{ item }}
    </li>
  {% endfor %}
{% endblock %}
└── my_theme
    ├── components
    │   └── functional
    │       └── download
    │           ├── _download.scss
    │           ├── download.config.yml
    │           └── download.twig
    │           └── download--list.twig
    ├── templates
    │   └── paragraphs
    │       └── paragraph--downloads.html.twig
    └── my_theme.info.yml

using Children FILTER

Reminder:
Host template Pattern

Working with Lists

{# themes/custom/my_theme/components/functional/download/download--list.twig #}

<section class="download--list-wrapper">
  <header>
    <h2>{{ 'Downloads'|t }}</h2>
  </header>
  <div class="row">
    <ul class="download--list">
      {% block content %}
        <li>
          {% include '@components/functional/download/download' %}
        </li>
        <li>
          {% include '@components/functional/download/download' %}
        </li>
      {% endblock %}
    </ul>
  </div>
</section>
{# general list structure  ../components/functional/component/component--list.twig #}

<section class="component--wrapper">
  <header>
    <h2>{{ 'Component Title'|t }}</h2>
  </header>
  <div class="component--row">
    <ul class="component--list">
      {% block content %}
        <li>
          {% include '@components/functional/component/component.twig' %}
        </li>
      {% endblock %}
    </ul>
  </div>
  <footer>
    {{ more_link }}
  </footer>
</section>

Fractal Data

title: 'Download'
label: 'Download'
preview: '@preview'
context:
  url: 'http://placeimg.com/480/480/people'
  title: 'Test Download'
  file_size: '200 kB'
  description: >
    <p>Ded pfenningguat dei hinter’m Berg san a no Leit dahoam, schüds nei gschmeidig.</p>

Fractal Component

<article {{ attributes }}>
  <a href="{{ url }}" class="download--row" target="_blank" download="">
    <aside class="file-type-indicator">
      <i class="icon--file-type-pdf"></i>
    </aside>

    <div class="description">
      <h4 class="is-h6">
        {{ title }}
      </h4>
      {{ description }}
    </div>

    <footer class="">
      <p>
        {{ file_size }}
      </p>
    </footer>
  </a>
</article>

UI Patterns
to the Rescue

UI Patterns

Use patterns as layouts

Tab:
Manage Display

E.g. Media Document

UI Pattern Config 

download_row:
  use: "@components/functional/download/download--row.twig"
  label: 'Download Row'
  description: Display a Download in row mode.
  fields:
    url:
      type: text
      label: Url
      description: Link to the file
      preview: 'www.test.com/test.pdf'
    title:
      type: text
      label: Title
      description: Title.
      preview: Test Title
    description:
      type: text
      label: Description
      description: Description
      preview: '<p>Test Description</p>'
└── my_theme
    ├── components
    │   └── functional
    │       └── download
    │           ├── _download.scss
    │           ├── download.config.yml
    │           └── download.twig
    │           ├── download.ui_patterns.yml
    │           └── download--list.twig
    ├── templates
    │   └── paragraphs
    │       └── paragraph--downloads.html.twig
    └── my_theme.info.yml

E.g. Media Document

For File Size
we cheat with a
pre
process hook

// themes/custom/my_theme/my_theme.theme

/**
 * Add Url to the pattern template.
 *
 * @param $variables
 *
 * @throws \Drupal\Core\Entity\EntityMalformedException
 */
function my_theme_preprocess_pattern_download_row(&$variables) {
  /** @var \Drupal\ui_patterns\Element\PatternContext $context */
  $context = $variables['context'];
  /** @var \Drupal\media\MediaInterface $entity */
  if ($entity = $context->getProperty('entity')) {
    $fileEntity = $entity->get('field_media_file')->entity;
    $variables['file_size'] = format_size($fileEntity->getSize());
  }
}

Two Usage
Szenarios

directly in
Drupal-template

aka.
Host Template

Called BY
View Mode

Ui Patterns
Layouts

... and there is
a Third way

Drupal Views
HTML Structure
Calling a view Mode

Remember this:

{# themes/custom/my_theme/components/functional/download/download--list.twig #}

<section class="download--list-wrapper">
  <header>
    <h2>{{ 'Downloads'|t }}</h2>
  </header>
  <div class="row">
    <ul class="download--list">
      {% block content %}
        <li>
          {% include '@components/functional/download/download' %}
        </li>
        <li>
          {% include '@components/functional/download/download' %}
        </li>
      {% endblock %}
    </ul>
  </div>
</section>

Since Drupal produces the "same" HTML structure

Views List
UL > LI*

Views Exposed Filter

View Mode
Row

Lessons

learned

#1

 

Use All
three
options

  • Host template Pattern

  • UI Patterns Layout

  • Follow Drupal HTML Structure

#2

 

Don't Reinvent
Drupal

Menus, Breadcrumbs, Pagination etc.
are better mocked in the Styleguide
than reimplemented

#3

 

It is only an
intermediate
Step

GraphQL (headless)
will bring
Good and Bad

#4

 

Use Drupal
Image
Processing

<picture>
    <!--[if IE 9]><video style="display: none;"><![endif]-->
  <source srcset="/sites/default/files/styles/4by3_large/public/uploads/2018-04/I-Bruecke-Kunstbauten.jpg?h=53f566ca&amp;itok=Ht2Vtmz0 1x, /sites/default/files/styles/4by3_xlarge/public/uploads/2018-04/I-Bruecke-Kunstbauten.jpg?h=53f566ca&amp;itok=Ux6ufRNK 2x" media="screen and (min-width: 46.25em)" type="image/jpeg">
  <source srcset="/sites/default/files/styles/16by9_large/public/uploads/2018-04/I-Bruecke-Kunstbauten.jpg?h=bc60f4d4&amp;itok=UylO_ucA 1x, /sites/default/files/styles/16by9_xlarge/public/uploads/2018-04/I-Bruecke-Kunstbauten.jpg?h=bc60f4d4&amp;itok=jonNtnK- 2x" media="screen and (min-width: 33.75em)" type="image/jpeg">
  <source srcset="/sites/default/files/styles/16by9_medium/public/uploads/2018-04/I-Bruecke-Kunstbauten.jpg?h=bc60f4d4&amp;itok=3KAPMOTo 1x, /sites/default/files/styles/16by9_large/public/uploads/2018-04/I-Bruecke-Kunstbauten.jpg?h=bc60f4d4&amp;itok=UylO_ucA 2x, /sites/default/files/styles/16by9_xlarge/public/uploads/2018-04/I-Bruecke-Kunstbauten.jpg?h=bc60f4d4&amp;itok=jonNtnK- 3x" media="screen and (min-width: 22.5em)" type="image/jpeg">
  <source srcset="/sites/default/files/styles/1by1_small/public/uploads/2018-04/I-Bruecke-Kunstbauten.jpg?h=23b30eb9&amp;itok=ibQlyHJU 1x, /sites/default/files/styles/1by1_medium/public/uploads/2018-04/I-Bruecke-Kunstbauten.jpg?h=23b30eb9&amp;itok=DR_P_JdQ 2x" media="screen and (min-width: 0em)" type="image/jpeg">
<!--[if IE 9]></video><![endif]-->
<img src="/sites/default/files/styles/4by3_medium/public/uploads/2018-04/I-Bruecke-Kunstbauten.jpg?h=53f566ca&amp;itok=9sknwFCk" alt="Bild Kompetenzfeld Areal und Umwelt" typeof="foaf:Image">
</picture>
<img src="{{ img_src }}" alt="{{ alt }}" typeof="foaf:Image">

Do this:

Instead of this:

#5

 

Dont't use it
working
alone

  • It is a team workflow

  • Too complex for a single dev

  • Not a low hanging fruit

Going Further

Thank
You.

@WondrousInc

hello@wondrous.ch

Frontend Workflow and Drupal Content Delivery

By WONDROUS LLC

Frontend Workflow and Drupal Content Delivery

DrupalCamp Transylvania 05/05/2018

  • 403

More from WONDROUS LLC