Skip to content

Instantly share code, notes, and snippets.

@fragdochkarl
Last active September 13, 2021 01:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save fragdochkarl/7e54be4edfb3a35388856a3df9aec62f to your computer and use it in GitHub Desktop.
Save fragdochkarl/7e54be4edfb3a35388856a3df9aec62f to your computer and use it in GitHub Desktop.

Script: Magento 2 Templating (an introduction)

Contents of this document

Requirements/Technologies

Check the official docuentation for detailed system requirements. Test

Basic Stack

❗Always use PHP7 in favour, since development will be waaaaaaaaay faster. ❗

Frontend Stack

Magento App Installation

Official Developer Documentation

Composer Installation

Have a look at my Blog Post with detailed instructions.

Initialize new project:

composer create-project --repository-url=https://repo.magento.com/ magento/project-community-edition <installation directory name>

Change to directory and ensure Magento CLI is executable:

cd <installation directory name> && chmod +x bin/magento

Install App using CLI:

bin/magento setup:install --base-url=http://awsome-shop.local/ --language=de_DE --backend-frontname=admin --db-host=localhost --db-name=DBNAME --db-user=DBUSER --db-password=DBPASSWORD --admin-email=max@mustermann.de --admin-user=maxmuster --admin-password=PASSWORD --admin-firstname=Max --admin-lastname=Mustermann

If you encounter issues with XDebug maximum nesting level, try setting xdebug.max_nesting_level in php.ini or use following bash command:

php -d xdebug.max_nesting_level[=250] bin/magento setup:install <args>

Set Developer Mode:

bin/magento deploy:mode:set developer

Install Demo Data (optional):

bin/magento sampledata:deploy && bin/magento setup:upgrade

Setup frontend workflow

Verify Node.js and Grunt CLI are installed on your system:

$ grunt
Running "default" task
 
I'm default task and at the moment I'm empty, sorry :/
 
Done, without errors.
 
 
Execution Time (2016-02-01 08:54:44 UTC)
loading tasks  393ms  ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 98%
default          5ms  ▇▇ 1%
Total 400ms

Create Gruntfile and Package file from samples:

cp Gruntfile.js.sample Gruntfile.js && cp package.json.sample package.json

Install Magento dependencies (defined in package.json)

npm install

Magento file system structure

Summary of the most important directories:

<installation directory name>/
├── <app>/
│   │   ├── <code>/						// Root for custom modules (project scope)
│   │   │   ├── ...
│   │   ├── <design>/
│   │   │   ├── <adminhtml> 			// Root for backend themes
│   │   │   ├── <frontend>				// Root for frontend themes
│   │   ├── <etc>/
│   │   │   ├── conf.php				// Module configuration (auto-generated)
│   │   │   ├── env.php					// System configuration (former local.xml)
│   │   │   ├── ...
├── <bin>/
├── <dev>/
│   │   ├── <tools>/
│   │   │   ├── <grunt>					// Grunt configuration files
├── <lib>/
│   │   ├── <web>/
│   │   │   ├── ...
│   │   │   ├── <css>
│   │   │   │   ├── <docs>				// CSS library documentation
│   │   │   │   ├── <source>			// CSS library source code
├── <pub>/
├── <var>/
│   │   ├── <cache>/					
│   │   ├── <view_preprocessed>/		// Collected, not yet compiled Less source files
│   │   ├── ...							// Magento 2 has a some more caches for you.. ;)
├── <vendor>/							// Composer Modules including Magento core & themes

Theme anatomy

Location Usage
etc/view.xml Configuration of media files and some vars
i18n Localisation files (CSV) like in Magento 1
media (preview.jpg) Preview image
[Vendor_Module] Module Context Folders
[Vendor_Module]/layout Module layout XML files
[Vendor_Module]/templates Module PHTML files
[Vendor_Module]/web Module static files
web static theme files (former skin folder)
web/images static images
web/images/logo.svg Default logo file
web/css Less and/or static CSS sources

Theme fallback

  • You can create nesting levels at will using <parent>Vendor/theme</parent> in your theme.xml
  • Leaving <parent/> empty will fallback to Core Theme Module. Be cautious, using this you are literally starting with an unstyled page. Make sure to define at least CSS entry points.

Create custom theme

Register your theme in app/design/frontend/<Vendor>/<Theme>/registration.php

<?php
\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::THEME,
    'frontend/<Vendor>/<Theme>',
    __DIR__
);

and app/design/frontend/<Vendor>/<Theme>/theme.xml

<theme xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Config/etc/theme.xsd">
    <title>Vendor: Theme Name</title>
    <parent>Magento/luma</parent>
</theme>

Assign your Theme to a store in Magento backend under Content > Design > Configuration, open the frontend in a browser and enjoy yourself a cup of coffee – you should now see the theme you inherited from.

Layout XML: basic concepts

Defines page structure and its order/hierarchy of elements. There

Layout Handles

  • Page type like catalog_product_view => Controller actions
  • Page layout like catalog_product_view_type_simple_id_128 => Controller actions with parameters
  • Other like 2columns-left

Layout Elements

  • Containers for structure
  • Blocks for features, implementing PHTML templates

Layout XML: how to edit your changes

Extend a Layout

Magento 2 extends layout files (as known from Magento 1 local.xml) by default if you put a corresponding, same named file in your theme:

Example for Page Config or generic layout

<your_theme_dir>/
├- Magento_Catalog/
   ├- layout/
      ├- catalog_product_view.xml

would extend

<Magento_Catalog_module_dir>/
├- view/
   ├- frontend/
     ├- layout/
       ├- catalog_product_view.xml

Example for Page Layouts

Add an extending layout file to the following location:

<your_theme_dir>/<Namespace_Module>/page_layout/<layout_file>.xml

Override a layout

Base Layout:

<your_theme_dir>/<Namespace_Module>/layout/override/base/<layout_file>.xml

Theme Layout:

<your_theme_dir>/<Namespace_Module>/layout/override/theme/<Parent_Vendor>/<parent_theme>/<layout_file>.xml

Layout XML: Syntax

In general, definition of layout XML is quite similar to Magento 1.

Blocks

<block class="Magento\Theme\Block\Html\Header\Logo" name="custom.footer.logo" as="custom.footer.logo">
    <arguments>
        <argument name="logo_img_width" xsi:type="number">189</argument>
        <argument name="logo_img_height" xsi:type="number">64</argument>
    </arguments>
</block>

Note, that the former type attribute is now called class and contains its full path, which is consistent with many other places where Magento 2 uses namespaces etc.

As you can see in this example, custom arguments may be passed to the block to be accessed in a corresponding PHTML file by using $block->get{ArgumentName}() or $block->getHas{ArgumentName}. The latter would return a boolean value.

Control structures

Moving elements was quite complicted in Magento 1. Now you can do this easily with the new node:

<move element="block.name" destination="another.block.name" before="-" after="-"></move>

Removing elements

Although it is IMHO a little bit inconsistent, elements can be removed by setting the attribute remove="true":

<referenceBlock name="top.links" remove="true"/>

Referencing elements

Both blocks and containers can be referenced, if they already exist. Other than in Magento 1, there is no unique <reference/> node anymore. Instead you use the following syntax:

<referenceBlock attributes />
<referenceContainer attributes />

Layout XML: XSD

The usage of XML Scheme Definitions is new. In PHPStorm you can get auto completion by using the Magento 2 Plugin for PhpStorm

Depending on your environment you might have to tell PhpStorm manually where to find the corresponding Definition. It helps you in doing so:

<page xmlns:xsi="..." xsi:noNamespaceSchemaLocation="urn:.../page_configuration.xsd">

If the URN part is displayed in red, PhpStorm hasn't found it. Place your Cursor on it and hit ALT + ENTER (Mac) and chose "Manually setup external resource". In the popup window you can look for the corresponding file (type and search does work).

The URN will turn green. Open it and check for sub-sequent Scheme definitions. Then you will have suggestions if you start typing.

Layout containers and "shadow markup"

Containers are new elements used to group blocks. Usage is quite simple:

<container name="..." as="..." label="..." htmlTag="div" htmlClass="class1 class2">
	<block attributes/>
	<block attributes/>
</container>

The mindful reader will notice two seemingly unconspicious new attributes htmlTag and htmlClass.

Yes, they do exactly, what you'd expect. And No it wouldn't be of good practice to use them a lot!

❌❌❌ No, seriously! Resist the temptation and stick to the strict separation of the originally intended layers:

  • PHTML is for rendering Markup
  • PHP Block classes are for preparing data for rendering
  • Layout XML is to control which block should be displayed where and how

IMHO Magento Core Developers did more potential harm than good in weakening this doctrine by allowing this.

Page layouts

  • Can inherit from existing layouts via <update handle="1column"/>
  • Can be assigned to the page tag of any Layout XML <page layout="3columns" ... />

Example to extend default 1column layout:
<your_theme_dir>/<Namespace_Module>/page_layout/<layout_file>.xml:

<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_layout.xsd">
    <update handle="1column"/>
    <!-- Do some custom stuff here... -->
</layout>

New page layouts need to be registered in a module context:
<your_theme_module>/view/frontend/layouts.xml

<?xml version="1.0" encoding="UTF-8"?>
<page_layouts xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/PageLayout/etc/layouts.xsd">
    <layout id="homepage">
        <label translate="true">Home Page</label>
    </layout>
</page_layouts>

PHTML

These are the template files, rendering the markup for a certain layout or feature (e.g. Block).

Compared to Magento 1 very few things changed here. You write HTML Markup and fill it with <?php ?> tags.

  • The former $this becomes $block to reference the corresponding block class
  • The former $this->__('Translation') becomes __('Translation')
  • There is no more "global" usage of $this->helper() possible anymore. They can only be used within blocks context via Dependency Injection. But you could use "headless" blocks for the same purpose

Excursus: Blocks

PHP CLasses to implement feature logic, to be rendered via PHTML templates.

Register a basic module for your theme

in app/code/<Vendor>/<Theme>/registration.php

\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Vendor_Theme',
    __DIR__
);

and app/code/<Vendor>/<Theme>/etc/module.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Vendor_Theme" setup_version="0.3.0">
        ...
    </module>
</config>

Add block class

For instance in app/code/<Vendor>/<Theme>/Block/MyBlock.php

namespace Vendor\Theme\Block;

use Magento\Framework\View\Element\AbstractBlock;
use Magento\Framework\View\Element\Context;

/**
 * Class MyBlock
 * @package Vendor\Theme\MyBlock
 */
class MyBlock extends AbstractBlock
{
	public function __construct(Context $context, array $data = []) {
		parent::__construct($context, $data);
	}
    
    /**
     * @return string
     */
    public function getSomething() {
    	return 'Something';
    }
}

The basic usage is again similar to Magento 1, except the use of namespaces and constructor dependency injection. For further details consult the official developer documentation, as we focus on frontend here.

Usage in PHTML Templates

/* @var \Vendor\Theme\Block\MyBlock $myBlock */
$myBlock = $block->getLayout()->createBlock('Vendor\Theme\Block\MyBlock');

echo $myBlock->getSomething();

Pre2processing in Magento 2

In Magento 2 LESS is not used the way you are possibly familiar with:

  • It is tied to the core system via server side implementation. Shipped Grunt tasks partially call Magento CLI
  • It can be invoked on layout loading, http request of static CSS files or the shipped Grunt tasks
  • Implementation respects theme fallback
  • Additional @magento_import directive; allows to fetch files with same name from different locations (e.g. modules)

It is a actually a two-step processing:

  1. Collect resources in var/view_preprocessed
  2. Compile collected resources to pub/static

❗ Always use provided Grunt tasks in favour of performance!

I attached a workflow visualisation SVG to this gist.

How to compile your CSS

First, register your theme to Grunt, by adding an entry to dev/tools/grunt/configs/themes.js

module.exports = {
    blank: {...},
    luma: {...},
    backend: {...},
    myTheme: {
        area: 'frontend',
        name: 'Vendor/theme',
        locale: 'de_DE',
        files: [
            'css/styles-m',
            'css/styles-l'
        ],
        dsl: 'less'
    }
};

Note that you will have to add an entire object with an unique identifier for every locale you want to address, since static files are compiled in locale context.

Now you can start using the shipped Grunt tasks i the following manner.

Grunt task workflow

Initially and every time you add or remove .less files run the following tasks in that specific order:

  1. grunt clean:myTheme
  2. grunt exec:myTheme
  3. grunt less:myTheme

Thereafter you can start grunt watch task and your CSS gets compiled every time you change an existing .less file.

❗Important note on the theme scope

Always! use the tasks in the scope of your theme/locale by using :myTheme. Here is why:

As you already may have noticed from the above declaration in themes.js, the default themes are also known to grunt. If you just execute the tasks, they will run in every context, which takes a lot longer.

But most importantly this will also affect the Backend, since it is treated as a normal theme, too.

So an unthoughtful grunt clean will wipe out all backend styles. Although they will be re-compiled in general, you certainly don't want to do that. And I personally experienced a case, when I broke the Backend irrevocably - leaving a complete re-install the only option.

Ooops, where did my styles go...!?

Sometimes things can go wrong, especially when changing preprocessing modes or locales. In case of general “unidentified” problems, work the following checklist to reset the system:

  1. Delete the static files relevant for the theme in:

    pub/static/frontend/
    var/view_preprocessed/less/
    var/view_preprocessed/source/

  2. Flush the Magento caches (Backend, bin/magento CLI or n98-magerun2)

  3. Run bin/magento setup:static-content:deploy <lang_LANG>

  4. Start Grunt task workflow as described above

How to edit your CSS

I would recommend a workflow in the following order

1. Check for available framework variables

  • lookup in magento/lib/web/css/source/lib/variables
  • set new values in your web/css/source/_variables.less

2. Modification of inherited theme variables (blank or luma)

  • set new values in your web/css/source/_theme.less
  • If you don't inherit from blank/luma, you can define theme relevant variables here as well to provide a hook for developers inheriting from your theme

3. Use the final _extend hook

Place a web/css/source/_extend.less file in your theme. This is recommended by Magento as the "simplest way to extend parent styles".

As shown in this gist it is called at the very end by using a @magento_import directive iterating over all modules. So you could use this in your cutom module, too.

A best practice would be to use `_extend.less' as a bootstrapping file to import your own resources like this:

	@import '_extend/_buttons.less';
	@import '_extend/_header.less';
	// ...

4. Extend/overide module or widget styles

You can place an own equivalent Vendor_Module/web/css/source/_module.less or Vendor_Module/web/css/source/_widget.less file in your theme.

5. Override base lib components

Every base lib file can be overriden by putting an equivalently named file in your theme's web/css/source/ directory

Customizing the built in workflow

You can define own entry points in Magento_Theme/layout/default_head_blocks.xmland basically do what ever you want ;)

Make sure to properly remove the default CSS files to prevent triggering of server side preprocessing.

Media Queries

❗Always use one of the following patterns to prevent multiple rendering of your styles. This is due to the fact that everything is executed both in styles-m.less and styles-l.less. The variables below control if a directive will be rendered on each run.

Common Styles:

& when (@media-common = true) {}

Breakpoint related styles:

.media-width(@extremum, @break) when (@extremum = 'max|max') and (@break = @screen__m) {}

This seems a little bit odd and my first, almost natural, reflex was to use it in a much more readable (and seemingly equivalent, yet less redundant) form:

.media-width('min|max', @screen__m) {}

But this doesn't work out well! This is because the .media-width() mixin is called in a recursive way to collect all Queries. As a result there are no duplicates and the size of compiled files is somewhat stable.

Built in CSS Library

Documentation besides sources can be found in magento/lib/web/css/.

At the time writing I still find it an issue that there is no proper framework documentation available on the official Developer Website. The only posiibility to get in-depth information is to take a look into the codebase addressed above. The provided HTML documentation is nothing to be compared with similar resources from twitter bootstrap or other frameworks. I would like to see it integrated with devDocs, especially since the new layout seems to be much more feasible.

Usage, complexity and sustainability

In principle it is pretty simple. Once you found a desired component, it will be most likely presented to you in the form of a mixin with several paramters. So just call it on your selector of choice and you're done.

Biggest issue for me is the framework layout itself. You will find it not very well documented. Naming is often not optimal or self-explanatory. Use of concepts and language features are high-level but overly complicated or abstract.

Let me show you an actual use case to guide you through and understand my point above:

Let's say we would like to have some fancy CSS arrows to decorate some hyper links. If we take a look at magento/lib/web/css/source/lib/_utilities.less we find the .lib-arrow() mixin at line 353:

.lib-arrow( @_position, @_size, @_color ) {
    border: @_size solid transparent;
    height: 0;
    width: 0;
    ._lib-abbor_el(@_position, @_color);
}

Okay, looks promising. It accepts parameters for its orientation, size and color, as we would kind of expect for this purpose. But wait! There is another mixin involved! One with an un-pronouncable name. Does this even have a meaning!?

So, if we want to find out the type of our parameters, we would have to rely on the documentation or we follow the mixin definition. Happily it's defined right below .lib-arrow():

._lib-abbor_el( @_position, @_color ) when (@_position = left) {
    .lib-css(border-right-color, @_color);
}

._lib-abbor_el( @_position, @_color ) when (@_position = right) {
    .lib-css(border-left-color, @_color);
}

._lib-abbor_el( @_position, @_color ) when (@_position = up) {
    .lib-css(border-bottom-color, @_color);
}

._lib-abbor_el( @_position, @_color ) when (@_position = down) {
    .lib-css(border-top-color, @_color);
}

From what we can tell now, it seems to simply set a border color. We can vaguely guess now, that abbor_el could mean something like "Add border to element". Anyway...

We also see a concept the core developers decided to use in many places. AFAIK Less doesn't come with the possibility to have some kind of value list like an array. They work around by defining the mixin several times with a different guarding expression. So the first one would only be rendered if @_position would match the expression left.

IMHO this approach generates a lot of redundant code. I can see no clear advantage in favor of using a "flat" mixin without real validation. But at least, we know our possible orientation values.

But how does it actually set the color? There is another mixin involved. And this is truly my "favorite" one:

.lib-css( @_property, @_value, @_prefix: 0 ) when (@_prefix = 1)
  and not (@_value = '')
  and not (@_value = false)
  and not (extract(@_value, 1) = false)
  and not (extract(@_value, 2) = false)
  and not (extract(@_value, 3) = false)
  and not (extract(@_value, 4) = false)
  and not (extract(@_value, 5) = false) {
  -webkit-@{_property}: @_value;
       -moz-@{_property}: @_value;
        -ms-@{_property}: @_value;
}

.lib-css( @_property, @_value, @_prefix: 0 ) when not (@_value = '')
  and not (@_value = false)
  and not (extract(@_value, 1) = false)
  and not (extract(@_value, 2) = false)
  and not (extract(@_value, 3) = false)
  and not (extract(@_value, 4) = false)
  and not (extract(@_value, 5) = false) {
    @{_property}: @_value;
}

Behold, the .lib-css() mixin! What it does in a nutshell is mainly passing through arbitrary CSS attribute:property pairs. So instead of writing border-color: black you are now involving a mixin .lib-css(border-color,black). Awesome, right?

Okay, it does some sanity checks. Still heavily complicated for this task.

And yes, it has another flavour. If the third parameter is set, it works as a prefixer.

This is my very personal point of view, but this is a great example of why CSS in Magento 2 is such a struggle and why so many are complaining. It provides heavily complex and hardly documented solutions to no-problems. Here we have a particular task which could be done perfectly by an autprefixer plugin. No need to re-invent the wheel. It blows up code and binds capacities to create and maintain codebase.

I see absolutely no disadvantage in flatten out those three mixin layers into one flat .lib-arrow() mixin, which could directly be understood and used.

Wrap-up

That took me a little bit away, but I hope by using this example, you can get a better/faster hang on how CSS works in Magento 2. It is up to you to decide how to leverage the framework. In many cases (by now) I see no real advantage in using the components besides from the initial styling of elements in blank/luma theme. But you should totally dive in a bit and play around!

Built-in jQuery widgets

A nice little feature in Magento 2 are built-in jQuery widgets. These are basically implementations of elements of the jQuery UI library. You can use widgets for the most common tasks like collapsibles, modals or tabs right out of the box. Consult the JavaScript Developer Guide for detailed information.

General usage

Magento 2 provides different ways of using the built in widgets as well as own libraries and modules. I will show the most simple approach using data-attributes.

Let's add a collapsible to some Magento form. We basically need two things:

  • a wrapping element holding plugin initialization and configuration
  • alternating elements for title and content inside that container

So this would be our (simplyfied) markup:

<fieldset class="fieldset director">
    <div class="fieldset" data-mage-init='{
    	"collapsible":{ 
    		"active": false, 
    		"animate": 200, 
    		"collapsible": true, 
    		"openedState": "opened"
		}}'>
        <div class="fieldset" data-role="title">...</div>
        <div class="fieldset" data-role="content">...</div>
        <div class="fieldset" data-role="title">...</div>
        <div class="fieldset" data-role="content">...</div>
    </div>
</fieldset>

The outer fieldset holds widget configuration by passing an array to data-mage-init attribute. Then we have sub-sequent fieldsets with alternating data-role attributes. Each title will be clickable and open the following content. Simple as hell.

Where to go from here?

  • Be welcome to use this script as a starting point for your work
  • I neither recommend nor discourage you to use the Magento 2 frontend standards as
    • there is much movement in the community by now
    • I haven't found my personal "best approach" yet
    • Magentos roadmap is unknown to me
  • Better try to understand the Magento way first, before you start implementing your own
  • Be creative, contribute yourself, share your mind and help improve!
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment