Marko makes building UI components extremely easy and fun! Today we are going to build a color picker component from scratch. We are going to learn how to:
- Create a project using marko-cli
- Create a basic and customizable color picker component
- Write component tests using marko-cli
Our final goal for today is create this component:
marko-cli comes packaged with useful commands for building Marko projects. Projects created using marko-cli come bundled with an HTTP server, and a build pipeline using lasso making it very easy to get started.
Let's first install marko-cli globally, so we can create our project:
Using npm
:
npm install -g marko-cli
Using yarn
:
yarn global add marko-cli
Now we are ready to create our Marko project:
# Creates a `color-picker-tutorial` project in the current directory
marko create color-picker-tutorial
Let's navigate to the newly created project and install the necessary dependencies:
cd color-picker-tutorial
npm install # Or `yarn`
We can now start our demo project and navigate to localhost:8080 to ensure that everything is working properly:
# Start the project!
npm start
NOTE: For a more detailed documentation of components, please see the markojs.com components documentation
In our new project, components are located in the color-picker-tutorial/components/
directory. Next we need to create our component in the components/
directory,
which should look like this:
color-picker-tutorial/
components/
color-picker/
index.marko
Marko also supports creating components using the file name. For example, the following is a valid directory structure:
color-picker-tutorial/
components/
color-picker.marko
Creating nested component directories is not required, but we recommend isolating most components in their own directories. Many components will contain additional files and tests that live alongside the component. Too many components living in a single directory will become very untidy and difficult to manage.
Let's begin by adding some initial component code to the color-picker
.
components/color-picker/index.marko
<ul>
<for|color| of=input.colors>
<li style={color: color}>
${color}
</li>
</for>
</ul>
input
in a Marko component is the input data that is passed to the component when
it is being rendered. Let's modify our index
route to demonstrate how a
parent component can use our color-picker
:
routes/index/index.marko
<html>
<head>
<title>Welcome | Marko Demo</title>
</head>
<body>
<h1>Welcome to Marko!</h1>
<color-picker colors=[
'#333745',
'#E63462',
'#FE5F55',
'#C7EFCF',
'#EEF5DB',
'#00B4A6',
'#007DB6',
'#FFE972',
'#9C7671',
'#0C192B'
]/>
</body>
</html>
Navigating to localhost:8080 should show us an
unordered list with list items for each of the colors that we passed as input
to our component.
We've created our first component! This component will eventually have nested components. When creating components, it's strongly recommended to consider how components can be broken down into multiple components. Each component can then be independently developed and tested.
Let's split our component into the following components:
<color-picker-header>
: The header will have the selected background color from the color picker and show the selected color's hex value
<color-picker-footer>
: The footer will contain a palette of colors and an input field for changing the hex value of the header
<color-picker-selection>
: The selection component is responsible for
displaying an individual color box and handling the associated click events
Marko automatically registers all components in nested components/
directories. Our new directory structure should look like this:
components/
color-picker/
components/
color-picker-footer/
index.marko
color-picker-header/
index.marko
color-picker-selection/
index.marko
index.marko
The color-picker
component should now have access to all of the child
components that we just created, and we can develop them all independently.
Let's start with with the <color-picker-header>
component. We've already
determined that the header should have a specific background color and display
the value of that background color in text. The color to display should be passed in as part of the input.
components/color-picker/components/color-picker-header/index.marko
// Inline styles!
style {
.color-picker-header {
width: 200px;
height: 100px;
border-radius: 20px 20px 0 0;
font: 30px Arial;
display: flex;
flex-direction: column;
text-align: center;
color: white;
}
.color-picker-header > p {
padding-top: 1.15em;
margin: 0;
}
}
// In Marko, we immediately start writing a single JavaScript statement by using
// `$`. For multiple JavaScript statements, use `$ { /* JavaScript here */ }
$ var color = input.color;
<!-- Our markup! -->
<div.color-picker-header style={backgroundColor: color}>
<p>${color}</p>
</div>
That's it! Our <color-picker-header>
is complete with styles and component
logic. This component is small enough to be contained in a single file, but
as components grow larger, we should split out the markup, component logic, and
styling. We will see an example of this soon.
Now let's look at what's going on. Marko has several
lifecycle methods including
onInput
, which contains a single parameter input
. As we discussed before
input
is the data that is passed to a Marko component upon initialization.
We can use inline javascript easily with $
(for a single statement) or $ { /* ... */ }
(for multiple statements),
which is great for creating variables that can be accessed inside of your
template. Additionally, single file components support inline styles, so the
component can truly be contained as a single unit if it's small enough.
Now we need to revisit our parent component and add the <color-picker-header>
tag to it, so it will be rendered.
components/color-picker/index.marko
class {
onInput(input) {
var colors = input.colors;
this.state = {
selectedColor: colors[0],
colors
};
}
}
<div>
<color-picker-header color=state.selectedColor/>
</div>
Marko will automatically watch the state
object for changes using getters and setters, and if the state changes then the UI component will be re-rendered and the DOM will automatically be updated.
Navigating to localhost:8080, we should see the
rendered <color-picker-header>
with a gray background like so:
Now let's create the <color-picker-selection>
component, which will be used
inside of the <color-picker-footer>
:
components/color-picker/components/color-picker-selection/index.marko
class {
handleColorSelected() {
this.emit('color-selected');
}
}
style {
.color-picker-selection {
width: 25px;
height: 25px;
border-radius: 5px 5px 5px 5px;
display: flex;
flex-direction: column;
margin: 5px 0px 0px 5px;
float: left;
}
}
<div.color-picker-selection
on-click('handleColorSelected')
on-touchstart('handleColorSelected')
style={
backgroundColor: input.color
}/>
In this component, we've introduced on-click
and on-touchstart
listeners and a single event handler function.
Marko components inherit from EventEmitter.
When this color is selected, it will emit a click
event and get handled by the
handleColorSelected
function. The handler then emits a color-selected
event to be handled by its parent. We will eventually write code to relay this information back to the <color-picker-header>
, so its background
color and text can be changed.
We are ready to create our final component, <color-picker-footer>
. This
component is going to contain a bit more logic than the other components, so
let's split it out into multiple files:
components/
color-picker/
components/
color-picker-footer/
component.js
index.marko
style.css
...
...
components/color-picker/components/color-picker-footer/index.marko
$ var colors = input.colors;
<div.color-picker-footer>
<div.color-picker-selection-container>
<div for(color in colors)>
<!--
Listen for the `color-selected` event emitted from the
<color-picker-selection> component and handle it in this
component's `handleColorSelected` method.
NOTE: We pass along the `color` to the event handler method
-->
<color-picker-selection
color=color
on-color-selected('handleColorSelected', color)/>
</div>
<input
key="hexInput"
placeholder="Hex value"
on-input('handleHexInput')/>
</div>
</div>
In the <color-picker-footer>
component we need to iterate over each color that was passed as input in colors
. For each color, we create a <color-picker-selection>
component and pass the color using the color
attribute. Additionally, we are listening for the color-selected
event emitted from the <color-picker-selection>
component and handling it in our own handleColorSelected
method. We provide the color
as the second argument so that it will be available to the event handler method. We also have added an input
field and a on-input
listener, which will trigger a change to the selected color when the user manually enters a hex color value.
components/color-picker/components/color-picker-footer/component.js
module.exports = class {
handleColorSelected (color) {
this.emit('color-selected', color);
}
handleHexInput () {
let hexInput = this.getEl('hexInput').value;
if (!hexInput.startsWith('#')) {
hexInput = '#' + hexInput;
}
if (!isValidHexValue(hexInput)) {
hexInput = this.input.colors[0];
}
this.emit('colorSelected', hexInput);
}
};
function isValidHexValue (hexValue) {
return /^#[0-9A-F]{6}$/i.test(hexValue);
}
When the component logic is split out from the index.marko
it needs to be
exported like a standard JavaScript module. We have an handleColorSelected
event handler, which is going to emit the event back up to the parent <color-picker-header>
component. We also have an handleHexInput
event handler
with some basic validation logic. handleHexInput
also emits color-selected
, which
will be handled the same way as the color-selected
event when it reaches
<color-picker-header>
.
components/color-picker/components/color-picker-footer/style.css
.color-picker-footer {
width: 200px;
height: 100px;
border-radius: 0px 0px 20px 20px;
font: 30px Arial;
display: flex;
flex-direction: column;
text-align: center;
color: white;
box-shadow: 0px 3px 5px #888888;
}
.color-picker-selection-container {
width: 75%;
margin: 5px 0px 0px 20px;
}
.color-picker-selection-container input {
margin-top: 8px;
border-radius: 0px 0px 0px 0px;
border-width: 0px 0px 1px 0px;
outline: none;
color: #A9A9A9;
}
We can now finalize our component! Let's revisit the parent <color-picker>
component and add the <color-picker-footer>
:
components/color-picker/index.marko
class {
onInput(input) {
var colors = input.colors;
this.state = {
selectedColor: colors[0],
colors
};
}
handleColorSelected(color) {
this.state.selectedColor = color;
}
}
<div>
<color-picker-header color=state.selectedColor/>
<color-picker-footer colors=state.colors on-color-selected('handleColorSelected')/>
</div>
Finally, we've added our <color-picker-footer>
, passed the state.colors
as input
to it, added a handleColorSelected
event handler for the color-selected
event emitted from <color-picker-footer>
. When we handle this event, we
update the state
of the <color-picker>
component, which is passed to
the <color-picker-header>
.
Congratulations! You have finished your first fully reactive Marko UI component!
Our finished product:
Now let's talk about some additional topics that will turn you into a Marko pro!
Marko also supports importing modules. We can easily import a module using
the familiar ES2015 import
syntax for single file components.
Let's fetch the default <color-picker>
colors from an external module:
npm install flat-colors --save
Let's create a new helper module for generating colors:
components/color-picker/util/getColors.js
const flatColors = require('flat-colors').colors;
const HEX_INDEX = 3;
module.exports = function getColors () {
let colors = [];
for (let i = 0; i < 10; i++) {
colors.push(flatColors[i][HEX_INDEX]);
}
return colors;
};
We can import our helper module into the color-picker
and use the generated
colors as the default when none are passed as part of the input
:
components/color-picker/index.marko
import getColors from './util/getColors';
class {
onInput(input) {
var colors = input.colors || getColors();
this.state = {
selectedColor: colors[0],
colors
};
}
handleColorSelected(color) {
this.state.selectedColor = color;
}
}
<div>
<color-picker-header color=state.selectedColor/>
<color-picker-footer colors=state.colors on-color-selected('handleColorSelected')/>
</div>
Now, if we do not pass colors
to the <color-picker>
, the colors will default
to the colors obtained from flat-colors
:
Try Online: marko-color-picker
Routes can be specified by creating subdirectories under the routes/
folder.
The routes/index
route is automatically registered as the index of the
application. In a route directory, an index.marko
or a route.js
that
exports a handler
method may be created. marko-starter
is the underlying project that handles the routing, and automatically resolves routes from the routes/
folder. See the
marko-starter route documentation
for more information.
Alternatively, having an index.marko
file in the root directory of your project (e.g. /marko-color-picker/index.marko
), will automatically get served as the index route's template.
marko-cli
comes packaged with testing frameworking built on top of
mocha. We can easily add tests for our components, by
adding a test.js
inside the directory of the component. First let's add a test
assertion library chai:
npm install chai --save-dev
Now we can add a simple test to any component. Here's a demonstration of a test
for the <color-picker-header>
:
components/color-picker/components/color-picker-header/test.js
/* global test */
const expect = require('chai').expect;
test('color-picker-header color', function (context) {
const output = context.render({
color: '#000000'
});
expect(output.$('div').attr('style')).to.contain('background-color:#000000');
});
test('color-picker-header class included', function (context) {
const output = context.render({
color: '#000000'
});
expect(output.$('div').attr('class')).to.equal('color-picker-header');
});
Here is another example of a test for <color-picker-selection>
:
components/color-picker/components/color-picker-selection/test.js
/* global test */
const expect = require('chai').expect;
test('color-picker-selection color', function (context) {
const output = context.render({
color: '#ff8080'
});
expect(output.$('div').attr('style')).to.contain('background-color:#ff8080');
});
test('color-picker-selection when clicked should emit color-selected event', function (context) {
const output = context.render({
color: '#ff8080'
});
var component = output.component;
var isCalled = false;
component.on('color-selected', function () {
isCalled = true;
});
var componentEl = component.el;
componentEl.click();
expect(isCalled).to.equal(true);
});
Let's add a test
script to our package.json
:
{
"scripts": {
"start": "marko-starter server",
"build": "marko-starter build",
"test": "marko test"
}
}
Now we can run our tests:
npm test
More information about Marko component testing can be found in the marko-cli component testing documentation.
Developing Marko UI components is fun and easy! As you're developing components, you should consider how a component can be split into multiple components. This makes developing, managing, and testing components significantly easier.
Marko gives you the tools to easily develop awesome UI components. Get started today!
Special thanks to Anthony Ng for helping with this tutorial!