Upgrade wf-html to v7 from v3-6
Intro
v7 comes with a rewritten editor/SSR code, based on Vue. As a consequence, all modules-related code using BackboneJS/Backbone Epoxy/jQuery must be rewritten. Amongst others, that means all the data-bind
and wf-bind
attributes.
Accessing properties
One of the biggest changes is in the way that you can address object properties. Due to limitations of Backbone/Epoxy, that didn't support nested objects, in previous versions all attributes were flattened, so you used page_id
, image_description
, currentPage_first_published_at
. In v7 these become: page.id
, image.description
, currentPage.firstPublishedAt
.
wf-module is not required anymore
For most cases, omitting wf-module
altogether should lead to the desired results. Check wf-module-guesser's documentation for details on how the module is guessed. If the guessed value doesn't correspond to the intended usage, you can still specify the module to be used using the wf-module
attribute.
Short module names
v7 still allows using wf-module="wfed/composite/module"
to specify the module type, but using the short name composite
is recommended. The list of built-in modules: composite
, body_text
, inline_text
, listing
, ad
, embed
.
NOTE: There is no wfed/free/html
equivalent in v7, use the embed
module with an HTML type
Text interpolation (data-bind=text)
By default, Vue uses {{
and }}
delimiters to interpolate text. As wf-html is being written in twig, that also uses the same, XalokNext changes Vue's text interpolation delimiters to [[
and ]]
.
Was:
<span data-bind="text:date('FORMAT', currentPage_first_published_at)"></span>
Is:
<span>[[ date('FORMAT', currentPage.firstPublishedAt) ]]</span>
Since the text
epoxy handler was changing the entire text, to output a text like "Publicado en 22.02.2022" a epoxy filter was needed to concatenate the "Publicado en" text in front of the formatted date. This is not needed anymore, you can interpolate text where you need to:
<span>Publicado en [[ date('FORMAT', currentPage.firstPublishedAt) ]]</span>
NOTE: the above is used only to illustrate not needing a JS function to concatenate strings, in "real world" usage, you're encouraged to use translations:
<span>{{ 'article.info.published_at'|trans({
'%date%': '[[ date('FORMAT', currentPage.firstPublishedAt) ]]'
}) }}</span>
The translator can then use %date%
placeholder in the translation, in the position where they want the date to appear to appear.
HTML Attributes (data-bind=attr)
To make an attribute dynamic, prefix its name with a semicolon (:
).
Was:
<span data-bind="attr: {'data-image-id': image_id}"></span>
Is:
<span :data-image-id="image.id"></span>
With Epoxy, the values passed to the handlers couldn't contain expressions, e.g. if two or more properties had to be passed to the handler, a filter was recommended to combine the values. Vue supports JS expressions as well, e.g.:
<span :data-image-details="JSON.stringify({id: image.id, title: image.title})"></span>
<!-- or -->
<span :data-image-details="`Title: ${image.title}, description: ${image.description}`"></span>
HTML classes (data-bind=classes)
These were usually used in conjunction with a module setting.
Was:
<wf-class name="hamburgermenuitem">
<title>{{ "settings.hamburger_menu.tiatle"|trans }}</title>
<option value="">{{ "settings.hamburger_menu.default"|trans }}</option>
<option value="current">{{ "settings.hamburger_menu.current"|trans }}</option>
</wf-class>
<script type="javascript" wf-script>
var computeds = {
classItem: () => this.getBinding('settings_hamburgermenuitem')
}
</script>
<a data-bind="classes:{ current : classItem }"></a>
Is:
<wf-class name="hamburgermenuitem">
<title>{{ "settings.hamburger_menu.tiatle"|trans }}</title>
<option value="">{{ "settings.hamburger_menu.default"|trans }}</option>
<option value="current">{{ "settings.hamburger_menu.current"|trans }}</option>
</wf-class>
<a :class="settings.hamburgermenuitem"></a>
NOTE: the intermediary computed is not needed anymore, use the settings
object directly.
Thumbnails (wf-bind=filter)
Was:
<img wf-bind="filter:image_800_600" />
Is:
<img wf-filter="image_800_600" />
For <picture>
elements:
Was:
<picture wf-bind="filter:image_800_600|image_480_300-min480|image_638_400-min600" loading="lazy"></picture>
Is:
<picture>
<source wf-filter="image_600_400" media="(min-width: 600px)" />
<source wf-filter="image_480_300" media="(min-width: 480px)" />
<img wf-filter="image_800_600" />
</picture>
Author avatar (wf-bind=avatar)
Read the next section
Video thumbnail (wf-bind=videoThumbnail)
The same wf-filter
wf-directive can be used (with a slightly altered syntax) to bind other image sources, like the thumbnail of a video or the avatar of the author of an article
Was:
<img wf-bind="avatar:currentPage_authors|image_50_50" />
Is:
<img wf-bind:image_50_50="currentPage.authors?.[0]?.avatar" />
NOTE: Due to the fact that attribute names are case insensitive in HTML, the Imagine filter name is being used as the wf-directive's arg
(:image_50_50
) and the image object is passed as value (currentPage.authors?.[0]?.avatar
).
For controlling other aspects of the wf-filter
wf-directive, read its documentation page
Author details (wf-bind=authorProperty)
Was:
<span wf-bind='authorProperty:currentPage_authors|/author/__slug__|name|target="_blank"'></span>
Is:
<a wf-href="currentPage.authors?.[0]" target="_blank">[[ currentPage.authors?.[0]?.name ]]</a>
Content model text (wf-bind=cmText)
Was:
<span wf-module="wfed/body_text/module"
wf-role="description"
wf-bind="cmText:image_description"
></span>
Is:
<span wf-module="body_text"
wf-role="description"
wf-cm-text="image.description"
></span>
A .fresh
modifier is supported by wf-cm-text
wf-directive where wf-bind="cmFreshText:..."
was being used:
<span wf-module="body_text"
wf-role="author_name"
wf-cm-text.fresh="currentPage.authors?.[0]?.name"
></span>
Formatting dates (wf-bind=date, data-bind=text:date)
If needed to be shown as text, read the Text interpolation section. If needed as HTML attributes, read the HTML attributes section.
Linking to contents (wf-bind=href, wf-bind=file)
Was:
// inside a composite module:
<a wf-bind="href"
data-bind="text:page_title"
></a>
Is:
// inside a composite module:
<a wf-href>[[ page.title ]]</a>
Unlike the v4-6 wf-bind=href
, that was limited to linking to page
content models only, wf-href
supports linking to any supported content model, e.g.:
<a wf-href="page.category">[[ page.category?.title ]]</a>
Or, linking to a category editable in the board:
<a wf-href="category"></a>
Or, linking to a file content model:
Was:
<a wf-bind="file"></a>
Is:
<a wf-href="file"
target="_blank"
:download="file.name"
></a>
Linking to a tag:
<ul>
<li v-for="tag in currentPage.simpleTags">
<a wf-href="tag">
[[ tag.title ]]
</a>
</li>
</ul>
Share URLs (wf-bind=shareUrl)
Was:
<a rel="nofollow" href="#" wf-bind="shareUrl:currentPage|https://www.facebook.com/sharer/sharer.php?u=__SLUG__.html">
Is:
<a rel="nofollow" :href="`https://www.facebook.com/sharer/sharer.php?u=${currentPage.slug}.html`">
Page supratitle (wf-bind=pageSupratitle)
Was:
<span wf-bind="pageSupratitle"></span>
Is:
<span>[[ page.supratitle || category.title ]]</span>
Module repeat (wf-bind=moduleRepeat, wf-bind=tags, data-bind=tags)
At the moment there is no full replacement for the moduleRepeat
handler - that is, the contents of a selection can be looped using Vue's v-for
, but their contents are not editable individually.
For automatically filling a text module with the authors names, see the second example of wf-cm-text.
For tags/secondary categories: note that the below example of the v6 code, taken from a live project, has the "limitation" that it allows editing the module's contents, but then the "Tags" section will show different contents than the actual tags. The v6 code was forced into this limitation because there was no other easy way of looping over collections.
Was:
<li wf-module="wfed/composite/module"
wf-role="tag"
wf-toolbar-position="none"
wf-allow=""
wf-new
wf-bind="moduleRepeat:currentPage_simpleTags"
wf-use-placeholder="true"
>
<a rel="tag" data-bind="attr:{title: item_title},text: item_title, href: hrefTag($item)">
</a>
</li>
Is:
<li
v-for="tag in currentPage.simpleTags"
>
<a rel="tag" :title="tag.title" wf-href="tag">
[[ tag.title ]]
</a>
</li>
Hiding elements (data-bind=toggle, data-bind=wfToggle)
In v4-6 an element couldn't be truly removed from the DOM on the admin side, removing it would've also removed the data-bind
attribute, so it couldn't have been brought back. For this, the wfToggle
epoxy handler was added to keep the toggle
handler's behaviour in the backend but removing it from the DOM during SSR. Vue's v-if
replacement doesn't have this limitation:
Was:
<ul data-bind="toggle:someFilterToDetermineWhetherThereAreTagsOrNot(currentPage_simpleTags)"></ul>
Is:
<ul v-if="currentPage.simpleTags.length"></ul>
Dynamic module (wfed/dynamic/generic, wf-bind=urlParams)
v4-v6 didn't have too many options for making content dynamic, so the wfed/dynamic/generic
was being used. Vue's templating language allows defining custom components that can handle these requirements, eliminating the need of working with modules (no more SidebarPageSerializer
needed 😄)
Was:
// in conjunction with `SidebarPageSerializer` to make sure the `sidebar` module was always present:
<div
wf-module="wfed/dynamic/generic"
wf-new
wf-role="sidebar"
wf-not-sortable
wf-toolbar-position="none"
wf-allow=""
wf-url="{{ path('cmm_article_sidebar', {id: "__id__"}) }}"
wf-bind="urlParams:id|currentPage_id"
class="u-grid-col-start-2-sm"
></div>
Is:
// you can safely delete `SidebarPageSerializer`
<wf-dynamic
:url="filters.path('app_article_sidebar', { id: currentPage.id })"
v-if="currentPage.id"
/>
Read details about using the path
filter here.
NOTE: The above v-if
gets rid of the GET http://admin.domain/article-sidebar/null?d=1678287028435 404 (Not Found)
error in the console that was showing up before the article was saved for the first time 😃
Highlight current page in a menu (wf-bind=currentUriClass)
Was:
<li
wf-module="wfed/composite/module"
wf-new
wf-role="item"
wf-bind="currentUriClass:active"
></li>
Is:
<li
wf-module="wfed/composite/module"
wf-new
wf-role="item"
wf-current-uri-class="active"
></li>
Custom modules (wfed/free/generic)
Was:
<div
wf-module="wfed/free/generic"
wf-new
wf-role="content_generic"
wf-toolbar-position="bottom"
wf-allow="+-"
wf-url="/custom_modules_json"
class="code"
></div>
Is:
<div
wf-module="wfed/free/generic"
wf-role="content_generic"
wf-new
wf-allow="+-">
</div>
NOTE: wf-url is not needed anymore. If you want to show only some available options, use wf-embed-types.
wfed/body_text/link
The wfed/body_text/link
module was removed, use wf-menu-item instead. It has the advantage that it uses an UI similar to the one used in the board for selecting articles/images/etc.
wfPollOptions binding expander
Polls' HTML can be created much more easily now, using Vue's templating language, without the need of having JS code generating it:
Was:
<div wf-module="wfed/composite/module"
wf-role="poll"
wf-toolbar-position="none"
wf-allow=""
wf-new
wf-bind="pollOptions"
Is:
<div wf-role="poll" wf-new>
[[ poll.question ]]
<ul>
<li v-for="option in poll.options">
<input type="radio"
:name="`poll-option-${poll.id}`"
:value="option.id"
:id="`poll-option-${poll.id}-${option.id}`">
<label :for="`poll-option-${poll.id}-${option.id}`">
[[ option.option_name ]]
</label>
</li>
</ul>
<div v-if="!poll?.id">{{ 'article.poll-choose'|trans }}</div>
</div>
Rad more details on the polls page.
Note: If you want to render the URL that should be used when voting, use path or url template filters.