Velo by Wix: Query selector for child elements
Get the child elements inside a parent node. In this post, we take a look deeper at $w()
selector and try to filter children elements by the specific parent node
Let's suppose we have a few containers with checkboxes in each of them. We don't know how many checkboxes will be in each container at the final design.
Each container has a "select all" named checkbox that allows us to check on/off all checkboxes into a current target container.
We want to provide a good abstract solution for avoiding hard coding of elements' ID. And ensure scaling for any count of the elements in the future.
In general, we need to find a way to query select all child elements into a specific parent node. There are exist similar selectors in DOM and CSS.
CSS selector work from right to left
Take a look at the CSS works in this case. If you are familiar with CSS selectors, you can see that they work from right to left.
For example, we want to apply styles for all <span>
child elements into parent nodes that have .content
class name:
<style>
/* Find all <span> element in ".content" elements */
.content span {
font-weight: bold;
color: #2e7e8b;
}
</style>
<!--
Browser matches the selectors from right to left.
The first, browser find all <span> element on the page
then browser filter only <span> elements
that have a parent element with the class="content"
-->
<p class="content">
CSS selector work from <span>right</span> to <span>left</span>
</p>
Reslut:
CSS selector work from right to left
Under the hood, a browser uses the "from right to left" algorithm to select child elements. The browser literally starts to find all <span>
elements on the page and then the browser filters only the <span>
that have a parent node with class="content"
.
So, in our task we need to do this two steps:
- Query select all needed elements by type
- Filter only elements that have a parent element with a specific ID.
Thinking about it, let's try to reproduce this query selector for Velo.
Child selector for Velo
I propose using the "from right to left" search for our task. Especially we have all needed API for this.
In one of my previous posts, we solved a very similar issue. There we have created a tiny library for getting a parent Repeater element from repeated items.
Select elements by type
For example, we can get a list of elements and manipulate this group's methods.
Select elements by type:
Fortunately, all type definitions for Velo APIs are available on the open source. We can find it on the GitHub repository or npm package.
Full list of the possible Wix element types
declare type TypeNameToSdkType = {
AccountNavBar: $w.AccountNavBar;
Anchor: $w.Anchor;
Box: $w.Box;
Button: $w.Button;
Checkbox: $w.Checkbox;
CheckboxGroup: $w.CheckboxGroup;
Column: $w.Column;
ColumnStrip: $w.ColumnStrip;
Container: $w.Container;
DatePicker: $w.DatePicker;
Document: $w.Document;
Dropdown: $w.Dropdown;
Footer: $w.Footer;
Gallery: $w.Gallery;
GoogleMap: $w.GoogleMap;
Header: $w.Header;
HtmlComponent: $w.HtmlComponent;
IFrame: $w.IFrame;
Image: $w.Image;
MediaBox: $w.MediaBox;
Menu: $w.Menu;
MenuContainer: $w.MenuContainer;
Page: $w.Page;
QuickActionBar: $w.QuickActionBar;
RadioButtonGroup: $w.RadioButtonGroup;
Repeater: $w.Repeater;
Slide: $w.Slide;
Slideshow: $w.Slideshow;
Table: $w.Table;
Text: $w.Text;
TextBox: $w.TextBox;
TextInput: $w.TextInput;
UploadButton: $w.UploadButton;
VectorImage: $w.VectorImage;
Video: $w.Video;
VideoBox: $w.VideoBox;
AddressInput: $w.AddressInput;
AudioPlayer: $w.AudioPlayer;
Captcha: $w.Captcha;
Pagination: $w.Pagination;
ProgressBar: $w.ProgressBar;
RatingsDisplay: $w.RatingsDisplay;
RatingsInput: $w.RatingsInput;
RichTextBox: $w.RichTextBox;
Slider: $w.Slider;
Switch: $w.Switch;
TimePicker: $w.TimePicker;
VideoPlayer: $w.VideoPlayer;
};
Get the parent element and the parent's ID
We can get a parent element with the self-titled property. The top-level elements Page, Header, and Footer have no parent.
// Gets a checkbox's parent element
const parentElement = $w('#checkboxAllYellow').parent;
// Gets the parent element's ID
const parentId = parentElement.id; // "boxYellow"
Realization
I guess it's enough theory by now. Let's start to implement a selector function.
Our selector will have the next signature. findIn()
will be accepted a parent element ID and return an object with method all()
that accepts searched children elements type.
// Find in #boxYellow element all child elements with type `$w.Checkbox`
findIn('#boxYellow').all('Checkbox');
First, we get all child elements:
export const findIn = (selector) => {
return {
all(...type) {
/** @type {$w.Node[]} */
const elements = $w(type.join());
},
};
};
The $w(type.join())
selector returns an array of elements. We transform this array to an array with ID's. Then we create a new multiple select from a joined list of IDs.
export const findIn = (selector) => {
return {
all(...type) {
/** @type {$w.Node[]} */
const elements = $w(type.join());
// Gets an array with elements ID
// ids === [ "#checkbox1", "#checkbox2", …]
const ids = elements.reduce((acc, element) => {
// Add hash symbol
acc.push(`#${element.id}`);
return acc;
}, []);
// Creates a new multiple select from list of IDs
return $w(ids.join()); // $w("#checkbox1,#checkbox2,…")
},
};
};
I use the array method arr.reduce()
because we need to filter and transform array items.
For the filtering, we will use the helper function hasParent()
. In this function, we return true
if the element has a parent element with the needed ID or false
if all parents don't have this ID.
const hasParent = (element, parentId) => {
while (element) {
// On each iteration, we get a next parent element
element = element.parent;
if (element?.id === parentId) {
return true;
}
}
return false;
};
Add the condition to filter.
export const findIn = (selector) => {
// Removes a hash symbol at the selector start
// Because the `element.id` doesn't have a hash (#) symbol in value.
const parentId = selector.replace(/^#/, '');
return {
all(...type) {
/** @type {$w.Node[]} */
const elements = $w(type.join());
const ids = elements.reduce((acc, element) => {
// Add condition:
// if the element has a parent node with the needed ID
// then add it to the return result.
if (hasParent(element, parentId)) {
acc.push(`#${element.id}`);
}
return acc;
}, []);
return $w(ids.join());
},
};
};
We created a child selector. Let's see an example of using and live demo
An example of use. Child selector for two groups of checkboxes:
$w.onReady(() => {
$w('#checkboxAllYellow').onChange((event) => {
findIn('#boxYellow').all('Checkbox').checked = event.target.checked;
});
$w('#checkboxAllBlue').onChange((event) => {
findIn('#boxBlue').all('Checkbox').checked = event.target.checked;
});
});
JSDoc
Finally, I want to add the JSDoc to provide autocomplete and type checking. For this, we have a built-in types annotation in Velo editor.
PageElementsMap
- it's a object types of all elements on the current site page. Unfortunately, we can't use this type on the public files. Only on the page files.TypeNameToSdkType
- it's an object types for all elements type.
Add types annotation:
/**
* @param {keyof PageElementsMap} selector
*/
export const findIn = (selector) => {
const parentId = selector.replace(/^#/, '');
return {
/**
* @template {keyof TypeNameToSdkType} T
* @param {...T} type
* @returns {TypeNameToSdkType[T]}
*/
all(...type) {…},
};
};
Below is a video demonstrating the benefits of using JSDoc annotations.
Code Snippet
Here is a full snippet of the child query selector function with JSDoc types.
findIn(selector).all(...type)
/**
* @param {$w.Node} element
* @param {string} parentId
* @returns {boolean}
*/
const hasParent = (element, parentId) => {
while (element) {
element = element.parent;
if (element?.id === parentId) {
return true;
}
}
return false;
};
/**
* @param {keyof PageElementsMap} selector
*/
export const findIn = (selector) => {
const parentId = selector.replace(/^#/, '');
return {
/**
* @template {keyof TypeNameToSdkType} T
* @param {...T} type
* @returns {TypeNameToSdkType[T]}
*/
all(...type) {
/** @type {$w.Node[]} */
const elements = $w(type.join());
const ids = elements.reduce((acc, element) => {
if (hasParent(element, parentId)) {
acc.push(`#${element.id}`);
}
return acc;
}, []);
return $w(ids.join());
},
};
};
Example of use
$w.onReady(() => {
$w('#checkboxAllYellow').onChange((event) => {
findIn('#boxYellow').all('Checkbox').checked = event.target.checked;
});
$w('#checkboxAllBlue').onChange((event) => {
findIn('#boxBlue').all('Checkbox').checked = event.target.checked;
});
});
If you have any questions, feel free to ask me on my Twitter. Cheers! 👨💻 👩💻