By Sergey Gernyak, Back-end Engineer at WeAreBrain.
The CMS Cockpit forms a separate part of the Hybris Platform which allows you to construct pages via visual editors and the drag&drop function. In general, there is a great deal of available functionality OOTB that covers almost all business requirements. However, we had one requirement that is unique and there are no built-in solutions as yet. What we needed, was the ability to select an item from a 3rd party API and save it inside Hybris DB.
During a code reading session, I’ve found that CMS Cockpit is based completely on the ZK Framework. With this knowledge, we set out to write glue code that instructs CMS Cockpit to work with a new editor using the ZK framework as a renderer. For this solution, I chose to use a combobox component which has autocomplete functionality — exactly what we needed.
To start, we decided to model all we needed to implement to use the editor we wanted. You’ll see what I mean in the diagram below:
All the functionality can be divided into the following parts:
Just coding the editor won’t help you to see it in the CMS Cockpit. In most cases the list of steps to get it done include:
Here is the most interesting part — the example source code:
package com.mycompany.cms2components.data;
public class ItemOption {
private Integer id;
private String title;
private String description;
private String thumbUrl;
public ItemOption()
{
}
public void setId(final Integer id)
{
this.id = id;
}
public Integer getId()
{
return id;
}
public void setTitle(final String title)
{
this.title = title;
}
public String getTitle()
{
return title;
}
@Override
public String toString() {
return this.getTitle();
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getThumbUrl() {
return thumbUrl;
}
public void setThumbUrl(String thumbUrl) {
this.thumbUrl = thumbUrl;
}
}
package com.mycompany.cms2components.service;
import com.mycompany.cms2components.data.ItemOption;
import java.util.List;
public interface ItemSelectDataSource {
List<ItemOption> doSearch(String term, String locale);
List<ItemOption> getByIds(List<String> ids);
}
package com.mycompany.cms2components.editor;
import com.mycompany.cms2components.data.ItemOption;
import com.mycompany.cms2components.editor.model.ItemSelectUIEditorModel;
import com.mycompany.cms2components.service.ItemSelectDataSource;
import de.hybris.platform.cockpit.model.editor.EditorListener;
import de.hybris.platform.cockpit.model.editor.ListUIEditor;
import de.hybris.platform.cockpit.model.editor.impl.AbstractUIEditor;
import de.hybris.platform.cockpit.services.values.ObjectValueContainer;
import de.hybris.platform.core.Registry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.ReflectionUtils;
import org.zkoss.zk.ui.HtmlBasedComponent;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.event.InputEvent;
import org.zkoss.zul.Combobox;
import org.zkoss.zul.Comboitem;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class ItemSelectUIEditor extends AbstractUIEditor implements ListUIEditor {
private static final Logger LOG = LoggerFactory.getLogger(ItemSelectUIEditor.class);
private Combobox editorView;
private ItemSelectDataSource dataSource;
private ItemSelectUIEditorModel model;
public ItemSelectUIEditor() {
LOG.info("ItemSelectUIEditor initialization started!");
this.dataSource = this.getDataSourceBean();
this.model = this.getModelBean();
this.editorView = new Combobox();
this.setupEditorViewDefaultBehaviour();
}
@Override
public HtmlBasedComponent createViewComponent(Object initialValue, Map<String, ? extends Object> parameters, EditorListener listener) {
this.setInitialSelection(initialValue);
this.setupEditorViewEvents(listener);
return this.getEditorView();
}
private void setInitialSelection(Object initialValue) {
if (initialValue == null) { return; }
List<ItemOption> options = dataSource.getByIds(Collections.singletonList(initialValue.toString()));
this.model.setAutocompleteResult(options);
this.updateAutoCompleteItemList();
this.getEditorView().setSelectedIndex(0);
}
private void setupEditorViewDefaultBehaviour() {
this.getEditorView().setAutocomplete(true);
this.getEditorView().setAutodrop(true);
this.getEditorView().setButtonVisible(false);
}
private void setupEditorViewEvents(EditorListener listener) {
this.getEditorView().addEventListener(Events.ON_CHANGING, event -> {
String term = ((InputEvent) event).getValue();
LOG.info("onChanging triggered with value '" + term + "'!");
Field valueHolderFields = ReflectionUtils.findField(listener.getClass(), "val$valueHolder");
ReflectionUtils.makeAccessible(valueHolderFields);
ObjectValueContainer.ObjectValueHolder valueHolder = (ObjectValueContainer.ObjectValueHolder) ReflectionUtils.getField(valueHolderFields, listener);
String locale = valueHolder.getLanguageIso();
ItemSelectUIEditor.this.updateAutocomplete(term, locale);
});
this.getEditorView().addEventListener(Events.ON_CHANGE, event -> {
LOG.info("onChange triggered!");
this.onSavingEvent(listener);
});
}
private void updateAutocomplete(String term, String locale) {
if (term.length() < 3) {
this.model.setAutocompleteResult(new ArrayList<>());
}else{
List<ItemOption> options = dataSource.doSearch(term, locale);
this.model.setAutocompleteResult(options);
}
this.updateAutoCompleteItemList();
}
private void updateAutoCompleteItemList() {
this.getEditorView().getItems().clear();
for (Comboitem comboitem : this.model.getAutocompleteComboitems()) {
this.getEditorView().appendChild(comboitem);
}
}
private void onSavingEvent(EditorListener listener) {
LOG.info("onSavingEvent called!");
if(this.getEditorView().getSelectedItem() == null) { return; }
Comboitem selectedOption = this.getEditorView().getSelectedItem();
this.setValue(selectedOption.getValue());
listener.valueChanged(this.getValue());
}
@Override
public boolean isInline() {
return true;
}
@Override
public String getEditorType() {
return "INTEGER";
}
@Override
public void setAvailableValues(List<? extends Object> list) {
}
@Override
public List<ItemOption> getAvailableValues() {
return null;
}
public void setFocus(HtmlBasedComponent rootEditorComponent, boolean selectAll) {
Combobox element = (Combobox)((CancelButtonContainer)rootEditorComponent).getContent();
element.setFocus(true);
if(this.initialInputString != null) {
element.setText(this.initialInputString);
}
}
protected Combobox getEditorView() {
return editorView;
}
private ItemSelectDataSource getDataSourceBean() {
return Registry.getApplicationContext().getBean("defaultItemSelectDataSource", ItemSelectDataSource.class);
}
private ItemSelectUIEditorModel getModelBean() {
return Registry.getApplicationContext().getBean("defaultItemSelectUIEditorModel", ItemSelectUIEditorModel.class);
}
}
package com.mycompany.cms2components.editor.model;
import com.mycompany.cms2components.data.ItemOption;
import org.zkoss.zul.Comboitem;
import java.util.List;
public interface ItemSelectUIEditorModel {
List<ItemOption> getAutocompleteResult();
void setAutocompleteResult(List<ItemOption> options);
List<Comboitem> getAutocompleteComboitems();
}
<?xml version="1.0" encoding="UTF-8"?>
<wizard-config showPrefilledValues="false" selectMode="true" createMode="true" displaySubtypes="true">
<displayed-properties>
<group qualifier="General" visible="true" initially-opened="true">
<label key="cockpit.config.label.General" />
<property qualifier="MyComponent.name"/>
</group>
<group qualifier="Properties" visible="true" initially-opened="true">
<label key="config.wizardConfig.properties" />
<property qualifier="contentId" editorCode="contentItemSelectAutocomplete"/>
</group>
</displayed-properties>
</wizard-config>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd"
default-autowire="byName">
<!-- Allows Additional PropertyEditorDescriptor to be added for all cockpits -->
<bean id="LookupEditorFactory" class="de.hybris.platform.cockpit.model.meta.LookupEditorFactory" init-method="init" lazy-init="false">
<property name="editorFactory" ref="EditorFactory"></property>
</bean>
<bean id="myCompanyIntegerEditors" class="de.hybris.platform.cockpit.model.meta.DefaultPropertyEditorDescriptor">
<property name="editorType" value="INTEGER"/>
<property name="defaultEditor" value="de.hybris.platform.cockpit.model.editor.impl.DefaultIntegerUIEditor" />
<property name="defaultMode" value="single"/>
<property name="editors">
<map>
<entry key="contentItemSelectAutocomplete" value="com.mycompany.cms2components.editor.ItemSelectUIEditor"/>
</map>
</property>
<property name="label" value="myCompanyIntegerEditors"/>
</bean>
</beans>
NOTE: all these files have different contexts, so do not pay a lot of attention to the classes/variables/ naming etc.
NOTE 2: I am newbie in the Hybris Platform development area, so if there’s something I missed or doesn’t make sense, holla in the comments section below.
NOTE 3: As github gist does not allow slashes inside file names I’ve used two underscore symbols in place of the slash .
Happy coding!
An executive’s guide to AI and Intelligent Automation. Working Machines takes a look at how the renewed vigour for the development of Artificial Intelligence and Intelligent Automation technology has begun to change how businesses operate.