Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MARKET-1466 Cascade Select #184

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
41 changes: 41 additions & 0 deletions components/bl-cascade-select/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Cascade Select

Cascade Select is a component of Backendless UI-Builder designer. This allows you to select a value from a nested structure of options.

## Properties

| Property | Type | Default Value | Logic | Data Binding | UI Setting | Description |
|-------------------|---------|---------------------|-----------------------------|--------------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Cascade | JSON | | Cascade Logic | YES | YES | Allows determinate an array of select items to display as the available options. Watch [Codeless Examples](#Examples). Signature of polygon: `[{name, code, ?children}]` |
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

name -> label

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code -> value

| Placeholder | String | | Placeholder Logic | YES | YES | Allows determinate placeholder for input |

## Events

| Name | Triggers | Context Blocks |
|---------------|-------------------------------|--------------------------------------------------------------|
| On Click Item | when the user select the item | Item: `{name: String, code: String, levelOfNesting: Number}` |

## Action

| Action | Inputs | Return |
|---------------|------------------|-----------------------------|
| Get Select in | | Object: of a select item |
| Set Cascade | cascade: `Array` | |
| Get Cascade | | Array: of s cascade |
| Set Code | code: `String` | |
| Get Code | | String: code of select item |

## <a name="Examples"></a> Codeless Examples

Addition of cascade data

![](example-images/cascade_example.jpg)

<details>
<summary>Try yourself</summary>

```
<block xmlns="http://www.w3.org/1999/xhtml" type="lists_create_with" id="I6`{YbX`1w)ZrZA[n(3l" x="-94.53923425078003" y="88.92089374100392"><mutation items="2"></mutation><value name="ADD0"><block type="create_object" id="hd^`S({p+5%(tCzQMIkl"><mutation><properties><item id="property" prop-name="name"></item><item id="property" prop-name="code"></item><item id="property" prop-name="children"></item></properties></mutation><value name="create_object_mutator_container_properties_stack_property0"><block type="text" id="Rmz=y,*(iJ0^*7tqt^wN"><field name="TEXT">Australia</field></block></value><value name="create_object_mutator_container_properties_stack_property1"><block type="text" id="73(!S~9Bj8[(1dd.hOG%"><field name="TEXT">AU</field></block></value><value name="create_object_mutator_container_properties_stack_property2"><block type="lists_create_with" id="+^[pE!*BrEq@/~$ZXJDM"><mutation items="2"></mutation><value name="ADD0"><block type="create_object" id="ely].XX{?.SFw}g*Ux$F"><mutation><properties><item id="property" prop-name="name"></item><item id="property" prop-name="code"></item><item id="property" prop-name="children"></item></properties></mutation><value name="create_object_mutator_container_properties_stack_property0"><block type="text" id="0,Ub7#U7iP^Uc;F1W,l%"><field name="TEXT">New South Wales</field></block></value><value name="create_object_mutator_container_properties_stack_property1"><block type="text" id="w#9]c/AL5qytPDCGEGh^"><field name="TEXT">AU-NSW</field></block></value><value name="create_object_mutator_container_properties_stack_property2"><block type="lists_create_with" id="NcaOOw}XzebfYg[$#]*V"><mutation items="3"></mutation><value name="ADD0"><block type="create_object" id="=@e``]rkt/IwmHZaxEGU"><mutation><properties><item id="property" prop-name="name"></item><item id="property" prop-name="code"></item></properties></mutation><value name="create_object_mutator_container_properties_stack_property0"><block type="text" id="e(%`Bzh$xiJ9{xFo2YL/"><field name="TEXT">Sydney</field></block></value><value name="create_object_mutator_container_properties_stack_property1"><block type="text" id="[.3Q:STX@sn9Lw}pm7Qc"><field name="TEXT">AU-NSW-SY</field></block></value></block></value><value name="ADD1"><block type="create_object" id="dTF-V$w2/}%bGhSyA%]Q"><mutation><properties><item id="property" prop-name="name"></item><item id="property" prop-name="code"></item></properties></mutation><value name="create_object_mutator_container_properties_stack_property0"><block type="text" id="0WOJ4K22**Az9=mscXx7"><field name="TEXT">Newcastle</field></block></value><value name="create_object_mutator_container_properties_stack_property1"><block type="text" id="^D)=+TRJD8Hbb(X%qAy."><field name="TEXT">AU-NSW-NC</field></block></value></block></value><value name="ADD2"><block type="create_object" id="ZAL1i-YAD!U*bPcteuR!"><mutation><properties><item id="property" prop-name="name"></item><item id="property" prop-name="code"></item></properties></mutation><value name="create_object_mutator_container_properties_stack_property0"><block type="text" id="101HuiZBCvUT)Q=k;c7O"><field name="TEXT">Wollongong</field></block></value><value name="create_object_mutator_container_properties_stack_property1"><block type="text" id="II{6/P]UldmqR84w=#yo"><field name="TEXT">AU-NSW-WG</field></block></value></block></value></block></value></block></value><value name="ADD1"><block type="create_object" id="23VyPHa2^%a{BnrD;:oc"><mutation><properties><item id="property" prop-name="name"></item><item id="property" prop-name="code"></item><item id="property" prop-name="children"></item></properties></mutation><value name="create_object_mutator_container_properties_stack_property0"><block type="text" id="qdN:ohQ^xc{~Rsn:GAq+"><field name="TEXT">Queensland</field></block></value><value name="create_object_mutator_container_properties_stack_property1"><block type="text" id="UFX@h3$}X52c}@S^*p/^"><field name="TEXT">AU-QS</field></block></value><value name="create_object_mutator_container_properties_stack_property2"><block type="lists_create_with" id="@=LxHd02i?4?t+rW|64h"><mutation items="2"></mutation><value name="ADD0"><block type="create_object" id="%wf4cFQJ/qGl{7/Il2A$"><mutation><properties><item id="property" prop-name="name"></item><item id="property" prop-name="code"></item></properties></mutation><value name="create_object_mutator_container_properties_stack_property0"><block type="text" id=".Tn=|{GrFAU]w[=21lH."><field name="TEXT">Brisbane</field></block></value><value name="create_object_mutator_container_properties_stack_property1"><block type="text" id="e`w,YXw1(ceOTdI2j9+L"><field name="TEXT">AU-QS-BB</field></block></value></block></value><value name="ADD1"><block type="create_object" id="%rbhI}Sb!@Vue942V_W}"><mutation><properties><item id="property" prop-name="name"></item><item id="property" prop-name="code"></item></properties></mutation><value name="create_object_mutator_container_properties_stack_property0"><block type="text" id="x4~`U{VoDZ3gMG`i3-Fg"><field name="TEXT">Townsville</field></block></value><value name="create_object_mutator_container_properties_stack_property1"><block type="text" id="v{y{ntd-%*4fjlFH4)=!"><field name="TEXT">AU-QS-TS</field></block></value></block></value></block></value></block></value></block></value></block></value><value name="ADD1"><block type="create_object" id="st!J)Cx*,C_Xx4E)5,,|"><mutation><properties><item id="property" prop-name="name"></item><item id="property" prop-name="code"></item><item id="property" prop-name="children"></item></properties></mutation><value name="create_object_mutator_container_properties_stack_property0"><block type="text" id="(*uAjQ7(V_NLb#`mc)!s"><field name="TEXT">Canada</field></block></value><value name="create_object_mutator_container_properties_stack_property1"><block type="text" id="JZrB.YadV5v/APu_6/xL"><field name="TEXT">CA</field></block></value><value name="create_object_mutator_container_properties_stack_property2"><block type="lists_create_with" id="It-{1g[_kl})XaTbVnlK"><mutation items="2"></mutation><value name="ADD0"><block type="create_object" id="[email protected]+b]ipJ3dnDD+"><mutation><properties><item id="property" prop-name="name"></item><item id="property" prop-name="code"></item><item id="property" prop-name="children"></item></properties></mutation><value name="create_object_mutator_container_properties_stack_property0"><block type="text" id="G7j*jnSN(Annx89J{Ko:"><field name="TEXT">Quebec</field></block></value><value name="create_object_mutator_container_properties_stack_property1"><block type="text" id="w[O4hm|+LN4`3kHsfo^v"><field name="TEXT">CA-QB</field></block></value><value name="create_object_mutator_container_properties_stack_property2"><block type="lists_create_with" id="h8Aa4cJAZ+Z2bTu;v=@j"><mutation items="2"></mutation><value name="ADD0"><block type="create_object" id="eKI@M]|TJAL_3O@|tV7}"><mutation><properties><item id="property" prop-name="name"></item><item id="property" prop-name="code"></item></properties></mutation><value name="create_object_mutator_container_properties_stack_property0"><block type="text" id="}l+-hZaLgz)z,SSO~yE`"><field name="TEXT">Montreal</field></block></value><value name="create_object_mutator_container_properties_stack_property1"><block type="text" id="lpCB5?#F[vP_b{!`yDGK"><field name="TEXT">CA-QB-MR</field></block></value></block></value><value name="ADD1"><block type="create_object" id="^LaaQXj.d}=ii[.0=`~;"><mutation><properties><item id="property" prop-name="name"></item><item id="property" prop-name="code"></item></properties></mutation><value name="create_object_mutator_container_properties_stack_property0"><block type="text" id="m0L`+S|g$=qv~fKFe(WF"><field name="TEXT">Quebec City</field></block></value><value name="create_object_mutator_container_properties_stack_property1"><block type="text" id="A;RO8hooP(x*Feh}~ohR"><field name="TEXT">CA-QB-CBC</field></block></value></block></value></block></value></block></value><value name="ADD1"><block type="create_object" id="zlA_eq+Dyde5(~QORUv0"><mutation><properties><item id="property" prop-name="name"></item><item id="property" prop-name="code"></item><item id="property" prop-name="children"></item></properties></mutation><value name="create_object_mutator_container_properties_stack_property0"><block type="text" id="Yt7Xd-sMIFsp]0DrG*~D"><field name="TEXT">Ontario</field></block></value><value name="create_object_mutator_container_properties_stack_property1"><block type="text" id="~8l2Q5MC/Ko3KR:EJeEs"><field name="TEXT">CA-OT</field></block></value><value name="create_object_mutator_container_properties_stack_property2"><block type="lists_create_with" id="_GWtd^*`0F1VY`aNycML"><mutation items="2"></mutation><value name="ADD0"><block type="create_object" id="RO2Sy[E4,FQU#z$IuFvx"><mutation><properties><item id="property" prop-name="name"></item><item id="property" prop-name="code"></item></properties></mutation><value name="create_object_mutator_container_properties_stack_property0"><block type="text" id="@P`$ZS$j6tVBtX_7*sqz"><field name="TEXT">Ottawa</field></block></value><value name="create_object_mutator_container_properties_stack_property1"><block type="text" id="%AxPNTGKb#_)r{welh5,"><field name="TEXT">CA-OT-OW</field></block></value></block></value><value name="ADD1"><block type="create_object" id="d|X]SW;eH:GO]Hi9ff(H"><mutation><properties><item id="property" prop-name="name"></item><item id="property" prop-name="code"></item></properties></mutation><value name="create_object_mutator_container_properties_stack_property0"><block type="text" id="yxz9yUj@6RM^$caT*;Y?"><field name="TEXT">Toronto</field></block></value><value name="create_object_mutator_container_properties_stack_property1"><block type="text" id="[Xw5938k[M2kk;lD_#Q5"><field name="TEXT">CA-OT-TR</field></block></value></block></value></block></value></block></value></block></value></block></value></block>
```
</details>

84 changes: 84 additions & 0 deletions components/bl-cascade-select/component.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
{
"id": "c_4709a015328b307e652d915fc3f36fb7",
"name": "Cascade Select",
"description": "Cascade Select is a component to select a value from a nested structure of options.",
"showInToolbox": true,
"faIcon": "check-double",
"mainJS": "dist/index.js",
"type": "custom",
"category": "Custom Components",
"properties": [
{
"type": "json",
"name": "cascade",
"label": "Cascade",
"showInSettings": true,
"hasLogicHandler": true,
"handlerId": "cascadeLogic",
"handlerLabel": "Cascade Logic",
"dataBinding": true,
"handlerDescription": "This is a handler for the logic to determine an array of select items to display as the available options."
},
{
"type": "text",
"name": "placeholder",
"label": "Placeholder",
"showInSettings": true,
"hasLogicHandler": true,
"handlerId": "placeholderLogic",
"handlerLabel": "Placeholder Logic",
"dataBinding": true,
"handlerDescription": "This is a handler for the logic to determine the default text to display when no option is selected."
}
],
"eventHandlers": [
{
"name": "onClickItem",
"label": "On Click Item",
"contextBlocks": [
{
"id": "item",
"label": "Item"
}
],
"handlerDescription": "This event is triggered when user select item"
}
],
"actions": [
{
"id": "getSelected",
"label": "Get Selected in",
"hasReturn": true
},
{
"id": "setCascade",
"label": "Set Cascade",
"inputs": [
{
"id": "cascade",
"label": "Cascade"
}
]
},
{
"id": "getCascade",
"label": "Get Cascade",
"hasReturn": true
},
{
"id": "setCode",
"label": "Set Code",
"inputs": [
{
"id": "code",
"label": "Code"
}
]
},
{
"id": "getCode",
"label": "Get Code",
"hasReturn": true
}
]
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions components/bl-cascade-select/preview.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div data-module-type="system" data-module-id="block" data-display data-uid="97c1e475f636c6292fddcff60a990462" style="display:flex;flex-shrink:0;min-width:100px;border:2px solid #aaaaaa;flex-direction:row;justify-content:space-between;align-items:center;padding:10px 10px 10px 10px;border-radius:6px 6px 6px 6px;"><span data-content="Cascade Select" data-module-type="system" data-module-id="text" data-display data-uid="398503af28abc33291c6092d9661cbe5" class="bl-text" style="color:#aaaaaa;"></span><i data-icon="arrow_forward_ios" data-size="small" data-module-type="system" data-module-id="icon" data-display data-uid="57c3cba667be0bfe1fdeafc5362aa967" style="color:#aaaaaa;"></i></div>
116 changes: 116 additions & 0 deletions components/bl-cascade-select/src/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
export const validate = (cascade, setItemsCascade, setParentItems, setItems) => {
const { isCircular, cycleLocation } = analyzeCircularDependencies(cascade);

if (isCircular) {
throw new Error('cascade have cycling object in ' + cycleLocation);
}

if (cascade) {
setItemsCascade(prepareCascade(cascade, setParentItems, setItems));
}
};

function analyzeCircularDependencies(obj) {
const keys = [];
const stack = [];
const stackSet = new Set();
let isCircular = false;
let cycleLocation;

function detectCircular(obj, key) {
if (obj && typeof obj != 'object') {
return;
}

if (stackSet.has(obj)) {
cycleLocation = keys.join('.') + '.' + key;
isCircular = true;

return;
}

keys.push(key);
stack.push(obj);
stackSet.add(obj);

for (const k in obj) {
if (Object.prototype.hasOwnProperty.call(obj, k)) {
detectCircular(obj[k], k);
}
}

keys.pop();
stack.pop();
stackSet.delete(obj);
}

detectCircular(obj, 'obj');

return { isCircular, cycleLocation };
}

const prepareCascade = (cascade, setParentItems, setItems) => {
let levelOfNesting = 0;
const parentItems = [];
const items = [];

const prepare = cascade => {
const validCascade = cascade.map(item => {
let validItem = { ...item, levelOfNesting };

if (item.children) {
levelOfNesting++;
validItem = {
...validItem,
children: prepare(item.children),
};

parentItems.push({ code: item.code, isOpen: false, levelOfNesting });
} else {
items.push(validItem);
}

return validItem;
});

levelOfNesting--;

return validCascade;
};

const preparedCascade = prepare(cascade);

setParentItems(getNestedItems(parentItems, levelOfNesting));
setItems(items);

return preparedCascade;
};

const getNestedItems = (items, levelOfNesting) => {
const groupParentItems = [];

for (let i = 0; i <= -levelOfNesting; i++) {
groupParentItems.push(items.filter(({ levelOfNesting }) => levelOfNesting === i));
}

return groupParentItems;
};

export const openCascade = (state, item) => {
const currentParentItems = [...state];
const { code, levelOfNesting } = item;

for (let i = 0; i < currentParentItems[levelOfNesting].length; i++) {
const { code: parentItemCode, isOpen } = currentParentItems[levelOfNesting][i];

currentParentItems[levelOfNesting][i].isOpen = parentItemCode === code ? !isOpen : false;
}

return currentParentItems;
};

export const findParentItem = (parentItems, item) => {
const { levelOfNesting, code } = item;

return parentItems[levelOfNesting].find(item => item.code === code);
};
65 changes: 65 additions & 0 deletions components/bl-cascade-select/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useCallback, useEffect, useState } from 'react';

import { openCascade, validate } from './helpers';
import { Cascade, CollapseButtonIcon } from './subcomponent';

const { cn } = BackendlessUI.CSSUtils;

export default function CascadeSelect({ component, eventHandlers, elRef }) {
const { display, classList, style, cascade, placeholder } = component;
const { onClickItem } = eventHandlers;

const [itemsCascade, setItemsCascade] = useState();
const [parentItems, setParentItems] = useState([]);
const [items, setItems] = useState([]);
const [selected, setSelected] = useState({ name: placeholder });
const [isOpen, setIsOpen] = useState(false);

useEffect(() => {
component.setCascade(cascade);
}, [cascade]);

const openCascadeHandler = useCallback(item => {
setParentItems(state => openCascade(state, item));
}, []);

const openItemHandler = useCallback(item => {
setSelected(item);
setIsOpen(false);

onClickItem({ item });
}, []);

const onClickInput = () => setIsOpen(state => !state);

component.getSelected = () => selected;

component.setCode = code => setSelected(state => items.find(item => item.code === code) || state);
component.getCode = () => selected.code || '';

component.getCascade = () => itemsCascade;
component.setCascade = cascade => validate(cascade, setItemsCascade, setParentItems, setItems);

if (!display) {
return null;
}

return (
<div ref={ elRef } className={ cn('bl-cascade-select', classList) } style={ style }>
<div
className={ cn('cascade-select__input', { 'cascade-select__input--selected': selected.code }) }
onClick={ onClickInput }>
<span>{ selected.name }</span>
<CollapseButtonIcon/>
</div>
<Cascade
isOpen={ isOpen }
selected={ selected }
itemsCascade={ itemsCascade }
parentItems={ parentItems }
openCascadeHandler={ openCascadeHandler }
openItemHandler={ openItemHandler }
/>
</div>
);
}
Loading