Create a Custom Select Box using Ember.Component

Billy Heaton 8 min read

Overview

The seven steps below cover the creation of a custom element using
Ember.Component so I'll use a simple Ember application using a single
index route, which I'll trust Ember to map the default index route.

Scroll to the end for links on the topic of custom elements and
Ember.Component.

If you've used Ember for any time now you're likely asking, "Why would
I not just use Ember.Select?" I'm glad you asked, I want to add my own
custom markup instead of the markup that is generated by Ember.Select.
Also, I want to define a custom binding to the selected choice in a
re-usable way. I'd rather not write a Handlebars helper to generate a
custom view, that's what Ember.Component is for. I'd like the custom
select box to work like a custom html element, e.g. {{faux-select}}
can be used where I would use a select element in my Handlebars templates.

The custom "faux" select box will look like this:

select.faux-select {
display: block;
filter: alpha(opacity=0);
opacity: 0;
position: absolute;
height: 20px;
width: 125px;
margin: 5px
}

.faux-select {
color: black;
position: relative;
font: 14px/18px "Andale Mono", AndaleMono, monospace;
letter-spacing: 1px;
text-transform: uppercase
}

.faux-select-box {
position: absolute;
min-width: 137px
}

.faux-select-selected, .faux-select-graphic {
border: 1px solid black;
height: 30px;
background: #fdfaed;
background: -moz-linear-gradient(top, #fdfaed 0%, #b5cbeb 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fdfaed), color-stop(100%, #b5cbeb));
background: -webkit-linear-gradient(top, #fdfaed 0%, #b5cbeb 100%);
background: -o-linear-gradient(top, #fdfaed 0%, #b5cbeb 100%);
background: -ms-linear-gradient(top, #fdfaed 0%, #b5cbeb 100%);
background: linear-gradient(to bottom, #fdfaed 0%, #b5cbeb 100%);
filter: progid: DXImageTransform.Microsoft.gradient(startColorstr='#fdfaed', endColorstr='#b5cbeb', GradientType=0 )
}

.faux-select-selected {
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
border-right: none;
float: left;
min-width: 117px;
padding: 5px 10px
}

.faux-select-graphic {
float: right;
border-top-right-radius: 10px;
border-bottom-right-radius: 10px;
padding: 5px 7px
}

Choose One



Choose One
Ember.js
Backbone.js
AngularJS

Step 1 - Start by using the Ember Starter-Kit repo

In this step the only work done is to remove some of the example code
that ships with the [emberjs/starter-kit] application.

An outlet in the Ember application template:

templates/application

<script type="text/x-handlebars">
  {{outlet}}
</script>    

An index template used to try out the custom element (component).

templates/index

<script type="text/x-handlebars" id="index">
  <h1>Components</h1>
</script>    

An empty model will be used for now, later I'll put a list of choices in
this object.

javascript/app.js

App = Ember.Application.create();

App.IndexRoute = Ember.Route.extend({
  model: function() {
    return {};
  }
});    

Step 2 - Add a component template with a select box

Add a component template, this looks like a normal Handlebars template,
however notice the naming convention components/faux-select. The
template name begins with components/ and the name of the component
uses a prefix faux-. This follows the recommendation for the W3C
Custom Element, see Web Components.

templates/components/faux-select

<script type="text/x-handlebars" id="components/faux-select">
  <h2>Faux Select</h2>
  <select>
    <option value="">Choose One</option>
  </select>
</script>    

To use the component just place the name of the component in a
Handlebars template, I've added it in the index template so it renders.
Notice the {{faux-select}} addition below:

templates/index

<script type="text/x-handlebars" id="index">
  <h1>Components</h1>
  {{faux-select}}
</script>    

Now the select element can be rendered, it's not much and not even a
much as Ember.Select provides yet, but it's a start toward a custom
element.

Step 3 - Add a component template with a select box

Now the model will need some conent to pass to the component as the list
of choices, I've started with only one choice the default "Choose One".

javascript/app.js

App = Ember.Application.create();

App.IndexRoute = Ember.Route.extend({
  model: function() {
    return [
      { choice: 'Choose One' }
    ];
  }
});    

The component template needs to enumerate over the list of choices:

templates/components/faux-select

<script type="text/x-handlebars" id="components/faux-select">
  <h2>Faux Select</h2>
  <select class="faux-select">
    {{#each choices}}
      <option {{bindAttr value=choice}}>{{choice}}</option>
    {{/each}}
  </select>
</script>    

Now the model defined in the route can be passed to the choices
property of the component. Below the model data is mapped to the
component's choices property.

templates/index

<script type="text/x-handlebars" id="index">
  <h1>Components</h1>
  {{faux-select choices=model}}
</script>    

Step 4 - Add some attributes and choices

The model now has an object with properties for className and name which can be
used to set custom properties on the select box. Each instance of the component
can use the dynamic data for the name and css class. The choices list now has a
few items to choose from.

javascript/app.js

App = Ember.Application.create();

App.IndexRoute = Ember.Route.extend({
  model: function() {
    return {
      name: 'one',
      className: 'dropdown',
      choices: [
        { choice: 'Choose One' }, { choice: 'First' }, { choice: 'Last' }
      ]
    };
  }
});

The select element's attributes are now dynamically set with bindAttr.
One static class name is kept for standard css styling, faux-select

templates/components/faux-select

<script type="text/x-handlebars" id="components/faux-select">
  <select {{bindAttr name=name class=":faux-select className"}}>
    {{#each choices}}
      <option {{bindAttr value=choice}}>{{choice}}</option>
    {{/each}}
  </select>
</script>

The instance of the faux-select component used in the index tempate now has
the dynamic properties mapped for name and className.

templates/index

<script type="text/x-handlebars" id="index">
  <h1>Components</h1>
  <h2>Faux Select</h2>
  {{faux-select choices=model.choices name=model.name className=model.className}}
</script>    

Step 5 - Scaffold markup and styles for faux select w/ zero opacity

The basic idea for this custom element is to hide the native select element
and style some custom markup instead. So position and opacity give this
result, more style is needed but this css will be the foundation.

css/style.css

/* FauxSelectComponent */
select.faux-select {
    opacity: 0;
    position: absolute;
}
.faux-select {
    position: relative;
}
.faux-select-box {
    position: absolute;
}
.faux-select-selected {}
.faux-select-graphic {}

To bind the selected value to the component's template a class (object)
is needed which defines the property for the selected choice. Later an
event will be added to respond to the user's choice.

javascript/app.js

App = Ember.Application.create();

App.IndexRoute = Ember.Route.extend({
  model: function() {
    return {
      name: 'one',
      className: 'dropdown',
      choices: [
        { choice: 'Choose One' }, { choice: 'First' }, { choice: 'Last' }
      ]
    };
  }
});

App.FauxSelectComponent = Ember.Component.extend({
  selected: 'Choose One'
});

The component's template now has the custom HTML to use instead of the
native select element.

templates/components/faux-select

<script type="text/x-handlebars" id="components/faux-select">
  <div class="faux-select">
    <div class="faux-select-box">
      <span class="faux-select-selected">{{selected}}</span>
      <span class="faux-select-graphic">&#x25BE;</span>
    </div>
    <select {{bindAttr name=name class=":faux-select className"}}>
    {{#each choices}}
      <option {{bindAttr value=choice}}>{{choice}}</option>
    {{/each}}
    </select>
  </div>
</script>

Step 6 - Add some custom style for the faux select box

Here is an example implementation of a custom select box with linear gradients.

Below is a list of required styles for this example of a faux select box.

  • The select element has zero (0) opacity and is positioned absolute
  • The .faux-select-box element is also positioned absolute
  • The above two elements are siblings as the select box follows the faux element resulting in a higher z-index value, so when user clicks the event is fired on the invisble select box
  • the dimensions of the select box and faux select box need to match so the invisble element can be used on top of the faux element

css/style.css

/* FauxSelectComponent */
select.faux-select {
    display: block;
    filter: alpha(opacity=0);
    opacity: 0;
    position: absolute;
    height: 20px;
    width: 125px;
    margin: 5px;
}
.faux-select {
    color: black;
    position: relative;
    font: 14px/18px "Andale Mono", AndaleMono, monospace;
    letter-spacing: 1px;
    text-transform: uppercase;
}
.faux-select-box {
    position: absolute;
    min-width: 137px;
}
.faux-select-selected,
.faux-select-graphic {
    background-image: linear-gradient(rgb(253, 250, 237), rgb(181, 203, 235));
    /* visit http://www.colorzilla.com/gradient-editor/ and create your own */
    border: 1px solid black;
    height: 17px;
}
.faux-select-selected {
    border-top-left-radius: 10px;
    border-bottom-left-radius: 10px;
    border-right: none;
    fl    oat: left;
    min-width: 117px;
    padding: 5px 10px;
}
.faux-select-graphic {
    float: right;
    border-top-right-radius: 10px;
    border-bottom-right-radius: 10px;
    padding: 5px 7px;
}

Step 7 - Bind the selected value

The App.FauxSelectComponent now has a change event binding the visible
choice to the choice selected by the user.

javascript/app.js

App = Ember.Application.create();

App.IndexRoute = Ember.Route.extend({
  model: function() {
    return {
      name: 'one',
      className: 'dropdown',
      choices: [
        { choice: 'Choose One' }, { choice: 'First' }, { choice: 'Last' }
      ]
    };
  }
});

App.FauxSelectComponent = Ember.Component.extend({
  selected: 'Choose One',
  change: function(e){
    this.set('selected', e.target.value);
  }
});

Summary

Advantages of using invisibe select element

  • Native browser support for select/dropdown behavior including arrow keys, typing to select an option
  • Mobile devices still use finger friendly behaviour for select behaviors
  • Screen readers behave as expected with a standard select box
  • You don't have to fix all the bugs for taking over all the native brower support listed above and the custom select box can now match the designer's branding needs

Disclaimer

The code in this example select box component has not been tested in various
browsers and cross-browser css has not been included in the demo code. The
concept of using 0 opacity for the select box does work in modern browsers.

Links

About the Author

Billy Heaton
Billy Heaton

Software engineer with two decades of experience who favors Ruby and JavaScript to build web applications