-
Notifications
You must be signed in to change notification settings - Fork 7
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
base: main
Are you sure you want to change the base?
Changes from 9 commits
fba6398
c374578
8314f6e
3bf0e3e
022fb51
95a4a7d
c1ffbc2
17372be
998d639
adbe54c
b5e1786
dc27d5d
557a11b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
# 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}]` | | ||
| 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 | | ||
|
||
## <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> | ||
v-excelsior marked this conversation as resolved.
Show resolved
Hide resolved
|
||
``` | ||
</details> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
{ | ||
"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 | ||
} | ||
] | ||
} |
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> |
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) { | ||
Valodya marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const keys = []; | ||
const stack = []; | ||
const stackSet = new Set(); | ||
let isCircular = false; | ||
let cycleLocation; | ||
|
||
function detect(obj, key) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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)) { | ||
detect(obj[k], k); | ||
} | ||
} | ||
|
||
keys.pop(); | ||
stack.pop(); | ||
stackSet.delete(obj); | ||
} | ||
|
||
detect(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--; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could you please explain the logic around if it needs to calculate nesting depth would not it be better to pass a new level into the
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As far as I understand, with this implementation, we will not be able to get the last depth value when this recursion ends. It's needed to get max depth There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do you need to know about the max depth? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. get sorted and separated array of |
||
|
||
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in order to avoid modifying the entire state each time I can propose the following approach:
|
||
} | ||
|
||
return currentParentItems; | ||
}; | ||
|
||
export const findParentItem = (parentItems, item) => { | ||
const { levelOfNesting, code } = item; | ||
|
||
return parentItems[levelOfNesting].find(item => item.code === code); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. to avoid reverse find you can build you data structure with the following way:
where There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it a cyclic object? Isn't it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. Is it problem? |
||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import { useState, useEffect, useCallback, useRef } from 'react'; | ||
import { CollapseButtonIcon, Cascade } from './subcomponent'; | ||
import { openCascade, validate } from './helpers'; | ||
|
||
const { cn } = BackendlessUI.CSSUtils; | ||
|
||
export default function CascadeSelect({ component, eventHandlers }) { | ||
const { display, classList, style, cascade, placeholder } = component; | ||
const { onClickItem } = eventHandlers; | ||
|
||
const [itemsCascade, setItemsCascade] = useState(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what's the difference between them?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I still do not understand why we need 3 lists instead of a single one There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. because it's easy to find items in an array, instead of an array of objects that have in turn, have their own array of objects There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. parent reference should solve that problem |
||
const [parentItems, setParentItems] = useState([]); | ||
const [items, setItems] = useState([]); | ||
const [selected, setSelected] = useState({ name: placeholder }); | ||
const [isOpen, setIsOpen] = useState(false); | ||
|
||
const cascadeSelectRef = useRef(); | ||
|
||
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; | ||
Valodya marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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); | ||
|
||
useEffect(() => component.el = cascadeSelectRef.current, []); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use |
||
|
||
if (!display) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<div ref={ cascadeSelectRef } className={ cn('bl-cascadeSelect-component', ...classList) } style={ style }> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
<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> | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about add an actions same as for default select?
Set Options
Get Options
Set Value
Get Value
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please describe these actions here in the doc