最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How to handle and pass events to the lit component from defining component - Stack Overflow

programmeradmin3浏览0评论

I am trying to design web ponents using lit Element and I need an help regarding the events. As we can see from the attached snippet we could use @change="${this.handleEvent}" in html template and handle the function handleEvent(e){} inside the lit Element ponent. By this the events are limited and controlled only in the lit web ponents.

However, when other people use our web ponents they don't have much control or access to the values over these events in the defining ponent. For instance, if we have an index.html file and I use it like <my-element onchange="handleEvent(e)"></my-element>, I should have the access to the onchange event and call a function inside the index file only.

So, Is there a way to achieve this to the similar behavior of regular html events instead of writing the events which is limited in the lit Element web ponents.

<script src="/@webponents/webponentsjs@latest/webponents-loader.js"></script>
<script type="module">
  import { LitElement, html, css } from '.js?module';
  
  class MyElement extends LitElement {
  
   static get properties() {
      return {
        checked:  { type: Boolean, attribute: true }
      };
    }
    
    static get styles() {
      return [
        css`
          div {
            padding: 10px;
            width: 90px;
            border: 2px solid orange;
          }
        `
      ];
    }
    
    render() {
      return html`
      <div>
         <input
            @change="${this.handleEvent}"
            ?checked="${this.checked}"
            type="checkbox" /> Checkbox
       </div>
      `;
    }
    
    handleEvent(e) {
      console.log(`Checkbox marked as: ${e.target.checked}`);
    }
  }
  customElements.define('my-element', MyElement);
</script>

// Index.html
<my-element></my-element> 
// I am expecting to pass an event and handle it in the importing 
ponent. 
// Something like: **<my-element onchange="handleEvent(e)"}></my- 
element>**

I am trying to design web ponents using lit Element and I need an help regarding the events. As we can see from the attached snippet we could use @change="${this.handleEvent}" in html template and handle the function handleEvent(e){} inside the lit Element ponent. By this the events are limited and controlled only in the lit web ponents.

However, when other people use our web ponents they don't have much control or access to the values over these events in the defining ponent. For instance, if we have an index.html file and I use it like <my-element onchange="handleEvent(e)"></my-element>, I should have the access to the onchange event and call a function inside the index file only.

So, Is there a way to achieve this to the similar behavior of regular html events instead of writing the events which is limited in the lit Element web ponents.

<script src="https://unpkg./@webponents/webponentsjs@latest/webponents-loader.js"></script>
<script type="module">
  import { LitElement, html, css } from 'https://unpkg./lit-element/lit-element.js?module';
  
  class MyElement extends LitElement {
  
   static get properties() {
      return {
        checked:  { type: Boolean, attribute: true }
      };
    }
    
    static get styles() {
      return [
        css`
          div {
            padding: 10px;
            width: 90px;
            border: 2px solid orange;
          }
        `
      ];
    }
    
    render() {
      return html`
      <div>
         <input
            @change="${this.handleEvent}"
            ?checked="${this.checked}"
            type="checkbox" /> Checkbox
       </div>
      `;
    }
    
    handleEvent(e) {
      console.log(`Checkbox marked as: ${e.target.checked}`);
    }
  }
  customElements.define('my-element', MyElement);
</script>

// Index.html
<my-element></my-element> 
// I am expecting to pass an event and handle it in the importing 
ponent. 
// Something like: **<my-element onchange="handleEvent(e)"}></my- 
element>**

Share Improve this question edited Oct 28, 2021 at 7:59 Onera asked Oct 27, 2021 at 21:24 OneraOnera 7174 gold badges16 silver badges36 bronze badges 0
Add a ment  | 

3 Answers 3

Reset to default 6

If you want to listen to events outside your element, you should dispatch an event like this:

const event = new Event('my-event', {bubbles: true, posed: true});
myElement.dispatchEvent(event);

The Lit documentation on events gives a good overview of how and when to dispatch events https://lit.dev/docs/ponents/events/#dispatching-events

Usually with your own custom elements you'll probably also want to define your own API to offer that to consumers of your ponents. That often also es with defining your own events which your ponent emits.

See this simple (non-lit, but you get the idea) example:

customElements.define('foo-bar', class extends HTMLElement {
  input = document.createElement('input');
  constructor() {
    super();
    this.input.type = 'checkbox';
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.appendChild(this.input);
    this.input.addEventListener('change', this.handleChange.bind(this));
  }
  // replaces the internal change event with an event you publish to the outside world
  // this event is part of your defined API.
  handleChange(e) {
    e.stopPropagation();
    const stateChange = new CustomEvent('state-change', { 
      bubbles: true, 
      posed: true,
      detail: { checked: this.input.checked }
    });
    this.dispatchEvent(stateChange);
  }
});

document.addEventListener('state-change', (e) => { console.log(e.detail); })
<foo-bar></foo-bar>

If you also want to support declarative event binding as the standard HTML elements do (and using which is widely considered bad practice), you can achieve that using observed attributes:

customElements.define('foo-bar', class extends HTMLElement {
  input = document.createElement('input');
  constructor() {
    super();
    this.input.type = 'checkbox';
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.appendChild(this.input);
    this.input.addEventListener('change', this.handleChange.bind(this));
    this.declarativeValueChangeListener = this.declarativeValueChangeListener.bind(this);
  }
  // replaces the internal change event with an event you publish to the outside world
  // this event is part of your defined API.
  handleChange(e) {
    e.stopPropagation();
    const stateChange = new CustomEvent('state-change', { 
      bubbles: true, 
      posed: true,
      detail: { value: this.input.value }
    });
    this.dispatchEvent(stateChange);
  }
  
  static get observedAttributes() { 
    return [ 'onstatechange' ];
  }
  
  attributeChangedCallback(attr, oldVal, newVal) {
    if (oldVal === newVal) return; // nothing changed, nothing to do here
    if (newVal === null) { // attribute was removed
      this.removeEventListener('state-change', this.declarativeValueChangeListener)
    } else { // attribute was added
      this.addEventListener('state-change', this.declarativeValueChangeListener)
    }
  }
  
  declarativeValueChangeListener() {
    const functionStr = this.getAttribute(this.constructor.observedAttributes[0]);
    eval(functionStr);
  }
});


function baz() { console.log('baz executed through declarative binding of an outside handler!'); }
<foo-bar onstatechange="baz()"></foo-bar>

I don't understand why you need Events,
when a click on the input can execute either global Functions or local Methods.

No need for oldskool bind mumbo-jumbo, no need for observedAttributes

You can use the default onchange, because all Events exist on HTMLElement
Only inputs like textarea actually fire the Change Event

Used on any other HTMLElement nothing happens, you have to call this.onchange() or document.querySelector("foo-bar").onchange() yourself.
You can not do that with your own attribute names, as those values will always be a String, and not parsed as JS code by the Browser engine.

You need eval(code) though to execute the code within Component scope and make onchange="this.baz()" work.

customElements.define('foo-bar', class extends HTMLElement {
  constructor() {
    let input = document.createElement("input");
    input.type = "checkbox";

    super()
      .attachShadow({mode: 'open'}) 
      .append(input,"click me to execute ", this.getAttribute("onchange"));

    this.onclick = (evt) => { // maybe you only want input.onclick
      //evt.stopPropagation();
      let code = this.getAttribute("onchange");
      try {
        eval(code); // this.onchange() will only execute global functions
      } catch (e) {
        console.error(e,code);
      }
    }
  }
  baz() {
    console.log("Executed baz Method");
  }
});

function baz() {
  console.log("Executed baz Function");
}
<foo-bar onchange="baz()"></foo-bar>
<foo-bar onchange="this.baz()"></foo-bar>

<style>
 foo-bar { display:block; zoom:2 }
</style>

Important note

shadowDOM is what saves your ass here.

The onchange Event from the input does not escape shadowDOM
(Like posed:true does on Custom Events)

Without shadowDOM all onchange declarations on parent Elements will fire, because the Event bubbles:

<div onchange="console.log(666)">
  <input onchange="console.log(this)" type="checkbox">
</div>

<style>
  input { zoom:3 }
</style>

This is a good example where a shadowRoot on a regular HTMLElement can have value, without declaring a Custom Element

发布评论

评论列表(0)

  1. 暂无评论