Retail:Developers Guide/How-to/Update To WebPOS New Skin
Contents |
Introduction
The following guide is intended to provide information and examples of changes that should be applied to the JS and CSS code in order to continue displaying properly the UI after upgrading from 19Q4 or lower versions.
Remove styling declared in JS
As a previous step required to make your custom module skinnable, all the styling declared in the JS (or inline CSS) should be moved to CSS files. In order to do that, three steps should be followed:
- Delete all inline styles
- Replace JS style functions
- Create CSS classes
Inline styles have maximum specificity value, are difficult to override and are not reusable. Due to that, they are not allowed anymore.
BEM
New CSS classes should be named using BEM CamelCase approach.
- DBPrefix should go at the beginning (obpos2TicketLine-value_disabled).
- Names are written in Latin letters.
- Each word inside a name begins with an uppercase letter.
- The separator for names of blocks, elements, are hyphen ( - ) and for modificator underscore are going to be used.
Example:
In order to provide the specific CSS class names from the code moved from JS, the following rules must be followed:
- IF (component HAS name) THEN className = camelCase(component name)
- ELSE IF (component HAS NO name) AND (component HAS kind) THEN className = camelCase(component kind)
- ELSE IF (component HAS NO name) AND (component HAS NO kind) AND (component HAS subcomponents) THEN className = camelCase(containerN)
- ELSE IF (component HAS NO name) AND (component HAS NO kind) AND (component HAS NO subcomponents) THEN className = camelCase(elementN)
Example:
PS: All new components must include a name. Components without name ARE NOT ALLOWED.
CSS files structure
Additionaly, some improvements can be made inside the CSS file structure: File & Folder header
/*********************** ***********************/ /* File: [...] */ /* Folder: [...] */ /*********************** ***********************/
Component information
/*
# Component: [...]
# Inherits: [...]
*/
CSS class inheritance
/* Inherits: [...] */
Example:
/*********************** ***********************/ /* File: ob-terminal-component.js */ /* Folder: source/component/ */ /*********************** ***********************/ /* # Component: OB.UI.Terminal */ .obUiTerminal { margin: 0; padding: 0; width: 100%; height: 100%; } .obUiTerminal-headerContainer { height: 0px; } .obUiTerminal-mainContainer-containerLoading { /* Inherits: [OB.UI.LoadingScrim] */ }
Get rid of "floats" of your CSS code
While doing the code migration from JS to CSS, some code enhacements could be also made: Replace CSS floats by FlexBox or CSS Grid:
- CSS Floats are not meant for layouts
- Created for allow floating text around an image (it started to use for layouts thanks to ‘clear: both’ style). Float-Clear structures problems:
- Usually leads to broken UI (mostly in responsive design)
- Difficult to maintain and modify
- The main idea behind the flex layout is to give the container the ability to alter its items' width/height (and order) to best fill the available space (mostly to accommodate to all kind of display devices and screen sizes).
- CSS Grid Layout (aka "Grid"), is a two-dimensional grid-based layout system that aims to do nothing less than completely change the way we design grid-based user interfaces.
- Flexbox helped out, but it's intended for simpler one-dimensional layouts, not complex two-dimensional ones (Flexbox and Grid actually work very well together). Grid is the very first CSS module created specifically to solve the layout problems.
Explore css utils ofered by mobile core
Our main css file obmobc-main.css, which is located in mobile core offers many utils which can be reused simply adding them to components as css classes. Explore that file to find all of them. Below are listed a short selection of utils
.u-clearBoth { clear: both; } .u-hiddeComponent { visibility: hidden; } .u-showComponent { visibility: visible; } .u-displayNone { display: none; } .u-displayBlock { display: block; } .u-displayInlineFlex { display: inline-flex; }
Use custom properties
Custom properties are a kind of cssConstants which can be overwriten by other skins. If your components uses this properties instead of "inline" values, your component will changed without any effort when a new skin is installed. Custom properties are defined in mobile.core module (obmobc-main.css). It is highly recommended to make use of them. Below are listed a short selection of utils.
/*Definition*/ --color-primary: #4d545c; --color-on-primary: #e8e8e8; --color-secondary: #6cb33f; --color-on-secondary: #ffffff; /*Usage*/ .obUiFormElementIntegerEditor-btnQtyMinus { /*Inherits: [OB.UI.Button]*/ margin: 0px; width: 50px; height: 50px; font-size: var(--font-xxlarge); background-color: var(--color-primary); color: var(--color-on-primary); }
If your project has a stylesheet you can create your own custom properties.
Adapt JS+CSS to use new components
Prior to 20Q1 there were no official CSS API, so custom modules could contain wide variety of JS+CSS code because at the end the developer built it following his own criteria. Because of this, it is difficult to write a detailed guide like "all 'A' components should be replaced by 'Z' components" could not be provided, because 'A' could be 'A' or 'B', 'C', 'D' or whatever kind of structure the developer could have built. The best way to determine which components need to be updated is by performing a visual inspection after the upgrade. All the new components you can reuse or inherit from are detailed in this link:
Some real examples of JS+CSS code migration will be provided below to help you in the upgrade of your custom code.
OB.UI.FormElement
New 'OB.UI.FormElement' component has been built, integrating in the same component the label, the icon (optional) and the component itself (input, select, checkbox, selector, ...).
Some migration examples:
- From 'enyo.Input' to 'OB.UI.FormElement.Input'
From:
{ kind: 'enyo.Input', type: 'text', name: 'username', classes: 'obObposLoginUiLogin-loginInputs-container2-username' } ... this.$.username.attributes.placeholder = OB.I18N.getLabel( 'OBMOBC_LoginUserInput' );
to:
{ kind: 'OB.UI.FormElement', name: 'formElementUsername', classes: 'obUiFormElement_dataEntry obObposLoginUiLogin-loginInputs-formElementUsername', coreElement: { kind: 'OB.UI.FormElement.Input', type: 'text', name: 'username', i18nLabel: 'OBMOBC_LoginUserInput', classes: 'obObposLoginUiLogin-loginInputs-formElementUsername-username' } }
- From 'enyo.Option' to 'OB.UI.FormElement.Select.Option'. This change is straightforward. It does not need any additional kind of refactor.
- From 'OB.UI.CheckboxButton' to 'OB.UI.FormElement.Checkbox'
From:
{ classes: 'obUiSearchProductCharacteristicHeader-container1-container3-container2', components: [ { kind: 'OB.UI.CheckboxButton', name: 'crossStoreSearch', classes: 'obUiSearchProductCharacteristicHeader-container1-container3-container2-crossStoreSearch', i18nLabel: 'OBMOBC_CrossStoreSearch', tap: function() { if (this.checked) { this.unCheck(); } else { this.check(); } var searchProduct = this.owner.owner.$.searchProductCharacteristicHeader; searchProduct.categories.reset(); searchProduct.loadCategories(null); return this; } } ] }
to:
{ kind: 'OB.UI.FormElement', name: 'formElementCrossStoreSearch', classes: 'obUiFormElement_dataEntry obUiSearchProductCharacteristicHeader-container1-container3-container2', newAttribute: { kind: 'OB.UI.FormElement.Checkbox', name: 'crossStoreSearch', classes: 'obUiSearchProductCharacteristicHeader-container1-container3-container2-crossStoreSearch', i18nLabel: 'OBMOBC_CrossStoreSearch', tap: function() { this.setChecked(!this.getChecked()); var searchProduct = this.formElement.owner.owner.$.searchProductCharacteristicHeader; searchProduct.categories.reset(); searchProduct.loadCategories(null); return this.formElement; } } }
- From 'OB.UI.SmallButton' (when it is used to create a selector) to 'OB.UI.FormElement.Selector'
From:
{ enyo.kind({ kind: 'OB.UI.SmallButton', name: 'OB.UI.Customer', classes: 'obUiCustomer customerShipBill-obUiSmallButton-generic', ... renderCustomer: function(newCustomer) { this.setContent(newCustomer); }, orderChanged: function(oldValue) { if (this.order.get('bp')) { this.renderCustomer(this.order.get('bp').get('_identifier')); } else { this.renderCustomer(''); } this.order.on( 'change:bp', function(model) { if (model.get('bp')) { this.renderCustomer(model.get('bp').get('_identifier')); } else { this.renderCustomer(''); } }, this ... ) } }); }
to:
{ enyo.kind({ kind: 'OB.UI.FormElement.Selector', name: 'OB.UI.Customer', classes: 'obUiCustomer', ... renderCustomer: function(newCustomerId, newCustomerName) { this.setValue(newCustomerId, newCustomerName); }, orderChanged: function(oldValue) { if (this.order.get('bp')) { this.renderCustomer(this.order.get('bp').get('id'), this.order.get('bp').get('_identifier') ); } else { this.renderCustomer(null, ''); } this.order.on( 'change:bp', function(model) { if (model.get('bp')) { this.renderCustomer(this.order.get('bp').get('id'), this.order.get('bp').get('_identifier') ); } else { this.renderCustomer(null, ''); } }, this ... ) } }); }
OB.UI.Button
New 'OB.UI.Button' is an improved button class that allows render buttons having (optionally) both image and label, while at the same time has some rich skinnability capabilities. Most of the application buttons (OB.UI.ToolbarButton, OB.UI.ToolbarButtonTab, OB.UI.ActionButton, OB.UI.ModalDialogButton, ...) inherit from it.
Previously created buttons may continue working, although the styling will not match with the new skin. In order to adapt it the following properties (if exist) should be removed:
- disabledClass
- buttonBeforeClass
- buttonAfterClass
- labelClass
If there are custom classes defined, thouse ones would need to be adapted too. Here there is an example of a button with custom styling (icon on the left and label on the right):
Javascript:
{{ enyo.kind({ name: 'OB.UI.CustomButton', kind: 'OB.UI.Button', classes: 'obUiCustomButton', ... }); }
CSS:
.obUiCustomButton { font-weight: normal; font-size: var(--font-medium); text-decoration: none; height: 50px; text-align: left; color: var(--color-on-primary); background-color: var(--color-primary); border: none; text-overflow: clip; overflow: hidden; padding: 0px; margin: 0px; } .obUiCustomButton .obUiButton-components { grid-template-columns: 30px 1fr; margin: 0px; padding: 0px 10px; } .obUiCustomButton .obUiButton-components-icon { background-position: center center; background-repeat: no-repeat; filter: var(--color-on-primary_img); width: 100%; height: 100%; } .obUiCustomButton .obUiButton-components-label { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; word-break: break-word; overflow: hidden; text-overflow: ellipsis; color: var(--color-on-primary); padding: 0px 5px 0px 5px; } /* hover state */ .obUiCustomButton:not(.disabled):hover { background-color: var(--color-primary_hover); } .obUiCustomButton:not(.disabled):hover .obUiButton-components-icon { filter: var(--color-on-primary_hover_img); background-image: url('./../../org.openbravo.mobile.core/assets/img/iconActButtonDeleteLine.svg'); background-size: auto 16px; } .obUiCustomButton:not(.disabled):hover .obUiButton-components-label { color: var(--color-on-primary_hover); } .obUiCustomButton .obUiButton-components:after { content: ''; position: absolute; border-right: 0.5px solid var(--color-on-primary_disabled); height: calc(100% - 26px); width: calc(100% - 0.5px); } /* active state */ .obUiCustomButton:not(.disabled):active, .obUiCustomButton.selected { background-color: var(--color-primary_active); } .obUiCustomButton:not(.disabled):active .obUiButton-components-icon, .obUiCustomButton.selected .obUiButton-components-icon { filter: var(--color-on-primary_active_img); } .obUiCustomButton:not(.disabled):active .obUiButton-components-label, .obUiCustomButton.selected .obUiButton-components-label { color: var(--color-on-primary_active); } .obUiCustomButton.selected .obUiButton-components-label:after { content: ''; position: absolute; left: 20%; top: 81%; width: 60%; height: 6px; background-color: var(--color-secondary); } /* focus state */ .obUiCustomButton:not(:active):not(.disabled):focus { box-shadow: inset 0px 0 2px 1px var(--color-primary_focus); border-color: var(--color-primary_focus); border-style: solid; } .obUiCustomButton:not(:active):not(.disabled):focus .obUiButton-components-icon { filter: var(--color-on-primary_focus_img); } .obUiCustomButton:not(:active):not(.disabled):focus .obUiButton-components-label { color: var(--color-on-primary_focus); } /* disabled state */ .obUiCustomButton.disabled { background-color: var(--color-primary_disabled); } .obUiCustomButton.disabled .obUiButton-components-icon { filter: var(--color-on-primary_disabled_img); } .obUiCustomButton.disabled .obUiButton-components-label { color: var(--color-on-primary_disabled); }
OB.UI.Modal
Both previous 'OB.UI.Modal' and 'OB.UI.ModalAction' has been merged into new 'OB.UI.Modal'
Previous implementations had only header and body, while new one has header, body and footer. In the following examples it is shown how the internal component structure has changed:
From:
enyo.kind({ name: 'OB.UI.Modal', kind: 'OB.UI.Popup', classes: 'obUiModal', components: [ { tag: 'div', classes: 'obUiModal-header', components: [ { name: 'closebutton', tag: 'div', classes: 'obUiModal-header-closebutton', components: [ { tag: 'span', ontap: 'hide', allowHtml: true, classes: 'header-closebutton-element1', content: '×' } ] }, { name: 'header', classes: 'obUiModal-header-header' } ] }, { tag: 'div', name: 'body', classes: 'obUiModal-body' } ], ... }); enyo.kind({ name: 'OB.UI.ModalAction', kind: 'OB.UI.Popup', classes: 'obUiModalAction', bodyContentClass: 'obUiModalAction-body', bodyButtonsClass: 'obUiModalAction-button', components: [ { classes: 'obUiModalAction-header', components: [ { name: 'headerCloseButton', classes: 'obUiModalAction-header-headerCloseButton', components: [ { tag: 'span', ontap: 'hide', allowHtml: true, content: '×', classes: 'header-headerCloseButton-element1' } ] }, { name: 'header', classes: ' obUiModalAction-header-header' } ] }, { classes: 'modal-dialog-body obUiModalAction-bodyParent', name: 'bodyParent', components: [ { name: 'bodyContent', classes: 'obUiModalAction-bodyParent-bodyContent' }, { name: 'bodyButtons', classes: 'obUiModalAction-bodyParent-bodyButtons' } ] } ], ... });
to:
enyo.kind({ name: 'OB.UI.Modal', kind: 'OB.UI.Popup', classes: 'obUiModal', components: [ { classes: 'obUiModal-header', components: [ { name: 'header', classes: 'obUiModal-header-header' }, { name: 'closebutton', classes: 'obUiModal-header-closebutton', components: [ { kind: 'OB.UI.ModalCloseButton', classes: 'obUiModal-header-closebutton-obUiModalCloseButton' } ] } ] }, { name: 'body', classes: 'obUiModal-body', showing: false }, { name: 'footer', classes: 'obUiModal-footer', showing: false } ], ... });
In order to maintain visual coherence with the rest of the application, all the primary and secondary buttons of the popup should be moved to the 'footer'.
Except for specific situations all popups build in Openbravo mobile platform should be build using "OB.UI.Modal". If specific requirements need to be covered in a fully customized popup OB.UI.Popup can be used.
FROM:
enyo.kind({ name: 'examplePopup', kind: 'OB.UI.Modal', bodyContent: { kind: 'examplePopupBodyContentKind', name: 'examplepopup-body' }, bodyButtons: { kind: 'examplePopupBodyButtonsKind', name: 'examplepopup-buttons' },
TO:
enyo.kind({ name: 'examplePopup', kind: 'OB.UI.Modal', body: { kind: 'examplePopupBodyKind', name: 'examplepopup-body' }, footer: { kind: 'examplePopupFooterKind', name: 'examplepopup-footer' },
Buttons which represents the most common action should be marked as a default action
New utility function available to be used by modals
OB.UTIL.getPopupFromComponent(componentToCheck) which returns the popup component which is parent of the provided component
From:
//go up to reach parent let parentComponent = this.parent.parent; //go down to reach certain component parentComponent.$.body.$.qtyKeypadBody.getQty();
To:
//go up to reach parent let parentComponent = OB.UTIL.getPopupFromComponent(this); //go down to reach certain component parentComponent.$.body.$.qtyKeypadBody.getQty();
OB.UI.ModalSelector
The component OB.UI.ModalSelector has been adapted to use new 'OB.UI.Modal' component
In order to migrate it, the previous points should be followed for each one of the components (inputs, buttons, modal popup structure, ...). At the end of the process, the structure should remain like this.
Transform OB.UI.WindowView with toolbar to new Skin
- OB.UI.WindowView:
The API of this component has not changed, so it should not require any change.
![]() | Tip: The developer can add a specific css class to the window container, it will help to define specific css classes |
OB.MobileApp.windowRegistry.registerWindow({ windowClass: 'OBAWO.Box.BoxProposalView', + containerCssClass: 'obawoBox',
- OB.UI.Multicolumn
The API of this component has not changed, so it should not require any change. It has been refactored to use css grid and by default, it generates a grid tempate-areas schema
grid-template-areas: "obUiMultiColumn-leftToolbar obUiMultiColumn-rightToolbar" "obUiMultiColumn-leftPanel obUiMultiColumn-rightPanel";
if we want to implement the layout change when the screen changes from landscape to portrait then a media query should be used to modify the grid-template-area
@media (max-aspect-ratio: 9/8) { .obawoBox .obUiMultiColumn-panels { grid-template-areas: 'obUiMultiColumn-rightToolbar' 'obUiMultiColumn-rightPanel'; grid-template-columns: 100%; grid-template-rows: 72px calc(100% - 72px); } }
Same strategy will be used to switch between right and left side. In this case, when just left side is shown we will show the right toolbar on the top and below the left toolbar
@media (max-aspect-ratio: 9/8) { .obawoBox .obUiMultiColumn-panels.showLeft { grid-template-areas: 'obUiMultiColumn-rightToolbar' 'obUiMultiColumn-leftToolbar' 'obUiMultiColumn-leftPanel'; grid-template-columns: 100%; grid-template-rows: 72px calc(100% - 72px); } }
- OB.UI.Multicolumn.Toolbar
It is required to define a grid template areas for the toolbar. When the toolbar is created if flag “ShowMenu” is enabled, then the button to open the main menu will be shown in the area called “obUiMainMenu”. Because of that, our grid-template-area should include an area with this name. Below component is defining the layout of a right toolbar with 2 small buttons and a container to show some info. (menu, back and information). Using the class defined above for my window (obawoBox) I'm creating a new css rule to define the structure desired for my toolbar.
.obawoBox .obUiMultiColumnToolbar-standardToolbar-toolbar { grid-template-areas: "obUiMainMenu back userinfo"; grid-template-columns: 1fr 1fr 5fr; align-items: center; }