Nov 06 2018
Nov 06

Pattern Lab (PL), a commonly known pattern library, is an open-source project to generate a design system for your site. In the last two years it has gotten a lot of attention in the Drupal community. It's a great way to implement a design system into your front-end workflow.

The following post describes how our client (the City and County of San Francisco) began to implement a pattern library that will eventually be expanded upon and re-used for other agency websites across the SF.gov ecosystem.

USWDS.

Using the U.S. Web Design System (USWDS), until their own pattern library was ready for prime time, was a client requirement.

USWDS uses a pattern library system called Fractal. I think Fractal is a great idea, but it lacked support for Twig, Twig is the template engine used by Drupal. Fractal out of the box uses Handlebars (templating engine in JavaScript), and thought the template language in Fractal can be customized I wasn’t able to make it work with Twig macros and iterations even with the use of twig.js

Creating the Pattern Lab

Ultimately, I decided to start from scratch. In addition to the USWDS requirement, the client also needed to be able to reuse this pattern library on other projects. I used the Pattern Lab Standard Edition for Twig, among other things this means that you need PHP in the command line in order to "compile" or generate the pattern library.

I added a gulpfile that was in charge of watching for changes in the PL source folders. Once a Twig, Sass or JavaScript file was changed, the pattern library was re-generated.

Generating the Pattern Library

I also needed Gulp to watch the file changes.

The following is a SIMPLE example of the Gulp task that generates the PL watching the folders. the following code snippet shows the config object containing an array of the folder directories.


{
  "css": {
    "file" : "src/sass/_all.scss",
    "src": [
      "pattern-lab/source/_patterns/*.scss",
      "pattern-lab/source/_patterns/**/*.scss",
      "pattern-lab/source/scss/*.scss"
    ],
    "pattern_lab_destination": "pattern-lab/public/css",
    "dist_folder": "dist/css"
  },
  "js": {
    "src": [
      "pattern-lab/source/js/*.js",
      "pattern-lab/source/js/**/*.js"
    ]
  }
}

And the following one is a common watcher in gulp:


gulp.task('watch', function () {
    gulp.watch(config.js.src, ['legacy:js']);
    gulp.watch(config.css.src, ['pl:css']);
    gulp.watch(config.pattern_lab.src, ['generate:pl']);
    gulp.watch(config.pattern_lab.javascript.src, ['generate:pl']);
});


The following task is in charge of generating the pattern library with PHP:


gulp.task('pl:php', shell.task('php pattern-lab/core/console --generate'));

Please NOTE that this is an oversimplified example.

Sass

Having generated the Pattern Library, I figured out that in order to use this Pattern Lab into my Drupal theme, I needed to generate a single CSS file and single JavaScript (JS) file.
The main Sass file imports all Sass code from USWDS by using the `@import` statement.
I imported the source Sass code from USWDS which I required with npm and imported  the source file directly from the node_modules folder:



//pattern-lab/source/scss/components.scss

// Styles basic HTML elements
@import '../../../node_modules/uswds/src/stylesheets/elements/buttons';
@import '../../../node_modules/uswds/src/stylesheets/elements/embed';

Then I imported the scss files that were inside my pattern elements:

// Styles inside patterns.
@import "../_patterns/00-protons/*.scss";
@import "../_patterns/01-atoms/**/*.scss";
@import "../_patterns/02-molecules/**/*.scss";
@import "../_patterns/03-organisms/**/*.scss";
@import "../_patterns/04-templates/**/*.scss";
@import "../_patterns/05-pages/**/*.scss";


All the styles were dumped into a single file called components.css

Having this single CSS file created I was able to use USWDS CSS classes along with the new ones.
I had to add a /dist folder where the transpiled Sass would live and be committed for later use in the Drupal theme.

JavaScript

I did something similar for JavaScript. The biggest challenge was to compile the USWDS JavaScript files exactly as they were. I resorted to copying all the source for the JavaScript into the src folder of the pattern library and set a watcher specifically for the USWDS JavaScript, and added another watcher for the new Pattern Lab JavaScript:

Example:

In the following example I compile all the JS that lives inside the components into a single file.

Then the resulting file is copied  to: ./pattern-lab/public/js which is the folder that reads the Pattern Lab when working on Pattern Lab only.
The other copy of the file goes to the distribution folder ./dist/pl/js which is the one I use in my Drupal theme.


// Component JS.
// -------------------------------------------------------------------- //
// The following task concatenates all the JavaScript files inside the
// _patterns folder, if new patterns need to be added the config.json array
// needs to be edited to watch for more folders.

gulp.task('pl:js', () => {
    return gulp.src(config.pattern_lab.javascript.src)
        .pipe(sourcemaps.init())
        .pipe(babel({
            presets: ['es2015']
        }))
        .pipe(concat("components.js"))
        .pipe(sourcemaps.write())
        .pipe(gulp.dest('./pattern-lab/public/js'))
        .pipe(gulp.dest('./dist/pl/js'));
});

The resulting files:


--/dist/
--/dist/css/components.css
--/dist/js/components.js

Were included the HEAD of my Pattern Lab by editing pattern-lab/source/_meta/_00-head.twig 

I included the following lines:


<link rel="stylesheet" href="https://www.chapterthree.com/blog/decoupling-pattern-lab-from-your-theme-a-city-of-san-francisco-project/../../css/components.css" media="all">
<script src="https://www.chapterthree.com/blog/decoupling-pattern-lab-from-your-theme-a-city-of-san-francisco-project/../../js/dist/uswds.min.js"></script>
<script src="https://www.chapterthree.com/blog/decoupling-pattern-lab-from-your-theme-a-city-of-san-francisco-project/../../js/dist/components.js"></script>

Please refer to the repo if you need the details of the integration: GitHub - SFDigitalServices/sfgov-pattern-lab: SFGOV Pattern Lab

Integrating the Pattern Lab with Drupal.

Composer and libraries:

I used the following plugin:


composer require oomphinc/composer-installers-extender

This plugin allowed me to put the pattern library in a folder different than vendor
Then I added some configuration to the composer.json

Under extra I specified where composer should install the repository of type github:


"extra": {
        "installer-paths": {
            "web/libraries/{$name}": ["type:github"],
          }

Then under repositories I set the type:github


"repositories": {
        "github": {
            "type": "package",
            "package": {
                "name": "sf-digital-services/sfgov-pattern-lab",
                "version": "master",
                "type": "drupal-library",
                "source": {
                    "url": "https://github.com/SFDigitalServices/sfgov-pattern-lab.git",
                    "type": "git",
                    "reference": "master"
                }
            }
        }
    }

and required the package under require: As you can see the name matches the name in the previously declared github repo:


"require": {
   "sf-digital-services/sfgov-pattern-lab": "dev-master",
}

A composer update should clone the github repo and place the Pattern Lab inside relative to the Drupal web folder:

/web/libraries/sfgov-pattern-lab

Components Libraries

The Component Libraries module was especially important because it allowed me to map the Pattern Lab components easily into my theme.

Then I had to map my Pattern Lab components with the Drupal theme:

The Drupal Theme:

I created a standard Drupal theme:

The sfgovpl.info.yml file:

In the following part of the sfgovpl.info.yml file I connected the Pattern Lab Twig files to Drupal:


component-libraries:
  protons:
    paths:
      - ../../../libraries/sfgov-pattern-lab/pattern-lab/source/_patterns/00-protons
  atoms:
    paths:
      - ../../../libraries/sfgov-pattern-lab/pattern-lab/source/_patterns/01-atoms
  molecules:
    paths:
      - ../../../libraries/sfgov-pattern-lab/pattern-lab/source/_patterns/02-molecules
  organisms:
    paths:
      - ../../../libraries/sfgov-pattern-lab/pattern-lab/source/_patterns/03-organisms
  templates:
    paths:
      - ../../../libraries/sfgov-pattern-lab/pattern-lab/source/_patterns/04-templates
  pages:
    paths:
      - ../../../libraries/sfgov-pattern-lab/pattern-lab/source/_patterns/05-pages

libraries:
  - sfgovpl/sfgov-pattern-lab

The sfgovpl.libraries.yml file:

In the last line of the previous code example, you can see that I required sfgov-pattern-lab library,  which will include the files compiled by Gulp into my Pattern Lab.


sfgov-pattern-lab:
  css:
    base:
      '/libraries/sfgov-pattern-lab/dist/css/components.css': {}
  js:
    '/libraries/sfgov-pattern-lab/dist/pl/js/components.js': {}

Using the Twig templates in our theme:

The following is an example of how to use a molecule from the pattern library into the Drupal theme:

You can include the @molecules/08-search-results/03-topic-search-result.twig twig like this:

Pattern Lab twig:

node--topic--search-index.html.twig


<div class="topic-search-result">
<div class="topic-search-result--container">
<div class="content-type"><i class="sfgov-icon-filefilled"></i><span>{{ content_type }}</span></div>
<a class="title-url" href="https://www.chapterthree.com/blog/decoupling-pattern-lab-from-your-theme-a-city-of-san-francisco-project/{{ url }}"><h4>{{ title }}</h4></a>
<p class="body">{{ body|striptags('<a>')|raw }}</p>
</div>
</div>

Drupal template:

The following example calls the Pattern lab molecule originally located at: web/libraries/sfgov-pattern-lab/pattern-lab/source/_patterns/02-molecules/08-search-results/03-topic-search-result.twig but thanks to the Components module we just call it as: @molecules/08-search-results/03-topic-search-result.twig


{# Set variables to use in the component. #}
{% set url = path('entity.node.canonical', {'node': elements['#node'].id()  }) %}
{% set description = node.get('field_description').getValue()[0]['value'] %}
{% set type = node.type.entity.label %} {# content type #}
{# Icluding the molecule in our Pattern Lab.#}

{% include "@molecules/08-search-results/03-topic-search-result.twig" with {
  "content_type": type,
  "url": url,
  "title": elements['#node'].get('title').getString(),
  "body": description
} %}

Recommendations

SFGOV Pattern Lab, Initial development was made in large part for Chapter Three and this post is intended to show the core concepts of decoupling your Pattern Lab from your Drupal theme.

You can find the full code implementation for the Pattern library and Drupal in the following Urls:

SFDigitalServices/sfgov and the Pattern Lab here: SFDigitalServices/sfgov-pattern-lab

You should try the value module, it is great for extracting values in the Drupal twig templates and connect them with your Pattern Lab twig templates.

Give a try to the UI Patterns module, looks promising and a great solution for decoupled Pattern Libraries.

Aug 07 2016
Aug 07

(This module was tested on drupal 8.1.8)

Twig Extensions, Views and Drupal 8

Problem to solve

Sometimes working with Views can be a challenge, in some cases you want to modify the values or process the content of the field.

Many developers use the preprocess functions, or alter the query of the view, however there is another option: twig filters.

The Goals:

Being able to calculate the reading time of any given node (body field).

The field needs to output: "5 min read", "9 min read", depending on the word count of the node.

Twig Filters:

Twig filters give us the ability to modify variables on twig template.

{% set salutevariable = "Hello Friend" %}"

If we output the variable {{ salutevariable }} is going to show: Hello Friend

If we want to make the variable lowercase, we use a filter.

{{ salutevariable | lower }} the "|" indicates that we are going to use a filter in our case "lower", so the value "Hello Friend" is going to be parsed by the filter lower.

The output will be: hello friend

Official Documentation:

Creating A Module:

Create a new module in: modules/custom/

Our module is going to have the following structure:

|readingtime/
|-- readingtime.info.yml
|-- readingtime.services.yml
|-- src/TwigExtension/ReadingTimeExtension.php

readingtime.info.yml contains:

name: readingtime
type: module
description: Provides a Twig filter that calculates the time of reading for any given string.
core: 8.x
package: Custom

Create a services file: readingtime.services.yml

services:
  readingtime.twig_extension:
    class: Drupal\readingtime\TwigExtension\ReadingTimeExtension
    tags:
      - { name: twig.extension }

The ReadingTimeExtension.php will contain the following:

<?php

namespace Drupal\readingtime\TwigExtension;

use Drupal\Core\Render\Markup;

class ReadingTimeExtension extends \Twig_Extension {
  /**
   * Here is where we declare our new filter.
   * @return array
   */
  public function getFilters() {
    return array(
      'readingtime' => new \Twig_Filter_Function(
        array('Drupal\readingtime\TwigExtension\ReadingTimeExtension', 'readingTimeFilter') // Here we are self referencing the function we use to filter the string value.
      )
    );
  }

  /**
   * This is the same name we used on the services.yml file
   * @return string
   */
  public function getName() {
    return "readingtime.twig_extension";
  }

  /**
   * @param $string
   * @return float
   */
  public static function readingTimeFilter($string) {

    if($string instanceof Markup) { // we check if the $string is an instance of Markup Object.

      // strip_tags help us to remove all the HTML markup.

      // if $string is an instance of Markup we use the method __toString() to convert it to string.

      $striped_markup = strip_tags($string->__toString()); 

      // On avarage we read 130 words per minute.

      // the PHP function str_word_count counts the number of words of any given string.

      $wpm = str_word_count($striped_markup)/130; // $wpm = Words Per Minute.

      $reading_time = ceil($wpm); // Round the float number to an integer.

      return $reading_time; // we return an integer with the number of minutes we need to read something.

    } else {

      return $string;

    }

  }

}

Testing our brand new filter:

Now we install our new module in admin/modules.

It's also helpful to generate dummy content with the devel module.

The View:

Then I proceed to create a simple view of articles:

View configuration:

FORMAT:

show: fields

FIELDS:

Content: Title
Content: Body ( this is the one we are going to apply our new twig filter )
Content: Body

We access the configuration of the second Content: Body

and under the REWRITE RESULTS option we are going to override the output of the field: 

overriding field views drupal 8

and the result is the following:

twig filters views result

Dec 14 2015
Dec 14

My installation settings:

  • Drupal 7
  • WYSIWYG Version 7.x-2.2+73-dev
  • ckeditor 4.5.1

Client requirements.

I created a simple CKeditor plugin that allows non-technical writers to change the text of a URL only by selecting (partially) the link and editing the content in a prompt.

This small plugin allows the end users to avoid typos or deleting the URL by mistake.

In order to make this plugin work in Drupal, I needed to create a plugin for the WYSIWYG Drupal module (the Drupal way).

Plugin demo:

The Drupal 7 Module:

Drupal 7 module structure:

 - ckeditor_linkchange/ <- Drupal 7 Module folder.
    - plugins/
        - linkchange/ <- Actual ckeditor plugin.
    - ckeditor_linkchange.info <- Drupal 7 info file.
    - ckeditor_linkchange.module <- Drupal 7 module file.

ckeditor_linkchange.info

This file declares our module and its dependencies:

name = "Ckeditor linkchange plugin"
description = "Adds the ability to change the text or a url with the linkchange button (CKEditor)"
package = Custom
dependencies[] = wysiwyg
core = 7.x

ckeditor_linkchange.module

The `hook_wysiwyg_plugin()` lets the WYSIWYG module know that we have a new plugin available.

 <?php
    
/**
 * Implements hook_wysiwyg_plugin().
 *
 */
function ckeditor_linkchange_wysiwyg_plugin($editor, $version) {
    $plugins = array();
    switch ($editor) {
        case 'ckeditor':
            if ($version > 4) {
                $plugins['linkchange'] = array(
                    'path' => drupal_get_path('module', 'ckeditor_linkchange') . '/plugins/linkchange',
                    'filename' => 'plugin.js',
                    'load' => TRUE,
                    'buttons' => array(
                        'linkchange' => t('Linkchange'),
                    ),
                );
            }
            break;
    }
    return $plugins;
}

In my case I checked that the ckeditor version is superior to version number 4.

the array $plugins['linkchange'] contains the path to the plugin and the buttons parameter which allows this plugin to be enabled from the WYSIWYG UI (admin/config/content/wysiwyg/).

The CKeditor plugin:

 - linkchange/
    - dialogs/
        - linkchange.js
    - icons/
        - linkchange.png    
    - plugin.js

plugin.js

 (function($) {
    CKEDITOR.plugins.add( 'linkchange', {
        icons: 'linkchange',
        init: function( editor ) {
            editor.addCommand( 'linkchange', new CKEDITOR.dialogCommand( 'linkchangeDialog' ) );
            editor.ui.addButton( 'linkchange', {
                label: 'Link text change',
                command: 'linkchange',
                toolbar: 'linkchange'
    
            });
            if ( editor.contextMenu ) {
                editor.addMenuGroup( 'linkchangeGroup' );
                editor.addMenuItem( 'linkchangeItem', {
                    label: 'Change link',
                    icon: this.path + 'icons/linkchange.png',
                    command: 'linkchange',
                    group: 'linkchangeGroup'
                });
                editor.contextMenu.addListener( function( element ) {
                    if ( element.getAscendant( 'linkchange', true ) ) {
                        return { abbrItem: CKEDITOR.TRISTATE_OFF };
                    }
                });
            }
            CKEDITOR.dialog.add( 'linkchangeDialog', this.path + 'dialogs/linkchange.js' );
        }
    });
})(jQuery)

dialogs/linkchange.js

 CKEDITOR.dialog.add( 'linkchangeDialog', function( editor ) {
    return {
        title: 'Text link change',
        minWidth: 400,
        minHeight: 200,
        contents: [
            {
                id: 'tab-basic',
                label: 'Basic Settings',
                elements: [
                    {
                        type: 'text',
                        id: 'txtchng',
                        label: 'Text update',
                        setup: function( element ) {
                            this.setValue( element.getText() );
                        },
                        commit: function( element ) {
                            element.setText( this.getValue() );
                        }
                    }
                ]
            }
        ],
        onShow: function() {
            var selection = editor.getSelection();
            var element = selection.getStartElement();
            this.element = element;
            if(element.$.nodeName === "A") { // Only if it's and anchor.
                this.setupContent( this.element );
            } else {
                this.hide();
            }
        },
        onOk: function() {
            var txt = this.element;
            this.commitContent( txt );
            if ( this.insertMode )
                editor.insertElement( txt );
        }
    };
});

The complete module can be found here you can take it as reference to implement yours.

About Drupal Sun

Drupal Sun is an Evolving Web project. It allows you to:

  • Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
  • Facet based on tags, author, or feed
  • Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
  • View the entire article text inline, or in the context of the site where it was created

See the blog post at Evolving Web

Evolving Web