Encasing a drop-down menu within a personalized container

I am looking to create a custom HTML element that mimics the behavior of the native <select> element but also triggers a specific update function whenever an attribute or child node is modified. This is essential for incorporating the bootstrap-select element within the Elm framework. For more context, you can refer to bootstrap-select and my previous inquiry on Stack Overflow.

Using the LitElement framework, I managed to construct a functional custom element called <lit-select> with similar functionality as described above. However, I encountered a limitation where it does not support nested html <option> or <optgroup> elements as children. Instead, users need to provide the option list as a JSON encoded string within a specific attribute.

Essentially, rather than using

<lit-select>
  <option>foo</option>
  <option>bar</option>
</lit-select>

users have to utilize

<lit-select items='["foo", "bar"]'></lit-select>

How should I modify the definition of <lit-select> to enable the former approach? While I am familiar with the <slot> element, it cannot be used within <select> tags as it gets stripped by the browser.

Thank you in advance!


Update 1

There are additional constraints complicating the issue:

  • Avoiding shadow DOM is necessary as my custom element needs to be styled/enhanced by bootstrap (including bootstrap-select) CSS/JS, which only interacts with the regular DOM. This means slot elements are not viable due to their association with shadow DOM.
  • The custom element must be fully responsive to changes such as adding/removing child nodes and modifying attributes. This adaptability is crucial for integrating the element into virtual DOM environments like Elm and React.

Appendix

Here is the current definition of <lit-select>:

import { LitElement, html, customElement, property } from 'lit-element';
import * as $ from 'jquery';
import 'bootstrap';
import 'bootstrap-select';

@customElement('lit-select')
export class LitSelect extends LitElement {

    @property({ type : Array }) items = []

    updated() {
        $(this).find(".selectpicker").selectpicker('refresh');
    }

    createRenderRoot() {
        return this;
    }

    private renderItem(item: string) {
        return html`
            <option>
                ${item}
            </option>
        `;
    }

    render() {
        return html`
            <select class="selectpicker" data-live-search = "true">
                ${this.items.map(item => this.renderItem(item))}
            </select>
        `;
    }
}

Answer №1

Why not begin by utilizing a Native W3C standard Custom Element

All that needs to be done is to rearrange some <option> elements.

For more information, make sure to check out: Unbelievable - the connectedCallback has no DOM?!?

<template id=MY-SELECT>
  <h3>My Never Todo List</h3>
  <select multiple></select>
</template>

<my-select name="NTD">
  <option>Grow up</option>
  <option>Learn React</option>
  <option>Use Lit</option>
  <option>Forget W3C Custom Elements API</option>
</my-select>

<script>
  customElements.define("my-select", class extends HTMLElement {
    static get observedAttributes() {
      return ["name"]; //use one to trigger attributeChangedCallback
    }
    connectedCallback() {
      console.log('connected');
      //clone template
      this.append(document.getElementById(this.nodeName).content.cloneNode(true));
      //MOVE options inside SELECT:
      this.querySelector('select').append(...this.querySelectorAll('option'));
    }
    attributeChangedCallback() {
      setTimeout(() => this.updated(...arguments))// execute after Event Loop is done
    }
    updated(name,oldValue,newValue){
      console.log('updated',name,oldValue,newValue);
    }
  })

</script>

Answer №2

What are your thoughts on this solution?

import {LitElement, html } from 'lit-element';


export class GraniteSelect extends LitElement {

  static get properties() {
    return {
      options: {
        type: Object,
      }
    }
  }

  constructor() {
    super();
    this.options =  [];
  }

  connectedCallback() {
    super.connectedCallback();

    this.observer = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        mutation.removedNodes.forEach((node) => {
          if (node.nodeName === 'OPTION' ) {
            this.options = [ ...this.options, node ];
           console.log(`options - ${this.options}`);
           console.dir(this.options);
          }
        });
      });
    });
    this.observer.observe(this, {
        childList: true,
    });
  }

  firstUpdate() {
    this.observer.disconnect();
  }

  createRenderRoot() {
  /**
   * Render template without shadow DOM. Note that shadow DOM features like 
   * encapsulated CSS and slots are unavailable.
   */
    return this;
  }


  render() {
    console.log('Rendering', this.options);
    return html`
      <select class="selectpicker" data-live-search = "true">
          ${this.options}
      </select>
    `;
  }
}

window.customElements.define('granite-select', GraniteSelect);

This approach utilizes light-dom (the createShadowRoot section), placing all the option elements inside the select element of your component. Any non-option children are disregarded but can be handled as needed.

For a complete demonstration, check out https://stackblitz.com/edit/js-14dcae

How do you feel about using this method?

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Having issues with django-autocomplete-light triggering JavaScript errors

My implementation of django-autocomplete-light is causing some issues with rendering autocomplete options. There is a section on the website where it functions perfectly, but in another section, it only works partially. The autocomplete options display c ...

Troubleshooting NodeJS CORS issue with heavy requests for file uploads

I'm currently working on a project that involves an Angular front end and a NodeJS API deployed in production using AWS services like S3 and Elastic Beanstalk. When attempting to upload images, I encounter a CORS error if the image is too large or wh ...

My picture is refusing to load... Why does it keep saying "image not found"? Any thoughts on why this might be

I've been trying to display a picture of myself on my html canvas, the image is stored in the correct folder. However, I keep encountering a strange error (shown above) and I can't seem to figure out what's causing it. If you have any insigh ...

Bringing in a feature within the Vue 3 setup

At the moment, I am attempting to utilize a throttle/debounce function within my Vue component. However, each time it is invoked, an error of Uncaught TypeError: functionTD is not a function is thrown. Below is the code snippet: useThrottleDebounce.ts imp ...

Load a webpage with two dropdown menus in JavaScript/jQuery that have the same selection option pre-selected

I am facing an issue with two dropdown menus that should display the same value when the page loads. These dropdowns are used for filtering products on a WooCommerce website based on the selected vehicle. Initially, the user selects the Make, Model, and Se ...

I'm trying to figure out the best way to successfully pass a prop to another component in TypeScript without running into the frustrating issue of not being able to

I have been facing an issue while trying to pass a prop from a custom object I have defined. The structure of the object is as follows: export type CustomObjectType = { data?: DataObject }; export type DataObject = { id: number; name: stri ...

SwipeJS experiencing technical difficulties

My Swipe.Js version : "^7.0.2" Today, I attempted to use Swipe.Js and encountered an issue with my import code. import { Swiper, SwiperSlide } from 'swiper/react'; as described on https://swiperjs.com/react#installation. However, when ...

Deleting the main node in a JSON file containing multiple values

I am working with a JSON data stored in a variable. Here is the JSON Data: var employees = {"emp":{{"firstName":"John"},{"secondName":"John"}}}; My goal is to remove the emp node from the above JSON Data and have it as follows: {{"firstName":"John"},{" ...

Failure to send Websocket connection

Currently working on PHP, here's a snippet: $room_array = array(); $room_array[0] = 'room-list'; $room_array['info'] = array('room_name' => $room['room_name'], 'owner' => $username['usernam ...

Angular: the xhrRequest is failing to be sent

I am facing an issue with a service function that handles an HTTP post request. The request does not get sent for some reason. However, when I add a subscription to the post method, the request is successfully executed. In other services that have the sam ...

The absence of a flickering flame is noticeable in the THREE.js environment

I have been working on creating a flame using THREE.js and spark.js. However, even after rendering the world, I am unable to see the flame and it seems like the world is empty. Although I checked the console for errors, there are no indications of what mig ...

The error message "Property 'DecalGeometry' is not found in the type 'typeof "..node_modules/@types/three/index"'."

Within my Angular6 application, I am utilizing 'three.js' and 'three-decal-geometry'. Here is a snippet of the necessary imports: import * as THREE from 'three'; import * as OBJLoader from 'three-obj-loader'; import ...

Incorporate VLC player into a webpage without any visible control options

Is there a way to embed a flash video in a webpage without showing any controls? I managed to embed a flash video using VLC with the following code: <embed src="img/Wildlife.wmv" height="480" width="640"> However, I want the video to play without ...

Having difficulty displaying a partial view within a view while making an AJAX call

Trying to figure out what's missing in my code. I have a view with some radio buttons and I want to display a different partial view when a radio button is selected. Here's the snippet of my code: Controller public ActionResult Method(string va ...

Tips for preloading an ENTIRE webpage

I am looking for a way to preload an entire web page using JavaScript in order to have it cached in the user's browser. While I know how to preload images with JS, my goal is to also preload the entire page itself. For example, on my website, there ...

How to maintain the focus within a jQuery dialog box

Exploring the world of jQuery dialog, I'm eager to incorporate it into my latest side project. My goal is to enhance accessibility by adding tabindex to the divs within the dialog for easy tab navigation. However, I encountered an issue where the focu ...

Encountering an error of TypeError while attempting to generate a new GraphQL

Currently using Apollo-Server/TypeScript with graphql-tool's makeExecutableSchema() to set up schema/directives. Encountering an error while attempting to add a basic GraphQL Directive: TypeError: Class constructor SchemaDirectiveVisitor cannot be in ...

What is the process of using a For loop to output a string in reverse order?

I'm attempting to reverse the string "hello" using a For loop, aiming for the output of "olleh". However, I'm facing an issue where the last character in the string is not being removed after being added to the array. Consequently, only the last ...

What's causing the member to be undefined?

client.on('raw', (e) => { if (e.t === 'MESSAGE_REACTION_ADD' && e.d.message_id === '813150433651851265' && e.d.emoji.name === "✅" ) { const guild = client.guilds.cache.get(e.d.gui ...

Find the highest value in a MySQL column

Currently, I am working with a mysql table in NodeJs where there is a column called packageId. My goal is to fetch the highest value from that particular column. For instance, if the values in the column are 2,3,4,5, I only want to retrieve 5. I attempted ...