0

Trying to bind the validate() function on blur of input as declared inside render function of class Textbox extends HTMLElement (Part-A), but getting the below error

detail.html:1 Uncaught TypeError: this.validate is not a function (but the validate() function is accessible when placing outside the call of the function outside string)

Part - A

class HTMLElement {
  constructor( ) {
    this.value = false;
  }

  validate() {
    console.log('A')
  }

}

class Textbox extends HTMLElement {
  constructor( ) {
    super();
  }

  render( elem_id, default_value, display_name, placeholder, permission ) {
    /*** this.validate() will work here but not inside onblur function ***/
    var string = "";
    string += "<input type='text' class='form-control' id='"+elem_id+"' value='"+default_value+"' placeholder='" + placeholder + "' onblur='this.validate()' >"
    return string;
  }

}

Part B

const ELEMENT_ARR = {
  "text":Textbox
}

class Elements {
  constructor( className, opts ) {
    return new ELEMENT_ARR[className](opts);
  }
}

function renderForm(){
  var ds = {
    "fields":[
      {"id":"f_name","type":"text","is_mandatory":1,"display_name":"First Name","default":"", "permission":"hidden/editable/readonly","placeholder":"Sample Placeholder 1" },
      {"id":"f_name","type":"text","is_mandatory":0,"display_name":"Hellp Name","default":"","permission":"hidden/editable/readonly", "placeholder":"Sample Placeholder 2"  }
    ]
  }, ks = JSON.parse(JSON.stringify(ds)), form_str="", elementor;

  for (let u = 0 ; u < ks['fields'].length; u++) {

    let type = ks['fields'][u].type,
        elem_id = ks['fields'][u].id,
        is_mandatory = ks['fields'][u].is_mandatory,
        default_value = ks['fields'][u].default,
        display_name = ks['fields'][u].display_name,
        placeholder = ks['fields'][u].placeholder,
        permission = ks['fields'][u].permission;

    elementor = new Elements( type )
    form_str += elementor.render( elem_id, default_value, display_name, placeholder, permission )
  }
  return form_str;
}

Part - C

window.onload = function(){

  document.getElementById('formGenx').innerHTML = renderForm();
};

What possibly wrong I am doing?

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
MD Danish
  • 179
  • 1
  • 10
  • This is but one of the reasons for my extreme dislike of creating HTML as string in JS. The problem is that `this.validate` *doesn't run in context of your class*. It will run in the global context. Remember, you're just creating text here, there is nothing to link that text to actual object instance. – VLAZ Jan 28 '20 at 13:05
  • Thanks VLAZ, Trying to create a generic form. What would you recommend? (CreateElement??) – MD Danish Jan 28 '20 at 13:13
  • Yes, programmatically create the elements. I'm drafing an answer right now. Basically strings = bad (usually) because they might work *for now* but then you run into something like this or any number of other problems and you backtrack quite a bit to try and fix the entire mess because at that point, it *is* a mess. – VLAZ Jan 28 '20 at 13:24
  • `HTMLElement` is core class in Javascript. When you run the code that core js class may be extended. Maybe that is the reason why can't you access the `validate` method. So, try with some other class name. – Gokulakannan T Jan 28 '20 at 13:53

1 Answers1

2

The problem is that you're generating a string then appending it to the DOM and only at that point it will be parsed into a DOM element and added to the page. This happens outside the context of your render method and has no link to either it or the object that created it. Moreover, the event handler is just set to the string "this.validate()" which will be executed in the global context.

To retain access to your validate method you have to attach it as a handler at the time render executes. The best way to go about this, in my opinion, is to just avoid the step where you generate a string and programmatically create the element:

This will generate the elements, you can then accumulate them in a DocumentFragment before adding them all at once to the DOM which will save you multiple insertions and potential performance hit because of triggering reflow.

Finally, the elements can be added via the normal DOM API but usually .appendChild is enough.

So, we get the following:

class HTMLElement {
  constructor( ) {
    this.value = false;
  }

  validate() {
    console.log('A')
  }

}

class Textbox extends HTMLElement {
  constructor( ) {
    super();
  }

  render( elem_id, default_value, display_name, placeholder, permission ) {
    /*** programmatically create the input element and set all relevant attributes ***/
    var element = document.createElement("input");
    
    element.setAttribute("type", "text");
    element.classList.add("form-control");
    element.setAttribute("id", elem_id);
    element.setAttribute("value", default_value);
    element.setAttribute("placeholder", placeholder);
    element.addEventListener("blur", () => this.validate())

    return element;
  }
}

const ELEMENT_ARR = {
  "text":Textbox
}

class Elements {
  constructor( className, opts ) {
    return new ELEMENT_ARR[className](opts);
  }
}

function renderForm(){
  var ds = {
    "fields":[
      {"id":"f_name","type":"text","is_mandatory":1,"display_name":"First Name","default":"", "permission":"hidden/editable/readonly","placeholder":"Sample Placeholder 1" },
      {"id":"f_name","type":"text","is_mandatory":0,"display_name":"Hellp Name","default":"","permission":"hidden/editable/readonly", "placeholder":"Sample Placeholder 2"  }
    ]
  }, 
    ks = JSON.parse(JSON.stringify(ds)), 
    //just a "container" for the elements. 
    //can be cahged to document.createElement("form") if needed
    form = document.createDocumentFragment(),
    elementor;

  for (let u = 0 ; u < ks['fields'].length; u++) {

    let type = ks['fields'][u].type,
        elem_id = ks['fields'][u].id,
        is_mandatory = ks['fields'][u].is_mandatory,
        default_value = ks['fields'][u].default,
        display_name = ks['fields'][u].display_name,
        placeholder = ks['fields'][u].placeholder,
        permission = ks['fields'][u].permission;

    elementor = new Elements( type );
    
    let newElement = elementor.render( elem_id, default_value, display_name, placeholder, permission );
    form.appendChild(newElement);//append as DOM elements
  }
  return form;
}

window.onload = function(){
  document.getElementById('formGenx')
    .appendChild(renderForm()); //append as DOM element
};
.form-control {
  margin: 5px;
  border-style: dashed;
  border-color: black
}
<div id="formGenx"></div>

The normal JavaScript API tends to be verbose when programmatically creating an element. Libraries like jQuery can actually make it a bit easier at the cost of another dependency, of course. While jQuery is not strictly needed, I'll just demonstrate how programmatic the element creation would work with it for reference:

var element = $("<input>")
  .attr("type", "text")
  .addClass("form-control")
  .id(elem_id)
  .val(default_value)
  .attr("placeholder", placeholder)
  .on("blur", () => this.validate())

return element;
VLAZ
  • 26,331
  • 9
  • 49
  • 67