Trying to propagate an event to a custom HTML element from one of its components throws

Viewed 155

I'm trying to make a control allowing the user to set a value both through a numeric input and through a slider. Having read the relevant docs, this is what I came with:

class NumberAndRange extends HTMLElement {
    constructor() {
        super()
        const shadow = this.attachShadow({mode: 'open'})
  
        this.number = document.createElement('input')
        this.number.type = 'number'
        this.range = document.createElement('input')
        this.range.type = 'range'
  
        this.number.addEventListener('input',
            ()=>{this.range.value = this.number.value}
        )
        this.range.addEventListener('input',
            ()=>{this.number.value = this.range.value}
        )
  
        shadow.appendChild(this.number)
        shadow.appendChild(this.range)
    }
}
customElements.define('number-and-range', NumberAndRange)
<number-and-range></number-and-range>

As you can see, it works (at least in browsers that support this). For brevity, I skipped the code that makes sure that setting the min, max, step etc attributes on numeric-and-range also sets them on both inputs and that reading these attributes from numeric-and-range returns meaningful results.

However, there is one functionality I'm not able to add. Clearly, we would like that triggering an input event on both numeric and range would also trigger this event on numeric-and-range; so that we may forget that numeric-and-range actually consists of two inputs and instead treat it as if it was a single control. Therefore:

class NumberAndRange extends HTMLElement {
    constructor() {
        super()
        const shadow = this.attachShadow({mode: 'open'})
  
        this.number = document.createElement('input')
        this.number.type = 'number'
        this.range = document.createElement('input')
        this.range.type = 'range'
  
        this.number.addEventListener('input',
            ()=>{this.range.value = this.number.value}
        )
        this.range.addEventListener('input',
            ()=>{this.number.value = this.range.value}
        )
    
        const whole = this
        this.number.addEventListener('input',
            (ev)=>{whole.dispatchEvent(ev)}
        )
        this.range.addEventListener('input',
            (ev)=>{whole.dispatchEvent(ev)}
        )
  
        shadow.appendChild(this.number)
        shadow.appendChild(this.range)
    }
}
customElements.define('number-and-range', NumberAndRange)
<number-and-range></number-and-range>

But this doesn't work! For reasons beyond my coprehension moving the slider throws. InvalidStateError: An attempt was made to use an object that is not, or is no longer, usable Which object is not usable? And why would it not be usable? I'm staring at my code and can't really find any mistake.

Could you enlighten me what am I doing wrong?

Edit: The idea suggested in the comments doesn't seem to work for me either:

class NumberAndRange extends HTMLElement {
    connectedCallback() {
        const whole = this
        this.number.addEventListener('input',
            (ev)=>{whole.dispatchEvent(ev)}
        )
        this.range.addEventListener('input',
            (ev)=>{whole.dispatchEvent(ev)}
        )
    }

    constructor() {
        super()
        const shadow = this.attachShadow({mode: 'open'})
  
        this.number = document.createElement('input')
        this.number.type = 'numeric'
        this.range = document.createElement('input')
        this.range.type = 'range'
  
        this.number.addEventListener('input',
            ()=>{this.range.value = this.number.value}
        )
        this.range.addEventListener('input',
            ()=>{this.number.value = this.range.value}
        )
  
        shadow.appendChild(this.number)
        shadow.appendChild(this.range)
    }
}
customElements.define('number-and-range', NumberAndRange)
<number-and-range></number-and-range>

2 Answers

Thanks to @Pavlo for pointing out the problem: events cannot be redispatched. Therefore the solution is to create a new event instead:

class NumberAndRange extends HTMLElement {
    // Handling of the value attribute should probably be done better
    // quick and dirty here
    get value() {return this.number.value}

    constructor() {
        super()
        const shadow = this.attachShadow({mode: 'open'})
  
        this.number = document.createElement('input')
        this.number.type = 'number'
        this.range = document.createElement('input')
        this.range.type = 'range'
  
        this.number.addEventListener('input',
            ()=>{this.range.value = this.number.value}
        )
        this.range.addEventListener('input',
            ()=>{this.number.value = this.range.value}
        )
    
        const whole = this
        this.number.addEventListener('input',
            ()=>{whole.dispatchEvent(new Event('input'))}
        )
        this.range.addEventListener('input',
            ()=>{whole.dispatchEvent(new Event('input'))}
        )
  
        shadow.appendChild(this.number)
        shadow.appendChild(this.range)
    }
}
customElements.define('number-and-range', NumberAndRange)
<number-and-range oninput="console.log(this.value)"></number-and-range>

You already found your problem that Events can't be re-dispatched.

But you don't need to dispatch any Custom Event, use the default oninput event

http://jsfiddle.net/dannye/6zk4vgc0

class NumberAndRange extends HTMLElement {
  connectedCallback() {
    let input = ['number','range'].map(
      (type, idx) => 
      Object.assign(
        this.appendChild(document.createElement('input')),
        { 
          type, 
          min : 20,
          max : 50,
          value : 42,
          oninput : _ => input[~~!idx].value = input[idx].value
        }))
  }
}
customElements.define('number-and-range', NumberAndRange);

note: ~~!idx is the same as idx == 0 ? 1 : 0, just some bytes shorter

Related