Experience Builder (XB)
Since 4.0.0CrafterCMS’ Experience Builder (XB) provides a UI layer on top of your applications that enables authors with in-context editing (ICE) for all the model fields defined in the content types of pages and components. CrafterCMS developers must integrate their applications with XB, essentially telling XB what field of the model each element on the view represents. See Content Modeling to learn more about the model.
Below is an example page with a sample content type side by side showing the relation between page elements and content type fields:
If you`re starting from a 3.x ICE system, see Upgrading JavaScript Applications’ In Context Editing for more information on how to move from the 3.x ICE system to the 4.x Experience Builder (XB) system discussed here.
Creating Experience Builder (XB) Enabled Sites
The concrete integration strategy with XB depends on what kind of application you are developing, however, overall all you need to do is mark the content element that display CrafterCMS content and initialize XB. CrafterCMS provides native XB integration for FreeMarker and React applications. Other types of applications (e.g. Angular, Vue, etc.) can be integrated with XB through the underlying libraries that power the FreeMarker and React applications. For reference on how to integrate, please see the sections below for your specific kind of application.
To integrate with XB, all you need to do is:
Register or mark CrafterCMS content field elements: you tell the system what HTML element represents any give model field. Registration can be manual (i.e. invoking specific methods from the XB JavaScript libraries), or by putting a specific set of attributes on each tag.
Initialize XB. Which can also be done manually or by invoking specific methods of the XB JavaScript libraries.
Overall, XB’s ICE engine works with a coordinate system that you — the developer — use to tell the CMS which field of the content type each element/component on your page/app maps to.
The coordinate system consists of the following pieces of data:
Path: the path to the content item (e.g.
/site/components/features/main_feature.xml
)Model ID (a.k.a. object ID, local ID): the internal GUID that every content object in CrafterCMS has (e.g.
5a06e244-64f4-4380-8619-1c726fe38e92
)Field ID: the ID of the field in the content type (e.g.
heroTitleText_t
)Field IDs may be compound, comprised of the full path to that field when such field is nested within repeat groups (e.g.
carouselSlides_o.slideTitle_t
)
Index: When working with collections (e.g. component selectors or repeat groups), the index of the item within it’s container collection (e.g.
0
)Indexes can be compound, comprised of the full path of indexes to that item in the collection (e.g.
0.1
)
XB’s ICE engine requires, at times, what might be considered slightly more verbose markup structure. In order for the system to be able to direct authors to every piece of the model, as well as allowing them to edit inline, you need to register each piece of the model as an element on your view.
For example, consider a carousel, where the carousel is modelled as a CrafterCMS component that has
a repeat group field called slides_o
which has two inner fields called caption_s
and image_s
.
The markup for a carousel may look like this:
<div class="carousel">
<div class="slide">
<img src="slide1.png" alt="">
<h2>Slide One</h2>
</div>
<div class="slide">
<img src="slide2.png" alt="">
<h2>Slide Two</h2>
</div>
</div>
In order to register each piece of the model, we would need to introduce a new element.
<div class="carousel"> <!-- Component (Carousel) -->
<div> <!-- Repeating group (slides_o) — Additional element introduced -->
<div class="slide"> <!-- Repeat group item (slides_o[0]) -->
<img src="slide1.png" alt=""> <!-- Repeat group item field (slides_o[0].images_s) -->
<h2>Slide One</h2> <!-- Repeat group item field (slides_o[0].caption_s) -->
</div>
<div class="slide"> <!-- Repeat group item (slides_o[1]) -->
<img src="slide2.png" alt=""> <!-- Repeat group item field (slides_o[1].images_s) -->
<h2>Slide Two</h2> <!-- Repeat group item field (slides_o[1].caption_s) -->
</div>
</div>
</div>
You can vary exactly where to add this additional element to suit your needs — or those of the libraries and frameworks that you use to develop your applications. The important aspects are that each field is represented by an element on the page/app and that the hierarchy of the fields is followed by the hierarchy of your markup.
Meaning, the component element is the parent of the repeat group element which is a parent of the repeat group items which are parents of the repeat group item fields, as shown below:
component
repeat-group
item
item-fields
For example, you could move the additional div
to be the top wrapper, and hence represent the component
instead of the repeat group. Naturally, the repeat group would then be represented by the div
with the
carousel class.
<div> <!-- Component (Carousel) -->
<div class="carousel"> <!-- Repeating group (slides_o) -->
...
</div>
</div>
XB Integration Guidelines
The HTML element that is registered with XB as a field must contain only that content, unwrapped.
Elements that represent fields of type text, html and other simple values, should print the content value directly inside of them without intermediate elements.
Incorrect
Correct
<!-- Author field (author_s) --> <div class="byline"> by ${author_s} </div>
<div class="byline"> by <!-- Author field (author_s) --><span>${author_s}</span> </div>
Elements that represent collections (i.e. repeat groups or component collections), must have their item elements as direct children.
Incorrect
Correct
<!-- Component collection field (components_o) --> <div> <div class="column"> <!-- Component collection item (components_o) --> <div class="feature> ... </div> </div> </div>
<!-- Component collection field (components_o) --> <div> <!-- Component collection item (components_o) --> <div class="column"> <div class="feature> ... </div> </div> </div>
XB makes available a set of classes and JavaScript to help in best supporting the authoring experience of your applications.
craftercms-ice-on
: When edit mode is active, thehtml
element gets this class.craftercms-ice-bypass
: When/while thez
key is pressed, thehtml
element gets this class.craftercms-highlight-move
: When edit mode is active and mode is move, thehtml
element gets this class.craftercms-highlight-all
: When edit mode is active and mode is edit (not move), thehtml
element gets this class.Empty fields are likely to have a height/width of 0 pixels, making them invisible for authors trying to edit those fields. XB makes available classes to be added to the the container element of the field which will add some padding and text content making the empty element visible and indicating it is an empty field to catch the author’s attention. The specific styles can be customized using the
globalStyleOverrides
prop of the ExperienceBuilder component either directly when using React, or through theprops
argument of the initExperienceBuilder macro. *craftercms-empty-collection
: Use it for collection type fields such as repeat groups and item selectors. *craftercms-empty-field
: Use it for simple fields such as text, html, etc.craftercms-drag-n-drop-active
: When drag and drop is active, thehtml
element gets this class.craftercms-edit-mode-padding
: When padding mode is active, thehtml
element gets this class. You may use the class to conditionally add padding during edit mode to the elements that may be hard to reach for authors due to the html structure and resulting rendering.data-craftercms-event-capture-overlay
: Elements like video players or iframes can sometimes interfere with the normal flow of pointer events and make it challenging for authors to access in-context editing for the field. Adding this attribute to an element wrapping one of these event-capturing elements (e.g. an iframe), will put styles in place to prevent it from blocking XB from getting the necessary pointer events for authors to edit the model. See also eventCaptureOverlay for usage on Freemaker.craftercms.editMode
: When edit mode is turned on/off, XB will dispatch this custom event on thedocument
object. You can listen to this event to trigger custom actions when the edit mode is turned on/off. For example, turn off a carousel from auto-rotating which could make it challenging for authors to edit. The event detail will be a boolean indicating if the edit mode is on or off.craftercms.xb:loaded
: When XB is fully loaded and ready to be used, XB will dispatch this custom event on thedocument
object. You can listen to this event to trigger custom actions when XB is fully loaded and ready to be used. For example, use a function that’s part of thecraftercms.xb
global (e.g. craftercms.xb.getICEAttributes({ … })), which might not be available before XB is loaded.craftercms.iceBypass
: On thez
(XB bypass key) keydown or keyup, XB will dispatch this custom event on thedocument
object. You can listen to this event to trigger custom actions whenz
is pressed. The event detail will be a boolean indicating if the key is pressed or not.
FreeMarker
In FreeMarker applications, in order to integrate with XB, you will use the macros provided by CrafterCMS, which in turn will set all the right hints (i.e. html attributes) on the markup for the ICE engine to make things editable to authors.
As mentioned earlier, you need to give XB’s ICE engine the coordinates to identify each model/field, so, in addition to their other arguments, each macro receives the following base parameters:
Model (
$model
)By providing the model, internally CrafterCMS extracts the path and model ID (a.k.a object ID)
Model is optional since by default it uses the
contentModel
FreeMarker context variable for the current templateIf you need to use a different model, please specify the
$model
argument of the macros
The HTML attributes for it are
data-craftercms-model-path
anddata-craftercms-model-id
Field ID (
$field
)The HTML attribute for it is
data-craftercms-field-id
.
Index (
$index
)The HTML attribute for it is
data-craftercms-index
.
For example, the following div
element macro
<@crafter.div $field="columns_o.items_o" $index="0.1">
...
</@crafter.div>
The above will print out to the HTML a div
with all the relevant hints for the ICE engine to pick up
this element as editable. Such div
would look as shown below:
1 <div
2 data-craftercms-model-path="/site/website/index.xml"
3 data-craftercms-model-id="f830b94f-a6e9-09eb-9978-daafbfdf63ef"
4 data-craftercms-field-id="columns_o.items_o"
5 data-craftercms-index="0.1"
6 >...</div>
Start by importing the crafter FreeMarker library on to your FreeMarker template.
<#import "/templates/system/common/crafter.ftl" as crafter />
Once you’ve imported crafter.ftl
, you can start converting tags to editable elements by switching
each of the tags that represent CrafterCMS content model fields, from plain HTML tags to a macro tag.
Will use the previous carousel example to illustrate.
As seen on the previous section, we introduced an additional element to represent the repeat group and we ended up with the following markup.
1 <div class="carousel"> <!-- Component (Carousel) -->
2 <div> <!-- Repeating group (slides_o) — Additional element introduced -->
3 <div class="slide"> <!-- Repeat group item (slides_o[0]) -->
4 <img src="slide1.png" alt=""> <!-- Repeat group item field (slides_o[0].images_s) -->
5 <h2>Slide One</h2> <!-- Repeat group item field (slides_o[0].caption_s) -->
6 </div>
7 <div class="slide"> <!-- Repeat group item (slides_o[1]) -->
8 <img src="slide2.png" alt=""> <!-- Repeat group item field (slides_o[1].images_s) -->
9 <h2>Slide Two</h2> <!-- Repeat group item field (slides_o[1].caption_s) -->
10 </div>
11 </div>
12 </div>
Assume you’re using a particular CarouselJS library that requires the div.carousel
element to be
the direct parent of the div.slide
elements. As mentioned earlier, we can flip around the elements
for the component and the repeat group.
1 <div> <!-- Component (Carousel) -->
2 <div class="carousel"> <!-- Repeating group (slides_o) -->
3 ...
4 </div>
5 </div>
Now, to start converting the elements to be editable, replace each tag, with the appropriate CrafterCMS macro.
Prepend @crafter.
to every tag so that <div>…</div>
becomes <@crafter.div>...</@crafter.div>
,
<h1>
becomes <@crafter.h1>
, <img>
becomes <@crafter.img>
, span
becomes <@crafter.span>
and so on.
Exceptions to this are the following:
For repeat group field elements and their children, use
@crafter.renderRepeatGroup
.For item selector controls that hold components to be rendered, use
@crafter.renderComponentCollection
.
To convert the carousel example, first, mark the component root by using @crafter.div
.
See HTML Elements Tag Macros for all the available customizations and configuration.
<#import "/templates/system/common/crafter.ftl" as crafter />
<@crafter.div>
...
</@crafter.div>
Next, let’s do the repeat group and its items. We use @crafter.renderRepeatGroup
to render repeat
groups. renderRepeatGroup for all the available customizations and configuration.
1 <@crafter.renderRepeatGroup
2 $field="slides_o"
3 $containerAttributes={ "class": "carousel" }
4 $itemAttributes={ "class": "slide" };
5 item, index
6 >
7 <@crafter.img
8 $field="slides_o.image_s"
9 $index="${index}"
10 src="${item.image_s}"
11 alt=""
12 />
13 <@crafter.h2 $field="slides_o.caption_s" $index="${index}">
14 ${item.caption_html!''}
15 </@crafter.h2>
16 </@crafter.renderRepeatGroup>
The renderRepeatGroup
macro does several things for us:
Prints the repeat group container element
Prints the repeat group item elements
Per-item, prints out what you pass down as the body (i.e.
<#nested />
) to the macroIt provides you with the
item
andindex
for each item, so you can use them appropriately as if you were iterating manually.
The complete FreeMarker template for the carousel component becomes:
1 <#import "/templates/system/common/crafter.ftl" as crafter />
2 <@crafter.componentRootTag>
3 <@crafter.renderRepeatGroup
4 $field="slides_o"
5 $containerAttributes={ "class": "carousel" }
6 $itemAttributes={ "class": "slide" };
7 item, index
8 >
9 <@crafter.img
10 $field="slides_o.image_s"
11 $index="${index}"
12 src="${item.image_s!''}"
13 alt=""
14 />
15 <@crafter.h2 $field="slides_o.caption_s" $index="${index}">
16 ${item.caption_html!''}
17 </@crafter.h2>
18 </@crafter.renderRepeatGroup>
19 </@crafter.componentRootTag>
FreeMarker Macros & Utilities
There are three macros in crafter.ftl
:
head
: used to inject templates from pluginsbody_top
: used to inject templates from pluginsbody_bottom
: used to inject templates from plugins and is also used by ICE as detailed below
The head
, body_bottom
and body_top
are macros that should be positioned in those positions that the
name suggests. Their purpose is to print strategic scripts, stylesheets or otherwise executions that should
take place in those moments of the page rendering or be printed in that position.
Plugins use these “hooks” to inject themselves on the right location so it’s important for ftl templates to
position them in accordance to their name. For example, a Google Tag Manager plugin will want to get injected
early on in the head
so it will print it’s script in the <@head />
hook.
See here for more information on injecting templates from plugins.
After importing crafter.ftl
, you’ll have all the available XB macros described below.
<#import "/templates/system/common/crafter.ftl" as crafter />
initExperienceBuilder
Initializes the ICE engine and the communication between the page/app and studio. Call is required to enable Studio to control the page and for XB to enable ICE.
The initExperienceBuilder
macro is automatically invoked by the <@crafter.body_bottom />
but you can opt out
of it by invoking body_bottom with initializeInContextEditing=false
.
<@crafter.body_bottom initializeInContextEditing=false />
In that case, you’ll need to invoke initExperienceBuilder
manually.
Parameter |
Type |
Description |
---|---|---|
isAuthoring |
boolean |
Optional as it defaults to modePreview FreeMarker context variable. When isAuthoring=false, in context editing is skipped all together. Meant for running in production. |
props |
JS object string |
This is passed directly to the JavaScript runtime. Though it should be passed to the macro as a string, the contents of the string should be a valid JavaScript object. Use it to configure/customize Crafter’s JavaScript SDK initialization. |
When invoked, initExperienceBuilder
returns an object with an unmount
prop/function, which
would indeed unmount XB from the current page.
Examples
<#-- Simple XB initialisation -->
<@initExperienceBuilder />
<#-- XB initialisation providing XB component props to customise behaviour or styles -->
<@initExperienceBuilder props="{ themeOptions: { ... }, globalStyleOverrides: { ... } }" />
<#-- `body_bottom` internally invokes `initExperienceBuilder`; xbProps can be provided. -->
<@crafter.body_bottom xbProps="{ scrollElement: '#mainWrapper' }" />
HTML Elements Tag Macros
CrafterCMS provides a comprehensive list of macros for the most common html elements that are used to
develop content-managed websites/webapps. All these tags provided are essentially an alias to the
underlying @crafter.tag
macro, which you can use when you wish to use an element that isn’t provided
in the out-of-the-box macros (e.g. if you’re using custom html elements), or if you need to set which
tag to use dynamically (see examples below).
The following tags are available:
article
, a
, img
, header
, footer
, div
, section
, span
, h1
, h2
, h3
, h4
, h5
,
h6
, ul
, p
, ul
, li
, ol
, iframe
, em
, strong
, b
, i
, small
, th
, caption
, tr
,
td
, table
, abbr
, address
, aside
, audio
, video
, blockquote
, cite
, em
, code
, nav
,
figure
, figcaption
, pre
, time
, map
, picture
, source
, meta
, title
Parameter |
Description |
---|---|
|
The content model for which this element belongs to. |
|
The field ID on the content type definition of the current model. When inside repeat groups,
a dot-separated-string of the full field path to the present field (e.g. |
|
When inside a collection (i.e. repeat group or component collection), the index of the present item. When nested
inside repeat groups, the full index path to this item (e.g. |
Html attributes |
For convenience, macro tags will print out to the HTML all the attributes you pass to them that aren’t one of
the Crafter custom arguments (i.e. |
|
Html attributes to print on to the element. Particularly useful for attributes that you can’t supply to
the macro as a direct argument due to FreeMarker syntax restrictions. For example, |
|
Specify which tag to use. For example |
|
By default set to true. When true, even if the value is blank (or not preset), the macro will still print out the tag. When false, the macro will not print out the tag. |
|
A value to use when the field is blank (or not preset). |
Examples
In this first example:
The template’s model root tag has no
$field
parameter as it is not a field; it represents the model itself.Specifying
$model
is not required in most cases because by default the macros use themodel
variable (set automatically by the system on the rendering template’s scope, containing the current template’s model).Finally the
$index
parameter is not used in either tags, since neither is an item of a collection.
<@crafter.section>
<@crafter.h1 $field="heading_t">${model.heading_t}</@crafter.h1>
</@crafter.section>
In this example, the html tag is printed dynamically using what’s specified on the content model.
<@crafter.tag $tag=(contentModel.headingLevel_s!'h2')>
<@crafter.span $field"text_s">${model.text_s}</@crafter.span>
</@crafter.tag>
Auto-print
The example below, uses the short-hand auto-print expression. The colon at the end of the field id, instructs the system to print the value of that field for you.
<@crafter.h1 $field"title_t:" />
-^- notice the `:`
The above is equivalent to <@crafter.h1 $field"title_t">${model.title_t!""}</@crafter.h1>
. By default,
auto-print renders to the innerHTML
, but you can print to an attribute by putting the target attribute
after the colon.
<@crafter.img $field"image_s:src" />
Note the @crafter.img
macro automatically prints to src
when you don’t supply the render target; hence,
<@crafter.img $field"image_s:" />
is equivalent to <@crafter.img $field"image_s:src" />
.
Note
Auto-print can only be used to print top-level model field values.
renderComponentCollection
Used to render Item Selector controls, which hold components. Internally, it prints out the tag for the field (item selector) and the tags for each of the component container items.
The way component collections are modelled on the ICE engine are in the following hierarchy:
<FieldTag>
<Item0>
<ComponentTag>
...
<Item1>
<ComponentTag>
...
<Item2>
<ComponentTag>
...
Note that the item tag is not the component tag itself, instead, the component is contained by the item and it’s not the item.
Parameters |
Description |
---|---|
|
The content model for which this element belongs to. |
|
The field ID on the content type definition of the current model. When inside repeat groups,
a dot-separated-string of the full field path to the present field (e.g. |
|
When inside a collection (i.e. repeat group or component collection), the index of the present item. When nested
inside repeat groups, the full index path to this item (e.g. |
|
When nested inside repeat groups, a dot-separated-string of the full field path to the present field
(e.g. |
|
When nested inside repeat groups, the full index path to this control (e.g. |
|
Contains the collection that the macro iterates through internally. By default, it is set to |
|
Html attributes to print on to the field element. |
|
The tag to use for the field element. |
|
The tag to use for the item tags. |
|
Html attributes to print on to the item elements. |
|
Html attributes to print by item index. For example, |
|
CrafterCMS’ renderComponent macro supports supplying additional arguments
( |
Example
<@crafter.renderComponentCollection $field="mainContent_o" />
The sample above would print out the following html:
<!-- Field element -->
<section
data-craftercms-model-path="/site/website/index.xml"
data-craftercms-model-id="8d7f21fa-5e09-00aa-8340-853b7db302da"
data-craftercms-field-id="mainContent_o"
>
<!-- Item 0 element -->
<div
data-craftercms-model-path="/site/website/index.xml"
data-craftercms-model-id="8d7f21fa-5e09-00aa-8340-853b7db302da"
data-craftercms-field-id="mainContent_o"
data-craftercms-index="0"
>
<!-- Component @ Item 0 -->
<div
data-craftercms-model-path="/site/components/component_hero/bd283e3b-3484-6b9e-b2d5-2a9e87128b69.xml"
data-craftercms-model-id="bd283e3b-3484-6b9e-b2d5-2a9e87128b69"
>
...
</div>
</div>
<!-- Item 1 element -->
<div
data-craftercms-model-path="/site/website/index.xml"
data-craftercms-model-id="8d7f21fa-5e09-00aa-8340-853b7db302da"
data-craftercms-field-id="mainContent_o"
data-craftercms-index="1"
>
<!-- Component @ Item 1 -->
<div
data-craftercms-model-path="/site/website/index.xml"
data-craftercms-model-id="2e8761a9-1268-581b-f8d0-52cad6a73e0a"
>
...
</div>
</div>
</section>
renderRepeatGroup
Used to render Repeat Group controls. Internally, it prints out the tag for the field (repeat group) and the tags for each of the items.
The way repeat group collections are modelled on the ICE engine are in the following hierarchy:
<FieldTag>
<Item0>
...
<Item1>
...
<Item2>
<ComponentTag>
...
...
Repeat groups introduce the possibility of having complex/compound $field
and $index
arguments when they
contain nested repeat groups or component collections.
Parameters |
Description |
---|---|
|
The content model for which this element belongs to. |
|
The field ID on the content type definition of the current model. When inside repeat groups,
a dot-separated-string of the full field path to the present field (e.g. |
|
When inside a collection (i.e. repeat group or component collection), the index of the present item. When nested
inside repeat groups, the full index path to this item (e.g. |
|
When nested inside repeat groups, a dot-separated-string of the full field path to the present field
(e.g. |
|
When nested inside repeat groups, the full index path to this control (e.g. |
|
Contains the collection that the macro iterates through internally. By default, it is set to |
|
Html attributes to print on to the field element. |
|
The tag to use for the field element. |
|
The tag to use for the item tags. |
|
Html attributes to print on to the item elements. |
|
Html attributes to print by item index. For example, |
Examples
<@crafter.renderRepeatCollection
$containerTag="section"
$containerAttributes={ "class": "row" }
$itemTag="div"
$itemAttributes={ "class": "col" }
$field="columns_o";
<#-- Nested content values passed down by the macro: -->
item, index
>
<@crafter.renderComponentCollection
$field="items_o"
$fieldCarryover="columns_o"
$indexCarryover="${index}"
$model=(contentModel + { "items_o": item.items_o })
/>
</@crafter.renderRepeatCollection>
The sample above would print out the following html:
<!-- The repeat group field element (columns_o) -->
<section
class="row"
data-craftercms-model-path="/site/website/index.xml"
data-craftercms-model-id="f830b94f-a6e9-09eb-9978-daafbfdf63ef"
data-craftercms-field-id="columns_o"
>
<!-- Repeat group item 0 element (i.e. columns_o[0]) -->
<div
class="col"
data-craftercms-model-path="/site/website/index.xml"
data-craftercms-model-id="f830b94f-a6e9-09eb-9978-daafbfdf63ef"
data-craftercms-field-id="columns_o"
data-craftercms-index="0"
>
<!-- An item selector field named `items_o` that's inside the repeat group (i.e. columns_o[0].items_o) -->
<div
data-craftercms-model-path="/site/website/index.xml"
data-craftercms-model-id="f830b94f-a6e9-09eb-9978-daafbfdf63ef"
data-craftercms-field-id="columns_o.items_o"
data-craftercms-index="0"
>
<!-- columns_o[0].items_o[0] -->
<div
data-craftercms-model-path="/site/website/index.xml"
data-craftercms-model-id="f830b94f-a6e9-09eb-9978-daafbfdf63ef"
data-craftercms-field-id="columns_o.items_o"
data-craftercms-index="0.0"
>
<!-- Embedded component hosted @ columns_o[0].items_o[0] -->
<h2
class="heading-component-root"
data-craftercms-model-path="/site/website/index.xml"
data-craftercms-model-id="57a30ade-f167-5a8b-efbe-30ceb0771667"
>
<span
data-craftercms-model-path="/site/website/index.xml"
data-craftercms-model-id="57a30ade-f167-5a8b-efbe-30ceb0771667"
data-craftercms-field-id="text_s"
>
This is a heading
</span>
</h2>
</div>
<!-- columns_o[0].items_o[1] -->
<div
data-craftercms-model-path="/site/website/index.xml"
data-craftercms-model-id="f830b94f-a6e9-09eb-9978-daafbfdf63ef"
data-craftercms-field-id="columns_o.items_o"
data-craftercms-index="0.1"
>
<!-- Embedded component hosted @ columns_o[0].items_o[1] -->
<div
class="paragraph-component-root"
data-craftercms-model-path="/site/website/index.xml"
data-craftercms-model-id="fff36233-34d9-f476-0a35-00b507b9420b"
>
<p
data-craftercms-model-path="/site/website/index.xml"
data-craftercms-model-id="fff36233-34d9-f476-0a35-00b507b9420b"
data-craftercms-field-id="copy_t"
>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua.
</p>
</div>
</div>
</div>
</div>
<!-- Repeat group item 1 element (i.e. columns_o[1]) -->
<div
class="col"
data-craftercms-model-path="/site/website/index.xml"
data-craftercms-model-id="f830b94f-a6e9-09eb-9978-daafbfdf63ef"
data-craftercms-field-id="columns_o"
data-craftercms-index="1"
>
<!-- An item selector field named `items_o` that's inside the repeat group (i.e. columns_o[1].items_o) -->
<div
data-craftercms-model-path="/site/website/index.xml"
data-craftercms-model-id="f830b94f-a6e9-09eb-9978-daafbfdf63ef"
data-craftercms-field-id="columns_o.items_o"
data-craftercms-index="1"
>
<!-- columns_o[1].items_o[0] -->
<div
data-craftercms-model-path="/site/website/index.xml"
data-craftercms-model-id="f830b94f-a6e9-09eb-9978-daafbfdf63ef"
data-craftercms-field-id="columns_o.items_o"
data-craftercms-index="1.0"
>
<!-- Embedded component hosted @ columns_o[1].items_o[0] -->
<span
data-craftercms-model-path="/site/website/index.xml"
data-craftercms-model-id="eb50be40-5755-5dfa-0ad0-15367b5cc685"
>
<img
src="https://place-hold.it/300"
alt=""
class=""
data-craftercms-model-path="/site/website/index.xml"
data-craftercms-model-id="eb50be40-5755-5dfa-0ad0-15367b5cc685"
data-craftercms-field-id="image_s"
>
</span>
</div>
<!-- columns_o[1].items_o[0] -->
<div
data-craftercms-model-path="/site/website/index.xml"
data-craftercms-model-id="f830b94f-a6e9-09eb-9978-daafbfdf63ef"
data-craftercms-field-id="columns_o.items_o"
data-craftercms-index="1.1"
>
<!-- Embedded component hosted @ columns_o[1].items_o[1] -->
<div
class="paragraph-component-root"
data-craftercms-model-path="/site/website/index.xml"
data-craftercms-model-id="4b68e47a-07a3-134f-a540-1b7907080cb0"
>
<p
data-craftercms-model-path="/site/website/index.xml"
data-craftercms-model-id="4b68e47a-07a3-134f-a540-1b7907080cb0"
data-craftercms-field-id="copy_t"
>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua.
</p>
</div>
</div>
</div>
</div>
</section>
eventCaptureOverlay
Elements like video players (e.g. a YouTube embed, an iframe) can sometimes interfere with the normal flow of pointer events and make it challenging for authors to access the in-context editing for the field being represented by these elements.
The eventCaptureOverlay
macro prints out an element with styles to overlay and stop the inner element from capturing
pointer events when in Edit mode.
<@crafter.eventCaptureOverlay $onlyInPreview=true $tag="section" class="craftercms-overlay-example">
<@crafter.tag
$tag="iframe"
id="ytplayer"
type="text/html"
width="640"
height="360"
src="https://www.youtube.com/embed/M7lc1UVf-VE?autoplay=1&origin=http://example.com"
frameborder="0"
></@crafter.iframe>
</@crafter.eventCaptureOverlay>
The iframe tag macro includes the event capture overlay by default, so the above can be achieved simply by using @crafter.iframe
.
<@crafter.iframe
id="ytplayer"
type="text/html"
width="640"
height="360"
src="https://www.youtube.com/embed/M7lc1UVf-VE?autoplay=1&origin=http://example.com"
frameborder="0"
></@crafter.iframe>
You may disable the overlay for the iframe macro by setting the $omitEventCaptureOverlay attribute to true or specify the eventCaptureOverlay macro arguments using the $eventCaptureOverlayProps.
forEach
Useful for iterating through crafter collections.
Examples
<@crafter.forEach contentModel.slides_o; slide, index>
<#assign
attributesByIndex = attributesByIndex + { index: { "data-bs-interval": "${slide.delayInterval_i?c}" } }
/>
</@crafter.forEach>
<@crafter.forEach contentModel.slides_o; slide, index>
<button
type="button"
data-bs-target="#${rootElementId}"
data-bs-slide-to="${index}"
aria-label="Slide ${index}"
${(initialActiveSlideIndex == index)?then('class="active" aria-current="true"', '')}
></button>
</@crafter.forEach>
cleanDotNotationString
Takes a dot-separated-string and returns a string that doesn’t have any dots at the beginning or end of the string and that there aren’t any consecutive dots.
Useful when working with repeat groups in Crafter as these introduce the possibility of field/index
carryovers and complex/compound fields (e.g. field1.field2
) and indexes (e.g. 0.1
).
<#assign str1 = ".hello." />
<#assign str2 = ".world." />
${crafter.cleanDotNotationString("${str1}.${str2}")}
<#-- Output is hello.world -->
${crafter.cleanDotNotationString("...foo...bar..")}
<#-- Output is foo.bar -->
${crafter.cleanDotNotationString("..")}
<#-- Output is an empty string -->
isEmptyCollection
Receives a Crafter collection and returns true if it’s empty or false otherwise.
emptyCollectionClass
Receives a collection and, if the collection is empty it will print a special crafter class, otherwise, it won’t print anything. This macro only prints in Crafter Engine’s preview mode.
The special class adds styles to the element so that it has a minimum height and width so that authors can visualize the area and drag components on it despite being empty — as otherwise, it would be invisible and virtually not editable.
One should use this macro on empty component or repeat group collections.
Component collection
<@crafter.renderComponentCollection
$field="mainContent_o"
$containerAttributes={ "class": crafter.emptyCollectionClass(contentModel.mainContent_o) }
/>
Repeat group
<@crafter.renderRepeatGroup
$field="slides_o"
$containerAttributes={ "class": crafter.emptyCollectionClass(contentModel.slides_o) }
/>
emptyFieldClass
Receives a field value and, if the field has no content it will print a special crafter class, otherwise, it won’t print anything. This macro only prints in Crafter Engine’s preview mode.
The special class adds styles to the element so that it has a minimum height and width so that authors can visualize the area and add content to this field — as otherwise, it would be invisible and virtually not editable.
One should use this macro on empty fields.
Example
<@crafter.h1
class="display-5 fw-bold ${crafter.emptyFieldClass(contentModel.title_s)}"
$field="title_s"
>
${contentModel.title_s!''}
</@crafter.h1>
printIfPreview
Receives a string which it will print if Crafter Engine is running in preview mode. Doesn’t print anything if Engine is running the published site.
<#-- Import the "debug" version of the script in preview. -->
<script src="/static-assets/js/bootstrap.bundle${crafter.printIfPreview('.debug')}.js"></script>
You can also use the FreeMarker context variable modePreview
to do similar things; in fact,
printIfPreview
uses it internally.
<#-- Import a in-context editing stylesheet only in preview. -->
<#if modePreview><link href="/static-assets/css/ice.css" rel="stylesheet"></#if>
printIfNotPreview
Receives a string which will be printed if Crafter Engine is not running in preview mode. Doesn’t print anything if Engine is running the published site.
<#-- Import the "minified" version of the script in delivery. -->
<script src="/static-assets/js/bootstrap.bundle${crafter.printIfNotPreview('.min')}.js"></script>
printIfNotEmpty
Receives the target value, the value to print if the target is not empty, and an optional fallback value to print if the target is empty.
<!-- Use an anchor if an href was provided, button otherwise. -->
<#local tag = crafter.printIfNotEmpty(contentModel.href, "a", "button")>
<#if tag == "a"><#local attributes = attributes + { "href": href }></#if>
<@crafter.tag $tag=tag $attributes=attributes>
<#nested>
</@crafter.tag>
notEmptyString
Receives the target property value and returns true/false. Can be used to check if a CrafterCMS model string field value is empty. Returns true if the trimmed value is not empty or null.
isEmptyString
Receives the target property value and returns true/false. Can be used to check if a CrafterCMS model string field value is empty. Returns true if the trimmed value is empty or null.
JavaScript Applications
XB offers a set of JavaScript (JS) libraries and utilities that you can use in various scenarios. When writing JS-powered applications including Single-page applications — like when using React, Angular, Vue or similar — all you need to do is invoke the various XB methods relevant to your application.
The simplest integration strategy for JS applications consist of marking the relevant HTML elements which represent a content model field, with a set of attributes that CrafterCMS sdk libraries generate for you based on a content model that you’ve previously fetched.
You may also dig deep into the system and manage the field element registrations manually to suit your application needs.
Usage
XB JS libraries can be used either via npm by importing @craftercms/experience-builder
or using the
JS UMD bundle and adding it into your app’s runtime.
React
CrafterCMS provides React bindings for integrating with XB. Because XB itself is a React application, React presents the tightest, most native integration with XB as it will essentially run as part of your app instead of as a parallel application like when using other technologies.
React bindings can be used either via npm or using the umd bundle that comes with CrafterCMS.
The components available for using on your React applications are listed below.
ExperienceBuilder
This is the main component that orchestrates and enables all of the In-context Editing. You must declare this component only once and it should be a parent of all the XB-enabled components.
Prop |
Type |
Default |
Description |
---|---|---|---|
|
boolean |
(Required) |
It controls the adding or bypassing of authoring tools. Should send true when running in Studio and authoring tools should be enabled. Authoring tools are completely absent when set to false. |
|
boolean |
false |
If your App consumes content from CrafterCMS in a headless way, certain options (e.g. editing the freemarker template or controller) aren’t applicable. Setting headless mode to true will disable XB options that aren’t relevant to headless application such as SPAs. |
|
XB’s defaults |
XB is powered by MUI. This argument allows you to customize MUI theme options and override XB’s defaults. |
|
|
ExperienceBuilderStylesSx |
XB’s defaults |
You may change XB-specific theming through this argument |
|
null |
Specify style overrides for various XB global styles. |
|
|
string |
html, body |
You may specify a different element for XB to scroll when scrolling the user to specific CrafterCMS field elements. |
Model
Use this component to render elements that represent the models themselves (i.e. CrafterCMS pages or components, not their fields).
Prop |
Type |
Default |
Description |
---|---|---|---|
|
Object (ContentInstance) |
(Required) |
The model being rendered |
|
string | React.ElementType |
“div” |
The component to be rendered |
|
Object |
undefined |
Any props sent at the root that aren’t own props are forwarded down to the rendered
component so in most cases you needn’t use |
ContentType
Use this component to render a specific component of your own library based on the content type of the
model. ContentType
component works with a “content type map” which you must supply as a prop. The
content type map, is essentially a plain object, a lookup table of your components indexed by content
type id. You may use it in conjunction with React.lazy
to optimize your app; specially considering the
content type map should contain all the possible components that you will be rendering via ContentType
component on a given piece of your app.
Prop |
Type |
Default |
Description |
---|---|---|---|
|
Object (ContentInstance) |
(Required) |
The model being rendered |
|
Object |
(Required) |
A map of components indexed by CrafterCMS content type id. The content type id of the model passed will be used to pick from the map the component that should render said model. |
|
React.ComponentType |
If the model passed to |
|
|
React.ComponentType |
If the content type of the model is not found in the |
RenderField
Use this component to render CrafterCMS model fields. Although it can also render collection-type fields, CrafterCMS provides specific components (see below) to render component collections or repeat groups.
Prop |
Type |
Default |
Description |
---|---|---|---|
|
Object (ContentInstance) |
(Required) |
The model being rendered |
|
string |
(Required) |
The id of the field to render |
|
string | number |
undefined |
If applicable, the index within the parent collections. |
|
string | React.ElementType |
“div” |
The component to be rendered |
|
Object |
undefined |
Any props sent at the root that aren’t own props are forwarded down to the rendered
component so in most cases you needn’t use |
|
string |
“children” |
The value(s) to be rendered will be passed with this prop name to the target element type
(see |
|
function |
(value, fieldId) => value |
If you need to do custom rendering logic for the value of the field being rendered, you may
supply a |
RenderComponents
Use this component to render item selectors that hold components. This component renders the field element (i.e. the item selector), the item element, and the component itself.
Prop |
Type |
Default |
Description |
---|---|---|---|
|
|
||
|
Object |
(Required) |
A map of components indexed by CrafterCMS content type id. The content type id of the model passed will be used to pick from the map the component that should render said model. |
|
Props Object |
{} |
Props to be passed down to the |
|
Record<number, object> |
|
You can pass specific props to components based on their index in the collection with this prop. |
|
function |
(component, index) => <ContentType … /> |
If the default component renderer is not sufficient for your use case, you can supply a custom renderer which is invoked with the current component and the current index in the collection. |
RenderRepeat
Use this component to render repeat groups and their items. This component renders the field element (i.e. the repeat group) and the item element. The body of each repeat group item is rendered by a function supplied by you, which is provided with the item, the index in the collection, the computed compound index (when applicable) and the collection itself.
Prop |
Type |
Default |
Description |
---|---|---|---|
|
Object (ContentInstance) |
(Required) |
The model being rendered |
|
string |
(Required) |
The id of the repeat group field |
|
string | number |
undefined |
When nested inside other repeats, the index inside the parent repeat |
|
React.ElementType |
“div” |
The React component to render the field element as |
|
Object |
undefined |
Any props sent at the root that aren’t own props are forwarded down to the rendered
component so in most cases you needn’t use |
|
React.ElementType |
“div” |
|
|
Object |
undefined |
|
|
function |
(item, index) => index |
A function that receives the item and the current index and should return the |
|
function |
(Required) |
Should return/render the inner item ( |
Angular, Vue and Other JS Applications
The easiest way to integrate XB with your JS application is by putting attributes on each HTML element that represents a model, field or item of a CrafterCMS content type and then invoking XB initializer.
To initialize XB, you need to invoke the initExperienceBuilder
function. This function receives a single argument which gets passed down to the ExperienceBuilder component. See argument details on the ExperienceBuilder component section.
At a minimum, you need to supply the isAuthoring
boolean flag, together with either a ContentInstance
or the path
for the main model to initialize XB with.
For example, a simple application that uses the UMD bundle for XB, would look something like this:
<script defer src="/studio/static-assets/scripts/craftercms-xb.umd.js"></script>
<script>
// Run when XB script has been loaded, as it is deferred.
document.addEventListener('craftercms.xb:loaded', () => {
// Determine if we're on authoring using `fetchIsAuthoring` utility. Remove/replace if you determine whether it is authoring/delivery through some other mechanism.
window.craftercms.xb.fetchIsAuthoring().then((isAuthoring) => {
// If we're in authoring, initialize XB
isAuthoring && window.craftercms.xb.initExperienceBuilder({ isAuthoring, path: '/site/website/index.xml' });
});
});
</script>
In contrast, in an npm project setup, this might look something like this:
import { fetchIsAuthoring, initExperienceBuilder } from '@craftercms/experience-builder';
// Determine if we're on authoring using `fetchIsAuthoring` utility. Remove/replace if you determine whether it is authoring/delivery through some other mechanism.
fetchIsAuthoring().then((isAuthoring) => {
// If we're in authoring, initialize XB
if (isAuthoring) {
initExperienceBuilder({ isAuthoring, path: '/site/website/index.xml' });
}
});
getICEAttributes
Use this method to get the set of attributes to place on each element that represents a CrafterCMS
model, field or item. Once you’ve fetched your content, you’d invoke getICEAttributes
and it will
return all the necessary attributes to inform the system how to make such element editable in XB.
You should first set all the attributes on your markup and afterwards, invoke initExperienceBuilder
Parameter |
Type |
Default |
Description |
---|---|---|---|
|
(Required) |
You must supply at a minimum the |
initExperienceBuilder
Use this method to initialize experience builder once you have printed all the attributes (see getICEAttributes) on your markup.
Parameter |
Type |
Default |
Description |
---|---|---|---|
|
(Required) |
See XB props. |
Example Applications
Lazy Loaded Content
Lazy loading is a strategy to identify non-critical content and load these only when needed. It’s a way to reduce page load times and memory consumption.
In templated projects, typically content is preloaded and pre-rendered on to the view. When the view loads, XB initialises normally. If content is loaded lazily, additional steps are required to enable XB on top of that content.
In simple JS applications, lazy loaded content without a page refresh also needs some programmatic management to notify XB about changes.
To enable XB for lazy loaded content, once the attributes are printed, register the new elements with XB using registerElements
.
Remember that the lifecycle of markup (lazy loaded content) needs to be integrated in XB. To de-register elements with XB,
use deregisterElements
.
Below is an example of registering/de-registering lazy loaded content with XB using the home.ftl
file from a project
created using the Website Editorial blueprint.
Sample registering/de-registering lazy loaded content in home.ftl
1<#import "/templates/system/common/crafter.ftl" as crafter />
2
3<!--
4 Editorial by HTML5 UP
5 html5up.net | @ajlkn
6 Free for personal and commercial use under the CCA 3.0 license (html5up.net/license)
7-->
8<!doctype html>
9<html lang="en">
10<head>
11 <#include "/templates/web/fragments/head.ftl">
12 <@crafter.head/>
13</head>
14<body class="is-preload">
15<@crafter.body_top/>
16
17<!-- Wrapper -->
18<div id="wrapper">
19
20 <!-- Main -->
21 <div id="main">
22 <div class="inner">
23
24 <!-- Header -->
25 <@crafter.renderComponentCollection $field="header_o"/>
26 <!-- /Header -->
27
28 <!-- Banner -->
29 <section id="banner">
30 <div class="content">
31 <@crafter.header $field="hero_title_html">
32 ${contentModel.hero_title_html}
33 </@crafter.header>
34 <@crafter.div $field="hero_text_html">
35 ${contentModel.hero_text_html}
36 </@crafter.div>
37 </div>
38 <span class="image object">
39 <@crafter.img $field="hero_image_s" src=(contentModel.hero_image_s!"") alt=""/>
40 </span>
41 </section>
42 <!-- /Banner -->
43
44 <!-- Section: Features -->
45 <section>
46 <header class="major">
47 <@crafter.h2 $field="features_title_t">
48 ${contentModel.features_title_t}
49 </@crafter.h2>
50 </header>
51 <@crafter.renderComponentCollection
52 $field="features_o"
53 $containerAttributes={ "class": "features" }
54 $itemAttributes={ "class": "feature-container" }
55 />
56 </section>
57 <!-- /Section: Features -->
58
59 <!-- Section: Articles -->
60 <section>
61 <header class="major">
62 <h2>Featured Articles</h2>
63 </header>
64 <div class="posts">
65 <!--
66 <#list articles as article>
67 <@crafter.article $model=article>
68 <a href="${article.url}" class="image">
69 <@crafter.img
70 $model=article
71 $field="image_s"
72 src=article.image???then(article.image, "/static-assets/images/placeholder.png")
73 alt=""
74 />
75 </a>
76 <h3>
77 <@crafter.a $model=article $field="subject_t" href="${article.url}">
78 ${article.title}
79 </@crafter.a>
80 </h3>
81 <@crafter.p $model=article $field="summary_t">
82 ${article.summary}
83 </@crafter.p>
84 <ul class="actions">
85 <li>
86 <a href="${article.url}" class="button">More</a>
87 </li>
88 </ul>
89 </@crafter.article>
90 </#list>
91 -->
92 </div>
93 </section>
94 <!-- /Section: Articles -->
95
96 </div>
97 </div>
98 <!-- /Main -->
99
100 <!-- Left Rail -->
101 <@crafter.renderComponentCollection $field="left_rail_o" />
102 <!-- /Left Rail -->
103
104</div>
105<!-- /Wrapper -->
106
107<script>
108 const page = {
109 articles: null,
110 template: `
111 <article {ice}>
112 <a href="{url}" class="image">
113 <img src="{img}" alt="">
114 </a>
115 <h3><a href="{url}">{title}</a></h3>
116 <p>{summary}</p>
117 <ul class="actions">
118 <li><a href="{url}" class="button">More</a></li>
119 </ul>
120 </article>`,
121 xbReady: new Promise((resolve) => {
122 <#if modePreview>
123 const resolveOnCheckIn = () => {
124 window.craftercms.xb.guestCheckIn$?.subscribe(() => {
125 resolve(true);
126 })
127 }
128 if (window.craftercms?.xb) {
129 resolveOnCheckIn();
130 } else {
131 document.addEventListener('craftercms.xb:loaded', () => {
132 resolveOnCheckIn();
133 });
134 }
135 <#else>
136 resolve(true)
137 </#if>
138 }),
139 renderArticles() {
140 page.xbReady.then(() => {
141 const postsContainer = document.querySelector('.posts');
142 let html = [];
143 window.craftercms?.xb.deregisterElements(postsContainer);
144 page.articles.forEach((article) => {
145 html.push(
146 page.template
147 .replaceAll('{url}', article.url)
148 .replaceAll('{img}', article.img)
149 .replaceAll('{title}', article.title)
150 .replaceAll('{summary}', article.summary)
151 .replace(
152 '{ice}',
153 <#if modePreview>
154 Object.entries(
155 window.craftercms.xb.getICEAttributes({
156 modelId: article.id,
157 path: article.path,
158 isAuthoring: true
159 })
160 ).map(([attr, value]) => attr + '=' + '"' + value + '"').join(' ')
161 <#else>
162 ''
163 </#if>
164 )
165 )
166 });
167 postsContainer.innerHTML = html.join('\n');
168 window.craftercms?.xb.registerElements(postsContainer);
169 });
170 },
171 loadArticles() {
172 setTimeout(() => {
173 page.articles = [
174 <#list articles as article>
175 {
176 path: "${article.storeUrl}",
177 id: "${article.objectId}",
178 img: '${article.image???then(article.image, "/static-assets/images/placeholder.png")}',
179 title: "${article.title}",
180 summary: "${article.summary}",
181 url: "${article.url}"
182 }<#sep>, </#sep>
183 </#list>
184 ];
185 page.renderArticles();
186 });
187 }
188 };
189 page.loadArticles();
190</script>
191<#include "/templates/web/fragments/scripts.ftl">
192<@crafter.body_bottom/>
193
194<#-- -- >
195<script src="https://unpkg.com/rxjs@7.8.1/dist/bundles/rxjs.umd.js"></script>
196<script src="https://unpkg.com/@craftercms/utils"></script>
197<script src="https://unpkg.com/@craftercms/classes"></script>
198<script src="https://unpkg.com/@craftercms/content"></script>
199<script>
200 const { getItem } = craftercms.content;
201 getItem(
202 '/site/components/tokenized.xml',
203 { baseUrl: 'http://localhost:8080', site: 'editorial' }
204 ).subscribe((response) => {
205 console.log(JSON.stringify(response, null, 2));
206 });
207</script>
208<#-- -->
209
210</body>
211</html>