diff --git a/components/bl-cascade-select/README.md b/components/bl-cascade-select/README.md
new file mode 100644
index 000000000..07a865ccf
--- /dev/null
+++ b/components/bl-cascade-select/README.md
@@ -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}]` |
+| 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 |
+
+## Codeless Examples
+
+Addition of cascade data
+
+![](example-images/cascade_example.jpg)
+
+
+Try yourself
+
+```
+ AustraliaAU New South WalesAU-NSW SydneyAU-NSW-SY NewcastleAU-NSW-NC WollongongAU-NSW-WG QueenslandAU-QS BrisbaneAU-QS-BB TownsvilleAU-QS-TS CanadaCA QuebecCA-QB MontrealCA-QB-MR Quebec CityCA-QB-CBC OntarioCA-OT OttawaCA-OT-OW TorontoCA-OT-TR
+```
+
+
diff --git a/components/bl-cascade-select/component.json b/components/bl-cascade-select/component.json
new file mode 100644
index 000000000..4033118c4
--- /dev/null
+++ b/components/bl-cascade-select/component.json
@@ -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
+ }
+ ]
+}
diff --git a/components/bl-cascade-select/example-images/cascade_example.jpg b/components/bl-cascade-select/example-images/cascade_example.jpg
new file mode 100644
index 000000000..1e751c587
Binary files /dev/null and b/components/bl-cascade-select/example-images/cascade_example.jpg differ
diff --git a/components/bl-cascade-select/preview.html b/components/bl-cascade-select/preview.html
new file mode 100644
index 000000000..4dd31a181
--- /dev/null
+++ b/components/bl-cascade-select/preview.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/components/bl-cascade-select/src/helpers.js b/components/bl-cascade-select/src/helpers.js
new file mode 100644
index 000000000..f2aee761a
--- /dev/null
+++ b/components/bl-cascade-select/src/helpers.js
@@ -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);
+};
diff --git a/components/bl-cascade-select/src/index.js b/components/bl-cascade-select/src/index.js
new file mode 100644
index 000000000..9db2638ab
--- /dev/null
+++ b/components/bl-cascade-select/src/index.js
@@ -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 (
+
+
+ { selected.name }
+
+
+
+
+ );
+}
diff --git a/components/bl-cascade-select/src/subcomponent.js b/components/bl-cascade-select/src/subcomponent.js
new file mode 100644
index 000000000..69bdb105a
--- /dev/null
+++ b/components/bl-cascade-select/src/subcomponent.js
@@ -0,0 +1,86 @@
+import { findParentItem } from './helpers';
+
+const { cn } = BackendlessUI.CSSUtils;
+
+export function Cascade(props) {
+ const { isOpen, itemsCascade, parentItems, levelOfNesting, openItemHandler, openCascadeHandler, selected } = props;
+
+ if (!isOpen) {
+ return null;
+ }
+
+ return (
+
+ { itemsCascade.map(item => (
+
+ )) }
+
+ );
+}
+
+export function CascadeItem({ item, parentItems, openItemHandler, openCascadeHandler, selected }) {
+ const { name, levelOfNesting, children, code } = item;
+
+ if (children) {
+ const { isOpen } = findParentItem(parentItems, item);
+
+ return (
+
+ openCascadeHandler(item) }>
+ { name }
+
+
+
+ { isOpen && (
+
+ ) }
+
+ );
+ }
+
+ return (
+ openItemHandler(item) }>
+ { name }
+
+ );
+}
+
+export function CollapseButtonIcon() {
+ return (
+
+ );
+}
+
+function CollapseParentIcon() {
+ return (
+
+ );
+}
diff --git a/components/bl-cascade-select/styles/index.less b/components/bl-cascade-select/styles/index.less
new file mode 100644
index 000000000..7ee8e4bf4
--- /dev/null
+++ b/components/bl-cascade-select/styles/index.less
@@ -0,0 +1,134 @@
+@bl-cascadeSelect-component-themePrimary: @themePrimary;
+@bl-cascadeSelect-component-themeBackgroundColor: @appBackgroundColor;
+@bl-cascadeSelect-component-themeTextColor: @appTextColor;
+
+@bl-cascadeSelect-component-collapse-icon-size: 15px;
+
+@bl-cascadeSelect-component-input-icon-size: 20px;
+@bl-cascadeSelect-component-input-min-width: 200px;
+@bl-cascadeSelect-component-input-border-color: if(@isLightTheme, rgba(0, 0, 0, 0.12), rgba(255, 255, 255, 0.12));
+@bl-cascadeSelect-component-input-color: #6c757d;
+
+@bl-cascadeSelect-component-list-z-index: 1;
+@bl-cascadeSelect-component-list: if(
+ @isLightTheme,
+ @bl-cascadeSelect-component-themeBackgroundColor,
+ lighten(@bl-cascadeSelect-component-themeBackgroundColor, 5%)
+);
+
+@bl-cascadeSelect-component-item-backround-color: if(@isLightTheme, rgba(0, 0, 0, 0.12), rgba(255, 255, 255, 0.12));
+@bl-cascadeSelect-component-item-color-open: lighten(@bl-cascadeSelect-component-themePrimary, 10%);
+@bl-cascadeSelect-component-item-backround-color-open: if(
+ @isLightTheme,
+ lighten(@bl-cascadeSelect-component-themePrimary, 40%),
+ darken(@bl-cascadeSelect-component-themePrimary, 55%)
+);
+
+@bl-cascadeSelect-component-item-border-focus: if(
+ @isLightTheme,
+ lighten(@bl-cascadeSelect-component-themePrimary, 30%),
+ darken(@bl-cascadeSelect-component-themePrimary, 40%)
+);
+
+.bl-cascadeSelect-component {
+ position: relative;
+ user-select: none;
+
+ .cascade-select {
+ &__input {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ padding: 10px;
+
+ cursor: pointer;
+
+ border: 1px solid @bl-cascadeSelect-component-input-border-color;
+ border-radius: 6px;
+
+ background-color: @bl-cascadeSelect-component-themeBackgroundColor;
+ color: @bl-cascadeSelect-component-input-color;
+
+ transition: border-color 0.3s;
+
+ &:hover {
+ border-color: @bl-cascadeSelect-component-themePrimary;
+ }
+
+ &--selected {
+ color: @bl-cascadeSelect-component-themeTextColor;
+ }
+ }
+
+ &__list {
+ position: absolute;
+ top: 100%;
+ z-index: @bl-cascadeSelect-component-list-z-index;
+
+ min-width: 100%;
+
+ margin: 0;
+ padding: 10px 0;
+
+ list-style: none;
+
+ background: @bl-cascadeSelect-component-list;
+ border-radius: 6px;
+ box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px;
+
+ animation: 0.1s linear list-open;
+ }
+
+ @keyframes list-open {
+ from {
+ opacity: 0;
+ transform: scale(0.8);
+ }
+
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
+ }
+
+ &__item {
+ display: flex;
+ justify-content: space-between;
+ padding: 10px 17px;
+
+ transition: background-color, color 0.2s;
+
+ &:hover {
+ background-color: @bl-cascadeSelect-component-item-backround-color;
+ }
+
+ &.open {
+ background-color: @bl-cascadeSelect-component-item-backround-color-open;
+ color: @bl-cascadeSelect-component-item-color-open;
+ }
+
+ &:focus {
+ border: 2px solid @bl-cascadeSelect-component-item-border-focus;
+ }
+ }
+
+ &__collapse-input-icon {
+ width: @bl-cascadeSelect-component-input-icon-size;
+ height: @bl-cascadeSelect-component-input-icon-size;
+
+ fill: #aaa;
+
+ transform: rotate(-90deg);
+ }
+
+ &__collapse-icon {
+ height: @bl-cascadeSelect-component-collapse-icon-size;
+ width: @bl-cascadeSelect-component-collapse-icon-size;
+
+ fill: #aaa;
+
+ transform: rotate(-180deg);
+ }
+ }
+}