Support Ukraine

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

concept art by television serial - tales from the loop

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.

Example of two groups with checkboxes
wix editor with two box groups

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-box class name:

<style>
/* Find all <span> element in ".content-box" elements */
.content-box 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-box"
-->
<p class="content-box">
  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-box".

So, in our task we need to do this two steps:

  1. Query select all needed elements by type
  2. 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

In Velo, we have an API for getting all elements on the page by type.

Velo API Reference:

To select by type, pass a selector string with the name of the type without the preceding `#` (e.g. "Button"). The function returns an array of the selected element objects. An array is returned even if one or no elements are selected.

For example, we can get a list of elements and manipulate this group's methods.

Select elements by type:

// Gets all buttons on the page
const buttonElements = $w('Button');
// Disable all buttons
buttonElements.disable();

// Gets all text elements on the page
const textElemets = $w('Text');
// Rewrite all text elements on the page
textElemets.text = 'Hello';

Each editor element has its own type. In Velo, API Reference documentation describes a small piece of the possible editor elements type. It's not a full list.

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;
  });
});
Live Demo:

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.

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.

JSDoc autocomplete and type checking

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! 👨‍💻 👩‍💻

Posts