Many designers are praising the benefits of Atomic Design. Rather than designing pages, Atomic Design focuses on designing systems of individual, reusable components. Designers aren't - or at least shouldn't be - the only ones thinking this way. From content strategy to QA, the entire team must be on the same atomic page.
Development is one area of a project that stands to benefit the most from this change in thought. Organizing a codebase by individual components keeps developers out of each other's hair, reducing the code and effort overlap that often occurs when building by page or section. It also makes the codebase much easier to understand and maintain. Developers will know where to find code and how to fix, alter, or extend it, regardless of the original author. After enforcing coding standards, only git’s history will know who wrote what. This all saves time and money.
Because there are many ways to do anything in Drupal, building every component with the same approach is crucial. In the Drupal world, this approach is known as "the Drupal way".
Building a component the Drupal way
Individual blocks, panel panes, or other UI elements would be examples of a component in Drupal. They are placed into regions within layouts to build pages. Other pages may use the same component in the same or different regions. A given component may vary across pages, but the design and intended functionality are similar. A simple search form is a good example, but they can be much more complex.
Design deliverables often arrive as complete pages. If the designers haven't already, identify the components that each page consists of. Break up the page’s layout into regions and those regions into components. Determine which components live on more than one page and if they vary between them. It also helps to identify different components that share design or functionality with others. It’s important to recognize early if they will be sharing code.
Before writing a line of code, determine where in the codebase the component will live. Organize custom modules by content types or sections and add relevant components to the same modules. A module exported with Features should be treated no differently than one created by hand; don't be afraid to add custom code to them (please do). The end goal is to have all back-end and (most) front-end code for a given component living in the same module.
Warning: This article is about to move fast and cover more ground than it should. It will move from back-end to front-end. There are many wonderful resources about each topic covered below, so they will be linked to rather than recreated. This will instead provide a high level overview of how they fit together and will highlight the most important pieces.
Component containers and placement
The most common container for a custom component is a block, created with a series of hooks. Contributed modules like Context can help place them on the page. More complex projects may choose to build pages with the Panels module. For pages built with Panels, custom panel page plugins are a component's container of choice.
The decision between blocks and Context, Panels, or another approach is important to make early in the project. It is also important to stick with the same approach for every component. This article will focus less on this decision and more on how to construct the markup within the container of choice.
View modes and entity_view()
If the component displays information from a node or another type of entity, render it with a view mode. View modes can render different information from the same entity in different ways. Among other benefits, this helps display content in similar ways among different components.
Create a view mode with
hook\_entity\_info\_alter()
or with the Entity view
modes contributed
module. This module also provide template suggestions for each entity
type in each view mode. Render an individual piece of information with a
view mode inside of a component using
entity\_view()
(you'll need the Entity API
module) or
node\_view()
.
Alter the entity's information as needed using a preprocess function and
adjust the markup in a template. Those pieces will be discussed later.
If a component lists more than one entity or node, build a view with the Views contributed module. It is best if the view renders content with view modes using the Format options. Create Views components with the Block (or Content pane for Panels) display(s). Views also provides template suggestions to further customize the markup of the component. The exported view should live in the same module as the code that customizes it. EntityFieldQuery might be worth considering as an alternative to using Views.
hook_theme() and render arrays
If the component does not display information from an entity, such as a
UI element, build it with
hook\_theme()
.
Drupal core and contributed modules use
hook\_theme()
to build elements like
links
and item
lists.
This allows other modules to override and alter the information used to
render the element. Default theme functions and templates can also be
overridden to alter their markup.
Choose a name for the element that will identify it throughout the
codebase. Outline what information the element will need to build the
desired output. Use these decisions to define it using
hook\_theme()
.
Again, keep this hook in the same custom module as the rest of the code
for the component.
To render a
hook\_theme()
implementation, construct a render
array. This array should contain
the name of the implementation to render and any data it needs as input.
Build and return this array to render the element as markup. The
theme()
function is a common alternative to render arrays, but it has been
deprecated in Drupal 8. There are advantages to using render arrays
instead, as explained in Render Arrays in Drupal
7.
Custom templates
Drupal renders all markup through templates and theme functions. Use templates to construct markup instead of theme functions. Doing so makes it easier for front-end developers to build and alter the markup they need.
Templates place variables provided by
entity\_view()
,
render arrays, and preprocess functions into the markup. They should
live in the “templates” directory of the same module as the rest of the
component’s code. The name of a template will come from theme hook
suggestions. Underscores get
replaced with dashes. Tell
hook\_theme()
about the template for each element it defines.
There should be no logic in the
template
and they should not have to dig deep into Drupal's objects or arrays.
They should only use an if
statement to determine if a
variable has a value before printing its markup and value. They can also
use a foreach
to loop through an array of data. Further
manipulation or function calls should happen in a preprocess function.
Preprocess functions
Use preprocess functions to extract and manipulate data such as field values and prepare them for the template. They are the middleman between the input and the output.
Preprocess functions follow the naming
convention
of
hook\_theme()
implementations. Common base themes often use Drupal core's preprocess
functions, such as
hook\_preprocess\_node()
,
in their template.php file. Keeping all preprocess functions in one file
will create a mess in no time. Instead, place preprocess functions in
the modules that define the parts their working with. This might be the
custom feature that contains the exported content type.
jQuery/JavaScript files
Create a separate JavaScript file for each component that needs custom JavaScript. Place it in a “js” directory within the module and name the file after the component. Be sure to use the Drupal behavior system and name the behavior after the module and component.
Add the JavaScript file to each page the component will appear
on.
If the component appears on most pages, it might be best to just add it
to every page. This will cause less HTTP requests with JavaScript
aggregation enabled. The best way to do
so is with
hook\_page\_build()
.
JavaScript files can also be attached to entities rendered through view
modes within
hook\_entity\_view()
.
The best way to add JavaScript to a
hook\_theme()
implementation is by attaching it to the render array.
Sass components
When using a CSS preprocessor like Sass, there isn’t much of a penalty to dividing the CSS into many files. Create a new Sass partial for each component and give the file the same name as the component. Keep them in a “components” directory within the Sass folder structure. Unlike all other code mentioned in this article, it is often best to keep all CSS for these components within the theme. Only keep CSS that supports the core behavior of the component in the module. Consider what styles should persist if it were a contributed module used with other themes.
In the component’s template, base the class names off of the component's name as well. This makes it easy to find the component’s Sass after inspecting the element in the source. Follow the popular BEM / SMACSS / OOCSS methodologies from there.
Coming up for air
As mentioned, there are often endless ways to complete the same task in Drupal. This makes learning best practices difficult and "the Drupal way" will vary in the minds of different experts. The best way to grasp what works best is to start building something with other people and learn from mistakes. The approach outlined in this article aligns with common practice, but mileage will vary per project.
Regardless of approach, focusing on components before pages will only become more important. Drupal content is already displayed on everything from watches to car dashboards. The web is not made of pages anymore. Designers have begun to embrace this and Drupal developers should too; everyone will benefit!