Data down actions up

Learn about this powerful approach to managing side effects in your Ember components.

Summary

For a component to be reusable, it should refrain from mutating data that it doesn't own. When a component's side effects will change from one use case to the next, those side effects should be handled using "actions up" pattern.

{{color-picker
  currentColor=backgroundColor
  on-change=(action (mut backgroundColor))}}

This gives other developers complete control over the component's side effects, leading to less surprising situations.

Transcript

When writing forms in Ember, we'll often end up with several form controls that are reusable throughout our application. In this video, we'll see how reusability should influence our design of these controls.

Here we've got a form that allows us to edit this site. Right now we can change the page's title. Our exercise today will be making a component that also lets us edit the background color of the page.

Let's create a color-picker component, and add it to our form.

We'll open up our color picker's template and start creating it's UI elements. The first element we'll add is a square that shows our color. For now it's going to be blank.

When the color-picker is clicked, we want to show a popup that lets the user select a new color. We've already installed the ember-modal-dialog addon, which we'll use to render the actual popup element for us.

So, we'll render our popup by using ember-modal-dialog's tether-dialog component from within our color-picker's template, and then show a handful of different colors within the popup:

<div class="Color Square">
</div>

{{#tether-dialog
  target=".Color"
  container-class="Popup"
  targetAttachment="middle right"
  attachment="top left"}}

  {{#each colors as |color|}}
    <div class="Bar" style="background-color: {{color}};">
    </div>
  {{/each}}

{{/tether-dialog}}

Now let's open our color picker component add those colors.

colors: ['red', 'green', 'blue', ...]

Great, we've got our popup working - but it's always showing. We should probably hide it until the user interacts with the color picker.

We'll wrap the dialog in an isShowingPopup conditional and toggle this boolean whenever the color-picker is clicked:

{{#if isShowingPopup}}

{{/if}}
isShowingPopup: false,

click() {
  this.toggleProperty('isShowingPopup');
}

Now that our popup is working, we need to keep track of the user's color selection. Let's move back to our color picker's template and use an action to set the current color whenever one of the color bars is clicked:

{{#each colors as |color|}}
  <a class="Bar" style="background-color: {{color}};"
    {{action "setCurrentColor" color)}}>
  </a>
{{/each}}

Then we'll write the corresponding action in our component that sets the currentColor property.

actions: {
  setCurrentColor(color) {
    this.set('currentColor', color);
  }
}

And once again we can move back to the template and display the current color as our square's background color.

<div class="Square" style="background-color: {{currentColor}};">
</div>

Now whenever we select a color it's shown inside of the picker's square.

Alright. What we've made is simple, but it's working perfectly so far. Originally, we wanted to use this color-picker to change our page's background color. So, how might we do that?

A straightforward way would be to use jQuery to set the container's background color whenever a color is clicked:

setCurrentColor(color) {
  Ember.$('.container').css('background-color', color);
}

Now whenever a color is selected our page's background color changes. So does this mean we're done with our component?

Unfortunately, no. As you're probably aware, this is a problematic approach. While our component satisfies our current use case, anyone else that uses the color-picker will have the background color of their website changed out from underneath them, which is quite a surprising effect. They asked for a color picker, and instead they got a page-background-color-changer.

In this situation it's easy to see how directly mutating the page's background-color violates the expectations that other developers have of this component. A color picker feels like a reusable component. Although changing the background-color is what we're interested in right now, it's easy to see how our color-picker could be reused elsewhere in the application. For example, we might add another input to this form that lets users choose the site's font color.

We've actually just unpacked an important principle: for a component to be reusable, it should refrain from mutating data that it doesn't own. In our example, our color-picker is too knowledgeable because it directly mutates the page's background color. By coupling its implementation to our current use case, we've made it nearly impossible for our color picker to be reused elsewhere in our application.

So, how might we design our color picker to be more reusable? This is where the data down, actions up pattern comes in.

First, the actions up part. We want to remove the assumption that selecting a color should perform the same function every time. To do this, our color picker won't be allowed change the page's background color, or have any side effects at all for that matter. Instead, whenever a new color is selected the picker will send up an action notifying us of the new color. We can then react to this new information however we would like, which in our case will be changing the pages background color.

Now for the data down part. Our color-picker displays the currently selected color, but that color is blank until we've made our first selection. What we want to do is seed the color picker with a starting color. So we'll pass down the page's background color as it's default value.

By using data down actions up we can make our color picker reusable because both the starting color and on-color-change action have been decoupled from the component.

Let's see what this looks like by updating our component to use data down actions up.

If we take a look at our caller's template, we can see that the background color is already being rendered from a variable called {{backgroundColor}}.

Let's use that variable as the color picker's default color. To do this, we'll use data down and start our component with it's currentColor equal to backgroundColor.

{{color-picker currentColor=backgroundColor}}

Next, we want to tell our color-picker's how to behave when a new color is selected, so we'll expose an on-change parameter that takes an action. Since our use case today is changing the background color, we'll use a closure action and call the mut helper to update the backgroundColor property.

{{color-picker
  currentColor=backgroundColor
  on-change=(action (mut backgroundColor))}}

To get our color-picker to use this new action, we'll remove its local setCurrentColor action.

Then let's drag the color picker's template down to the bottom half of the screen.

Now, we'll invoke the passed in on-change action whenever a new color is selected. This is why it's called actions up, because our color picker is delegating it's action up to the parent template.

{{#each colors as |color|}}
  <a class="Bar" style="background-color: {{color}};"
    {{action on-change color)}}>
  </a>
{{/each}}

Whenever we select a new color our action mutates the background color property, which flows back down into the picker as the current color.

We've made a generic, reusable color picker, whose behavior is explicitly controlled from the outer template. When other developers use this component, nothing surprising or unexpected will happen.


The example of our component using jQuery to override the page's background color was egregious. But the larger lesson here has to do with the relationship between reusability and data mutation. While building any Ember application, you'll typically write several generic components that will be reused throughout the app. For these components, it is especially important for them to not be too knowledgeable about what their side effects will be.

In the next video, we'll look at a different type of form - one whose side effects are the same each time that form is used - and discuss how this behavior should affect our design of that form's interface.

Questions?

us, or ask in #media on Discord