#Components
Components are reusable, self-contained, configurable chunks of code you can use throughout your site. They encapsulate related markup, style, and script which makes them very easy to use, understand, and maintain.
The Component system works by using custom HTML tags in your markup that are then replaced with the Component markup during the Build. Any style for the Component is then included on the Page that uses it as well as any Script.
Components can be extremely simple, such as a button with some style associated with it that can be used across the Site, or more complex such as a reactive Vue Component that lists sorted Blog Posts and has integrated search functionality.
You are encouraged to use Components often. Each website will have many custom Components created for it. Any time you need to use the same code multiple times, even within the same individual Page, you should make a Component. The Component system is designed to be easy to quickly make any Components you may need and use them without needing the help of a developer.
#Component Basics
Component names determine their tag using kebab-case. For example, a Component named "Site Button" will be used anywhere you use <site-button></site-button>
in a Page, Layout, or other Component's markup.
You can use multiple of the same Component on a single Page and each will be given a unique ID. They can each have their own style and attributes that may differ from each other, which you'll learn about below.
#Markup and Style
The simplest Components combine some basic markup you want to reuse with its associated style. Components must always have a single root tag. Any extra tags will be ignored. This allows the parent markup that uses it to be able to add attributes to it and modify its style such as height, width, and margin.
Here's an example of a simple button that says "Click Me".
.site-button Click Me
.site-button
display inline-block
padding 8px 16px
border 1px solid rgba(0, 0, 0, 0.1)
border-radius 4px
cursor pointer
transition background 0.2s
&:hover
background rgba(0, 0, 0, 0.1)
And here's what it looks like if we use it on a Page:
.home-page
h1 Site Button Test
p Hey! Check out my cool new site buttons!
site-button
site-button
site-button
<!DOCTYPE html>
<html lang="en">
<head>
<title>Home</title>
<link rel="stylesheet" href="style.css">
<meta property="og:title" content="Home">
<meta property="og:type" content="website">
<meta property="og:url" content="https://example.com/">
<meta property="og:description" content="Home">
<meta property="og:locale" content="en_US">
<meta property="og:site_name" content="My Site">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="Home">
</head>
<body>
<div class="home-page">
<h1>Site Button Test</h1>
<p>Hey! Check out my cool new site buttons!</p>
<div class="site-button" data-cid-1>Click Me</div>
<div class="site-button" data-cid-2>Click Me</div>
<div class="site-button" data-cid-3>Click Me</div>
</div>
</body>
</html>
.site-button {
display: inline-block;
padding: 8px 16px;
cursor: pointer;
border: 1px solid rgba(0,0,0,0.1);
border-radius: 4px;
transition: background .2s
}
.site-button:hover {
background: rgba(0,0,0,0.1)
}
Notice how each site button was replaced with its defined markup and the style was added to the Page a single time to cover all of the buttons. It's recommended to always give your Component's root tag a class the same as its name (in this case .site-button
). If you don't do this, it will automatically be added so its parent has a way to easily style the Component to control its size and position.
#Basic Configuration
Components are supposed to be configurable so you don't have to write a new version of a Component for every small change you want to make. For this, we can use the Attributes tab.
Let's add an attribute called "Label". In the Attributes tab, add a new attribute and call it "Label". Give it a default value of "Click Me" since our website has been using this button and expects that value if Label isn't specified.
Now let's give it another attribute called "Big" and make it a Boolean type. We'll leave the default value as false.
Now let's update the markup and style to use the Label and Big attributes.
.site-button(class=attr.big && 'big') #{ attr.label }
.site-button
display inline-block
padding 8px 16px
cursor pointer
border 1px solid rgba(0, 0, 0, 0.1)
border-radius 4px
transition background 0.2s
&.big
padding 16px 24px
&:hover
background rgba(0, 0, 0, 0.1)
Now we can update the Page to use the new Attributes.
.home-page
h1 Site Button Test
p Hey! Check out my cool new site buttons!
site-button(label="Self Destruct")
site-button(label="Release the Kraken" big)
site-button
You can see that attributes use kebab-case in the parent template within the tag and use camelCase within the Component's code.
Here's the output we get:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Home</title>
<link rel="stylesheet" href="style.css">
<meta property="og:title" content="Home">
<meta property="og:type" content="website">
<meta property="og:url" content="https://example.com/">
<meta property="og:description" content="Home">
<meta property="og:locale" content="en_US">
<meta property="og:site_name" content="My Site">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="Home">
</head>
<body>
<div class="home-page">
<h1>Site Button Test</h1>
<p>Hey! Check out my cool new site buttons!</p>
<div class="site-button" data-cid-1>Self Destruct</div>
<div class="site-button big" data-cid-2>Release the Kraken</div>
<div class="site-button" data-cid-3>Click Me</div>
</div>
</body>
</html>
.site-button {
display: inline-block;
padding: 8px 16px;
cursor: pointer;
border: 1px solid rgba(0,0,0,0.1);
border-radius: 4px;
transition: background .2s
}
.site-button.big {
padding: 16px 24px
}
.site-button:hover {
background: rgba(0,0,0,0.1)
}
#Scoped Styling
You'll notice as well that each button was given a data-cid-n
attribute. This is used for scoped styling. You can see that the resulting markup on the Page is generated per button, and so far, the style has applied to all of the different variations of the button. But what if we want there to be custom style per button?
Let's add a "Main Color" attribute with a type of color and use a default value of "#ffffff". Then we can update the Component's style:
.site-button
display inline-block
padding 8px 16px
background {{ attr.mainColor }}
cursor pointer
border 1px solid rgba(0, 0, 0, 0.1)
border-radius 4px
transition background 0.2s
&.big
padding 16px 24px
&:hover
background darken({{ attr.mainColor }}, 10%)
Now let's update the colors of the buttons on the Page:
.home-page
h1 Site Button Test
p Hey! Check out my cool new site buttons!
site-button(label="Self Destruct" main-color="green")
site-button(label="Release the Kraken" big main-color="red")
site-button
Here's what the HTML and CSS will look like:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Home</title>
<link rel="stylesheet" href="style.css">
<meta property="og:title" content="Home">
<meta property="og:type" content="website">
<meta property="og:url" content="https://example.com/">
<meta property="og:description" content="Home">
<meta property="og:locale" content="en_US">
<meta property="og:site_name" content="My Site">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="Home">
</head>
<body>
<div class="home-page">
<h1>Site Button Test</h1>
<p>Hey! Check out my cool new site buttons!</p>
<div class="site-button" data-cid-1>Self Destruct</div>
<div class="site-button big" data-cid-2>Release the Kraken</div>
<div class="site-button" data-cid-3>Click Me</div>
</div>
</body>
</html>
.site-button[data-cid-1] {
display: inline-block;
padding: 8px 16px;
background: #008000;
cursor: pointer;
border: 1px solid rgba(0,0,0,0.1);
border-radius: 4px;
transition: background .2s
}
.site-button[data-cid-1].big {
padding: 16px 24px
}
.site-button[data-cid-1]:hover {
background: #007300
}
.site-button[data-cid-2] {
display: inline-block;
padding: 8px 16px;
background: #f00;
cursor: pointer;
border: 1px solid rgba(0,0,0,0.1);
border-radius: 4px;
transition: background .2s
}
.site-button[data-cid-2].big {
padding: 16px 24px
}
.site-button[data-cid-2]:hover {
background: #e60000
}
.site-button[data-cid-3] {
display: inline-block;
padding: 8px 16px;
background: #fff;
cursor: pointer;
border: 1px solid rgba(0,0,0,0.1);
border-radius: 4px;
transition: background .2s
}
.site-button[data-cid-3].big {
padding: 16px 24px
}
.site-button[data-cid-3]:hover {
background: #e6e6e6
}
You can see that MercuryCMS generates unique CSS for each button and scopes the style using the [data-cid-n]
selector.
#Script
Some Components will need script associated with them. For example, you may want to make a simple Tab List Component. The user can click the tabs to view the content.
.tab-list
.tabs
.tab(onclick="goToTab(event, 0)") First Tab
.tab(onclick="goToTab(event, 1)") Second Tab
.tab-content
div This is the first tab's content.
.tab-content
div This is the second tab's content.
.tab-list
.tabs
display flex
.tab
padding 8px 16px
border 1px solid rgba(0, 0, 0, 0.1)
border-left-width 0
border-bottom-color transparent
border-top-left-radius 4px
border-top-right-radius 4px
cursor pointer
position relative
top 1px
z-index 1
&:first-child
border-left-width 1px
&.selected
border-bottom-color white
.tab-content
border 1px solid rgba(0, 0, 0, 0.1)
padding 16px
border-radius 4px
border-top-left-radius 0
// show the clicked tab
window.goToTab = function (e, tabIndex) {
let tabListElement = e.target.closest('.tab-list')
// add selected class to selected tab only
tabListElement.querySelectorAll('.tab').forEach((tabElement, index) => {
if (index == tabIndex && !tabElement.classList.contains('selected')) tabElement.classList.add('selected')
if (index != tabIndex) tabElement.classList.remove('selected')
})
// show the tab-content for the clicked tab
tabListElement.querySelectorAll('.tab-content').forEach((tabContentElement, index) => {
if (index == tabIndex) tabContentElement.style.display = 'block'
else tabContentElement.style.display = 'none'
})
}
// hide all but the first tab within each tab-list when the page loads
document.querySelectorAll('.tab-list').forEach(tabListElement => {
// add selected class to the first tab
tabListElement.querySelector('.tab').classList.add('selected')
// hide all tab-content divs except the first
tabListElement.querySelectorAll('.tab-content').forEach((tabContentElement, index) => {
if (index) tabContentElement.style.display = 'none'
})
})
This script is added to any Page that uses the Component. Even if you add multiple of the same Component to the Page, the script will only be added once to handle all of them, so it's up to you to ensure the script works with multiple instances of the Component on the Page.
Because of this, you cannot use the attr
object within the script since that only applies to individual Component instances.
#Slots
This <tab-list>
Component works well, but the content is hardcoded into it. If you want the Page to be able to determine what's in each .tab-content
area, you'll need to use Slots.
Slots allow the parent to put content into the child Component's markup. The Component decides whether it supports Slots, and if it does, where that content goes.
Let's work our way up to adding Slots to the <tab-list>
and instead make a new example Component for now.
.site-notification
.heading Notification:
slot
.button(onclick="dismissSiteNotification(event)") Dismiss
.site-notification
padding 16px
border 1px solid rgba(0, 0, 0, 0.1)
border-radius 4px
.heading
color rgba(0, 0, 0, 0.7)
.button
border 1px solid rgba(0, 0, 0, 0.1)
border-radius 4px
padding 8px 16px
display inline-block
cursor pointer
transition background 0.2s
&:hover
background rgba(0, 0, 0, 0.1)
window.dismissSiteNotification = function (e) {
e.target.closest('.site-notification').remove()
}
Now let's use the <site-notification>
Component on the Home Page:
.home-page
site-notification
p A very important message.
site-notification
p We've been trying to reach you about your car's extended warranty.
Here's the output we'll get:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Home</title>
<link rel="stylesheet" href="style.css">
<meta property="og:title" content="Home">
<meta property="og:type" content="website">
<meta property="og:url" content="https://example.com/">
<meta property="og:description" content="Home">
<meta property="og:locale" content="en_US">
<meta property="og:site_name" content="My Site">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="Home">
</head>
<body>
<div class="home-page">
<div class="site-notification" data-cid-1>
<div class="heading">Notification:</div>
<p>A very important message.</p>
<div class="button" onclick="dismissSiteNotification(event)">Dismiss</div>
</div>
<div class="site-notification" data-cid-2>
<div class="heading">Notification:</div>
<p>We 've been trying to reach you about your car 's extended warranty.</p>
<div class="button" onclick="dismissSiteNotification(event)">Dismiss</div>
</div>
</div>
<script type="module" src="script.js"></script>
</body>
</html>
#Named Slots
We'll need multiple, named Slots to get different content into each tab in the <tab-list>
Component. If you don't give a Slot a name, it will be named "default".
Let's upgrade the <tab-list>
to have named slots. We only need to update its markup:
.tab-list
.tabs
.tab(onclick="goToTab(event, 0)") First Tab
.tab(onclick="goToTab(event, 1)") Second Tab
.tab-content
slot(name="firstTab")
.tab-content
slot(name="secondTab")
Now we can template in whatever content we want on the Home Page.
.home-page
tab-list
template(slot="firstTab")
div This goes in the first tab of the first list.
template(slot="secondTab")
div This goes in the second tab of the first list.
tab-list
template(slot="firstTab")
div This goes in the first tab of the second list.
template(slot="secondTab")
div This goes in the second tab of the second list.
Of course, this is pretty rigid since you can't change the tab names or how many tabs there are. Let's complete this example and at the same time, introduce Array attributes.
Let's add an Attribute called "Tab Names", make it an Array type, and give it a default value of []
. Then let's update its markup again:
.tab-list
.tabs
each tabName, index in attr.tabNames
.tab(onclick=`goToTab(event, ${index})`) #{ tabName }
each tabName in attr.tabNames
.tab-content
slot(name=tabName)
Now let's put some content on the Home Page:
.home-page
tab-list(tab-names=['Fruit', 'Vegetables'])
template(slot="Fruit")
ul
li Apples
li Oranges
li Pears
li Bananas
template(slot="Vegetables")
ul
li Peas
li Carrots
li Potatoes
li Onions
tab-list(tab-names=['Bedroom', 'Living Room', 'Basement', 'Office'])
template(slot="Bedroom")
ul
li Bed
li Nightstands
li Dresser
template(slot="Living Room")
ul
li TV
li Couch
li Coffee Table
template(slot="Basement")
ul
li Pool Table
li Arcade Cabinet
li Dart Board
template(slot="Office")
ul
li Desk
li Computer
li Office Chair
#Additional Slot Features
You can also mix named and unnamed Slots. If you don't name the Slot in the Component, it's given the name "default". If you add content in the parent outside of a <template>
tag, or don't give the <template>
tag a name
attribute, that content will go into the "default" Slot.
You can also add default content to all Slots. Within the Component, you can put data within any <slot>
tag and that content will show up if the parent doesn't specify any content for that Slot. This allows you to have default content used in most places but then allow the parent to override that if needed, or it can be useful to remind you to replace placeholder content.
#Attributes
We've already used Attributes in the above examples, but we haven't talked about exactly how they work.
Attributes are used for configurability of Components. It's not very useful to have to make a whole new Component just because you want a slightly different sized button or different content within its tabs.
You are encouraged to think of Attributes as an options screen that can be modified by the user of the Component. For example, rather than templating {{ theme.mainColor }}
repeatedly within your Component's style, use {{ attr.mainColor }}
, then let the user type {{ theme.mainColor }}
into Main Color's default value in the Attributes tab.
This also means the user can now use <some-component main-color="red"></some-component>
on an individual Component. This approach implicitly documents all of the configurable options the Component has. This also makes it much easier to import and export Components between Sites.
Here's a quick explanation of each Attribute type:
Type | Description |
---|---|
Text | The value will be cast to a string. This is used for most attributes. |
Text Area | The value will be a string with a multi-line input field. |
HTML | The value will be a string with an HTML input field. |
Color | The value is a color and has a color picker. This is useful for providing options to the user for how a Component should look. |
Number | The value will be cast to a number. |
Boolean | The value will be either true or false. If the value is "false", "off", "no", or "0", the value will be false . If it's "true", "on", "yes", or "1", the value will be true . You can also use terse tags. For example, <site-button big></site-button> would set attr.big to true without having to write big="true" . If the Attribute isn't present on the tag, it will be the default value. |
Media | A string value representing the _id of a Media item. |
Asset | A string value representing a path to an Asset. |
Enum | Enum values allow you to specify a list of valid values separated by commas, which can then be picked from in a dropdown. This is an easy way to document what values the Component expects. For example, you could have a "Position" Attribute with "static, relative, absolute, fixed, sticky" available which is then used in the Component's style. |
Array | The value expects an array of items. This can have whatever structure you want. It's recommended to provide a default value with placeholder content to easily show the user what the Component expects. |
Object | The value expects an object with keys and values. This can have whatever structure you want. It's recommended to provide a default value with placeholder content to easily show the user what the Component expects. |
A very important behavior of Attributes is that they are "consumed" at Build time. This means any Attributes you specify in the Attributes tab will be removed from the Component's tag on the Page, processed, and become available in the attr
object for templating into the Component's markup, style, script, and data.
Any attributes defined on a tag that are not defined in the Attributes tab are not consumed and are forwarded onto the Component's root tag defined in its markup. This allows the parent to add any classes, event handlers, etc. that it wants to. The Component's classes and the classes added by the parent are merged. All other attributes are overridden if the parent defines them.
With Pug, you can define class= multiple times and they will all merge. This is useful for dynamically adding classes due to boolean attributes
Attributes are consumed because you can pass in large and complex object references like entire Collection entries that shouldn't be baked into the output Page. They are intended to be used only at Build time.
Because they are consumed, you should avoid conflicting names. It's common to want to name an Attribute "Title", but this would conflict with the regular HTML attribute called "title" making it impossible for the parent markup to use the original functionality of this attribute. An easy way to avoid this is to always use two word names for Attributes.
You can, however, name Attributes a conflicting name and then still forward that attribute to the Component. For example, you could make a <site-button>
Component with an "Href" Attribute that gets put into an <a>
tag using <a href=attr.href></a>
.
#Tags
There are often times you will write Components that depend on other scripts, stylesheets, or other tags to be present on the Page. For information about how this works, see Tags.
#Data
Components have access to all Collections, the Site, the Page, and their assigned Attributes. There may be times you want to process some of that data in a way that makes it easier to Template, or manipulate data passed in via the Component's attributes. For this, you can use the Data tab.
The script in the Data tab is run once per Component at Build time. We haven't talked about Collections or Media yet, but a common use case is to get a Media object or Collection Entry that has been uploaded and use it in the Component. Let's make a simple <image-info>
Component for this.
First, add an Attribute called "Media ID" with no default value. Then write the markup:
if !attr.mediaId
.image-info You must specify a Media ID.
else if !data.media
.image-info The Media with ID #{ attr.mediaId } was not found.
else if data.media.type != 'image'
.image-info The Media with ID #{ data.media._id } is not an image.
else
table.image-info
tr
td ID
td #{ data.media._id }
tr
td Name
td #{ data.media.name }
tr
td Alt Text
td #{ data.media.altText }
tr
td Caption
td #{ data.media.caption }
In the Component's Data script:
data.media = mediaList.getById(attr.mediaId)
#Importing and Exporting Components
It's inevitable that you will build up an extensive catalog of configurable Components and use them across your websites. Rather than copy-pasting their markup, style, and script across Sites, you can export the Component from the Components screen and import it into another Site.
When you do this, you'll realize that you should write your Components in a way that doesn't directly interact with the Site's Theme Collection or other settings, but rather allows the user to Template those values into the Attributes screen on a per-site basis.
This makes maintaining your Components much easier and clearly documents what values the Component uses throughout its markup, style, and script.
#Build Scripts
Build scripts are an advanced feature used by developers to generate files for use on the website at Build time that the Component can use. This is most commonly done to create JSON configuration files and indexes for search Components. This is documented with an example in Vue Components.
#Help
To help others use your Component, you can write Help Pages. Help Pages are written in Markdown and support Github Flavor Features. This means you can add code blocks for your examples using triple-backtick syntax. For example:
# Site Button
This Component is used for all buttons on the site. Here's an example:
```pug
site-button(icon="check" secondary) Click Me
```
This syntax supports languages like pug
, html
, stylus
, css
, js
, etc.
Help Pages work much like their own miniature documentation site. This means each Help Page you make has its own path. The Help Page with a /
path will be shown by default in the Component's Help tab. You can make additional Help Pages with different paths and link between them.
To make a link to a Help Page with the path /another-page
, you would write [Another Page](component:/another-page)
. Any link that starts with component:
will link to this Component's Help Pages.
If you want to link to another component, use its HTML tag name in the link, like this: [Site Banner](/component:site-banner)
or [Site Banner](/component:site-banner/some-page)
.
#Best Practices
For best practices regarding creating and using Components, see Components Best Practices.