-
Notifications
You must be signed in to change notification settings - Fork 35
guide 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 sub-tree.
The following gives an example for component decomposition.
Shown is a screenshot from a style guide 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.
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.
This leads to the following 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.
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 back-end
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.
Listpicker
state modelexport 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.
Listpicker
State transitionexport 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
});
}
}
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.
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 line of code is:
-
not understandable
-
not maintainable
-
not testable
-
not reusable
So when implementing a template with more than 50 line of code you should think about decomposition.
This documentation is licensed under the Creative Commons License (Attribution-NoDerivatives 4.0 International).