Creating components

Edit on GitHub

As Spryker Shop implements the Component Model, adding new functionality to it usually means implementing a new component. In this document, we shall review creation of a new component on the example of a simple block that displays the count of DOM elements of a certain type. To implement it:

1. Create a component folder

First of all, you need to create a folder on the file system where all component files will be located. By default, project level components are located under src/Pyz/Yves/ShopUi/Theme/default/components. This folder should contain subfolders for each component type (atoms, molecules, organisms). A links counter is a simple molecule, so it will be created under the molecules subfolder. Per naming conventions, the folder name follows Kebab Casesrc/Pyz/Yves/ShopUi/Theme/default/components/molecules/new-component-counter.

Open the new-component-counter folder and create the following files:

  • index.ts - Webpack entry point;
  • new-component-counter.scss - styles;
  • new-component-counter.ts - Javascript code;
  • new-component-counter.twig - component template.

2. Define a template

The first thing to do when creating a component is to define a template for it. A template specifies which blocks a component consists of and how they are arranged. This is done in Twig. Open the new-component-counter.twig file.

First, we need to define the inheritance of the component. A component can inherit from a model or another component. Since we are creating a new component, it can be inherited from the following models defined in Spryker Shop Application:

  • atoms, molecules and organisms extend model component:
{% extends model('component') %}
  • templates and views extend model template
{% extends model('template') %}

As we are creating a molecule, it must inherit the component model. For this purpose, add the following:

{% extends model('component') %}

After that, we need to define a configuration object for our new component. A configuration consists of the following:

  • name - specifies the component name. It is also used as the class name of the component.
  • tag (optional) - specifies the name of the DOM tag that will be used to render the component. It also defines the component Javascript class name (jsName) automatically.

If the tag name is not defined, div is used by default.

  • jsName (optional) - explicitly specifies the Javascript class name (.js-classname) of the component.
Separation of Logic from Styles

To enforce separation between logic and visual styles and achieve clear understanding as to which code is responsible for what, the following convention has been put in place:
- code related to styles is always contained in {{config.name}}__element classes;
- code related to behavior is always contained in {{config.jsName}}__element classes;

The same as with files and folders, Kebab Case should be used

We will use a custom tag for the component. It will have the same name as the name of the component. Add the config element as follows:

{% define config = {
    name: 'new-component-counter',
    tag: 'new-component-counter'
} %}

Now, we need to define a contract for the component. Contract consists of the attributes required for the component to function properly. Attributes can be either required or optional. Required attributes must always be defined whenever a component is used, while optional ones can be left undefined. Nevertheless, by convention, attributes cannot have their value undefined. For this reason, if you define an optional attribute in your contract, you must set a default value for it.

Let us define 2 attributes. They will be used to pass the component title and description displayed on a page. Title will be required and description will be optional with the default value of no description.

{% define data = {
    name: required,
    description: 'no description'
} %}

In addition to the data contract, you can also add attributes that will be passed in the HTML tag of the component. The same as data attributes, they can be required or not.

For our component, we will use an attribute called element-selector. It will be used to specify the type of HTML elements to count. Let us add the attribute and make it required:

{% define attributes = {
    'element-selector': required
} %}

With the above configuration, if we want our component to count a tags, we need to embed it on a page as follows:

{% include molecule('new-component-counter') with {
                ...
                attributes: {
                    'element-selector': 'a'
                }
            } only %}

Finally, let us define the template. You can do this like you would normally do in Twig.

{% block body %}
    {% spaceless %}
       <strong class="{{config.name}}__name">
            {{data.name}}
       </strong>
      <em class="{{config.name}}__description">
            {% if data.description is not empty %}{{data.description}}{% endif %}
        </em>
        {% block counter %}
            Found <strong class="{{config.name}}__counter {{config.jsName}}__counter"></strong> elements
        {% endblock %}
    {% endspaceless %}
{% endblock %}

3. Create styles

Now, let us create visual styles used to display the component on a page. When creating styles, use BEM methodology. To link the style to the new component, the class name must be the same as the component name, also in Kebab Case.

Open file new-component-counter.scss file and add the following code:

.new-component-counter {
    &__name {
        display: block;
    }

    &__description {
        display: block;
        color: $setting-color-dark;
    }

    &__counter {
        color: $setting-color-alt;
    }

    &--big {
        @include helper-font-size(big);
    }
}

As shown in the example, you can use global variables, functions and mixins in your styles, for example $setting-color-alt or $setting-color-dark. They can be found in the vendor/spryker-shop/shop-ui/src/SprykerShop/Yves/ShopUi/Theme/default/styles folder. For more details, see the SASS Layer section in Atomic Frontend.

Also, the styles must be locatable by Webpack. For this purpose, we need to add them to the entry point of the component. Open the index.ts file and add the following line:

import './new-component-counter.scss';

4. Implement behavior

Finally, we need to implement the actual code that will count the elements. Open the new-component-counter.ts file.

The component we are creating is a molecule which is inherited from the Component model. Because of this, it must extend the Component base class defined in the ShopUI module. First, we need to import the base class:

import Component from 'ShopUi/models/component';

After that, we need to create the new component class extending the base class. The new component class must implement a DOM callback. You can use any callback defined by the Web Components Specification. When the component receives the callback you define, it should execute the behavioral logic.

It is recommended to use ready callback. This callback is triggered when the component is ready and all other components have already been loaded in the DOM. It is the safest approach from the point of view of DOM manipulation.

Let us implement the ready callback. Upon receiving the callback, the component will count the number of tags defined by element-selector.

To fulfill our goal, we can use keyword this. It provides direct access to the public API of the HTML element associated with the component.

Names of Javascript classes follow Camel Case, thus, the behavior of our component will be implemented by Javascript class NewComponentCounter:

export default class NewComponentCounter extends Component {
    protected counter: HTMLElement
    protected elements: HTMLElement[]

    protected readyCallback(): void {
        this.counter = <HTMLElement>document.querySelector(`.${this.jsName}__counter`);
        this.elements = <HTMLElement[]>Array.from(document.querySelectorAll(this.elementSelector));
        this.count();
    }

    count(): void {
        this.counter.innerText = `${this.elements.length}`;
    }

    get elementSelector(): string {
        return this.getAttribute('element-selector');
    }
}

After implementing the behavior, we also need to bind the Javascript class to the DOM. For this purpose, we need to use the register function of the Spryker Shop application. It accepts 2 arguments:

  • name - specifies the component name.This name will be associated with the component and can be used in Twig to insert the component in a template. Also, it will be used in the DOM as a tag name. Whenever a tag with the specified name occurs in the DOM, the Shop Application will load the component. It must be the same as the data.tag specified in the component Twig on step 2.

  • importer - must be a call of Webpack’s import function to import Typescript code for the component.

The call must include a Webpack magic comment that specifies which type of import you want for the component, ‘lazy’ or ‘eager’. For details, see Dynamic Imports.

In twig, we used tag name new-component-counter. Let us bind it to the Javascript class we created and use ‘lazy’ import. To do this, open file index.ts again and attach the following code:

// Import the 'register' function from the Shop Application
import register from 'ShopUi/app/registry';

// Register the component
export default register(
    'new-component-counter',
    () => import(/* webpackMode: "lazy" */'./new-component-counter')
);

5. Compile and use the component

Our component is almost complete. The only thing left is to compile it. Execute the following line in the console: npm run yves

When done, you can include it into other components, views and templates.

  • Copy file vendor/spryker-shop/shop-ui/src/SprykerShop/Yves/ShopUi/Theme/default/page-layout-main/page-layout-main.twig to src/Pyz/Yves/ShopUi/Theme/default/page-layout-main/page-layout-main.twig. Doing so overrides the default main page on the project level.

  • Add the following code to the very beginning of the <main> block. It will include our new component and configure it to count a tags:

{% include molecule('new-component-counter') with {
    modifiers: ['big'],
    data: {
        name: 'Counting a tags...',
        description: 'How many links are there on this page?'
    },
    attributes: {
        'element-selector': 'a'
    }
} only %}
See resulting file (page-layout-main.twig)
{% extends template('page-blank') %}

{%- block class -%}js-page-layout-main__side-drawer-container{%- endblock -%}

{% block body %}
    {% block notifications %}
        {% include organism('notification-area') only %}
    {% endblock %}

    {% block sidebar %}
        {% include organism('component-side-drawer') with {
            class: 'is-hidden-lg-xl',
            attributes: {
                'container-selector': 'js-page-layout-main__side-drawer-container',
                'trigger-selector': 'js-page-layout-main__side-drawer-trigger'
            }
        } only %}
    {% endblock %}

    {% block outside %}{% endblock %}

    {% block header %}
        {% embed organism('header') only %}
            {% block mobile %}
                <a href="#" class="link link--alt js-page-layout-main__side-drawer-trigger">
                    {% include atom('icon') with {
                        modifiers: ['big'],
                        data: {
                            name: 'bars'
                        }
                    } only %}
                </a>
            {% endblock %}
        {% endembed %}
    {% endblock %}

    <div class="container">
        {% block pageInfo %}
            <div class="box">
                {% block breadcrumbs %}
                    {% include molecule('breadcrumb') only %}
                {% endblock %}

                <hr />

                {% block title %}
                    <h3>{{data.title}}</h3>
                {% endblock %}
            </div>
        {% endblock %}

        <main>
            {#
                Use the new component
            #}
            {% include molecule('new-component-counter') with {
                modifiers: ['big'],
                    data: {
                    name: 'Counting a tags...',
                    description: 'How many links are there on this page?'
                },
                attributes: {
                    'element-selector': 'a'
                }
            } only %}
            {% block content %}{% endblock %}
        </main>

        {% block footer %}
                {% include organism('footer') only %}
        {% endblock %}

        {% block copyright %}
            <p class="text-center text-small text-secondary">
                Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
                <br>Aenean commodo ligula eget dolor. Aenean massa.
                <br>© ACME Company
            </p>
        {% endblock %}
    </div>

    {% block icons %}
        {% include atom('icon-sprite') only %}
    {% endblock %}
{% endblock %}

Now, open the front page of Spryker Shop. The new component will appear on the top of the page, below the header.

New component counter