Working with Magento 2 UiComponents can be challenging.
This talk is about how to create and customize UiComponents without going crazy. The first part covers some general advice for writing self documenting code, the second (and in my opinion more interesting one) is about managing shared state in the view.
The slides where created for MageTitans Italy in April 2018.
2. SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
3. Alternative titles:
Lessons learned from UiComponents
or
My own best practices for UiComponents
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
4. I'm assuming you have at least a (very)
basic understanding of
— require.js
— Knockout.js
— jQuery UI Widgets
— UiComponents
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
5. SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
6. SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
7. SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
8. UiComponents
So what is the problem?
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
9. For me grokking UiComponents was one
of the hardest things
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
10. It still takes me a long time to do some
things that I expect to be quick
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
11. I try to understand UiComponents mainly
by reading code
Reading Code > Reading Docs
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
12. So I try to optimize code for reading
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
13. This talk
1. A little bit of whining
2. General advice on readable code
3. View state management
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
14. Three issues I frequently encounter:
1. High cognitive load
2. Duplication of knowledge
3. State is coupled to the DOM
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
15. If it was my job...
..to fix UiComponents
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
16. Cognitive load
Super verbose declaration
(no sane defaults)
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
17. Cognitive load
Mixing of concepts
(Knockout.js and jQuery UI Widgets)
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
20. Rules I try to apply to code I write.
When working with the core code...
well, it is what it is.
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
21. 4 Rules of simple design by Kent Beck:
1. Passes all tests
2. Reveals intent
3. No duplication
4. Fewest elements
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
22. Passes all tests
The code has to work.
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
24. Reveals intent
Good naming
— Descriptive base class names
— Intent of functions, not
implementation
— Descriptive properties and variables
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
35. No duplication
Keep knowledge in one place
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
36. No duplication
Duplication is a common problem in M2
EAV, DataInterfaces, UiComponents,
GraphQL, ...
How does it apply in the view layer?
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
37. No duplication
Avoid selectors in view models
loginFormSelector = 'form[data-role=email-with-possible-login]',
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
39. No duplication
If selectors are required?
Keep the definition and the configuration
as close as possible,
ideally in the same file
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
40. No duplication
DOM selector declaration and config
<div data-bind="scope: 'foo'">
<div data-role="message-container">...</div>
</div>
<script type="text/x-magento-init">
{
"*": {
"Magento_Ui/js/core/app": {
"components": {
"foo": {
"component": "MyCompany_MyModule/js/view/foo",
"containerSelector": "div[data-role=message-container]"
}
}
}
}
}
</script>
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
41. No duplication
UiComponent selector declaration and config
<div data-bind="scope: 'messages'">
...
</div>
<script type="text/x-magento-init">
{
"*": {
"Magento_Ui/js/core/app": {
"components": {
"messages": {
"component": "Magento_Theme/js/view/messages"
}
}
}
}
}
</script>
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
42. No duplication
Keep knowledge in one file and close together.
<script type="text/x-magento-init">
{
"*": {
"Magento_Ui/js/core/app": {
"components": {
"appRoot": {
"component": "My_Module/js/root"
"children": {
"thingA": {
"component: "My_Module/js/thing-a"
},
"thingB": {
"component: "My_Module/js/thing-b",
"sibling":" "thingA"
}
}
}
}
}
}
}
</script>
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
43. No duplication
Injecting DOM-Nodes is better
E.g. with a custom binding:
<div data-bind="scope: 'myViewModel', bindDomNode: true">
</div>
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
44. No duplication
Custom binding handler declaration:
define(['ko'], function(ko) {
'use strict';
ko.bindingHandlers.bindDomNode = {
init: function(element, valueAccessor, _a, _b, bindingContext) {
if (valueAccessor()) {
bindingContext.$data.domNode = element;
}
}
};
});
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
45. No duplication
Example access of injected DOM node the view model:
this.domNode.getBoundingClientRect();
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
47. Fewest elements
If something works,
expresses intent
and contains no duplicate knowledge,
don't split it further
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
48. Fewest elements
The Single Responsibility Principle is not
the Single Public Method Principle.
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
49. Fewest elements
If things are related, keep them together.
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
51. Context: Magento <= 2.2.2
Not a SPA/PWA
SRP with several frontend "components",
each with its own more or less
independent state.
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
52. We have three options.
1. Keep state in the DOM or
UiComponents
2. Pass state from root down to children
3. Keep state outside of the view
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
54. Example TicTacToe
Shamelessly stolen from https://reactjs.org/tutorial/tutorial.html
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
55. Example TicTacToe
Simple example for each approach to
managing shared state:
https://github.com/Vinai/example-module-tictactoe
$ git branch
* 0-in-component-state
1-pass-to-children
2-external-state
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
56. 1. Keep state in the DOM or
UiComponents
E.g. jQuery or pure Knockout.js
Only for simplest, single component cases
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
57. 1. State in the DOM
How is state stored in the DOM?
— Form element values
— CSS classes (e.g. "active" or "invalid")
— Inline styles (e.g. "display: none")
— Element attributes (e.g. disabled)
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
58. 1. State in the DOM
DOM state storage is common with jQuery
self.element.off('submit');
// disable 'Add to Cart' button
addToCartButton = $(form).find(this.options.addToCartButtonSelector);
addToCartButton.prop('disabled', true);
addToCartButton.addClass(this.options.addToCartButtonDisabledClass);
form.submit();
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
59. 1. State in UiComponents
Properties on the knockout view model
Shared state: imports, exports, links
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
60. 1. State in UiComponents
Component.extend({
defaults: {
links: {
squares: '${ $.provider }:squares'
},
imports: {
xIsNext: '${ $.provider }:xIsNext',
winner: '${ $.provider }:winner'
}
},
square: function () {
return this.squares[this.squareIndex];
}
handleClick: function () {
const square = this.square();
if ('' === square() && !this.winner) {
square(this.xIsNext ? 'X' : 'O');
}
}
});
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
61. 1. Shared state in DOM or UiComponents
Strong coupling through
cross-component dependencies
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
62. 1. Derived state
In this approach state derived from one
model is o!en stored in another model
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
63. 1. Derived State
updateFoo: function() {
this.foo = this.other.bar * this.other.bar;
}
// vs.
foo: function() {
return this.other.bar * this.other.bar;
}
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
64. 2. Pass state from root down to children
E.g. pure react components
Works within one component hierarchy
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
65. 2. Example from React TicTacToe
<Board
squares={current.squares}
onClick={i => this.handleClick(i)}
/>
class Board extends React.Component {
renderSquare(i) {
return (
<Square
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
/>
);
}
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
66. 2. Pass state from root down to children
Problem when state is required outside
one hierarchy
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
67. 2. Pass state from root down to children
Not common in Magento 2
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
68. 2. Pass state from root down to children
No callback on parent, only on child
Component.extend({
initContainer: function (parent) {
const state = parent.state;
this.cells = [...state.squares.keys()].map(function (i) {
return new Square({index: i, state: state});
});
return this._super(parent);
}
...
}
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
69. 3. Keep state outside the view
E.g. Elm architecture, flux, redux/vuex
Most versatile but needs more plumbing
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
70. 3. Keep state outside the view
There are differences:
Naming, (im)mutability, 1 or 2 way data
flow, (F)RP, effects handling, amount of
architecture...
But there always is a single state storage
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
71. 3. Separate state with UiComponents
State object with observable properties
define(['ko'], function (ko) {
'use strict';
return ko.track({
moves: [],
squares: [...Array(9).keys()].map(() => ko.observable('')),
xIsNext: true,
winner: false
});
});
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
72. 3. Separate state with UiComponents
Require and use state container object
define(
['uiComponent', 'ticTacToeState'],
function (Component, state) {
'use strict';
return Component.extend({
...
handleClick: function () {
if ('' === this.value() && !state.winner) {
state.squares[this.index](state.xIsNext ? 'X' : 'O');
}
},
});
}
);
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
74. 3. Multiple state storage objects
define(
['gameComponent', 'ballState', 'frameState', 'gameState'],
function (GameComponent, ball, frame, game) {
'use strict';
return GameComponent.extend({
initialize: function () {
this._super();
this.initDimensions(frame);
frame.width = function () {
return game.width * .98;
};
frame.height = function () {
return game.height * .80;
};
frame.left = function () {
return game.width * .01;
};
frame.top = function () {
return game.height * .19;
};
},
...
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
75. 3. State outside the view
Benefits:
— State is simple to share
— View models contain mostly only logic
— Less code
— Easier to understand
— More maintainable
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
77. The code needs to...
1. Pass all tests
2. Reveal intent
3. Contain no duplication
4. Have the fewest possible elements
5. Separate shared state from the view
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018
78. Thanks to Kent Beck for his timeless
4 rules of simple design.
Thank you for your attention!
SOS UiComponents - (c) @VinaiKopp - for #MageTitansIT, Milano, 06. April 2018