Skip to content

Composite Components Demo

Micah Godbolt edited this page Oct 15, 2018 · 2 revisions

The great thing about making small, unopinionated, reusable components is that you can start to combine them together to make larger, more capable, slightly opinionated, reusable components.

Why Create Composites

Why don't we just stick with primitive controls and never worry about combining them together? Anything we create here could be created by the application dev using primitives, just like we are. Here are a few reasons:

  1. Many controls that you might consider primitives are actually composite controls. A dropdown is a button that opens up a list of buttons.
  2. Many different developers will implement a composite control in many different ways. Having a consistent UI from page to page, from app to app, is incredibly important.
  3. Good composite controls are full of best practices, great accessibility, and optimized performance.
  4. Sometimes you need an "opinion" about how a given UI should look or work. That's part of the design system.

Creating Our TodoItem

After starting with our obligatory $ npm run scaffold TodoItem we can start pulling in the components that we are going to be combining. This is a pretty simple composite, but now that we've established it, we can continue to improve it over time as we add more functionality.

import Button from "../Button/Button.vue";
import Textfield from "../Textfield/Textfield.vue";

export default {
  components: {Button, Textfield},
  props: ['todo']
};

You'll notice we have a single prop being passed into this component, a singular todo. As I said above, this component is a bit more opinionated than our textfield, or button. We know "what" this component will be representing, and we know some of the basic actions we can take on a todo, but we will avoid any actual business logic so that this component could be used in various applications that need todo items.

Creating An Interface

Here are things we know a Todo should be able to do.

  1. Render a todo item that includes a todo title
  2. Modify the title contained in that todo
  3. Emit that editing is finished (passing the updated todo)
  4. Signal that it would like to be deleted

Note that none of these callbacks/emits actually DO anything. They just tell the parent that they want something to happen, and the parent can decide how to achieve that goal.

So here's how the markup will look.

  <div class="TodoItem">
    <Textfield
      variant="edit"
      :value="todo.title"
      @input="(value) => { todo.title = value }"
      @blur="$emit(`doneEdit`, todo)"
      @enter="$emit(`doneEdit`, todo)"
    />
    <Button class="delete" @click="$emit(`removeTodo`, todo)">×</Button>
  </div>

Let's quickly walk through each line.

  1. We have two different Textfield variants. This is the one called "edit".
  2. Set the value property of our Textfield to the value found in the todo.title. Notice the : before the property that is Vue's shorthand for v-bind.
  3. On new input, we'll change the value of the todo.title to whatever the new value is
  4. On both blur and enter we'll emit doneEdit that "editing is completed", and we'll pass the new todo
  5. Our delete button will emit that we'd like to removeTodo and includes a copy of the todo to be removed

Styles in Composite Components

Since we're using primitive controls to build up this composite, we aren't going to need many styles. As a rule, I try to avoid writing styles in the parent that change anything other than layout or visibility.

.TodoItem {
  display: flex;
  .delete {
    display: none;
  }
  &:hover {
    .delete {
      display: block;
    }
  }
}

No need for floats here! Just one display flex and some hide/show on hover for the button, and we're done.