The 6M tutorial, or: How To Write a 6M component
So you want to write a 6M component for the first time? Don’t worry, we’ve got you covered. This page provides a step-by-step tutorial on building such a component.
If you don’t even know what 6M means, please have a look at the 6M Introduction to learn about the basic concepts. Also, please note that this tutorial shows just one way to build a 6M component. There is an actual specification which describes the mandatory traits of a 6M component; you are free to use your favourite tools or frameworks to implement them.
Prequisites
This tutorial assumes that you have working knowlegde of Node.JS and ES6+. You should have a recent version of NPM installed, along with common development tools such as Git. To execute the commands, you need a shell like Bash or zsh, preferably on a Linux/Unix/MacOSX operating system. To run containers, you need Docker and must be able to use the docker
command line tools.
NOTE: Completing this tutorial will take two or more hours, depending on your skills. Take your time to do it thoroughly and try to understand what happens in each step.
Setting up our work environment
In this tutorial, we will create a mono-repo component which contains both the frontend and the middle layer in one Git repository. Start with initializing our project directory:
# create project directory and go inside
mkdir 6m-tutorial
cd 6m-tutorial
# init repo
echo "This is my first 6M component." > README.md
git init
git add .
git commit -m "initial commit"
The Frontend
Create Directories and Files
The frontend will be implemented as a web component which provides a custom element. The following commands will create some files and folders. Take the following structure as an example; your actual project will usually look different.
# create frontend directory and go inside
mkdir frontend
cd frontend
#
# NOTE: From now on, everything will happen inside, or relative to, the “frontend” directory,
# until we explicitely leave it
#
# create a few files we’re going to need later
touch index.html main.js
# create package.json
npm init --yes
# ignore some auto-generated files
echo -e "node_modules/\ndist/\n.cache/" > .gitignore
# commit changes
git add .
git commit -m "basic frontend structure"
Create a Web Component
Open the main.js
file in an editor of your choice and insert the following code:
export class Tui6mTutorialComponent extends HTMLElement
{
constructor() {
super()
this.shadow = this.attachShadow({ mode: 'open' })
}
connectedCallback() {
this.shadow.innerHTML = `
<style>
:host {
/* The "all" property resets any CSS inherited from the outer context */
all: initial;
display: block;
border: 5px solid #aaa;
background: #daffd5;
padding: 20px;
}
</style>
<div>Hello World!</div>
`
}
}
customElements.define('tui-6m-tutorial-component', Tui6mTutorialComponent)
The above code is a very simple custom element. If you don’t know what this is, please take some time to familiarize yourself with web components, custom elements and Shadow DOM.
# commit the changes from this chapter
git add .
git commit -m "basic frontend structure"
Let’s View it in the Browser
To test our work so far, we’ll create a simple HTML file containing the component. This is not a 6M requirement, but it will help us during development. Open the index.html
file and insert the following HTML.
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Web Component Demo</title>
</head>
<body>
<script src="/main.js" async defer></script>
<tui-6m-tutorial-component></tui-6m-tutorial-component>
</body>
</html>
To bundle the JavaScript and serve the test page, we will use Parcel, a lightweight JavaScript bundler with an integrated development server.
npm install --save-dev parcel
npx parcel --port 3000 index.html
After opening the URL printed by Parcel, you should now see our frontend in the browser. Hooray!
NOTE: Parcel creates the dist/
and .cache/
directories where it puts auto-generated stuff. You can ignore these directories and add them to your .gitignore
file.
# commit the changes from this chapter
git add .
git commit -m "add Parcel to serve the demo page"
Locale support
A proper 6M component must support multiple languages through the locale
attribute. For our tutorial, we will use the l10n library for this, but you may use any other suitable solution as well.
First, install the package:
npm install --save @tuicom/l10n@4
# TODO: We need to migrate this tutorial to use the latest version
Add the following line at top of your main.js
file:
import l10n from "@tuicom/l10n"
In order to make the component aware of the locale
attribute, you must add the static get observedAttributes
to the class:
static get observedAttributes() {
return ['locale']
}
Now replace the <div>Hello World!</div>
line with <div>${l10n.tl("Hello World", this.getAttribute("locale"))}</div>
.
We now have implemented localisation support, but we still need some actual translations. The l10n library will help with this. First, add the following entry to the package.json
file:
"l10n": {
"directory": "l10n",
"locales": [ "de-DE", "fr-FR" ],
"extract": [
"main.js"
],
"tables": {
"l10n/translations.json": [
"main.js"
]
}
}
(To understand in detail what these entries do, please refer to the documentation of the l10n library.)
The following command will extract the translatable strings (i.e. those wrapped into the l10n.tl
and related functions) in the code and create language catalog files from them.
npx l10n extract
This will create the l10n
directory, as well as the de-DE.po
and fr-FR.po
files. You may have seen this type of files before, they contain pairs of source (msgid
) and target (msgstr
) messages. Open these files in an editor, and fill the empty strings in the msgstr
lines:
# in de-DE.po (German)
msgstr "Hallo Welt"
# in fr-FR.po (French)
msgstr "Bonjour le monde"
Now the catalogs are updated with the correct translations, and we can create the translations table:
npx l10n tables
This will create the l10n/translations.json
file which contains all available translations. In your main.js
, add the following lines below the existing import …
:
import translations from "./l10n/translations.json"
l10n(translations)
Now the component is fully multilingual. To test this, open the index.html
file and add the locale="de-DE"
attribute to the tui-6m-tutorial-component
element, to make it look as follows:
<tui-6m-tutorial-component locale="de-DE"></tui-6m-tutorial-component>
If everything went well, you should now see your component displaying the Hallo Welt
text in your browser.
You may wonder why we need both translation catalogs (i.e. the .po
files) as well as the JSON translation table. There are two reasons for this: First, the PO format is widely supported by translation tools as well as translation agencies, which can be handy if you need your application translated by third parties. Second, the catalog contains all translations of your application per language, whereas the JSON file contains all translations for all languages – either for the entire application or a subset.
# commit the changes from this chapter
git add .
git commit -m "make the component multi-lingual"
Custom Attributes
Web components are configured through attributes on the custom element. We already know the locale
attribute from the previous section, let’s now add another one. How about our component would change the border color depending on the value of an attribute? Let’s introduce the level
attribute which can have the values warning
or alert
.
In the observedAttributes
method, add the level
value to the array, so that it looks as follows:
static get observedAttributes() {
return ['locale', 'level']
}
Add the following CSS rules to the <style>
block:
:host([level="warning"]) {
border-color: #ff9900
}
:host([level="alert"]) {
border-color: #900
}
Finally, add the level="warning"
attribute to the custom element. If everything went right, you should now see an orange frame around the component.
# commit the changes from this chapter
git commit -am "change border color when 'level' attribute is set"
Events
6M components use an event handler called “Cotton Ball” to communicate with other 6M components or with the host site. The Cotton Ball has to be loaded by the host site, therefore you must add the following line to your index.html
’s head:
<script src="https://api.tui.com/ml/cotton-ball/"></script>
To emit and receive events, there are the publish
and subscribe
methods. Use them to inform other 6M components, or your host context, about things that happen inside your component, or to learn about other components.
One important thing to know about the Cotton Ball is that it allows scoping events. We will explain later what this is, for now you only need to add the scope
string to the array in the observedAttributes
method:
static get observedAttributes() {
return ['locale', 'scope', 'level']
}
We will now extend our component to publish an event and subscribe to another one.
To demonstrate the event mechanism, we’ll simply throw a 6M event when our element is clicked, then propagate the coordinates as the payload data. Add this code to the component’s constructor:
this.addEventListener("click", ev => {
let rect = ev.target.getBoundingClientRect()
tuiCottonBall.publish(
"tutorial-component",
this.getAttribute("scope") || "*",
"click",
{ xPos : ev.clientX - rect.left, yPos : ev.clientY - rect.top }
)
})
To check that there really is an event, we’ll listen for it on the host page. Add this script somewhere in the index.html
document:
<script>
tuiCottonBall.subscribe(
"tutorial-component",
'*',
"click",
(componentName, scope, eventName, data) =>
console.log(componentName, scope, eventName, data)
)
</script>
Now, when you click the component’s element, you’ll see the parameters logged to the console.
Let’s implement an event subscriber, too. It will allow changing our background color through a 6M event. Add this to the constructor:
tuiCottonBall.subscribe(
"tutorial-component",
this.getAttribute("scope") || "*",
"background.change",
(componentName, scope, eventName, data) => {
this.style.backgroundColor = data.color;
}
)
Again, we need some code in index.html
to check if this works. In this case, we’ll turn the background blue after three seconds:
<script>
setTimeout(() => {
tuiCottonBall.publish(
"tutorial-component",
'*',
"background.change",
{ color: "#00c2ff" }
)
}, 3000)
</script>
Great, isn’t it?
So what’s the deal with the scope
? The scope
propagates an event with the same componentName
and eventName
amongst all components registered in the same scope. This allows to have multiple instances of a component on the same page and addressing them separately. The *
scope is a bit special: When you use the *
scope in the publish
method, all components registered for the event names receive the data, regardless of their actual scope.
# commit the changes from this chapter
git commit -am "add event subscriber and publisher"
Skeleton
A skeleton is a snippet of HTML code which acts as a placeholder until the component is loaded. It should resemble the actual components with a minimal design and take up (if possible) the same space, i.e. have the same width and height. It is important that the skeleton is styled with inline CSS; if it had to wait for a CSS file to load, it would be pointless.
This is what a skeleton looks like:
We will store the skeleton with other 6M meta data in a separate directory structure:
# create the 6m/skeleton folder
mkdir -p 6m/skeleton
# create skeleton file
touch 6m/skeleton/minimal.html
Now put the following HTML snippet into the 6m/skeleton/minimal.html
file.
<div style="display: block; background: #eee; padding: 30px">
<div style="background: #bbb; height: 1em; width: 10em"></div>
</div>
Consumers of your component will now be able to build their sites with your placeholder, providing a smoother user experience to their visitors.
# commit the changes from this chapter
git add 6m/skeleton
git commit -m "add skeleton placeholder"
User documentation
The best software is useless without proper documentation. There are two places where you should document your component. The first is the repository’s README.md
, which explains other developers how your code works. (We’re not going into that, you should know what to do here.)
Additionally, you must create documentation specifically for website maintainers who want to use your component. This part only needs to explain the business side of your component. Explain, in a few simple paragraphs, what your component does and which business problems it solves. You don’t need to provide technical documentation on attributes and events here, that stuff goes to the 6m.json
file, see below.
The documentation is to be provided as an Markdown file. So let’s create one. For this tutorial, we’re not going to write too much:
mkdir 6m/docs
echo "This 6M component prints **Hello World** in different languages." \
"It is able to display states as different border colors and" \
"emits Cotton Ball events when clicked." > 6m/docs/main.md
As always, don’t forget to commit your changes:
# commit the changes from this chapter
git add 6m/docs
git commit -m "add main documentation"
Icon
In order to have your component be shown in the Components Catalog, you must provide an icon with a size of 300x300 pixels. Luckily, we have one right here for you:
Create a directory 6m/images
and store the above icon as icon.png
in that directory.
# commit the changes from this chapter
git add 6m/images
git commit -m "add icon"
The 6m.json
file
We’re almost done with the frontend, but one big task is still at hand: Each 6M component must come with a 6m.json
file, describing the 6M-related aspects of the component.
# create the file
touch 6m.json
The following JSON snippet, which you must insert into the 6m.json
file, contains the mandatory entries of such a file. Have a look at the 6M reference for a detailled explaination and other, optional fields.
{
"name": "My component",
"description" : "This 6M component prints **Hello World** in different languages",
"icon" : "6m/images/icon.png",
"documentation" : "6m/docs/main.md",
"maintainer" : "you@example.com",
"6m-version" : "2.0",
"locales" : ["en-US", "de-DE"],
"tag" : "tui-6m-tutorial-component",
"attributes" : [],
"events" : {
"publish" : [],
"subscribe" : []
},
"skeletons" : []
}
Most of these entries should be self-explanatory, but you have certainly noticed a few empty arrays.
Attributes
Let’s start with attributes
. For each attribute which our component supports (except for locale
and scope
), the attributes
array must receive one entry describing the attribute. In our case, that’s the level
attribute. Therefore, we add this to the attributes
array:
{
"name" : "level",
"description" : "Allows setting a severity level, which will paint the border of the component accordingly.",
"schema" : {
"type": "string",
"enum": ["warning", "alert"]
},
"required" : false
}
Notice the schema
and required
fields: The schema
field contains JSON Schema and formally describes the possible values for the attribute. The required
field tells us if the attribute must be set on the element. In our case, it doesn’t, because we have info
as internal fallback.
Events
Events are specified in a similar way, though the objects are a bit different. Therefore we must add this to the publish
array …
{
"name" : "click",
"description" : "Occurs when the element is clicked and provides the coordinates of the click, relative to the element.",
"data" : [
{
"name" : "xPos",
"description" : "The horizontal position of the click.",
"schema" : {
"type": "integer",
"minimum": 0
}
},
{
"name" : "yPos",
"description" : "The vertical position of the click.",
"schema" : {
"type": "integer",
"minimum": 0
}
}
]
}
… and this to the subscribe
array:
{
"name" : "background.change",
"description" : "Allows changing the background color.",
"data" : [
{
"name" : "color",
"description" : "The color code. Currently, only 3- or 6-digit hex codes are supported.",
"schema" : {
"oneOf": [
{
"type": "string",
"pattern": "^#[A-Fa-f0-9]{3}$"
},
{
"type": "string",
"pattern": "^#[A-Fa-f0-9]{6}$"
}
]
},
"default" : null
}
]
}
Skeleton
Finally, add our skeleton to the skeletons
array:
{
"location" : "6m/skeleton/minimal.html",
"description" : "A minimal, non-animated skeleton for bright backgrounds."
}
You will later learn how to validate the 6m.json
file and the files referenced therein. But for now, that’s all: we are done with the frontend!
# commit the changes from this chapter
git add 6m.json 6m
git commit -m "create 6m.json spec file"
The Middle Layer
A 6M middle layer is an optional service which connects the frontend to the world of backends. It is not mandatory for a 6M component to have a middle layer, but it is handy when e.g. you need to hide confidential information, or as a proxy for complex APIs.
For this tutorial, we don’t have any of these requirements; however we will still create a middle layer to demonstrate the combination of frontend and middle layer.
Middle Layer Setup
The middle layer in this tutorial will be a lightweight ExpressJS application. ExpressJS is written in JavaScript, but middle layers can be written in other languages, too. For instance, Go has proven to be particularily well suited for middle layers. Anyway, ExpressJS will do in our case.
Open a new shell and navigate to the 6m-tutorial
directory. Then run the following commands.
# create the middlelayer directory and enter it
mkdir middlelayer
cd middlelayer
# create the main server file
touch server.js
# init NPM package and install dependencies
echo "node_modules/" > .gitignore
npm init --yes
npm install --save express
npm install --save-dev nodemon
# add middlelayer to Git
git add .
git commit -m "basic middlelayer structure"
A Simple Server
Open the server.js
file in a text editor and add the following code:
const express = require('express')
const server = express()
const port = process.env.PORT || 80;
server.listen(port, () => console.log(`Server started listening on http://localhost:${port}.`))
server.get('/', (req, res) => {
res.send({ value: Math.round(Math.random() * 10000) })
})
This is a very simple server which will provide a JSON object with a random integer number. The server can be started by running:
PORT=3500 npx nodemon server.js
The server would listen on port 80 by default, this is the mandatory default for a 6M middle layer. However, this won’t work in your dev environment, because you will (hopefully) not run the server as root, and therefore you cannot occupy port numbers lower than 1024. This is why we made the port number configurable and allow passing it as an environment variable.
To test the server, open your browser at localhost:3500. You will see that it returns a JSON object like { value : 1234 }
.
# as always, commit changes
git commit -am "simple server, returning random numbers"
Status Endpoints
6M middle layers need to implement two special endpoints, /health
and /monitor
to inform the runtime environment of their status. The /health
endpoint just needs to return a HTTP 200 response together with a body containing OK
, the /monitor
endpoint will output a JSON object with details. (The format/content is currently not specified, but it’s a good idea to return extended health information about the service itself and its backends, if applicable).
To satisfy these requirements, let’s add the following code to the server.js
file:
server.get('/health', (req, res) => res.send('OK'))
server.get('/monitor', (req, res) => res.send({ self : "OK" }))
# as always, commit changes
git commit -am "add /health and /monitor endpoints"
CORS Support
As the middle layer runs on a different server, you may run into problems with CORS. Therefore, we need to enable CORS support in the middle layer. First, install the cors package:
npm install --save cors
Now add the following to the server.js
below the const port …
line:
const cors = require('cors')
server.use(cors())
A quick commit doesn’t hurt.
# as always, commit changes
git commit -am "add CORS support"
Connecting the Frontend with the Middle Layer
The middle layer would be useless if the frontend wouldn’t talk to it, right? So let’s get them connected.
Open the frontend/main.js
file and add the following code at the bottom, above the customElements.define…
line:
let assetsUrl = "__TUI_6M_ASSETS_URL__",
middlelayerUrl = "__TUI_6M_MIDDLELAYER_URL__";
// development fallback
if (assetsUrl.indexOf("__") === 0)
{
assetsUrl = "http://localhost:3000";
middlelayerUrl = "http://localhost:3500";
}
What’s this, you ask? When the frontend and the middlelayer are deployed, the assets need to be informed of their own URL (for loading additional assets) and the URL of the middle layer. The __TUI_6M_ASSETS_URL__
and __TUI_6M_MIDDLELAYER_URL__
placeholders will be replaced with the actual URLs during deployment. The section labeled “development fallback …” is there to set fallback URLs during development (in our case: localhost:3000
/localhost:3500
), because the URLs won’t be injected there.
Now the frontend knows where to find the middlelayer. Let’s extend the frontend to actually make requests. Add the following method to the Tui6mTutorialComponent
class in frontend/main.js
:
loadRandomNumber()
{
const xhr = new XMLHttpRequest(),
shadow = this.shadow; // must be reassigned due to closure scope
xhr.addEventListener("load", function() {
shadow.innerHTML += ` ${JSON.parse(this.response).value}`;
});
xhr.addEventListener("error", error => console.error(error));
xhr.open("GET", middlelayerUrl);
xhr.send();
}
Then add a call to this.loadRandomNumber()
at the end of the connectedCallback
method. This will cause the component to make a middle layer call and add the result to the element each time the content is rendered.
Reload the frontend page in your browser to see if it works. It does? Excellent, then we’re done with the implementation.
# as always, commit changes
git commit -am "connect frontend with middlelayer"
The Build Process
We have now successfully implemented the frontend and the middlelayer. But for a 6M-compliant component, we need to create a unified build process for both of them.
To set up the build process, open a third shell and navigate to the 6m-tutorial
directory again.
The 6m-env
tool
Before we implement the actual build process, we will install the 6m-env tool. The tool itself is not a mandatory part of a 6M component, but it will help you with the development. It has two main features:
- It will validate your
6m.json
file and the contents referenced therein (documentation, skeletons, images). - It will build and run the component in a prod-like environment compliant with 6M specs, as described in the next chapter.
For more about 6m-env, see its documentation. Install the tool as NPM module in your repository root:
echo "node_modules/" > .gitignore
npm init --yes
npm install --save-dev @tuicom/6m-env
Now let’s validate the 6m.json
file using the validator:
npx 6m-validate frontend/6m.json
If the validator prints errors, fix them. (If you followed the frontend part of the tutorial properly, there shouldn’t be any.) If the above command doesn’t print anything, your 6m.json
file is valid.
# as always, commit changes
git add .gitignore package.json package-lock.json
git commit -m "add 6m-env tool"
Build Files
Frontend
The 6M frontend build happens inside a Docker environment, because it allows you maximum flexibility, while the build job doesn’t need to know anything about your dependencies.
# create the directory for build-related files
mkdir build
# create Dockerfiles
touch build/Dockerfile.assets build/Dockerfile.middlelayer
Put the following code into build/Dockerfile.assets
. It will perform all necessary frontend build steps while building the Docker image and put them into a specified location. We use the WORKDIR
directive to tell the frontend build process where to find the generated assets.
FROM node:11-alpine
COPY frontend/. /opt/src
RUN mkdir /opt/build && \
cd /opt/src && \
([ -d node_modules ] && rm -rf node_modules || :) && \
npm install && \
PARCEL_WORKERS=1 npx parcel build --out-dir /opt/build --out-file main.js main.js && \
cp -r 6m 6m.json /opt/build/
WORKDIR /opt/build/
In your own component, you can do whatever you want during the building of the Docker image and running the container. The only crucial thing is that you put your assets (in this case: the main.js
file, the 6m.json
file, the docs and the skeletons) into the WORKDIR
directory.
Middlelayer
The middle layer build will also produce a Docker image. Therefore, put the following code into build/Dockerfile.middlelayer
:
FROM node:11-alpine
COPY middlelayer/. /srv/
RUN cd /srv/ && \
([ -d node_modules ] && rm -rf node_modules || :) && \
npm install --only=prod --no-audit
WORKDIR /srv/
CMD ["node", "/srv/"]
In this tutorial, we have a very simple Docker build process. If your middle layer technology demands a “fat” build environment to produce a “slim” runtime image, you might want to look into multistage builds.
Now run the following commands to build/run the component with the tools provided by 6m-env
:
# builds the assets of the project as well as the middle layer image
npx 6m-build
# puts the assets into a webserver and starts it together with the middlelayer container
npx 6m-run
If everything went right, you should now see the following output and be able to open the frontend in a browser:
========== Demo site is running on http://localhost:4500/
If that’s the case, you have successfully completed this tutorial.