marko-color-picker

0.0.3 • Public • Published

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:

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-devtools

Using yarn:

yarn global add marko-devtools

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

Creating 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.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
    color-picker.component.js
    color-picker.style.css

Creating 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.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. 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-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({
    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 test

More 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!

Additional Resources

Readme

Keywords

none

Package Sidebar

Install

npm i marko-color-picker

Weekly Downloads

1

Version

0.0.3

License

MIT

Last publish

Collaborators

  • austinkelleher