Component Builder javascript Clientside
This module renders the component and populates it with data. It also supports efficient update of component for the new
data.
What does it do?
- Renders component content and returns React/DOM element
- Populates attributes and text valeus with mapped data
- Repeats nodes Mappeds
- Updates previously rendered component for new data
- Provides both react and javascript revisions of Clientside
- (optional) Fetches of component from the cloud
What does it NOT do?
- Does not fetch the data: It needs to be explicitly given as
data
attribute. - Does not offer any event listening helpers: We recommend using bubbling events instead
Usage
React
import * as FEAAS from '@sitecore-feaas/clientside/react'
return (
<FEAAS.Component component='...' version='...' revision='published' cdn='...' data={{ user: { name: 'John' } }} />
)
Web component
import * as FEAAS from '@sitecore-feaas/clientside';
<feaas-component template={`<p>Hello <var data-path="user.name" /></p>`} data={JSON.stringify({user: {name: "John"}})} />
<feaas-component component="..." version="..." revision="published" cdn="..." data={JSON.stringify({user: {name: "John"}})} />
JS Dom
import * as FEAAS from '@sitecore-feaas/clientside'
const element = FEAAS.renderComponent({
library: '...',
component: '...',
version: '...',
revision: 'production',
cdn: '...',
data: { user: { name: 'John' } }
})
document.body.appendChild(element)
FEAAS.renderComponent(
{
data: { user: { name: 'Bill' } }
},
element
)
const element = FEAAS.renderComponent(
{
template: `<p>Hello <var data-path="user.name" /></p>`,
data: { user: { name: 'Bill' } }
}
)
document.body.appendChild(element)
FEAAS.renderComponent(
{
data: { user: { name: 'Sarah' } }
},
element
)
Data fetching
Components can fetch the data from remote endpoints on their own. Component Builder offers ability to generate the fetch
settings through embedding code generation. It leverages the ability of data
attribute of <feaas-component />
to
accept a special form of DataSettings type: {"url": "url", "params": {}, "headers": {}, "body": {}, "method": "post"}
,
which is largerly compatible with fetch()
shape of arguments.
For data sources depending on dynamic values or authentication, it's best to provide sample data as json manually in
Builder in addition to the fetching options, so data mapping can happen without real authentication during process of
component building. Requests to actual endpoints will only run in the actual clientside app. To customize fetch requests
(e.g. to provide authorization header dynamically, or to run preflight request), a DataSettings.fetchImplementation
function needs to be redefined with custom function that can intercept requests.
import { DataSettings } from '@sitecore-feaas/clientside';
DataSettings.fetchImplementation = async (url: RequestInfo | URL, options: RequestInit = {}) => {
if (typeof url == 'string' && url.includes('my-private-api.com')) {
const preflight = await fetch('https://my-auth-server/token');
const response = await preflight.json();
options = {
...options,
headers: {
...options.headers,
Authorization: response.token
}
}
}
return fetch(url, options);
}
<feaas-component src="..." data='{"url": "http://my-private-api.com/secrets"}'></feaas-component>
Fetching styles
Components use style guide, a stylesheet shared per component library. In short, it has to be included on the page.
Stylesheets are cached with immutable
Cache-Control, meaning that the browser will never attempt to re-fetch them if
it was cached once. That ensures the fastest rendering time on final website. The small price to pay is to use
Clientside on the page, that will invalidate the stylesheet automatically.
Recommended: Linking stylesheet in HTML
Placing stylesheet into HTML is benefitial to get the fasted loading speed. This allow browser to start fetching the css
before it has finished loading, parsing and executing js.
<link rel='stylesheet' href='https://feaas.blob.core.windows.net/styles/:tenant_id/published.css' />
<script type="module" src="https://cdn.jsdelivr.net/npm/@sitecore-feaas/clientside/+esm"></script>
Linking stylesheet in Clientside app
Loading styles from javascript is as simple as calling loadStyles
function. It is safe to use this method together
with HTML inclusion, the stylesheet will not be loaded twice.
import * as FEAAS from '@sitecore-feaas/clientside'
// Load stylesheet (will automatically add it )
<feaas-stylesheet='library_id'></feaas-stylesheet>
Entrypoints
There are three different entrypoints for clientside: UI, React and Headless
- The main entrypoint
@sitecore-feaas/clientside
targets the UI package which references frontend from components
monorepo. This includes both the core functionality and some ui components like the pickers and the embedded editor - The
@sitecore-feaas/clientside/react
entrypoint targets the React package which is similar to the main entrypoint
but also includes some components that use React as a dependency like external react components. - The
@sitecore-feaas/clientside/headless
entrypoint targets the Headless package which includes just the core
functionality of clientside without any dependencies on frontend or react.
NOTE 1: React library is not included in any clientside package, we assume that it is already installed in the
container that uses the @sitecore-feaas/clientside/react
entrypoint. We mark React library as external in
scripts/build.js
NOTE 2: UI has its own tsconfig that includes references to both /frontend
and /clientside
. Clientside main
tsconfig excludes /ui
avoiding circular imports
Bring your own code components
You can use Components clientside SDK to register and render your own code components to your FEAAS components.
Registration steps:
- Install and import the Components React SDK to your App
npm install @sitecore-feaas/clientside
import * as FEAAS from '@sitecore-feaas/clientside/react'
FEAAS.registerComponent(component, options)
FEAAS.registerComponent(MyComponent, {
name: 'My example component',
description: 'Description of my example component',
required: ['firstName'],
thumbnail: 'https://mss-p-006-delivery.stylelabs.cloud/api/public/content/3997aaa0d8be4eb789f3b1541bd95c58',
group: 'Examples',
properties: {
firstName: {
type: 'string',
title: 'First name',
required: true
},
lastName: {
type: 'string',
title: 'Last name'
},
telephone: {
type: 'string',
title: 'Telephone',
minLength: 10
},
bold: {
type: 'boolean',
title: 'Show text in bold weight'
}
},
ui: {
firstName: {
'ui:autofocus': true,
'ui:placeholder': 'Write your first name'
},
telephone: {
'ui:options': {
inputType: 'tel'
}
},
bold: {
'ui:widget': 'radio'
}
}
})
component
is the actual react component, while options
may contain any of the below items:
Name
Type: string
Required: yes
Description: Name of your Component as it will be shown to the user.
If not provided, will be function name in title case.
Description
Type: string
Required: no
Description: Describe your Component and anything a user should
know when using it.
Thumbnail
Type: url
Required: no
Description: A URL of a thumbnail image to be displayed in the
Builder. Should have some dimension guidelines.
Group
Type: string
Required: no
Description: Used to group related Components together in the UI.
E.g. Typography
Properties
Type: JSONSchema object
Required: no
Description: Inputs (Props) to be provided by the
user when embedding this Component.
Required
Type: String Array
Required: no
Description: Array of keys from Inputs (Props) that should
be required
UI
Type: JSONSchema object
Required: no
Description: Any UI specific configuration for the inputs
form rendering in the builder
We use React JSONSchema Form to generate a form for the component properties. You can read more about React JSONSchema
Form here: https://react-jsonschema-form.readthedocs.io
You can check an implementation example in the following repo: https://bitbucket.org/stylelabsdev/feaas-nextjs-example
Component anatomy
- It’s possible to edit/swap component without changing embedding HTML code (everything happens on CDN)
- User does not need to decide upfront if component will ever need to be edited in line, the only requirement is to
provide a unique id in embedding code
- Editing disables component FEAAS updates, but it’s possible to unfork component to return to original state/behavior
##Embedding
Components/versions are designed to be embedded on external pages. FEAAS approach allows then components to be updated
without changing the pages. There're multiple pathways for editing component:
- Editing component inside Builder
- Editing component externally (Pages or on the website)
In all of these cases the main requirement, is that no code is updated in place of embedding. The only time Components
is able to influence the embed site is initially by providing the code to copy and paste for the user. In case user
writes their embedding code by hand, there's no control at all. The system is designed to not require any changes.
<feaas-component library="..." component="..." version="..." revision="production" data="{}" />
Generated HTML
Components builder allows designer to create HTML templates and then style them. The elements inside components are the
usual suspects one can find in any web design editor - headings, paragraphs, pictures, containers. In addition to those,
the data model offers embedding of raw HTML islands and nesting components into each other. That last feature can be
used by designers to split big components into small, but it is also used internally to implement responsive/forked
components.
<div class="-feaas">
<section>
<h1>Hello world</h1>
</section>
</div>
<div class="-feaas">
<section>
<feaas-component component="component-id" version="version-id"></feaas-component>
</section>
</div>
Responsive components
Each component has multiple versions, which make up a set of mutually exclusive respresentations of the same data.
Versions can be handily used to create a set of designs to be used for different screen sizes. In addition to embedding
a specific version of a component to the page, it is possible to embed a set of responsive components instead that
choose one of the versions based on window or container size.
Resposive component is a special aggregated version that analyzes all component versions and their breakpoints, and
ensure that for every breakpoint there's a fitting version. Responsive component version bundles all its versions into
one HTML file which hides its parts based on current css driven by the stylesheet. This approach allows weak binding to
definition of breakpoints, so breakpoint definitions can be changed after components are embedded.
Every time any of the versions in component is updated, responsive version is regenerated automatically.
<div class="-feaas">
<div class="-breakpoint--xs -breakpoint--sm">
<section>
<h1>Small version</h1>
</section>
</div>
<div class="-breakpoint--md -breakpoint--lg -breakpoint--xl">
<section>
<h1>Large version</h1>
</section>
</div>
</div>
Component CDN storage
Editing in builder
The simpliest case is when component was changed inside the builder. The backend simply republishes the file on CDN, so
the changes are visible the next time page is reloaded. If the previous version of the component file was cached in the
browser, the Clientside code will check if version of CDN file is more recent than the cached one, and will forcefully
reload the file and re-render it. The caching is implemented using a custom variation of stale-while-invalidate strategy
found in service workers. The difference is that the rendered cached component gets swapped upon recieving update
ensuring that there's no latency penalty to be paid for cache busting.
In case of builder publishing the changed component, all instances of that component on all sites and pages will see the
change, with exception of "forked" components (see below).
Editing in place
At the time of embedding a component may be assigned a special instance attribute which determines unique identifier of
embedded component instance. That allows that component to be editable in place. Initially it will share its version
content, styles and settings with all the other instances of the same component. However after embedding user may decide
to edit the contents, styles or datasources of the component instance using an inline version of Component Builder
(inside Pages or on the website).
Further changes to the original component (staged or published) will stop affecting such edited component instance.
Change history of a that component instance becomes divergent from the history of the original, creating a metaphorical
fork in the road for the component. Editing component instance inline "forks" it.
A forked version of a component has simplified publishing flow: Changes to the forked component automatically become
visible in Pages and on the website (as if they were both staged and published). After initial integration, this will be
improved to make forked component a subject to Pages publishing lifecycle, acting exactly as other XM/SXA component
would.
It is possible to "unfork" component, resetting its changes and reverting to the shared state. It will both reset the
state, and will re-enable component receieve updates when the original is updated.
Technical difficulty of forking components lies in the hard requirement of being able to do forking and unforking
without the need for changing the embed code/configuration. Which makes the most intuitive sense in the use case of
editing component on the final HTML page, that lacks the mechanisms to communicate with backend to save the changes. It
could be that the component is even used in the context of static HTML that does not have any underlying CMS, so
changing the embedding code would require updating the remote file or template (perhaps even requiring a full release of
the app).
When the component is displayed on the page, it needs to fetch its HTML contents fron CDN. HTML files are put on the CDN
using predictable name, like: /components/:component_id/:version_id/:status.html. If a component has instance attribute
set, the clientside code will fetch a second url from CDN in parallel: /instances/:instance.html, thus determining if
that instance of a component was forked or not.
If the second request had 404 Not Found status, the original component definition returned by the first request would be
displayed. In that case the instance is considered not forked, so any staged/published changes to that component will be
reflected as usual.
If the second request was successful, then that forked version of a component will be displayed instead of the shared
one. Further updates to shared state will be shadowed by the customization until the component is unforked. But the
forked component remains editable.
Versioning in UI
Bringing up the Editor on the forked component, displays Fork status instead of Draft. Staged and Published statuses can
be still previewed, showing the current upstream version of the component, allowing to visualize the changes that
happened to the original since forking. It will allow merging upstream changes to a fork, using CKEditor's operational
transformations feature. That requires computing diff of a fork against its original version, and then resolving it
against diff of upstream changes. OT ensures conflict-free merging of changes.
In addition to that, this same UI allows unforking, by reverting to either staged or published version.
Swapping component
The most tricky case of in-line editing comes in case of swapping a component or version to another. Reference to the
component and the version is a part of embed code. In Pages we can simply update the component rendering parameters. But
for static HTML pages updating the embedding code is not an option.
Swapping a component in this case works as if instance of a component was forked, and its whole contents was replaced
with the contents of another component. The forked component HTML file on CDN, just like all other HTML files contains
metadata that indicate which component and version it relates to, thus allowing the UI to properly visualize upstream
staged/published states of the new component.
The limitation of this approach is that a swapped content will not reflect updates made to its definition. It could be
alleviated in the future by making yet another request to CDN to check for changes, but in the first implementation of
in-place editing this is not going to be addressed
Component styling
Style guides is a tool for editing css stylesheets. It offers designers the ability to predefine reusable chunks of
style for individual elements and their compositions. The resulting styles are then packaged as a static CSS file
available for Component Builder, Page Builder, or regular HTML webpages.
Style guide can be invoked from within component builder too, allowing the builders to create one-off "instance" styles.
In that case the resulting style that is only applicable to a specific element, is placed directly into HTML document.
It is possible to use the styles as a part of BYOC component as well.
Using elements and rules
Each element type like button, card, section has a dedicated class making it recieve the style. Designers can create
their own element types, but some are built in. FEAAS class name starts with dash.
Text elements
-paragraph
or <p>
-heading1
or <h1>
-heading2
or <h2>
-heading3
or <h3>
-heading4
or <h4>
-heading5
or <h5>
-heading6
or <h6>
###Block elements
-card
-section
or <section>
-blockquote
or <blockquote>
-block--media
-block--my-custom-type
- custom block type
Inline elements
-button
or <button>
-link
or <a>
-inline--badge
-inline--my-custom-type
- custom inline type
Applying the above classes inside an container with -feaas -theme--name
classes will apply default theme styles for
each. It is possible to use specific element styles defined in style guides overriding theme defaults with extra
--name
at the end:
-button--primary
will apply Primary
style of a button, and -inline--badge--important
will apply Important
style
of the badge
<section class="-feaas -theme--default">
<button class="-button--primary">
Default primary button
</button>
<button class="-button--primary -typography--chunky">
Chunky primary button
</button>
<style>
.-typography--chunky[data-instance-id="unique"][class][class][class] {
font-size: 32px;
line-height: inherit;
}
</style>
<button data-instance-id="unique" class="-button--primary -typography--chunky">
Customized chunky primary button
</button>
</section>
```
## Using themes
Style guides embraces controlled style cascade to allow parent elements to provide default styles for its children
elements. Children have flexibility to redefine any aspect of those inherited styles. [#Specificity-Layers] is used to
ensure styles do not clash.
Besides convenience of editing, this approach is essential to handle rich data mapping. HTML content coming from the CMS
needs to be styled "implicitly" through applying a theme to its parent element. This way paragraphs, headings, links and
pictures will have predictable styled look.
NOTE: composites feature is not yet exposed in the style guides UI.
It is also possible to specify theme styles. Supposed there's a theme button style with id `deadbeef`:
* `-feaas -theme--default -use--deadbeef` will make all buttons use that style inside the elementg
* `-button -deadbeef` - will make specific button use this style
```HTML
<section class="-feaas -theme--dark">
<div class="-section">
<div class="-card">
<div>
<div class="-button">
Yes
</div>
<button>
Yes
</button>
<button class="-typography--chunky">
No
</button>
</div>
<h2>Test<h2>
<p>Test<p>
<ul>
<li>Hey</li>
<li>Yo</li>
</ul>
<img src="..." />
<p>Test<p>
</div>
<div class="-card--promo">
<button></button>
</div>
<div class="-card -theme--light -subtheme">
<button></button>
</div>
<div class="-block--accordion">
<button data-role="next">Forward</button>
<div data-role="list">
<div class="-card">
<img>
</div>
<div class="-card">
<img>
</div>
</div>
<button data-role="prev">Back</button>
</div>
</div>
</div>
Advanced: Specificity layers
Style guides use limited cascading, and five levels of style hierarchy of styles (Resets, Themes, Elements, Rules and
Instance styles). In addition styles have to be order-independent to ensure the easiest integration with pages or apps.
These requirements call for very particular approach to selectors, in order for all styles to co-exist predictably.
Ideally this problem has to be solved through the use of Layers feature of CSS Level 4, but it's barely supported
anywhere. It is possible to emulate it for the needs of style guides, by spreading out each layers in specificity
values. To increase specificity of a selector, we employ repetition of [class]
attribute selector. Below you can find
specs for all of the layers and the expected specificity values.
CSS resets
Components need some basic styles reset in order to establish style baseline.
[class*="-theme--"] * {
}
Themes
Themes are composite styles that descend to the children. Children can then completely override them if necessary.
Themes are applied to the component root, or to a card if theme allows pairing
.-theme--cool:not(#_) h1 {}
.-theme--cool:not(#_) h1 a {}
.-theme--cool:not(#_) .-card {}
.-theme--cool:not(#_) .-block--accordion h1 {}
.-theme--cool:not(#_) .-block--accordion [role="accordion-button"] {}
.-use--deadbeef:not(#_):not(x):not(x) h1 {}
.-use--deadbeef:not(#_):not(x):not(x) h1 a{}
.-use--deadbeef:not(#_):not(x):not(x) .-card {}
.-use--deadbeef:not(#_):not(x):not(x) .-card h1 {}
.-use--deadbeef:not(#_):not(x):not(x):not(x):not(x) .-block--accordion [role="accordion-button"] {}
.-theme--nested.-subtheme:not(x#_):not(x):not(x):not(x) h1 {}
.-theme--nested.-subtheme:not(x#_):not(x):not(x):not(x) h1 a {}
.-theme--nested.-subtheme:not(x#_):not(x):not(x):not(x) .-button {}
.-theme--nested.-subtheme:not(x#_):not(x):not(x):not(x) .-button h1 {}
.-theme--nested.-subtheme:not(x#_):not(x):not(x):not(x) .-block--accordion [role="accordion-button"] {}
Elements
Elements are created with a choice of default reusable rules (typography, decoration, spacing, etc.). All styles are
merged together into a single definition of element class, allowing other reusable rules applied on top to override the
defaults. So only a single class needs to be applied for all default styles to take effect.
One benefit of that approach is that the designers may change the defaults for elements retroactively. For example, they
may change "Primary button" to have "Bold typography" by default. This take effect in all places where that button style
is used and which dont have typography rule override.
Some elements consist of parts (carousel has buttons, accordion has title & details, etc). Creating a style for
composite element is similar to theme, it adds default choice of buttons and styles. Later user can change an element
style, or only an aspect of it.
.-card--cool:not(#_._._._._) {}
.-card--cool:not(#_._._._._) [data-role="prev"] {}
.-theme--dark .-card--cool.--deadbeef:not(x#_._._) {}
.-theme--dark .-card--cool.--deadbeef:not(x#_._._) [role="accordion-button"] {}
.-typography--rule:not(#_._._._._._._) {}
.-typography--rule:not(#_._._._._._._)[data-instance-id="unique-element"] {}
Reusable rules
Reusable rules represent different aspects of styling: Typography, decoration, fill, etc. Creating element consists of
choosing applicable and default rules. Allowing more than one rule per category for an element, makes it possible for
designer to choose alternatives when placing that element on a page or component.
.-typography--rule:not(#_._._._._._._) {}
.-typography--rule:not(#_._._._._._._)[data-instance-id="unique-element"] {}
Properties
Style guides are meant to simplify the CSS creation, instead of being simply visual way to edit css. To remove
redundancy or complexity certain changes or additions had to be done:
Spacing
Margins are pretty tricky in CSS (issues like margin-collapsing or auto value, and redundancy with padding can be
confusing). Style guides offer no direct manipilation of margin, and instead offer simplier options, either layout gap,
or paragraph spacing. To achieve that, the system uses a shared
[https://css-tricks.com/using-custom-property-stacks-to-tame-the-cascade/](variable stack) to ensure that value
customizations and redefinitions do not rely on specificity.
.element {
marign-top: var(---typography--paragraph-spacing, var(---self--row-gap, 0px));
marign-left: var(---self--column-gap, 0px);
}
-
Gap - Space between elements in layout (columns, block elements), defined on the parent to control spacing between
its children.
.-layout--horizontal:not(#_._._._._._._) > :nth-child(1n+3) {
---spacing--row-gap: 10px;
}
-
Gap override (codename) - Child element can override the gap set by its parent. Used in builder. Similar to _
align-self_ and justify-self logic of flexbox. Independent from direction of layout.
.-card--my-element:not(x#_._._._._) {
---self--row-gap: 10px;
---self--column-gap: 10px;
}
-
Paragraph spacing - like gap, but for text elements. Takes precedence over vertical gap. Because text elements can
not be direct children inside horizontal layouts, does not care about direction.
.element {
---typography--paragraph-spacing: 10px;
}