Skip to content

Latest commit

 

History

History
169 lines (128 loc) · 6.91 KB

guide-component-decomposition.asciidoc

File metadata and controls

169 lines (128 loc) · 6.91 KB

Component Decomposition

When implementing a new requirement there are a few design decisions, which need to be considered. A decomposition in Smart and Dumb Components should be done first. This includes the definition of state and responsibilities. Implementing a new dialog will most likely be done by defining a new Smart Component with multiple Dumb Component children.

In the component tree this would translate to the definition of a new subtree.

Component Tree With Highlighted Sub Tree
Figure 1. Component Tree with highlighted subtree

Defining Components

The following gives an example for component decomposition. Shown is a screenshot from a styleguide to be implemented. It is a widget called Listpicker.

The basic function is an input field accepting direct input. So typing otto puts otto inside the FormControl. With arrow down key or by clicking the icon displayed in the inputs right edge a dropdown is opened. Inside possible values can be selected and filtered beforehand. After pressing arrow down key the focus should move into the filter input field. Up and down arrow keys can be used to select an element from the list. Typing into the filter input field filters the list from which the elements can be selected. The current selected element is highlighted with green background color.

Component Decomposition Example 1v2
Figure 2. Component decomposition example before

What should be done, is to define small reusable Dumb Components. This way the complexity becomes manageable. In the example every colored box describes a component with the purple box being a Smart Component.

Component Decomposition Example 2v2
Figure 3. Component decomposition example after

This leads to the following component tree.

Component Decomposition Example component tree
Figure 4. Component decomposition example component tree

Note the uppermost component is a Dumb Component. It is a wrapper for the label and the component to be displayed inside a form. The Smart Component is Listpicker. This way the widget can be reused without a form needed.

A widgets is a typical Smart Component to be shared across feature modules. So the SharedModule is the place for it to be defined.

Defining state

Every UI has state. There are different kinds of state, for example

  • View State: e.g. is a panel open, a css transition pending, etc.

  • Application State: e.g. is a payment pending, current URL, user info, etc.

  • Business Data: e.g. products loaded from backend

It is good practice to base the component decomposition on the state handled by a component and to define a simplified state model beforehand. Starting with the parent - the Smart Component:

  • What overall state does the dialog have: e.g. loading, error, valid data loaded, valid input, invalid input, etc. Every defined value should correspond to an overall appearance of the whole dialog.

  • What events can occur to the dialog: e.g. submitting a form, changing a filter, pressing buttons, pressing keys, etc.

For every Dumb Component:

  • What data does a component display: e.g. a header text, user information to be displayed, a loading flag, etc.
    This will be a slice of the overall state of the parent Smart Component. In general a Dumb Component presents a slice of its parent Smart Components state to the user.

  • What events can occur: keyboard events, mouse events, etc.
    These events are all handled by its parent Smart Component - every event is passed up the tree to be handled by a Smart Component.

These information should be reflected inside the modeled state. The implementation is a TypeScript type - an interface or a class describing the model.

So there should be a type describing all state relevant for a Smart Component. An instance of that type is send down the component tree at runtime. Not every Dumb Component will need the whole state. For instance a single Dumb Component could only need a single string.

The state model for the previous Listpicker example is shown in the following listing.

Listing 1. Listpicker state model
export class ListpickerState {

  items: {}[]|undefined;
  columns = ['key', 'value'];
  keyColumn = 'key';
  displayValueColumn = 'value';
  filteredItems: {}[]|undefined;
  filter = '';
  placeholder = '';
  caseSensitive = true;
  isDisabled = false;
  isDropdownOpen = false;
  selectedItem: {}|undefined;
  displayValue = '';

}

Listpicker holds an instance of ListpickerState which is passed down the component tree via @Input() bindings in the Dumb Components. Events emitted by children - Dumb Components - create a new instance of ListpickerState based on the current instance and the event and its data. So a state transition is just setting a new instance of ListpickerState. Angular Bindings propagate the value down the tree after exchanging the state.

Listing 2. Listpicker State transition
export class ListpickerComponent {

  // initial default values are set
  state = new ListpickerState();

  /** User changes filter */
  onFilterChange(filter: string): void {
    // apply filter ...
    const filteredList = this.filterService.filter(...);

    // important: A new instance is created, instead of altering the existing one.
    //            This makes change detection easier and prevents hard to find bugs.
    this.state = Object.assing({}, this.state, {
      filteredItems: filteredList,
      filter: filter
    });
  }

}
Note:

It is not always necessary to define the model as independent type. So there would be no state property and just properties for every state defined directly in the component class. When complexity grows and state becomes larger this is usually a good idea. If the state should be shared between Smart Components a store is to be used.

When are Dumb Components needed

Sometimes it is not necessary to perform a full decomposition. The architecture does not enforce it generally. What you should keep in mind is, that there is always a point when it becomes recommendable.

For example a template with 800 loc is:

  • not understandable

  • not maintanable

  • not testable

  • not reusable

So when implementing a template with more than 50 loc you should think about decomposition.