With the Magento Imagine conference came a lot of talk about the Magento 2 frontend. While I wasn’t there, I witnessed it via Twitter and was excited to see what was happening. The reason it has my attention is because I am a front-end developer and work with Magento every day &emdash; and have for years. Magento is a large, flexible, and extensible platform that must fit the needs of many people. The ability to meet everyone’s goals is no small feat. However, I believe that a well built front-end will better facilitate that.

For this, I want to focus on the CSS and HTML aspect of Magento 2’s front end. I have worked quite a bit with the UI Component / Javascript side and, while there are many areas of potential improvement there, I actually like much of it. Conversely, when I started working with the CSS, I was disappointed, and that has continued despite getting a more full understanding of Magento 2’s stylesheets. I believe that there is tremendous potential for the CSS and hope to participate in improving it. With that, my goal is to reflect on what I, a Magento frontend developer, consider isn’t right with the current stylesheets as well as my perspective on what can and should be improved with the next generation.

First, I want to talk about specific things that I like about the Magento 2 frontend as it is today in v2.1. After that, I will lay out specific shortcomings and my proposed improvements to that. Also, below I am referring to everything as CSS, but with the assumption that it would be a preprocessor. For the type of preprocessor, I hope that SCSS will be used in the future instead of LESS, but at the end of the day, it doesn’t make a tremendous difference.

The good things about Magento 2’s CSS

The _module partial inside of a module’s directory is brilliant. That module’s partials can go into a module directory on the same level. Ideally, the _module partial would simply @import a number of other partials from the module directory. But this structure is fundamentally great.

The ability to override source stylesheets is incredibly natural and works very well. This is a fantastic iteration to the previous version of Magento.

Areas for improvement and specific suggestions:

Variables

One can’t open a Magento 2 stylesheet partial without seeing at least a few variables. In my opinion, preprocessor variables are way overused. Since so many values have been abstracted into variables, finding the actual location to change a particular value takes longer. In addition, it increases the chances of introducing collateral damage because that variable may have been used elsewhere. This leaves a developer in a predicament: remove the variable’s usage in the property, or risk changing it.

Variables can also be overridden based on source order, which means, at least in SCSS, that there may well be multiple locations where the variable is declared. Sometimes those variables even have other variables from different files as their values. The last declaration of the variable still must be found in order to update it. This creates an intricate web of values spread throughout files.

One example that comes to mind is the use of the $indent__[size] variables. If I changed the value of one of the variables, I suspect many things on the site would break. While they may seek standardize spacing, if there are no rules to recommend which size to use in which instance, they actually become merely abstracted values.

Another example is all of the shades of gray. Colors are often one of the best times to use variables. However, as it is, there are a little over 40 different shades of gray, along with many other colors, declared in variables and used throughout the site. When I need to set the colors of a theme I’m working on based off of a design prototype that I have for a site, I find myself wondering which values to change. Changing them all is tedious and usually well beyond the scope of the design prototype.

Variable overuse also leads to a unique problem: how does one name such a specific variable? Naming in the current stylesheets is quite verbose. Take this for an example: product-grid-items-per-row-layout-2-right-screen-s. This variable appears to define a value used for one specific screen size, in one layout context, in one output component. While long variable names are not bad in their own rite, I think they are indications of a problem. If a variable is used only one time, why declare the variable at all? With the advent of amazing browser developer tools, using a variable to simply name a property isn’t necessary, and instead, adds a step for finding it.

My suggestion: define a specific goal or requirement of variables and err on the side of less rather than more. Ensure that variables are only for values that are, and should be, repeated and intimately related to each other. For example, the goal could be: “only use a variable for values related to each other and that can be changed (within reason) without causing unexpected damage.” Local variables that are declared just before they are used have their place also, but from my experience that is not often.

Mixins or Extends

Both are used extensively in the Magento 2 frontend. This is also a tool that I think is very over-used. For example, when I opened the _pages stylesheet which handles the paging display, I was surprised to see approximately 100 parameters used for the mixin declaration. There are a number of other instances like this. On some projects, I found it so tedious to work through the parameters that I wanted to change, that I removed the mixin in my override. One file went from about 400 lines of source code to less than 100. That’s a lot easier to maintain! The goal of using mixins and extends was good in that it theoretically allowed us to reuse those components. However, in practice, when the mixins are that large, it seems to me that it would be just as easy to reuse the class names to replicate the desired end goal.

But there’s another problem with mixins: when they are so large, there are wasted properties and not enough properties. What if I want to add a box-shadow but there is no parameter that lets me pass in the value? Or, what if I don’t need a gradient, yet it’s declared in the mixin, and sent to thousands of users over time with an inherit value?

Extends are used frequently. There are some incredibly long selectors that are output due to their use. Here is a real example:

.abs-account-blocks .block-title>strong,
.abs-account-blocks .box-title>span,
.abs-block-title>strong,
.account .column.main .block:not(.widget) .block-title>strong,
.account .column.main .block:not(.widget) .box-title>span,
.block-compare .block-title>strong, 
.block-giftregistry-results .block-title>strong, 
.block-giftregistry-shared-items .block-title>strong, 
.block-reorder .block-title>strong, 
.block-wishlist .block-title>strong, 
.magento-rma-guest-returns .column.main .block:not(.widget) .block-title>strong, 
.magento-rma-guest-returns .column.main .block:not(.widget) .box-title>span, 
.multicheckout .block-title>strong, .multicheckout .box-title>span, 
.paypal-review .block .block-title>strong, 
.paypal-review .block .box-title>span, 
.sales-guest-view .column.main .block:not(.widget) .block-title>strong, 
.sales-guest-view .column.main .block:not(.widget) .box-title>span, 
.widget .block-title>strong, 
[class^=sales-guest-] .column.main .block:not(.widget) .block-title>strong, 
[class^=sales-guest-] .column.main .block:not(.widget) .box-title>span, 
h1, h2, h3, h4, h5, h6 { ... }

There is a very large file (_extends) that contains many abstract definitions which are extended other places. Extends are quite difficult to manage because they can drastically change the source order of declarations. As seen in the above example (with :not(.widget)), small additions to the source code can balloon into massive changes in the output. They also go against the core idea of an object-oriented approach to CSS (discussed more further on) by conglomerating many unrelated blocks in the output.

Another significant problem for mixins and extends is the time it takes to track down the original declaration of a property. As it stands, there are mixins inside mixins, mixins inside extends, and extends inside extends. Tracing through the source, looking for the base declaration is something that even source maps don’t help with and is a remarkably unpleasant experience.

My suggestion: use mixins only for small, reusable, single-responsibility blocks of styles. The current media query mixin is a perfect example of this. Also, Harry Roberts has an interesting article on CSS Wizardry about why mixins are better than extends due to performance. In my opinion, extends should be almost completely eradicated from the code base. For the few times where they may be a reasonable choice, and if SCSS is used, the %classname syntax should be used over .classname for an abstract definition.

Selectors

The selectors in Magento 2 are actually better than Magento 1, but not by much. Selectors are nested freely even when they don’t need to be. There is no standard naming convention in place, and selectors are broken apart by preprocessor nesting. To make it worse, the & is used to break up single class names. Overriding a declaration often becomes a specificity war. I think almost everyone agrees that this is not ideal, which is great.

My suggestion: fixing this is not quite as easy as it may sound. From my experience, using a naming convention is hard because it forces a developer to come up with a name for everything. And we all know that naming is hard. However, the end result is certainly worth the effort. I personally use and like the BEM naming convention. It is one of many, and the exact one chosen matters far less than how well the system is implemented. The important part is that the author of the stylesheets fully understands what it means to write object-oriented CSS and the principles related to that. For example, a few of these concepts include the following:

  • Encapsulation: ensuring that styles don’t leak into or out of the object.
  • Naming: choosing a parent class name that is appropriate, understandable, and flexible.
  • Single responsibility: determining when to put a child component into its own object versus include it with the parent.
  • Convention: adherence to the naming convention. For example, not doing this in BEM: block__child__grandchild.

Also, I personally have no problem with preprocessor string concatenation being used for selectors. The problem is when it is done ad lib and outside of clear rules. For example, I often split “elements” out inside of a “block” (based on the BEM naming convention) but rarely beyond that.

Partial Size

Currently the _module partial is a rather large file that encompasses almost all of the output components for that Magento module. There are some exceptions, like the checkout and catalog modules, but in general, the _module file tries to achieve too much.

My suggestion: I feel that using a naming convention (like BEM) will ultimately solve this problem because each object will be in its own partial. There could be cases where the block is so small that it hardly merits its own partial. I doubt there will be a clear cut way to determine what is too small or too large but can picture general guidance being written for this.

CSS Units

Aside from some layout declarations, Magento 2 stylesheets use almost strictly one type of length unit: px. The px unit is used for font-size, line-height, spacing, and such. From my experience the px unit tends to be used too much in CSS in general because it is easy to understand and predictable. However, it is very brittle. For example, if a user zooms in on the screen, the media queries don’t work correctly. Aside from accessibility disadvantages, the px is not relative to the parent element requiring responsive adjustments to be needlessly verbose.

My suggestion: use more relative unit types. As I understand it, CSS has about 30 different length unit types. The important relative units include em, viewport-based units, and rem. I try to follow these very-general guidelines when declaring values:

  • Use px for border-width, or other values that would be low values (1-2px).
  • Use em for padding, margin, border-radius, and font-size on child elements.
  • Use rem for font-size of parent elements in particular, or anything else that should be relatively fixed.
  • Use viewport units (vw, vh, vmin, vmax) where possible, or in conjunction with other units. For example: margin-top: calc(1em + 1vw).

When relative units are leveraged effectively, it drastically increases the flexibility of a layout. Also, on this note, the current percentage widths are good, and units such as color hex codes seem fine.

Context-dependent styles

Magento has the option to set the page layout on a per-page basis. While this is great, it adds complexity to many of the layouts. While better usage of relative units and consideration of more flexible ways to handle layout helps with this, there are inevitably going to be times where context-dependent styles are required. As it is, body classes are nested within the parent selector to adjust the style as necessary. The catalog module’s _listings partial contains a number of examples like that.

My suggestion: what about creating a mixin that would handle the classes? Perhaps @include layout($columns-three) {}. While I haven’t prototyped this myself, it would have a unique advantage of allowing developers to disable certain layout styles to decrease the size of the stylesheets somewhat. In my experience, not all websites utilize all layout options, and this would be an easy to way to clean that up some. It is a small detail, but I could see it being helpful. One thing that I see as important, though, is keeping the contextual declarations in the same block as the rest of the styles. This allows developers to get a complete view of a component without looking through other files.

Media Queries

Currently media queries are added to the bottom of the file in single declarations with many selectors inside. There doesn’t seem to be a standard on whether the primary styles are intended for smaller or larger screens by default. The comments in the files are confusing because they refer to specific devices. This makes it unclear where to add a new media query for devices that may be in between “mobile” and “desktop,” or for very large screens. By having the media queries separated to the bottom of the file, it requires developers to search through multiple places to find all the properties for a particular element. This increases the amount of time it takes to update styles as well as the likelihood that something will be missed in the process.

My suggestion: add media queries directly into the base definitions. This is one area where preprocessors hold a clear advantage. Media queries can be nested like this:

.class {
  font-size: calc(1rem + 1vw):

  @media (min-width: $screen__s) {
    font-size: 1.5rem;
  }
}

By moving everything together, all the style declarations can be seen at once without searching to the bottom of the file. Selectors are not duplicated which keeps specificity the same for all media queries, and it demonstrates which media queries have precedent (by source order). Thanks to gzip compression, the differences in output have been proven to be negligent. Also, avoid using the terms “mobile” and “desktop” and focus on screen size like the variables do.

Defaults

Style declarations for HTML elements seem fairly aggressive in the current stylesheets. I’ve found myself resetting margins on lists multiple times, or overriding the height: auto property on the img tag.

My suggestion: this is certainly a mixed bag. Element defaults have their place, but in my experience, less is often better. One thing I suggest to combat this issue is to work toward [scoped CMS areas] https://csswizardry.com/2015/03/more-transparent-ui-code-with-namespaces/#scope-namespaces-s-). This allows those defaults to apply to areas where they are helpful while not getting in the way of other components. Ultimately, defaults are an important part but should be handled with great caution.

Icon system

The font icon system that is bundled with Magento 2 works fine. However, it is a font icon system and comes with the inherent downsides associated with that. Font icons have very strange sizing details, require a more involved process to modify, and have limited control by CSS. With browser support where it is, there is no need to use an approach that was a workaround for not having access to a true vector image format.

My suggestion: switch to an SVG icon system. This would be a significant change, but it is an important one so I wanted to put this out there. The benefits of using an SVG icon system are quite clear with better accessibility, fantastic CSS control, ease of editing and adding to the icons, and natural sizing. SVGs are easy to understand with a code format that is human readable to an extent. There are other advantages in addition to those I’ve briefly noted here.

There are a few hurdles that would need to be crossed (namely, Internet Explorer and Edge) but nothing very difficult. I built an icon system for Magento 1 which is running a site with a large quantity of icons. I think it would be particularly amazing to be able to include SVG icons within modules like we can with CSS.

New CSS properties

This is at the end because it is very minor, but it would nice to ship with some of the new CSS properties that are available on most browsers now. The most obvious is display: grid which would reduce the amount of code necessary to layout product grids. A fallback would still be necessary, but the @supports feature detection query allows these new properties to be used easily.

Wrap up

I’m excited to see what is ahead for Magento 2. While I listed my current pain points, along with possible solutions, I think the current architecture provides a solid structure with which to build on. The community is also eager to assist with these improvements after the direction has been set. Many of the concepts listed above are also reasonably simple but would need to be communicated to those working on development and enforced in some measure in order for them to be effective.

If there are things that should be added, areas that could be clarified, or to discuss this further, ping me in the comments or on Twitter (@bassplayer_7).

I recently reloaded my laptop and upgraded it to Sierra. As Apple continues to tighten down security measures, more and more programs are having trouble installing properly (or living through the upgrade process).

The problem I experienced was with installing VMWare Fusion: particularly with calling vagrant up. In doing so, I received the following cryptic error: “Error occurred: The file which defines networking interfaces for VMware Fusion could not be found. Please verify Fusion is installed properly and try again. If this problem persists, please contact support.” Additionally, when I tried to create a dummy VM in VMWare Fusion, I also received a cryptic “Internal error” message.

I found that the file /Library/Preferences/VMware Fusion/networking was non-existent. Since there was another machine that I use that works as expected, I copied the networking file over to my laptop. Vagrant booted the machine just like it was supposed to.

For your (and my) reference, here are the contents of the file:

VERSION=1,0
answer VNET_1_DHCP yes
answer VNET_1_DHCP_CFG_HASH D57F26DD20520022B91D6F44905AC85450450858
answer VNET_1_HOSTONLY_NETMASK 255.255.255.0
answer VNET_1_HOSTONLY_SUBNET 172.16.203.0
answer VNET_1_VIRTUAL_ADAPTER yes
answer VNET_8_DHCP yes
answer VNET_8_DHCP_CFG_HASH BA7AFE70F85B41C6F5C490924040A16C48AE08B9
answer VNET_8_HOSTONLY_NETMASK 255.255.255.0
answer VNET_8_HOSTONLY_SUBNET 172.16.32.0
answer VNET_8_NAT yes
answer VNET_8_VIRTUAL_ADAPTER yes
add_nat_portfwd 8 tcp 2200 172.16.32.159 22
add_nat_portfwd 8 tcp 2222 172.16.32.150 22

Magento 2 Template Paths

January 04, 2017

While working on a Magento 2 project yesterday, I encountered a scenario where I needed to change a block's template to be a custom template. In Magento 1, it's pretty easy to do this with the setTemplate() method:

<reference name="block_to_change">
    <action method="setTemplate">
        <template>path/to/your/template.phtml</template>
    </action>
</reference>

Magento 2 has similar functionality:

<referenceBlock name="order_shipping_view">
    <action method="setTemplate">
        <argument name="template" xsi:type="string"><![CDATA[The topic of the blog post.]]></argument>
    </action>
</referenceBlock>

You would then set the path to the template inside the <argument> node. This is a handy way to make UI-related customizations, without having to create a new block.

As any Magento developer could tell you, the path to your template in Magento 1 is pretty straightforward. Because templates are stored in the app/design/[area]/[package]/[theme]/[template] directory, you place the template anywhere inside the theme’s template directory. You then use its path, relative to the template directory, as the path in layout.

Since Magento 2 is more decoupled, it is approached a little differently. The Magento 2 module structure introduces the idea of templates inside a module’s directory (ModuleName/view/[area]/templates). Any templates your module depends on should be inside that directory. If a block’s template is in your module, the path will be relative to the ModuleName/view/[area]/templates directory.

However, if you are referencing a template for a block from another module, including core blocks (like Magento\Framework\View\Element\Template), the only path that works will be like this: ModuleName::path/to/your/template.phtml. In this example, the path/to/your/template.phtml part is relative to the view/[area]/templates directory in the module you are working with. The system won't find the template without the ModuleName:: part.

I was a bit puzzled when I found this. Why would the Magento 2 team choose an approach like this? And, what was going on in the Magento 2 core when it was attempting to locate a template?

Using PhpStorm and Xdebug to step through the process, I found the Magento\Framework\View\Filesystem::getTemplateFileName() method and quickly figured out the answer to my questions. Here's the code from that method:

 // Lines 108-126

 /**
 * Get a template file
 *
 * @param string $fileId
 * @param array $params
 * @return string|bool
 */
public function getTemplateFileName($fileId, array $params = [])
{
    list($module, $filePath) = \Magento\Framework\View\Asset\Repository::extractModule(
        $this->normalizePath($fileId)
    );
    if ($module) {
        $params['module'] = $module;
    }
    $this->_assetRepo->updateDesignParams($params);
    return $this->_templateFileResolution
        ->getFile($params['area'], $params['themeModel'], $filePath, $params['module']);
}

Let's break that down. The method accepts a variable named $fileId, which I observed to be the template path from layout (the value set in the template argument above) and an array of parameters. The $fileId variable is passed to another method: Magento\Framework\View\Asset\Repository::extractModule(). Let's have a look at that method.

//Lines 415-434

/**
 * Extract module name from specified file ID
 *
 * @param string $fileId
 * @return array
 * @throws \Magento\Framework\Exception\LocalizedException
 */
public static function extractModule($fileId)
{
    if (strpos($fileId, self::FILE_ID_SEPARATOR) === false) {
        return ['', $fileId];
    }
    $result = explode(self::FILE_ID_SEPARATOR, $fileId, 2);
    if (empty($result[0])) {
        throw new \Magento\Framework\Exception\LocalizedException(
            new \Magento\Framework\Phrase('Scope separator "::" cannot be used without scope identifier.')
        );
    }
    return [$result[0], $result[1]];
}

In the above code, the constant FILE_ID_SEPARATOR is defined as ::. At this point, the code becomes simple PHP: if the template path doesn't contain the string ::, an array with two elements is returned. The first element would be an empty string, and the second element would be the template path. If, however, the template file path has the string :: in it, it is split on that substring, and an array is returned with two elements. The first element is a string that contains the first part of the split template path. The second element contains the second part of the split template path.

At this point, you may be wondering what the returned value of that method has to do with anything. Let's look at the first method again:

list($module, $filePath) = \Magento\Framework\View\Asset\Repository::extractModule(
    $this->normalizePath($fileId)
);
if ($module) {
    $params['module'] = $module;
}

I've reposted the relevant lines here, but if you want to see the whole method, scroll up and review it.

Take a look at the first line there—the one with list(). PHP uses the list() construct to create and assign multiple variables from an array. The variable names that are passed into the parentheses will then be assigned to the elements of the array, in order. The Magento\Framework\View\Asset\Repository::extractModule() method returns an array with two elements. The first element will be either an empty string or a string containing the ModuleName part of a template file path. The second element will have the path/to/your/template.phtml part. The list() construct will assign the first element to the $module variable, and the second element to the $filePath module.

Next, the system checks if the $module variable is a string with contents, or empty. If it's not empty, the $params array will have an element, module, defined as the first part of the file path in the template.

Finally, the system passes some of the $params and the $filePath off to another class and method that actually takes that information and finds the template file.

Let's recap what we've seen up to this point: Magento 2 tries to extract a module name from the first part of the template path. If it finds one, it will pass that as the module that contains the template.

But, what happens if there's no first part in that template path or if the system never finds the :: substring? The answer is in the method that calls the first function we looked at. The first function was Magento\Framework\View\Filesystem::getTemplateFileName(), and the method that calls it is Magento\Framework\View\Element\Template::getTemplateFile(). Here it is:

    //Lines 199-207

    public function getTemplateFile($template = null)
    {
        $params = ['module' => $this->getModuleName()];
        $area = $this->getArea();
        if ($area) {
            $params['area'] = $area;
        }
        return $this->resolver->getTemplateFileName($template ?: $this->getTemplate(), $params);
    }

This is where the $params array is initialized. Do you see, in the first line, how the default module element is provided by another function (getModuleName)? In Magento 2, Magento\Framework\View\Element\Template is the generic template block, very similar to Mage_Core_Block_Template in Magento 1. All that function (getModuleName) does is return the name of the module that the current block is from.

If the template path doesn't provide directions on what module to find the template in, the core code assumes that the template is associated with the same module as the block. This is why it's so important to include ModuleIdentifier:: before the template path when changing a block's template. It is the same as prepending your module name to the template path.

We started this deep dive with two questions: Why would the team behind Magento 2 choose an approach like this? And, what was going on internally to the Magento 2 codebase when the system was trying to locate a template? While the core team is the only people who can definitively answer question number 1, it seems to allow for a great degree of modularity, especially in comparison to the old app/design template structure. And, I think we've seen what goes on internally in finding a template!

I'll close with a best practices tip: I think using the ModuleIdentifier:: syntax is a good thing to be in the habit of, even when it's not required. I can see it keeping things more clean and clear. Happy coding!

Attributes are a key part of the Magento ecosystem. These little bits and pieces of data help describe products. One of the easiest-to-understand attributes would be attribute “color”. Another common attribute is “size”. Not every product on your website will have a value for the attribute “color” or “size”, but many likely will.

For anyone who is managing a Magento storefront on a daily basis, you have worked with attributes. Not only adding new values (perhaps a new size is available for a popular sweater that you sell), but managing existing values (perhaps you are no longer going to carry the color “red”).

Let’s say you needed a list of all the products on your web site that have a value for “color” (regardless of what that value is). How would you do that in vanilla Magento? Unfortunately, there is no easy way to do this. Your best approach is going to be exporting your product database to CSV (System->Import/Export->Export) and then using Excel to sort and filter to the products with a value for that attribute, as each attribute will appear in its own column in Excel. We are always looking for ways to improve processes and make things smoother. As always, there is a better way.

We created a small Magento plugin for the Catalog->Attributes->Manage Attributes panel which adds a new tab to the left side: “Products Uses”. This tab displays a grid of products which have a value for that attribute. The products can be sorted and filtered using any of the inputs across the top of the grid. For example, you could filter to view only the products have the value “red” for the attribute “color”. There is even an “edit” link which opens the back-end Magento Admin panel for the product in a new tab so edits can easily be made to the product.

Example

This little plug-in is just one of many plugins available to SwiftOtter clients. Best of all, we are releasing it today for you to use.

You can download the plugin on GitHub.

If you keep running into hassles or annoyances while managing your Magento storefront, reach out to your 2nd level Magento support agency. They may well have a tool which can help solve the problem you are facing.

Magento's WYSIWYG editor isn't particularly great and hasn’t been changed for a long, long time. However, in order to ensure that non-technical content managers can carry out their work effectively, it is important that it is integrated correctly with the site's theme. Most themes have custom CSS functionality that can be used on the site.

There are generally two ways to facilitate the communication of their use to content managers. One is to focus on the HTML classes and perhaps set up a document that would have information on how to use them on the site. The other approach is to automate it by building custom modules.

This article focuses on the former: adding classes to the styles menu in the Magento WYSIWYG editor. It’s an easy solution and doesn't take more than an after plugin to add options to the styles menu. The goal of this ost is to provide details on what is required to add the options.

For a succinct tutorial on how to add styles to the WYSIWYG, go the end of the article.

Add classes to WYSIWYG editor Styles menu:

You will need to create a module first for handling this. In the etc/ folder of your module, create a di.xml file. The di.xml is where you will specify the class in your module that will be a plugin to the core class. For this, you will use an after Plugin. This plugin will be for the Magento\Cms\Model\Wysiwyg\Config class which outputs configuration that the WYSIWYG editor uses to display. The di.xml should look like the following example (where the module namespace is SwiftOtter\Editor):

<?xml version="1.0" ?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="\Magento\Cms\Model\Wysiwyg\Config">
        <plugin name="add_wysiwyg_data" type="\SwiftOtter\Editor\Plugin\WysiwygConfig" sortOrder="10" />
    </type>
</config>

Now create a class in the Plugin namespace of your module. While that namespace isn't required, it is a convention. That class needs to have a public function called afterGetConfig(). Because the function starts with after, Magento will know that this plugin is to be run after the original method. The GetConfig part is the name of the method inside of the base class: Magento\Cms\Model\Wysiwyg\Config::getConfig(). This function should receive two parameters: $subject and $config. The $config is the return value of the base method. At its simplest, this is all we need in order to be able to add some extra data to it. Because $config is a \Magento\Framework\DataObject, I suggest type hinting that as well.

Inside the method, we will add data to the config. The style options need to be provided through the theme_advanced_styles property. If you want to dig into the editor a little more, the best place to start is the setup.js file which can be found here: /lib/web/mage/adminhtml/wysiwyg/tiny_mce/setup.js. The theme_advanced_styles value expects a specific format: "Hero=hero;Hero Image=hero__image;". That will display two options within the styles menu: Hero and Hero Image. When adding data to $config with the addData() method, you will use an array like this where the value is a key that the Javascript class expects: ['theme_advanced_styles' => 'valueString...'].

However, there is a small catch here due to a nuance with the Javascript class for the WYSIWYG editor. You need this array to be the value of another array that has a key of 'settings'. This is because of the way that the options are loaded into the Javascript class. The values of the settings object extend the default configuration options. As a result, the parameter within addData() should look like this: ['settings' => ['theme_advanced_styles' => 'valueString...']].

To make it easier to add options and understand what is currently available, I chose to use an associative array as the basis for the additional configuration. This is not necessary, but I find it much easier to work with than the awkward protocol for the styles. I used a concise array_map() to compile the associative array into an indexed array with the key and value separated with =:

$styles = array_map(function($title, $class) {
    return "{$title}={$class}";
}, array_keys($styleArray), array_values($styleArray));

With those things put together, the method call that actually adds the data looks like this:

$config->addData(["settings" => ["theme_advanced_styles" => implode("; ", $styles)]]);

Don't forget to return the updated $config object. A return value is actually required for after plugins. After that, clear the cache, and you should have options display under the "Styles" menu in the WYSIWYG editor. As a result, content managers will be able to easily add custom styles to markup.

Areas for improvements

While there are many areas in the Magento 2 editor that could be improved, one is that this doesn't allow you to add containers or control which elements it should be applied to. For example, if a user selects all the elements in a list (<ul><li /><li /></ul>), it will add the selected class to each individual list item. I would prefer to have a degree of control over whether it is applied to the <ul /> or even a surrounding <div /> added with that class.

The other thing that would be good to do is move the associative array that provides the classes out into a separate file—like a model.

Short Tutorial:

Step #1: Inside a module, add a plugin for the \Magento\Cms\Model\Wysiwyg\Config class:

<?xml version="1.0" ?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="\Magento\Cms\Model\Wysiwyg\Config">
        <plugin name="add_wysiwyg_data" type="\SwiftOtter\Editor\Plugin\WysiwygConfig" sortOrder="10" />
    </type>
</config>

Step #2: Create a plugin class that will add data to the configuration data inside the file app/code/SwiftOtter/Editor/Plugin/WysiwygConfig.php:


namespace SwiftOtter\Editor\Plugin;

class WysiwygConfig {
    public function afterGetConfig($subject, \Magento\Framework\DataObject $config)
    {
        $styleArray = [
            'Hero - Image' => 'hero__image',
            'Hero - Heading' => 'hero__heading',
            'Hero - Action Button' => 'hero__primary-action'
        ];

        $styles = array_map(function($title, $class) {
            return "{$title}={$class}; ";
        }, array_keys($styleArray), array_values($styleArray));

        $config->addData(["settings" => ["theme_advanced_styles" => implode("; ", $styles)]]);

        return $config;
    }
}

Step #3: Clear cache and refresh admin page.