This Guide will teach you to create a new CMS block and element via a plugin.
Let's get started with adding your first custom block. By default, Shopware 6 comes with several blocks, such as a block called "image_text". It renders an image element on the left side and a simple text element on the right side. For this HowTo, you're going to create a new block to swap those two elements, so the text is on the left side and the image on the right side.
All blocks can be found in the directory text
, image
, text-image
and commerce
.
text
image
text-image
commerce
Creating a new block means adjusting the administration with your plugin, and that's what you're going to do first.
Your plugin's structure should always match the core's structure. When thinking about creating a new block, you should
recreate the directory structure of core blocks in your plugin.
The block, which you're going to create, consists of an image
and a text
element, so it belongs to the category text-image
.
Thus, create the directory <plugin root>/src/Resources/app/administration/src/module/sw-cms/blocks/text-image
.
In there, you have to create a new directory for each block you want to create, the directory's name representing
the block's name. For this example, the name image-text-reversed
is going to be used, so create this directory in there.
Now create a new file index.js
inside the image-text-reversed
directory, since it will be automatically loaded
when importing this block in your main.js
.
Speaking of that, right after having created the index.js
file, you can actually import your new block directory in
the main.js
file already:
import './module/sw-cms/blocks/text-image/image-text-reversed';
Back to your index.js
, which is still empty.
In order to register a new block, you have to call the registerCmsBlock
method of the cmsService.
Since it's available in the Dependency Injection Container, you can fetch it from there.
First of all, access our Applicaton
wrapper, which will grant you access to the DI container. This Application
wrapper has access to the DI container, so go ahead and fetch the cmsService
from it and call the
mentioned registerCmsBlock
method.
Shopware.Service('cmsService').registerCmsBlock();
The method registerCmsBlock
takes a configuration object, containing the following necessary data:
Go ahead and create this configuration object yourself. Here's what it should look like after having set all of those options:
Shopware.Service('cmsService').registerCmsBlock({
name: 'image-text-reversed',
label: 'sw-cms.blocks.textImage.imageTextReversed.label',
category: 'text-image',
component: 'sw-cms-block-image-text-reversed',
previewComponent: 'sw-cms-preview-image-text-reversed',
defaultConfig: {
marginBottom: '20px',
marginTop: '20px',
marginLeft: '20px',
marginRight: '20px',
sizingMode: 'boxed'
},
slots: {
left: 'text',
right: 'image'
}
});
The properties name
and category
do not require further explanation.
But you need to create a snippet files in you plugin directory for the label
property.
To do this, create a folder with the name snippet
in your sw-cms
folder. After that create the files for the languages. For example de-DE.json
and en-GB.json
.
The content of your snippet file should look something like this:
{
"sw-cms": {
"blocks": {
"imageText": {
"imageTextReversed": {
"label": "Bild mit Text (gedreht)"
}
}
}
}
}
For both fields component
and previewComponent
, components that do not yet exist were applied. Those will be created
in the next few steps as well.
The defaultConfig
just gets some minor margins and the sizing mode 'boxed', which will result in a CSS class is--boxed being applied
to that block later.
The slots are defined by an object, where the key represents the slot's name and the value being the technical name of the element to be used in this slot. This will be easier to understand when having a look at the respective template in a few minutes. Instead of using the technical name of an element, you could also apply another configuration to a slot.
This is not necessary for this HowTo, but here's an example of what this would look like:
slots: {
'name-of-the-slot': {
type: 'name-of-the-element-to-be-used',
default: {
config: {
displayMode: { source: 'static', value: 'cover' }
},
data: {
media: {
url: '/administration/static/img/cms/preview_camera_large.jpg'
}
}
}
}
}
In the next step, you're going to create the component for the actual representation of the block in the administration.
You've set the component
to be used when rendering your block to be 'sw-cms-block-image-text-reversed'.
This component does not exist yet, so let's create this one real quick.
First of all, create a new directory component
in your block's directory. In there, create a new index.js
file
and register your custom component sw-cms-block-image-text-reversed
.
import template from './sw-cms-block-image-text-reversed.html.twig';
import './sw-cms-block-image-text-reversed.scss';
Shopware.Component.register('sw-cms-block-image-text-reversed', {
template
});
Just like most components, it has a custom template and also some styles.
Focus on the template first, create a new file sw-cms-block-image-text-reversed.html.twig
.
This template now has to define the basic structure of your custom block. In this simple case, you only need a
parent container and two sub-elements, whatever those are.
That's also were the slots come into play: You've used two slots in your block's configuration, left
and right
.
Make sure to create those slots in the template as well now.
{% block sw_cms_block_image_text_reversed %}
<div class="sw-cms-block-image-text-reversed">
<slot name="left">{% block sw_cms_block_image_text_reversed_slot_left %}{% endblock %}</slot>
<slot name="right">{% block sw_cms_block_image_text_reversed_slot_right %}{% endblock %}</slot>
</div>
{% endblock %}
You've got a parent div
containing the two required slots. If you were to rename the first slot left
to something else,
you'd have to adjust this in your block's configuration as well.
Those slots would be rendered from top to bottom now, instead of from left to right.
That's why your block comes with a custom .scss
file, create it now.
In there, simply use a grid here to display your elements next to each other.
You've set a CSS class for your block, which is the same as its name.
.sw-cms-block-image-text-reversed {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
grid-gap: 40px;
}
That's it for this component! Make sure to import your component
directory in your index.js
file, so your
new component actually gets loaded.
import './component';
Shopware.Service('cmsService').registerCmsBlock({
...
});
Your block can now be rendered in the designer. Let's continue with the preview component.
You've also set a property previewComponent
containing the value sw-cms-preview-image-text-reversed
.
Time to create this component as well. For this purpose, stick to the core structure again and create a new directory
preview
.
In there, again, create an index.js
file, register your component by its name and load a template and a .scss
file.
import template from './sw-cms-preview-image-text-reversed.html.twig';
import './sw-cms-preview-image-text-reversed.scss';
Shopware.Component.register('sw-cms-preview-image-text-reversed', {
template
});
The preview element doesn't have to deal with mobile viewports or anything alike, it's just a simplified preview of your block. Thus, simply create a template containing a text and an image and use the styles to place them next to each other.
The template:
{% block sw_cms_block_image_text_reversed_preview %}
<div class="sw-cms-preview-image-text-reversed">
<div>
<h2>Lorem ipsum dolor</h2>
<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr.</p>
</div>
<img :src="'/administration/static/img/cms/preview_mountain_small.jpg' | asset">
</div>
{% endblock %}
Just a div containing an example text and an example image next to each other. For the styles, you can simply use the grid property of CSS again. Since you don't have to care about mobile viewports, this is even easier this time.
.sw-cms-preview-image-text-reversed {
display: grid;
grid-template-columns: 1fr 1fr;
grid-column-gap: 20px;
padding: 15px;
}
Two-columns, some padding and spacing, done.
Import this component in the index.js
of the block as well.
The index.js
should look like this now:
import './component';
import './preview';
Shopware.Service('cmsService').registerCmsBlock({
name: 'image-text-reversed',
label: 'Text next to image',
category: 'text-image',
component: 'sw-cms-block-image-text-reversed',
previewComponent: 'sw-cms-preview-image-text-reversed',
defaultConfig: {
marginBottom: '20px',
marginTop: '20px',
marginLeft: '20px',
marginRight: '20px',
sizingMode: 'boxed'
},
slots: {
left: 'text',
right: 'image'
}
});
Now go ahead and install your plugin via ./psh.phar plugin:install --activate CustomCMSBlock
and re-build the administration via
./psh.phar administration:build
.
You should now be able to use your new block in the "Shopping Experiences" module.
While your new block is fully functional in the administration already, you've never defined a Storefront template for it.
A block's storefront representation is always expected in the directory platform/src/Storefront/Resources/views/storefront/block. In there, a twig template named after your block is expected.
So go ahead and re-create that structure in your plugin:
<plugin root>/src/Resources/views/storefront/block/
In there create a new twig template named after your block, so cms-block-image-text-reversed.html.twig
that is.
Since the original 'image_text' file is already perfectly fine, you can simply extend from it in your storefront template.
{% sw_extends '@Storefront/storefront/block/cms-block-image-text.html.twig' %}
And that's it for the Storefront as well in this example! Make sure to have a look at the other original templates to get an understanding of how the templating for blocks works.
Imagine you'd want to create a new element to display a YouTube video. The shop manager can configure the link of the video to be shown and maybe add some more configurations, such as 'Show controls', which hides / shows the control elements of a YouTube video.
That's exactly what you're going to build in this HowTo.
Creating a new element requires you to extend the administration.
Your plugin's structure should always match the core's structure. When thinking about creating a new element, you should
recreate the directory structure of core elements in your plugin.
Thus, recreate this structure in your plugin:
<plugin root>/src/Resources/app/administration/src/module/sw-cms/elements
In there you create a directory for each new element you want to create, in this example a directory youtube
is created.
Now create a new file index.js
inside the youtube
directory, since it will be automatically loaded
when importing this element in your main.js
.
Speaking of that, right after having created the index.js
file, you can actually import your new element's directory in
the main.js
file already:
import './module/sw-cms/elements/youtube';
Now open up your empty index.js
file. In order to register a new element to the system, you have to call the method registerCmsElement
of the cmsService.
Since it's available in the Dependency Injection Container, you can fetch it from there.
First of all, access our Applicaton
wrapper, which will grant you access to the DI container. This Application
wrapper has access to the DI container, so go ahead and fetch the cmsService
from it and call the
mentioned registerCmsElement
method.
Shopware.Service('cmsService').registerCmsElement();
registerCmsElement
takes a configuration object, containing the following necessary data:Go ahead and create this configuration object yourself. Here's what it should look like after having set all of those options:
Shopware.Service('cmsService').registerCmsElement({
name: 'youtube',
label: 'sw-cms.elements.customYouTubeElement.label',
component: 'sw-cms-el-youtube',
configComponent: 'sw-cms-el-config-youtube',
previewComponent: 'sw-cms-el-preview-youtube',
defaultConfig: {
videoSrc: {
source: 'static',
value: 'Y4mGIZZL8jA'
},
showControls: {
source: 'static',
value: true
}
}
});
The property name
does not require further explanation.
But you need to create a snippet files in you plugin directory for the label
property.
For all three fields component
, configComponent
and previewComponent
, components that do not yet exist were applied. Those will be created
in the next few steps as well.
The defaultConfig
defines the default values for the element's configurations. There will be a text field to enter a YouTube video's ID, videoSrc
, and a
toggle to en-/disable the option to show the control elements in the YouTube video, showControls
.
By default, it will now use the ID of a Shopware video and the controls will be shown.
Now you have to create the three missing components, let's start with the preview component.
Create a new directory preview
in your element's directory youtube
. In there, create a new file index.js
, just like for all components.
Then register your component, using the Component
wrapper.
import template from './sw-cms-el-preview-youtube.html.twig';
import './sw-cms-el-preview-youtube.scss';
Shopware.Component.register('sw-cms-el-preview-youtube', {
template
});
Just like most components, it has a custom template and also some styles.
Focus on the template first, create a new file sw-cms-el-preview-youtube.html.twig
.
So what do you want to show here? Maybe the default 'mountain' preview image, that's already being used for the image
element.
And on top of that, you could place our icon multicolor-action-play
. Head over to your icon library to find this icon.
That means: You'll need a container to contain both the image and the icon.
In there, you create an img
tag and use the sw-icon component to display the icon.
{% block sw_cms_element_youtube_preview %}
<div class="sw-cms-el-preview-youtube">
<img class="sw-cms-el-preview-youtube-img" :src="'/administration/static/img/cms/preview_mountain_small.jpg' | asset">
<sw-icon class="sw-cms-el-preview-youtube-play" name="multicolor-action-play"></sw-icon>
</div>
{% endblock %}
The icon would now be displayed beneath the image, so let's add some styles for this by creating the file sw-cms-el-preview-youtube.scss
.
The container needs to have a position: relative;
style. This is necessary, so the child sw-icon
can be positioned absolute and will do so
relative to the container's position.
Thus, the icon receives a position: absolute;
style, plus some top
and left
values to center it.
.sw-cms-el-preview-youtube {
position: relative;
.sw-cms-el-preview-youtube-play {
$icon-height: 80px;
$icon-width: $icon-height;
position: absolute;
height: $icon-height;
width: $icon-width;
left: calc(50% - #{$icon-width/2});
top: calc(50% - #{$icon-height/2});
}
}
The centered positioning is realised by using 50% on top
and left
. Since that would be 50% from the upper left corner of the icon, this wouldn't really center
the icon yet. Subtract the half of the icon's width and height and then you're fine.
Two more things missing here. First of all, the image itself has no styling yet. Just make it scale to 100% of its parent's size.
.sw-cms-el-preview-youtube {
position: relative;
.sw-cms-el-preview-youtube-img {
margin: 0;
display: block;
max-width: 100%;
}
...
}
Second, the icon is grey by default. Since our sw-icon
component will render an .svg
, you can apply styles to the elements.
In this case, it's mainly a circle
element. Use the YouTube red for the fill color here: #FF0000
This is what your preview's final .scss
should look like now:
.sw-cms-el-preview-youtube {
position: relative;
.sw-cms-el-preview-youtube-img {
margin: 0;
display: block;
max-width: 100%;
}
.sw-cms-el-preview-youtube-play {
$icon-height: 80px;
$icon-width: $icon-height;
position: absolute;
height: $icon-height;
width: $icon-width;
left: calc(50% - #{$icon-width/2});
top: calc(50% - #{$icon-height/2});
circle {
fill: #FF0000;
opacity: 0.9;
}
}
}
One last thing: Import your preview component in your element's index.js
file, so it's loaded.
import './preview';
Shopware.Service('cmsService').registerCmsElement({
...
}
The next would be the main component sw-cms-el-youtube
, the one to be rendered when the shop manager actually decided to use your element by clicking
on the preview.
Thus, you want to show the actually configured video here now.
Start with the basic again, create a new directory component
, in there a new file index.js
and then register your component sw-cms-el-youtube
.
import template from './sw-cms-el-youtube.html.twig';
import './sw-cms-el-youtube.scss';
Shopware.Component.register('sw-cms-el-youtube', {
template
});
Also create the template file sw-cms-el-youtube.html.twig
and the .scss
file sw-cms-el-youtube.scss
.
The template doesn't have to include a lot. Having a look at how YouTube video embedding works, you just have to add an iframe
with an src
attribute pointing to the video.
{% block sw_cms_element_youtube %}
<div class="sw-cms-el-youtube">
<iframe :src="videoSrc"></iframe>
</div>
{% endblock %}
You can't just use a static src
here, since the shop manager wants to configure the video he wants to show. Thus, we're fetching
that link via VueJS now.
Let's add the code to provide the src
for the iframe. For this case you're going to use a computed property of VueJS.
import template from './sw-cms-el-youtube.html.twig';
import './sw-cms-el-youtube.scss';
Shopware.Component.register('sw-cms-el-youtube', {
template,
computed: {
videoSrc() {
return 'https://www.youtube.com/embed/' + this.element.config.videoSrc.value;
}
}
});
The link being used has to follow this pattern: https://www.youtube.com/embed/<videoId>
, so the only variable you need from the shop manager
is the video ID.
And that's what you're doing here - you're building the link like mentioned above and you add the value of videoSrc
from the config.
This value will be provided by the config component, that you're going to create in the next step.
In order for this to work though, you have to call the method initElementConfig
from the cms-element
mixin.
This will take care of dealing with the configComponent
and thus providing the configured values.
import template from './sw-cms-el-youtube.html.twig';
import './sw-cms-el-youtube.scss';
const { Component, Mixin } = Shopware;
Component.register('sw-cms-el-youtube', {
template,
mixins: [
Mixin.getByName('cms-element')
],
computed: {
videoSrc() {
return 'https://www.youtube.com/embed/' + this.element.config.videoSrc.value;
}
},
created() {
this.createdComponent();
},
methods: {
createdComponent() {
this.initElementConfig('youtube');
},
}
});
Now the method initElementConfig
is immediately executed once this component was created.
Also, you could already add the showControls
config here as well.
The showControls
config has to be applied to the YouTube link itself, by adding another GET parameter controls
, so
adjust the link in the videoSrc
computed property.
computed: {
videoSrc() {
return 'https://www.youtube.com/embed/'
+ this.element.config.videoSrc.value
+ '?controls='
+ (this.element.config.showControls.value ? 1 : 0);
}
},
If the configuration of showControls
is set to true
, set the GET value to 1, otherwise 0.
Time to add the last remaining part of this component. The styles to be applied.
Since YouTube takes of responsive layouts itself, you just have to scale the iFrame to 100% width and 100% height.
Yet, there's a recommended min-height
of 315px, so add that one as well.
.sw-cms-el-youtube {
height: 100%;
width: 100%;
min-height: 315px;
iframe {
height: 100%;
width: 100%;
}
}
That's it for this component! Import it in your element's index.js
file.
import './component';
import './preview';
Shopware.Service('cmsService').registerCmsElement({
...
}
Let's head over to the last remaining component. Create a directory config
, an index.js
file in there and register your config component sw-cms-el-config-youtube
.
import template from './sw-cms-el-config-youtube.html.twig';
const { Component, Mixin } = Shopware;
Component.register('sw-cms-el-config-youtube', {
template,
mixins: [
Mixin.getByName('cms-element')
],
created() {
this.createdComponent();
},
methods: {
createdComponent() {
this.initElementConfig('youtube');
}
}
});
Just like always, it comes with an template, no styles necessary here though. Create the template file now.
Also, the initElementConfig
method has to be called in here as well, just the same way like you've done in your main component.
A little spoiler: This file will remain like this already, you can close it now.
Open up the template sw-cms-el-config-youtube.html.twig
instead.
What do we need to be displayed in the config?
Just a text element, so the shop manager can apply a YouTube video ID, and a toggle to en-/disable the controls.
Quite simple, right?
{% block sw_cms_element_image_config %}
<div class="sw-cms-el-config-youtube">
<sw-field class="swag-youtube-field"
v-model="element.config.videoSrc.value"
type="text"
label="YouTube Video ID"
placeholder="Enter ID...">
</sw-field>
<sw-field class="sw-cms-el-config-youtube__show-controls"
v-model="element.config.showControls.value"
type="switch"
label="Show video controls">
</sw-field>
</div>
{% endblock %}
This would render the sw-field
component two times. Once as a text
field, once as a switch
field. The v-model
takes care
of binding the field's values to the values from the config.
Don't forget to include your config in your index.js
.
import './component';
import './config';
import './preview';
Shopware.Service('cmsService').registerCmsElement({
...
}
That's it! You could now go ahead and fully test your new element!
Install this plugin via bin/console plugin:install --activate CustomCmsElement
, rebuild the administration via ./psh.phar administration:build
and start using your new element in the administration!
Of course, the Storefront implementation is still missing, so your element wouldn't be rendered in the Storefront yet.
Just like the CMS blocks, each element's storefront representation is always expected in the directory platform/src/Storefront/Resources/views/storefront/element.
In there, a twig template named after your custom element is expected, in this case a file named cms-element-youtube.html.twig
is expected.
So go ahead and re-create that structure in your plugin:
<plugin root>/src/Resources/views/storefront/element/
In there create a new twig template named after your element, so cms-element-youtube.html.twig
that is.
The template for this is super easy though, just like it's been in your main component for the administration. Just add an iFrame again. Unfortunately, styles have to be applied using inline-styles as of now. This is to be changed and updated in the next few days, just stay tuned. Simply apply the same styles like in the administration, 100% to both height and width that is.
{% block element_youtube %}
<div class="cms-element-youtube" style="height: 100%; width: 100%">
{% block element_image_inner %}
<iframe style="min-height:315px; height: 100%; width: 100%;"
src="https://www.youtube.com/embed/{{ element.config.videoSrc.value }}?controls={{ element.config.showControls.value|number_format }}"
allowfullscreen>
</iframe>
{% endblock %}
</div>
{% endblock %}
The URL is parsed here using the twig variable element
, which is automatically available in your element's template.
Once more: That's it! Your element is now fully working! The shop manager can choose your new element in the 'Shopping Experiences' module, he can configure it and even see it being rendered live in the administration. After saving and applying this layout to e.g. a category, this element will also be rendered into the storefront!