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:
Our final goal for today is create this component:
#333745
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 directorymarko create color-picker-tutorial
Let's navigate to the newly created project and install the necessary dependencies:
cd color-picker-tutorialnpm 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><forcolor of=input.colors><li style={color: color}></li></for></ul>
ulforcolor of=input.colorsli style={color: color} --
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>
htmlheadtitle -- Welcome | Marko Demobodyh1 -- Welcome to Marko!color-picker colors="#333745","#E63462","#FE5F55","#C7EFCF","#EEF5DB","#00B4A6","#007DB6","#FFE972","#9C7671","#0C192B"
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#333745
<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.markocolor-picker-header/index.markocolor-picker-selection/index.markoindex.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 {}// In Marko, we immediately start writing a single JavaScript statement by using// `$`. For multiple JavaScript statements, use `$ { /* JavaScript here */ }<!-- Our markup! --><div.color-picker-header style={backgroundColor: color}><p></p></div>
// Inline styles!style {}// In Marko, we immediately start writing a single JavaScript statement by using// `$`. For multiple JavaScript statements, use `$ { /* JavaScript here */ }// Our markup!div.color-picker-header style={backgroundColor: color}p --
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<div><color-picker-header color=state.selectedColor/></div>
classdivcolor-picker-header color=state.selectedColor
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:
#333745
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
classstyle {}<div.color-picker-selectionon-click'handleColorSelected')on-touchstart'handleColorSelected')style={backgroundColor: input.color}/>
classstyle {}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.jsindex.markostyle.css......
components/color-picker/components/color-picker-footer/index.marko
<div.color-picker-footer><div.color-picker-selection-container><div forcolor in colors)><!--Listen for the `color-selected` event emitted from the<color-picker-selection> component and handle it in thiscomponent's `handleColorSelected` method.NOTE: We pass along the `color` to the event handler method--><color-picker-selectioncolor=coloron-color-selected'handleColorSelected', color)/></div><inputkey="hexInput"placeholder="Hex value"on-input'handleHexInput')/></div></div>
div.color-picker-footerdiv.color-picker-selection-containerdiv forcolor in colors)<!--Listen for the `color-selected` event emitted from the<color-picker-selection> component and handle it in thiscomponent's `handleColorSelected` method.NOTE: We pass along the `color` to the event handler method-->color-picker-selection [color=coloron-color-selected"handleColorSelected", color)]input key="hexInput" placeholder="Hex value" on-input"handleHexInput")
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
moduleexports = class{this;}{let hexInput = thisvalue;if !hexInputhexInput = '#' + hexInput;if !hexInput = thisinputcolors0;this;};{return /^#[0-9A-F]{6}$/i;}
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
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<div><color-picker-header color=state.selectedColor/><color-picker-footer colors=state.colors on-color-selected'handleColorSelected')/></div>
classdivcolor-picker-header color=state.selectedColorcolor-picker-footer [colors=state.colorson-color-selected"handleColorSelected")]
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:
#333745
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 = colors;const HEX_INDEX = 3;module {let colors = ;for let i = 0; i < 10; i++colors;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
importclass<div><color-picker-header color=state.selectedColor/><color-picker-footer colors=state.colors on-color-selected'handleColorSelected')/></div>
importclassdivcolor-picker-header color=state.selectedColorcolor-picker-footer [colors=state.colorson-color-selected"handleColorSelected")]
Now, if we do not pass colors
to the <color-picker>
, the colors will default to the colors obtained from flat-colors
:
#1abc9c
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 = expect;;;
Here is another example of a test for <color-picker-selection>
:
components/color-picker/components/color-picker-selection/test.js
/* global test */const expect = expect;;;
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!
EDITSpecial thanks to Anthony Ng for helping with this tutorial!
Helpful? You can thank these awesome people! You can also edit this doc if you see any issues or want to improve it.