Theming Components with D8 and Fractal

Tassilo Gröper

Thanks

Overview

  1. Decoupled workflow & agile

  2. SC5

  3. Fractal

  4. Integration with D8

  5. Discussion

Decoupeling the Workflow

Design

 

Prototyping

 

Development

 

Maintenance

 

Reusabiltiy

living Styleguide will help!

everybody wants to be agile

why change

a running the current

system?

  • Waterfall workflow

  • Black Box of development, for

    • clients

    • financial goals

    • timing milestones

  • Iteration over Designs is hard

  • Can not be agile

The Wins of
Decoupled and Agile

Change

becomes
less effort

  • while prototyping

  • while development

  • while maintenance

The Wins of
Decoupled and Agile

Involve
the hole team from the beginning

  • spot hard tasks early

  • identify financial problems

The Wins of
Decoupled and Agile

Better communication
with clients

  • common knowledge base

  • common visual language

.. oh, and by the way:


responsive

Designs

Lessons
learned

using a
living Styleguide

SC5 STYLEGUIDE GENERATOR

Style guide is based on KSS , spiced up with some very useful features and a nifty UI built with AngularJS. It supports SASS , LESS , PostCSS  styles as well as pure CSS style sheets.

KSS
Example

// Box
//
// This class defined the style of a box link.
//
// .bottom-box - create footer box styling
// .engagement-box - create content box styling
//
// markup:
// <li class="{$modifiers}">
//   <h2><a href="">Heading</a></h2>
//   <p>Lorem ipsum dolor sit amet</p>
//   <a href="" class="button">Link button</a>
// </li>
//
// sg-wrapper:
// <ul class="box-list">
//  <sg-wrapper-content/>
// </ul>
//
// Styleguide 6.1
@mixin box-base {
  .button { @include button-base; }
}
@mixin box-large {
  h2 { @include header-lvl1-style; }
  .button { @include button-large; }
}
@if $generate-styleguide-classes {
  .bottom-box { @include box-base; }
  .engagement-box { @include box-base; @include box-large; }
}

Pros and Cons

  • KSS annotations in SASS

  • SASS Mixin pattern

  • Static file generator

  • Node based, no CMS setup

  • Using the Shadow DOM

  • Designer and Frontender start before
     backend implementation

  • Focus on Variables & Components

    • parallel content production and Backend implementation

Putting it together
is hard

Example: Assembly ("marriage") of vehicles body and chassis

#1

 

CMS demands
differ from
Frontend demands

quick edit or other backend functionalities;

additional container;

accessibility topics mostly addressed by the CMS

#2

 

CMS structures

frontend code

common understanding needed of "Drupal speak"

(e.g. blocks, views, nodes, view_modes)

#3

 

frontend is NEVER

as independent

as implied

Always keep in mind the application layer

#4

 

Maintenance

will

mess up things

E.g. using KSS annotation for templates is cumbersome and rarely up to date

we are
still looking
for the best
solution

Current Developments

Why Fractal?

  • Complete freedom of templating language

  • Integrate component library into whatever site

  • Component-focussed workflow - living part of application

  • Preview data: hardcoded, Faker generated or HTTP API calls

  • Local web server or static HTML

  • Customized theme or default theme

  • created by Clearleft

├── 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

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"

Example
*.config.js


module.exports = {
    title: "Amazing Mega Buttons",
    status: "prototype",
    tags: ['sprint-1', 'author:mark'],
    preview: '@preview-layout',
    context: {
        "button-text": "Click me!",
        "is-sparkly": true
    },
    variants: [{
        name: 'large',
        notes: 'Only use this when you need a really big button!',
        context: {
            modifier: 'is-large'
        }
    },{
        name: 'warning',
        status: 'wip',
        context: {
            modifier: 'is-warning',
            button-text: 'Do not click'
        }
    }]
};

Integrating Fractal
into D8

Component Library

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

Fractal Compound
Handles

# my_theme.info.yml
component-libraries:
  components:
    paths:
      - components
{# my_theme/templates/page.html.twig #}
{% include "@components/box" with {
    'url': content.field_link_url
    'content': content.field_link_text
} %}

{# optionally #}
{% include "@components/box/box.twig" with {
    'url': content.field_link_url
    'content': content.field_link_text
} %}
{# my_theme/components/box/box.twig #}
<a href="{{ url }}">
  {{ content }}
</a>
└── my_theme
    ├── components
    │   └── box
    │       └── box.twig
    ├── templates
    │   └── page.html.twig
    └── my_theme.info.yml
{
  "name": "Fractal Drupal 8",
  "version": "1.0.0",
  "description": "A Fractal Drupal 8 integration",
  "author": "WONDROUS LLC <hello@wondrous.ch> (https://www.wearewondrous.ch)",
  "dependencies": {
    "gulp": "gulpjs/gulp#4.0",
    "gulp-sass": "latest",
    "node-sass": "latest",
    "@frctl/fractal": "latest",
    "@frctl/twig": "https://github.com/WondrousLLC/twig-drupal.git",
    "faker": "latest"
  }
}

PACKAGE.JSON

const pkg = require('./package.json');
const fractal = require('@frctl/fractal').create();
const twigAdapter = require('@frctl/twig');
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.js
    │       └── contact.twig
    ├── templates
    │   └── content
    │       └── node--contact.html.twig
    └── my_theme.info.yml

Fractal Context Config

// contact.config.js
const faker = require('faker');

module.exports = {
  title: 'Contact',
  label: 'Contact',
  status: 'ready',
  preview: '@preview',
  context: {
    contact: {
      name: faker.name.findName(),
      image: 'https://nosrc.io/600x340/people4',
      functions: [
        faker.company.companyName(),
        faker.company.companyName()
      ],
      phone: [
        faker.phone.phoneNumber(),
        faker.phone.phoneNumber()
      ],
      email: [
        faker.internet.email(),
        faker.internet.email()
      ],
    }
  }
};
{
  "contact": {
    "name": "Zechariah Boyle",
    "image": "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"
    ]
  }
}
└── my_theme
    ├── components
    │   └── contact
    │       ├── contact.scss
    │       ├── contact.config.js
    │       └── contact.twig
    ├── templates
    │   └── content
    │       └── node--contact.html.twig
    └── my_theme.info.yml

Fractal Twig template

{# contact.twig #}
<div class="contact">
  {% if contact.image is not empty %}
    <div class="contact__image">
      <img src="{{ contact.image }}" alt="{{ contact.name }}">
    </div>
  {% endif %}
  <div class="contact__content">
    <h4 class="headline--semitransparent headline--uppercase">
      {{ contact.name }}
    </h4>
    <p>
      {% for function in contact.functions %}
        <strong>{{ function }}</strong><br>
      {% endfor %}
      {% for phone in contact.phone %}
        <strong>{{ phone }}</strong><br>
      {% endfor %}
      {% for email in contact.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.js
    │       └── contact.twig
    ├── templates
    │   └── content
    │       └── node--contact.html.twig
    └── my_theme.info.yml
{
  "contact": {
    "name": "Zechariah Boyle",
    "image": "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"
    ]
  }
}

Drupal include

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

{% include '@components/contact' 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|field_value|e|striptags|trim
  }
} %}
└── my_theme
    ├── components
    │   └── contact
    │       ├── contact.scss
    │       ├── contact.config.js
    │       └── contact.twig
    ├── templates
    │   └── content
    │       └── node--contact.html.twig
    └── my_theme.info.yml
// 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;
  }
}
{# Get the value as plain string. Separated by comma for lists. #}
{{ label['#items'].getString }}

{# Create plain text render array #}
{{ content.field_email|field_value }}

{# Get rid of theme suggestions if only url is rendered. #}
{{ content.field_image|field_value|e|striptags|trim }}

RENDERING
VALUES

{ kint(name['#items'].getString) }}
{{ kint(name|field_value) }}
{% set catch_cache = name|render %}
{{ kint(catch_cache) }}

Rendering
values

{# my_theme/component/overview-link/overview-link.twig #}
<a href="{{ url('view.frontpage.page_1') }}">{{ 'View all content'|t }}</a>
{# my_theme/component/overview-link/overview-link.config.yml #}
title: Overview Link
label: Overview Link
status: 'ready'
preview: '@preview'
context:
  url: '#'
  url_text: 'View all content'


{# my_theme/templates/node--article.html.twig #}
{% include '@components/link' with {
    'url': url('view.frontpage.page_1')
    'url_text': 'View all content'|t
  }
}%}


{# my_theme/component/overview-link/overview-link.twig #}
<a href="{{ url }}">{{ url_text }}</a>

vs.

Wrapping
it up

Wrapping up

It's the workflow, not the tools

Decouppeling is possible
but at what cost

Wrapping up

Soft or rigid separation
between
styleguide and CMS

Over time it
will become blurry

Wrapping up

Breaking
what is
already there?

Theme Suggestions,
Inline Editing,
Quick Editing

Wrapping up

Extend?

Reuse?

Cache?

Open Questions

Thanks!

What is your Opinion?

http://bit.ly/2ncqX6o

 

wondrous.slides.com/wondrousllc/
theming-components-with-d8-and-fractal

Theming Components with D8 and Fractal

By WONDROUS LLC

Theming Components with D8 and Fractal

  • 469

More from WONDROUS LLC