marko-color-picker v0.0.3
Color Picker Tutorial
Introduction
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-devtools
- Create a basic and customizable color picker component
- Write component tests using marko-devtools
Our final goal for today is create this component:
Getting Started
marko-devtools comes packaged with useful commands for building Marko projects. Projects created using marko-devtools come bundled with an HTTP server, and a build pipeline using lasso making it very easy to get started.
Let's first install marko-devtools globally, so we can create our project:
Using npm:
npm install -g marko-devtoolsUsing yarn:
yarn global add marko-devtoolsNow we are ready to create our Marko project:
# Creates a `color-picker-tutorial` project in the current directory
marko create color-picker-tutorialLet'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 startCreating Components
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.markoMarko also supports creating components using the file name. For example, the following is a valid directory structure:
color-picker-tutorial/
components/
color-picker.marko
color-picker.component.js
color-picker.style.cssCreating nested component directories is not required. 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
class {
onInput(input) {
input.colors = input.colors || [
'red',
'green',
'blue',
'orange',
'yellow'
];
this.state = {
backgroundColor: input.colors[0]
};
}
}
<ul>
<for(color in input.colors)>
<li style={color: color}>
${color}
</li>
</for>
</ul>input in a Marko component is the data that is passed to the component when
it is being initialized. 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. If a colors attribute is not passed to the component as
input, our component automatically falls back to a default set of colors:
<!-- Colors default to red, green, blue, orange, yellow -->
<color-picker/>Child Components
We've created our first component! This component will act as the entry point for children components that we will create. When creating components, it's strongly recommended to consider how components can be broken down into multiple components. Each component can then be independently tested and managed.
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 pallete 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 the associated click events
Marko automatically registers all components in a 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.markoThe 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. Since the background color is
subject to changing its value, it should be part of the state. An initial
background color should be allowed to be passed, so that will be part of the
component's input.
components/color-picker/components/color-picker-header/index.marko
class {
onInput(input) {
// Set the current component state based on the background color passed
// to the component as `input` or fall back to a default color.
this.state = {
backgroundColor: input.backgroundColor || 'red'
};
}
}
// In Marko, we immediately start writing a single line of JavaScript by using
// `$`. For multi-line JavaScript, use `$ { /* JavaScript here */ }
$ var backgroundColor = state.backgroundColor;
// 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 {
margin-top: 1.15em;
}
}
<!-- Our markup! -->
<div.color-picker-header style={backgroundColor}>
<p>${backgroundColor}</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 $ or $ { /* ... */ } for blocks,
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 revist 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) {
input.colors = input.colors || [
'red',
'green',
'blue',
'orange',
'yellow'
];
this.state = {
backgroundColor: input.colors[0]
};
}
}
<color-picker-header backgroundColor=state.backgroundColor/>Navigating to localhost:8080, we should see the
rendered <color-picker-header> with a red 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 {
onInput(input) {
input.backgroundColor = input.backgroundColor || 'green';
}
onColorSelected() {
this.emit('colorSelected', this.input.backgroundColor);
}
}
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('onColorSelected') style={
backgroundColor: input.backgroundColor
}></div>In this component, we've introduced an on-click listener and handler function.
Marko components inherit from EventEmitter.
When this color is selected, it will emit an event and get handled by the
onColorSelected function. The handler then emits a colorSelected event
with the background color to be handled by its parent. We will eventually
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 `colorSelected` event emitted from
the <color-picker-selection> component and handle
it in this component's `onColorSelected` method.
-->
<color-picker-selection backgroundColor=color on-colorSelected('onColorSelected')/>
</div>
<input
key="hexInput"
placeholder="Hex value"
on-input('onHexInput')/>
</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 as the value for
backgroundColor. Additionally, we are listening for the colorSelected event
emitted from the <color-picker-selection> component and handling it in our
own onColorSelected method. We also have added an input field and an
on-input listener, which will be used to manually change the
background color of the <color-picker-header> to a color that is not in
the palette.
components/color-picker/components/color-picker-footer/component.js
function isValidHexValue (hexValue) {
return /^#[0-9A-F]{6}$/i.test(hexValue);
}
module.exports = {
onInput (input) {
input.colors = input.colors || ['red', 'green', 'blue'];
},
onColorSelected (backgroundColor) {
this.emit('colorSelected', backgroundColor);
},
onHexInput () {
let hexInput = this.getEl('hexInput').value;
if (!hexInput.startsWith('#')) {
hexInput = '#' + hexInput;
}
if (!isValidHexValue(hexInput)) {
hexInput = this.input.colors[0];
}
this.emit('colorSelected', hexInput);
}
};When the component logic is split out from the index.marko it needs to be
exported like a standard JavaScript module. We have an onColorSelected
event handler, which is working to bubble the event back up to eventually
reach the <color-picker-header>. We also have an onHexInput event handler
with some basic validation logic. onHexInput also emits colorSelected, which
will be handled the same way as the colorSelected 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 wrap up our component! Let's revisit the parent <color-picker>
component and add the <color-picker-footer>:
components/color-picker/index.marko
class {
onInput(input) {
input.colors = input.colors || [
'red',
'green',
'blue',
'orange',
'yellow'
];
this.state = {
backgroundColor: input.colors[0]
};
}
onColorSelected(backgroundColor) {
// Handler for the event that bubbled up from the <color-picker-footer>
// We set the background color state and rerender the component. This
// will cause the <color-picker-header> background color to change.
this.state.backgroundColor = backgroundColor;
}
}
<color-picker-header backgroundColor=state.backgroundColor/>
<color-picker-footer colors=input.colors on-colorSelected('onColorSelected')/>Finally, we've added our <color-picker-footer>, passed the input.colors
as input to it, added a onColorSelected event handler for the colorSelected
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>.
Our final result:
Routing
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. See the
marko-starter route documentation
for more information.
Testing
marko-devtools 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-devNow 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({
backgroundColor: '#000000'
});
expect(output.$('div').attr('style')).to.contain('background-color:#000000');
});
test('color-picker-header default color', function (context) {
const output = context.render();
expect(output.$('div').attr('style')).to.contain('background-color:#ff0000');
});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 testMore information about Marko component testing can be found in the marko-devtools component testing documentation.
Conclusion
Developing complicated Marko 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!