For our next exercise we start with creating a small very basic component that can be the basis of several end-customer components. When we have created this basic component we are going to extend it.
The basic component we are going to create is a card-component. It is commonly used on websites as a small panel with a summary of another page and a link to that page. It often has a header and an image.
Fire up a code editor, and create five files simple-card.html
, simple-card.js
, demo/index.html
, bower.json
and a package.json
.
<link rel="import" href="../bbconf2017-lithtml/bbconf2017-lit-element.html">
<script src="simple-card.js"></script>
class SimpleCard extends HTMLElement {
connectedCallback() {
console.log('simple-card element added to the DOM!');
}
}
customElements.define('simple-card',SimpleCard);
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Simple card component</title>
<style>
body {
width: 90%;
max-width: 980px;
background-color: #f5f5f5;
margin: 0 auto 32px;
}
</style>
</head>
<body>
<h1>Example page of the simple-card</h1>
</body>
</html>
{
"name": "simple-card",
"version": "0.0.1",
"description": "",
"main": "simple-card.html",
"devDependencies": {
"polyserve": "0.23.0"
},
"scripts": {
"start": "polyserve -o"
},
"repository": {
"type": "git",
"url": "https://github.com/The-Guide/bbconf2017-simple-card-component.git"
},
"engines": {
"node": ">=6.0"
}
}
{
"name": "simple-card",
"main": "simple-card.html",
"dependencies": {
"bbconf2017-lithtml": "https://github.com/The-Guide/bbconf2017-lithtml.git"
},
"devDependencies": {
"webcomponentsjs": "webcomponents/webcomponentsjs#^1.0.0"
}
}
Install the needed dependencies as follows:
npm install
to install the node dependencies.bower install
to install the bower dependencies.simple-card
componentTo employ <simple-card>
, you need to:
demo/index.html
.body
.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0,minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Simple-card-component</title>
<link rel="import" href="../simple-card.html">
<style>
body {
width: 90%;
max-width: 980px;
background-color: #f5f5f5;
margin: 0 auto 32px;
}
</style>
</head>
<body>
<h1>Example page of the simple-card</h1>
<h2>An basic example</h2>
<simple-card></simple-card>
</body>
</html>
Run npm start
to run your component in the browser. Just open the demo/index.html
page in the browser, open the developer tools a look at the Console
tab where you should see:
Lit-HTML lets you write HTML templates with JavaScript template literals, and efficiently render and re-render those templates to DOM. lit-html provides two main exports:
class SimpleCard extends Bbconf2017LitElement {
connectedCallback() {
console.log('simple-card element added to the DOM!');
}
}
// Register custom element definition using standard platform API
customElements.define('simple-card', SimpleCard);
Now that we use the Bbconf-lit-Element to extend we have to set this element as a dependency. Let's create a bower.json containing it.
{
"name": "simple-card",
"main": "simple-card.html",
"dependencies": {
"bbconf2017-lithtml": "https://github.com/The-Guide/bbconf2017-lithtml.git"
}
}
lit-html
utilizes some unique properties of HTML <template>
elements and JavaScript template literals. So it's helpful to understand them first.
A JavaScript template literal is a string literal that can have other JavaScript expressions embedded in it:
`My name is ${name}.`
A tagged template literal is prefixed with a special template tag function:
let name = 'Monica'; tag`My name is ${name}.`
Tags are functions of the: tag(strings, ...values)
, and strings
is an immutable array of the literal parts, and values are the results of the embedded expressions. In the preceding example, strings
would be ['My name is ', '.']
, and values would be ['Monica']
.
<template id="mytemplate">
<img src="" alt="great image">
<div class="comment"></div>
</template>
<template>
ElementsA <template>
element is an inert tree of DOM (script don't run, images don't load, custom elements aren't upgraded, etc) that can be efficiently cloned. It's usually used to tell the HTML parser that a section of the document must not be instantiated when parsed, but by code at a later time, but it can also be created imperatively with createElement
and innerHTML
.
The first time html
is called on a particular template literal it does one-time setup work to create the template. It joins all the string parts with a special placeholder, "[[_twoWayBinding('')]]"
, then creates a <template>
and sets its innerHTML
to the result. Then it walks the template's DOM and extracts the placeholder and remembers their location.
Every call to html
returns a TemplateResult
which contains the template created on the first call, and the expression values for the current call.
render()
takes a TemplateResult
and renders it to a DOM container. On the initial render it clones the template, then walks it using the remembered placeholder positions, to create Part
s.
APart
is a "hole" in the DOM where values can be injected. lit-html
includes two type of parts by default: NodePart
and AttributePart
, which let you set text content and attribute values respectively. The Part
s, container, and template they were created from are grouped together in an object called a TemplateInstance
.
Rendering can be customized by providing alternate render()
implementations which create different kinds of TemplateInstances
and Part
s, like PropertyPart
and EventPart
included in lib/lit-extended.ts
which let templates set properties and event handlers on elements.
We are going to make a simple card webcomponent. It should have a header, body and a footer. Let's start with the header. It should be a template with a slot.
class SimpleCard extends Bbconf2017LitElement {
get headerTemplate() {
return html`
<header id="header">
<slot name="header" id="headerslot"></slot>
</header>
`}
connectedCallback() {
console.log('simple-card element added to the DOM!');
}
}
// Register custom element definition using standard platform API
customElements.define('simple-card', SimpleCard);
Let's also create a body and footer template in the same way.
class SimpleCard extends Bbconf2017LitElement {
get headerTemplate() {
return html`
<header id="header">
<slot name="header" id="headerslot"></slot>
</header>
`;
}
get bodyTemplate() {
return html`
<div id="body">
<slot id="bodyslot"></slot>
</div>
`;
}
get footerTemplate() {
return html`
<footer id="footer">
<slot name="footer" id="footerslot"></slot>
</footer>
`;
}
}
// Register custom element definition using standard platform API
customElements.define('simple-card', SimpleCard);
We want to see if our templates work. Let's put some content in the card.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0,minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Simple-card-component</title>
<link rel="import" href="../simple-card.html">
<style>
body {
width: 90%;
max-width: 980px;
background-color: #f5f5f5;
margin: 0 auto 32px;
}
</style>
</head>
<body>
<h1>Example page of the simple-card</h1>
<h2>An basic example</h2>
<simple-card>
<h3 slot="header">Title</h3>
<div>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis varius quam quam.
</div>
<div slot="footer">Footer</div>
</simple-card>
</body>
</html>
Out lit-element provides a render()
to render the templates. Let's add them in the component.
class SimpleCard extends Bbconf2017LitElement {
get headerTemplate() {
return html`
<header id="header">
<slot name="header" id="headerslot"></slot>
</header>
`;
}
get bodyTemplate() {
return html`
<div id="body">
<slot id="bodyslot"></slot>
</div>
`;
}
get footerTemplate() {
return html`
<footer id="footer">
<slot name="footer" id="footerslot"></slot>
</footer>
`;
}
render() {
return html`
${this.headerTemplate}
${this.bodyTemplate}
${this.footerTemplate}
`;
}
}
// Register custom element definition using standard platform API
customElements.define('simple-card', SimpleCard);
Our card shows the content but is still unstyled. Let's add some styling. In your simple-card
add a styling template the same way you created the other templates. It should contain a style tag and some basic styling on the :host
element.
class SimpleCard extends Bbconf2017LitElement {
get styleTemplate() {
return html`
<style>
:host {
width: 100%;
position: relative;
display: block;
background: #ffffff;
border-radius: 4px;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14),
0 1px 5px 0 rgba(0, 0, 0, 0.12),
0 3px 1px -2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
line-height: 1.5;
font-weight: normal;
}
</style>
`;
}
...
render() {
return html`
${this.styleTemplate}
${this.headerTemplate}
${this.bodyTemplate}
${this.footerTemplate}
`;
}
}
// Register custom element definition using standard platform API
customElements.define('simple-card', SimpleCard);
Add now some styling on the header, body and footer:
get styleTemplate() {
return html`
<style>
...
#header ::slotted(*),
#body ::slotted(*),
#footer ::slotted(*) {
padding: 24px 24px;
}
#header {
color: inherit;
}
#header ::slotted(h1),
#header ::slotted(h2),
#header ::slotted(h3),
#header ::slotted(h4),
#header ::slotted(h5),
#header ::slotted(h6) {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
...
</style>
`;
Check the differences in the browser.
We have a very simple card component. It has some very generic features. Imagine that we want a specific card with a custom footer. How can we create that from the simple-card? It is easy, we can extend the simple card.
We are now creating a new component, the extended-card. Create extended-card.html
and extended-card.js
.
<link rel="import" href="../simple-card/simple-card.html">
<script src="extended-card.js"></script>
class ExtendedCard extends SimpleCard {
}
customElements.define('extended-card', ExtendedCard);
First, we want to extend some of the styling of the simple-card:
simple-card
.
class ExtendedCard extends SimpleCard {
get styleTemplate() {
return html`
${super.styleTemplate}
<style>
:host {
font-family: Roboto;
}
#header {
color: orangered;
font-size: 1.4em;
}
</style>
`;
}
}
customElements.define('extended-card', ExtendedCard);
Create a demo/index.html
to view the component, just like we did in the simple-dialog
.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>Extended-card Demo</title>
<link rel="import" href="../extended-card.html">
<style>
body {
width: 90%;
max-width: 980px;
background-color: #f5f5f5;
margin: 0 auto 32px;
}
</style>
</head>
<body>
<h1>Example page of the extended-card</h1>
<h2>An basic example</h2>
<extended-card last-updated="6">
<h3 slot="header">Title</h3>
<div>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis varius quam quam. Praesent gravida urna id
nunc luctus, nec varius tortor tristique. Mauris non sem vel magna consectetur sodales. Aenean ut urna
sodales, auctor elit condimentum, cursus neque. Sed quam mauris, ultricies ac congue ut, volutpat
volutpat dui. Quisque sagittis diam et justo fringilla condimentum. Cras ut dignissim dolor, nec
efficitur mi. Praesent vitae consectetur odio. Donec lectus nulla, tristique eget lacinia consequat,
porttitor in arcu. Fusce mattis nec leo nec vulputate. In hac habitasse platea dictumst.
</div>
</extended-card>
</body>
</html>
This card element we are creating should have a fixed footer like this (scroll down to the footer example) bootstrapped card footer. The background color should be light orange and the text should be centered. Please create a new footerTemplate
with the text "Last updated 5 minutes ago" and add styles too. Please add this styles:
#footer {
border-top: 1px solid #d5d5d5;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
background-color: #ffeedd;
font-size: 1em;
text-align: center;
color: grey;
padding: 4px 24px;
}
And add the following template:
get footerTemplate() {
return html`
<footer id="footer">
Last updated 5 mins ago.
</footer>
`;}
}
Now we see fancy footers with the info that it is updated 5 minutes ago. But of course it would be nice to set this dynamically.
First of all we need a property list with one property lastUpdated
, which we can add to the extended-card
static get properties() {
return {
lastUpdated: {
type: String,
value: '',
reflectToAttribute: 'last-updated',
}
};
}
The next step is to make use of this property, so we change our footerTemplate
with the following codesnippet:
get footerTemplate() {
return html`
<footer id="footer">
Last updated ${this.lastUpdated} mins ago.
</footer>
`;}
}
And of course we need to add this feature to the demo/index.html
.
<extended-card last-updated="6">
...
</extended-card>
We also need a fallback if no lastUpdated
attribute is set. First we create a second extended-card
in our demo/index.html
file.
<h2>An example without last-updated time stamp</h2>
<extended-card>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis varius quam quam. Praesent
gravida urna id nunc luctus, nec varius tortor tristique. Mauris non sem vel magna consectetur
sodales. Aenean ut urna sodales, auctor elit condimentum, cursus neque. Sed quam mauris,
ultricies ac congue ut, volutpat volutpat dui. Quisque sagittis diam et justo fringilla
condimentum. Cras ut dignissim dolor, nec efficitur mi. Praesent vitae consectetur odio.
Donec lectus nulla, tristique eget lacinia consequat, porttitor in arcu. Fusce mattis nec leo
nec vulputate. In hac habitasse platea dictumst.
</p>
</extended-card>
This will give a wrong message without a time.
We need to create a backup message if no last-updated time is provided.
get footerTemplate() {
if (this.lastUpdated) {
return html`
<footer id="footer">
Last updated ${this.lastUpdated} mins ago.
</footer>
`;}
else {
return html`
<footer id="footer">
Not updated recently!
</footer>
`;
}
}
Check the demo again to see the second extended-card
be rendered correctly.
In this codelab we first created a basic component, with hardly any styling and functionality. After that we created a more specific card based on the basic element. For all kind of use cases different components could be made based on the basic element, or even extensions of an extended-card.
We can have very simple, small, basic components which have only all basic functions covered. Every custom desire can be an extension of the basic element. Not only styling or properties can be added, but basically everything can be added.
Go to ing.nl, ing.be and ing.de and take a look at all different types cards. Each page already exists out of different type of cards. Some are having differ more than other, but in general they all have the same basis.
Try to create two, three or four extensions of the simple-card
or some more extended card. Which can match these different cards.