1

So, the parent class Select declares this.elem as a DOM-element <select> and this.value, that links to a value of selected option

class Select  {

    constructor(classList, isTwoLevel, index){

        this.elem = document.createElement("select") 
        this.value = this.elem.children[this.elem.selectedIndex].value;// error here!   
    }
}

child class MySelect adds options, assigns values to them and appends them to this.elem.

class MySelect extends Select {

    constructor(){

        super();

        let opt1 = document.createElement("option");
        opt1.value = "foo";
        this.elem.appendChild(opt1);

        let opt2 = document.createElement("option");
        opt2.value = "bar";
        this.elem.appendChild(opt2);

    }
}

As expected, when creating a new exemplar of the MySelect class an error occurs:

let testSelect = new MySelect(); // Uncaught TypeError: Cannot read property 'value' of undefined

document.body.appendChild(testSelect.elem);

I don't want to move declaration of this.value to the child classes as it is supposed to be a universal properties for all the child classes, what should I do?

Igor Cheglakov
  • 525
  • 5
  • 11

3 Answers3

3

You could make value into a getter:

class Select  {

    constructor(classList, isTwoLevel, index){

        this.elem = document.createElement("select") 
    }
    get value() {
        return this.elem.children[this.elem.selectedIndex].value;
    }
}
class MySelect extends Select {

    constructor(){

        super();

        let opt1 = document.createElement("option");
        opt1.value = "foo";
        this.elem.appendChild(opt1);

        let opt2 = document.createElement("option");
        opt2.value = "bar";
        this.elem.appendChild(opt2);

    }
}

const testSelect = new MySelect();
document.body.appendChild(testSelect.elem);
console.log(testSelect.value);

You can also assign to a property directly on the instance the first time it's accessed, to improve performance, so that the getter only runs once:

class Select  {

    constructor(classList, isTwoLevel, index){

        this.elem = document.createElement("select") 
    }
    get value() {
        console.log('getter running');
        const theValue = this.elem.value;
        Object.defineProperty(this, 'value', { value: theValue });
        return theValue;
    }
}
class MySelect extends Select {

    constructor(){

        super();

        let opt1 = document.createElement("option");
        opt1.value = "foo";
        this.elem.appendChild(opt1);

        let opt2 = document.createElement("option");
        opt2.value = "bar";
        this.elem.appendChild(opt2);

    }
}

const testSelect = new MySelect();
document.body.appendChild(testSelect.elem);
console.log(testSelect.value);
console.log(testSelect.value);

You can also simplify

this.elem.children[this.elem.selectedIndex].value;

to

this.elem.value;

if you wanted. This also sidesteps the problem of a selectedIndex of -1 throwing an error (the value will be the empty string):

class Select  {

    constructor(classList, isTwoLevel, index){

        this.elem = document.createElement("select") 
    }
    get value() {
        console.log('getter running');
        const theValue = this.elem.value;
        Object.defineProperty(this, 'value', { value: theValue });
        return theValue;
    }
}
class MySelect extends Select {

    constructor(){

        super();

        let opt1 = document.createElement("option");
        opt1.value = "foo";
        this.elem.appendChild(opt1);

        let opt2 = document.createElement("option");
        opt2.value = "bar";
        this.elem.appendChild(opt2);

    }
}

const testSelect = new MySelect();
document.body.appendChild(testSelect.elem);
console.log(testSelect.value);
console.log(testSelect.value);
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • 1
    Good idea. @ Igor - Could still use a check to make sure `this.elem.selectedIndex` is >= 0 before trying to use it. – T.J. Crowder May 09 '19 at 06:49
  • Actually, there will be an issue if the dropdown selects a different option. The getter will still return the initialized value. Not the updated one. – RichS May 09 '19 at 07:32
  • Regarding your snippet with defining object property. From my understanding there is a `get value` getter, that creates a `value` property and after the first call the property is getting called instead of the getter. how it's so there's no conflict? – Igor Cheglakov May 09 '19 at 16:27
  • @IgorCheglakov Prototypal inheritance. The getter exists on the prototype; then, the `Object.defineProperty` puts the *plain value* on the *instance*. Further lookups of the property will try to retrieve the value from the instance before looking up the prototype chain. – CertainPerformance May 09 '19 at 20:53
  • @IgorCheglakov I'm not sure. This technique is similar to [efficient lazy evaluation](https://stackoverflow.com/questions/37977946/lazy-getter-doesnt-work-in-classes), though it's not that common, performance doesn't matter in 99% of cases – CertainPerformance May 09 '19 at 21:35
  • This may be helpful too: [get_Vs._defineProperty](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get#get_Vs._defineProperty) – RichS May 09 '19 at 23:10
1

These aren't declarations, they're just assignments.

If you want value at the parent level but don't have a value to assign to it until the child class has done its initialization, you can:

  1. Assign it null at the parent level and then assign it another value later, or

  2. Pass the options for the select into the parent constructor, or

  3. Make value an accessor property (a getter) as CertainPerformance pointed out.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
0

This is yet a slightly modified approach to the previous answers. If you pass in a cfg object into your classes, you can then choose what properties of the element you want bound. This snippet here has also added an event handler to demonstrate the binding of the value property.

The binding of properties was moved to it's own method.

Improvements to the code could include checking to see that the property on the element exists first.

class Select  {

    constructor(cfg){
        this.elem = document.createElement("select") 
        this.bindProps(cfg.bindProps)
    }
    
    bindProps(props){
      if( !props ) return;
      props.forEach((prop) => {
        Object.defineProperty(this, 'value', {
          get: () => this.elem[prop],
          set: (val) => this.elem[prop] = val
        })
      })
    }
}

class MySelect extends Select {

    constructor(cfg){

        super(cfg);
        
        let children = [
          { label: 'Foo', value: 'foo' },
          { label: 'Bar', value: 'bar' }
        ]
        children.forEach(({label, value}) => {
          let opt = document.createElement("option");
          opt.label = label;
          opt.value = value;
          this.elem.appendChild(opt);
        })
    }
}

const testSelect = new MySelect({bindProps: ['value']});
document.body.appendChild(testSelect.elem);

testSelect.elem.addEventListener('change', (evt) => {
  console.log('testSelect.elem.value', testSelect.elem.value);
  console.log('testSelect.value', testSelect.value);
})
RichS
  • 913
  • 4
  • 12