Creating a colorful dropdown list

Hi I am trying to create a colorful dropdown list for the code. I am looking to have color green for Yes value and color red for No value in the dropdown list. for example: image

// app/controllers/my-route-name.js import Controller from ‘@ember/controller’; import { action } from ‘@ember/object’; import { tracked } from ‘@glimmer/tracking’;

export default class MyRouteNameController extends Controller { @tracked selectedOption = null; options = [“Yes”, “No”];

@action optionChanged(newOption) { this.selectedOption = newOption; } }

// app/templates/my-route-name.hbs <MySelect @value={{this.selectedOption}} @options={{this.options}} @onChange={{this.optionChanged}} />

The currently selected option is: {{this.selectedOption}}

Thanks in advance.

There are two primary ways you could do this:

  • make the select component more complex by giving it the ability to render colored options, which means you have to pass it colors to the options array, this isn’t the way i would go
  • refactor the select component to make it more flexible by making it a “contextual” component

To refactor it we might do something like this:

ORIGINAL VERSION
// app/components/my-select.hbs
<select  {{on "change" (pick "target.value" @onChange)}}>
  {{#each @options as |theOption|}}
    <option value={{theOption}} selected={{eq theOption @value}}>{{theOption}}</option>
  {{/each}}
</select>

NEW VERSION
// app/components/my-select-option.hbs
<option
  value={{@optionValue}} 
  selected={{eq @currentOption @optionValue}}
  ...attributes
>
  {{or @optionLabel @optionValue}}
</option>

// app/components/my-select.hbs
<select  {{on "change" (pick "target.value" @onChange)}}>
  {{#if (has-block)}}
    {{yield (component "my-select-option" currentValue=@value)}}
  {{else}}
    {{#each @options as |theOption|}}
      <MySelectOption
        @optionValue={{theOption}}
        @optionLabel={{theOption}}
      />
    {{/each}}
  {{/if}}
</select>

Wow there’s a lot going on there. I’ll try to break it all down in case you haven’t looked at “contextual component” patterns yet. Basically what we did is extract the option into it’s own component and supercharged the select component to give it a lot of flexibility. Let’s look line by line:

Main Select Component

Line 1

<select  {{on "change" (pick "target.value" @onChange)}}>

This doesn’t change. We still need to render a select tag and give it a change handler.

Line 2

  {{#if (has-block)}}

This one is weird if you haven’t seen it before. has-block is a helper that tells you if this component was rendered with a block. What’s a block? Well there are two ways to render any component: with and without “blocks”. You can also use “named blocks” but we’re using the “default block” here.

A blockless component is rendered with a self-closing tag, like this:

<MySelect ... />

But a “block” component is rendered with an opening and closing tag, and other stuff in the middle:

<MySelect ... >
  {{!-- this is the block! --}}
</MySelect>

And a “named block component” might look something like this:

<MyCard>
  <:heading>
    {{!-- this is where you'd render stuff in the card heading --}}
  </:heading>
  <:body>
    {{!-- this is where you put stuff in the card body --}}
  </:body>
</MyCard>

Now, you may notice that we’re putting the block thing in an if conditional, this means we want this component to support both blockless and block forms. So if you give it a block it does one thing, if you don’t give it a block it does exactly what it did before. We didn’t change the component arguments at all we simply extended its API.

Line 3

    {{yield (component "my-select-option" currentValue=@value)}}

Woah another weird one. Yield is how you can tell the component where to render the block(s). So when you render this component in block form:

<MySelect ... >
  <div>This is nonsense, just an example.</div>
</MySelect>

That <div> tag is going to go right where this {{yield}} helper is.

Ok but we’re also giving the yield yelper something else… a component! We’ve extracted our option component out and now we’re yielding the option component to our block. We could yield other things to, like strings or arrays or numbers or other values, but here we’re yielding a “contextual child component” meaning we’re adding a little context to the option component (the currentOption arg) before we yield it. So now we can use this component like this:

<MySelect@value={{this.selectedOption}} @onChange={{this.optionChanged}} as |MySelectOption|>
  <MySelectOption @optionValue="Yes" />
  <MySelectOption @optionValue="No" />
</MySelect>

So here you notice we didn’t pass @options in, because we’re defining our options in the block. Neato!

Lines 4-10

  {{else}}
    {{#each @options as |theOption|}}
      <MySelectOption
        @optionValue={{theOption}}
        @optionLabel={{theOption}}
      />
    {{/each}}

So as I mentioned this is supporting the “blockless” form, so using it how you were before:

<MySelect @value={{this.selectedOption}} @options={{this.options}} @onChange={{this.optionChanged}} />

still works exactly like it did before. The major difference is that now instead of manually rendering the <option> tags we’ve extracted that into an MySelectOption component. We’ll walk through that next.

I think the most important thing to note in this template is the if statement and what happens in each part of it. We’re saying “if we have a block, yield the option component to the block and let the block take care of the options, OTHERWISE we’ll render the options that we’re given automatically”. So the block form gives you extra flexibility over how you want the select to be rendered, and the blockless form gives you fast easy select rendering if you don’t need to customize it (e.g. add colors or extra options or whatever).

Select Option Component

Line 1

<option

We’re just rendering the opening of the option tag, no big deal

Line 2

  value={{@optionValue}} 

Here we’re just passing the @optionValue arg to the option tag value attribute. Again, standard stuff.

Line 3

  selected={{eq @currentOption @optionValue}}

Once again this looks pretty much the same as it did in the parent component before. We know this option is selected if the currentOption === our option value.

Line 4-5

  ...attributes
>

Here’s where things are getting a little more interesting. This is called “splattributes” and it means any attributes we pass to the option component get “forwarded” to this option tag. Attributes are distinct from args in that they don’t have a @ on them, and usually we assign attributes to an HTML element, not a component, but this lets us forward those through. So you can do this, for example:

<MySelectOption data-test-option="Yes" class="color-green" @optionValue="Yes" />

and it will forward the data-test-option and class attributes through to the <option> tag that is rendered inside the MySelectOption component.

Line 6

  {{or @optionLabel @optionValue}}

I just added this to be extra flexible. This allows us to use either the value or an extra “label” arg to render the select label. So you could do something like this:

<MySelectOption @optionLabel="Heck yes!" @optionValue="yes" />

This means the select will use “yes” as the behind-the-scenes value of the option, but it will render “Heck yes!” in the actual dropdown. This is often necessary when you’re dealing with multi-language support or you have a numeric value that has a human-readable string label, you need to separate the option value and option label. Here we’re using the or helper to let it use the optionLabel if you give it one, but use optionValue as a fallback, so either will work.

Conclusion

Anyway, I hope all that makes sense. Let me know if you have any other questions about any of it. This pattern is called “contextual components” and they are very powerful. In this case what it would allow you to do is use the block form of the select component to customize the options as you see fit. So what does that look like? Let’s put it all together:

<MySelect 
  @value={{this.selectedOption}} 
  @onChange={{this.optionChanged}} 
  as |MySelectOption|
>
  <MySelectOption class="color-green" @optionValue="Yes" @optionLabel="Yes, totally" />
  <MySelectOption class="color-red" @optionValue="No" @optionLabel="Nah" />
</MySelect>

Here we’re using the “block form” of the component, yielding out the option component as MySelectOption, and using it to render two customized options. Each with their own distinct value/label pairs but more importantly a CSS class! The yes option has “color-green” CSS class applied, and the no option has the “color-red” CSS class applied. You could change these or use them as is, and as long as you have CSS that actually changes the background color correctly, this should work.

2 Likes

Hi, Thanks for your help. And what if the data is coming from the backed and the list has n options (for eg: 13) and each then each option has different colour?

That would be pretty easy but you’d have some decisions to make. The options can be rendered in an {{#each}}...{{/each}} to dynamically render any number of them. As for the colors you’d want to decide how the colors should be applied (presumably through CSS classes), and then where you want the colors to be applied. You could add a @color arg to the option component, or you could just add the classes on the option components like I did above. Both are pretty much the same thing. By adding the @color arg you’d basically be formally declaring that the “select option” component is meant to support colors.

I don’t know anything about your data payload or how your colors are set up so I’ll make some assumptions and give an example. Let’s say your payload looks something like this:

[{
  label: 'Safe Option',
  value: 'safe'
  color: 'green',
}, {
  label: 'Default Option',
  value: 'default',
  color: 'blue'
}, {
  label: 'Caution Option',
  value: 'caution',
  color: 'orange'
}, {
  label: 'Danger Option',
  value: 'danger',
  color: 'red'
}]

and let’s say that you had that payload available to you on your backing class or controller as the options property. Now let’s say you have CSS classes defined for all the colors that look something like:

color-red { background-color: #F99 }
color-green { background-color: #9F9 }
color-blue { background-color: #99F }

(those are probably terrible colors this is just a basic example of setup)

In that case you could render it like this:

<MySelect 
  @value={{this.selectedOption}} 
  @onChange={{this.optionChanged}} 
  as |MySelectOption|
>
  {{#each this.options as |option|}}
    <MySelectOption
      class="color-{{option.color}}"
      @optionValue={{option.value}}
      @optionLabel={{option.label}}
    />
  {{/each}}
</MySelect>

This will dynamically render all the options from your payload/options object with the correct parameters.

2 Likes

Thanks for your help!