wf-components
Intro
Warning
This feature is available in XalokNext starting with version 7.1.
What are wf-components
wf-components are a way of sharing wf-html code between templates.
Example: defining and using a wf-component
<!-- in src/App/Bundle/CmsBundle/Resources/views/Template/wfc/board/article-module.html.twig -->
<article
:wf-role="wfc.role"
wf-new>
<h3
wf-role="title"></h3>
</article>
<!-- in src/App/Bundle/CmsBundle/Resources/views/Template/Board/home/board1.html.twig -->
<div class="board1">
<wfc-board--article-module role="article1" />
<wfc-board--article-module role="article2" />
</div>
Motivation
Traditionally, in wf-html code has been reused by using the {% include %}
twig statement. But this can lead to huge HTML documents, since the same module can be included over and over. On v7, this HTML is handed to the vue-loader, that uses quite a lot of memory to parse it and produce the final JS code. In the case of a single board in one of the project, this included 5 different versions of the board, each with 3-6 different boxes for articles, each of these boxes could be switched between 11 different versions, some with 2-3 sub-boxes, each defining its own (duplicated) settings form with 3 fields. This lead in the single <div class="article-module">
container being included 195 times, leading to a final HTML of ~600KB. After being processed by wfcms4-loader, this HTML was converted to a 5MB vue file. Processing this vue file lead vue-loader to use ~1.5GB of RAM - only for this single file, and since the available RAM memory is scarce, this lead to using the swap disk, which in turn lead to very high compilation times (~5 minutes, in some cases).
wf-components solves this issue by eliminating the repeated HTML - define it once, then use the resulting component - a single tag instead of ~140 lines as it was in the case of this project's module.
For these reasons, wf-components is the recommended way to reuse code over using twig's {% include %}
in v7.
Progressive adoption
You can identify the largest HTML templates by using the following command:
ls -lR web/bundles/wfcmsbaseadmin/javascripts/html_template | grep '^-' | sort -k 5 -n | tail -n 20
Note: this command assumes you've already ran make wf-assets
or make dev
in the project.
It will list the 20 biggest HTML files in the project. As a rule of thumb, any HTML file over 300KB must be converted to use wf-components, any file over 100KB should be converted and files bigger than 50KB would be nice to be converted.
Article/default.html.twig
is (usually) another good candidate for conversion to wf-components, because this is usually extended by many other templates: platform specific versions (e.g. Article/default-amp.html.twig
, Article/default-instant.html.twig
), but also other article templates (e.g. Article/opinion.html.twig
, Article/brandslab.html.twig
- each with their specific platform versions). All these different templates usually result in a lot of duplicated wf-html code.
Usage
Defining a wf-component
To define a wf-component, add an .html.twig
file in the src/App/Bundle/CmsBundle/Resources/views/Template/wfc
directory (a wfc
subdirectory in the same Template
directory where the article/board templates are being defined)
Example: defining a wfc-body-text-module
wf-component, inside src/App/Bundle/CmsBundle/Resources/views/Template/wfc/body-text-module.html.twig
file:
<p wf-role="body_text"></p>
Using a wf-component
Every .html.twig
file inside the wfc
directory creates a component based on its file name. In the example above, the wf-component defined in wfc/body-text-module.html.twig
defines a wfc-body-text-module
wf-component.
Example: Using the wfc-body-text-module
wf-component:
<div class="body-modules">
<wfc-body-text-module></wfc-body-text-module>
</div>
Self closing tags
Do NOT self-close the tags, the HTML specification allows a few specific elements to omit close tags
Example:
<!-- BAD -->
<wfc-body-text-module />
<!-- GOOD -->
<wfc-body-text-module></wfc-body-text-module>
The components can be grouped into subdirectories, every /
in the path is converted to a double dash (--
):
Example:
wfc/home/board1-v1.html.twig -> wfc-home--board1-v1
wfc/home/board1/v1.html.twig -> wfc-home--board1--v1
Naming conventions
- Use only lowercase letters in the names of the files (for the same reasoning that Vue also recommends using lowercase letters when using Vue components: https://vuejs.org/guide/essentials/component-basics.html#case-insensitivity)
- Use a single dash (
-
) as a word separator
Example:
// BAD
wfc/textModule.html.twig
// BAD
wfc/textModule_v1.html.twig
// GOOD
wfc/text-module-v1.html.twig
Passing props to wf-components
You can pass props to a wf-component by setting attributes when using them. All these props will be available using the wfc.
prefix.
Example: Passing and using props
<!-- in Article/default.html.twig -->
<div class="body-modules">
<wfc-body-text-module role="paragraph" class="paragraph"></wfc-body-text-module>
<wfc-body-text-module role="quote" class="quote"></wfc-body-text-module>
</div>
<!-- in wfc/body-text-module.html.twig -->
<p :wf-role="wfc.role" :class="wfc.class"></p>
Note: XalokNext uses the same convention as Vue for binding dynamic values to the wf-directives by prefixing the attribute name with a semicolon (:
).
Prop names with multiple words
A prop with a dash (-
) in its name can be referenced by its camelCased version:
Example: Passing and using a prop with a dash (-
) in its name:
<!-- in Article/default.html.twig -->
<div class="body-modules">
<wfc-body-text-module
role="paragraph"
container-class="paragraph"
module-class="text-small"></wfc-body-text-module>
</div>
<!-- in wfc/body-text-module.html.twig -->
<div :class="wfc.containerClass">
<p wf-role="paragraph" :class="wfc.moduleClass"></p>
</div>
Using props as part of expressions
Props can be used as part of any JavaScript expression
Example:
<!-- using a prop with string interpolation -->
<div :class="`article-${wfc.class}`">...</div>
<!-- using a prop with string concatenation -->
<div :class="'article-' + wfc.class">...</div>
<!-- using a prop inside a filter -->
<div data-placeholder="filters.trans(wfc.placeholder)">...</div>
Default prop values
You can define a default value for a prop:
Example:
<script wfc-defaults>
const role = "paragraph";
const allow = "+-";
</script>
<p :wf-role="wfc.role" :wf-allow="wfc.allow" />
If a wf-component is using a prop that doesn't have a default value and you're not passing that prop when using the wf-component, Vue will log a warning in the console:
[Vue warn]: Missing required prop: "allow"
at ...
Non-string props
If you define a default prop value with a Number or Boolean type, you must prefix it with a semicolon (:
) when using the prop:
Example:
<!-- in src/App/Bundle/CmsBundle/Resources/views/Template/wfc/module.html.twig -->
<script wfc-defaults>
// Note: `new` is a reserved keyword in JS, `wf-*` is used internally by XalokNext,
//this is why we're using `nbNew` here
const nbNew = 1;
const hasEpigraph = false;
</script>
<div wf-role="article" :wf-new="wfc.nbNew">
<div v-if="wfc.hasEpigraph">
<p wf-role="epigraph"></p>
</div>
</div>
<!-- in src/App/Bundle/CmsBundle/Resources/views/Template/Article/default.html.twig -->
<!-- Note: passing the value prefixing the property name with a `:` -->
<!-- `:nb-new', instead of `nb-new` -->
<!-- `:has-epigraph', instead of `has-epigraph` -->
<wfc-module :nb-new="2" :has-epigraph="true"></wfc-module>
Otherwise Vue will generate a warning
[Vue warn]: Invalid prop: type check failed for nb-new="2". Expected Number, got String.
or
[Vue warn]: Invalid prop: type check failed for has-epigraph="true". Expected Boolean, got String.
Dynamic wf-directives values
Passed props can be used to pass values to wf-directives. For this, prefix the name of the wf-directive with a semicolon (:
):
Example:
<p :wf-role="wfc.role"></p>
Supported wf-directives
The code of each wf-directive must be tweaked to accept dynamic values. At the moment, only the following wf-directives accept dynamic values:
- wf-allow
- wf-article-types
- wf-filter
- wf-group
- wf-max ( v7.2+)
- wf-new
- wf-role
- wf-toolbar-position
- wf-embed-types
Adding support to a wf-directive
First, the wf-directive code should be changed to use the isExpression binding key and treat its value accordingly, if necessary.
Then support for evaluating its value at runtime must be added in this file .
Custom setup script
You can include a custom setup script inside a wf-component:
<script setup>
const getBorderWidth = (settings) =>
settings.type === 'thick' ? 5 : 1;
</script>
<div
wf-role="test"
wf-new
:style="`border: ${getBorderWidth(settings)}px solid red;`">
<wf-setting name="type">
<title>Border width</title>
<option value="">Thin</option>
<option value="thick">Thick</option>
</wf-setting>
test module
</div>
Note: Module's data is not available inside the custom <script setup>
, this is why it's being passed from the wf-component's template.
IDE support
PhpStorm
wfcms4-loader
emits a web-types.json file in web/dist/web-types.json
. To use it in your project, edit package.json
of the project and add:
{
...
"web-types": "./web/dist/web-types.json",
...
}
This will offer autocompletion for the wf-components, both for the names of the components and for the attributes (props). It also warns you if the wf-component has missing props.
At the moment "Go to declaration" doesn't work - there's a github issue filed for it.
VSCode
wfcms4-loader
emits a html.html-data.json file in web/dist/html.html-data.json
. To use it, edit (or create) .vscode/settings.json
and add:
{
...
"html.customData": ["./web/dist/html.html-data.json"],
...
}
This will offer autocompletion for the wf-components, both for the names of the components and for the attributes (props). Unlike PhpStorm, VSCode doesn't warn if there are missing required props and, it also doesn't seem to support "Go to definition" for custom HTML tags.