Javascript custom element's oninput function issue - javascript

Say, I need to call a function in a custom element, such that when a slider moves in that element the text gets updated(only for that element). The main.js file,
class OninputClassDemo extends HTMLElement {
constructor(){
super();
const shadow = this.attachShadow({mode:'open'});
this.parent = document.createElement('div');
this.slider = document.createElement('input');
this.slider.setAttribute('type','range');
this.slider.setAttribute('min','0');
this.slider.setAttribute('max','99');
this.slider.setAttribute('value','0')
this.text = document.createElement('input');
this.text.setAttribute('type','text');
this.text.setAttribute('value','');
this.parent.appendChild(this.slider);
this.parent.appendChild(this.text);
shadow.appendChild(this.parent);
this.slider.setAttribute('oninput','OninputClassDemo.changeValue()');
}
changeValue = function(){
this.text.setAttribute('value',this.slider.getAttribute('value'));
}
}
window.customElements.define('demo-element',OninputClassDemo);
On the template side,
<!DOCTYPE html>
<html lang="en">
<head>
<script src="main.js"></script>
</head>
<body>
<demo-element></demo-element>
</body>
</html>
The error I am getting is,
OninputClassDemo.changeValue is not a function
at HTMLInputElement.oninput
I do not know how to reference the method for that particular object in this.slider.setAttribute('oninput','WhatShouldIPutHere'),so that the text box for only that object gets changed.

You should be binding event listeners with addEventListener. You should be binding to the method with this, not the class name. Set the value property, do not set an attribute.
class OninputClassDemo extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({
mode: 'open'
});
this.parent = document.createElement('div');
this.slider = document.createElement('input');
this.slider.setAttribute('type', 'range');
this.slider.setAttribute('min', '0');
this.slider.setAttribute('max', '99');
this.slider.setAttribute('value', '0')
this.text = document.createElement('input');
this.text.setAttribute('type', 'text');
this.text.setAttribute('value', '');
this.parent.appendChild(this.slider);
this.parent.appendChild(this.text);
shadow.appendChild(this.parent);
this.slider.addEventListener('input', (event) => this.changeValue());
}
changeValue() {
this.text.value = this.slider.value;
}
}
window.customElements.define('demo-element', OninputClassDemo);
<demo-element></demo-element>

You should write
changeValue() {
this.text.setAttribute('value',this.slider.getAttribute('value'));
}
instead of
changeValue = function(){
this.text.setAttribute('value',this.slider.getAttribute('value'));
}

Related

Is there a way to append html to web component?

I've a custom js web component:
class MainDiv extends HTMLElement {
constructor() { super(); }
connectedCallback() {
this.innerHTML = '<div id="mydiv"><input type="text" oninput="onInput(event)"/></div>'
}
}
and in main.js I've the function onInput*()
function onInput(event) {
const mainDiv = document.getElementById("mydiv");
const newLabel = document.createElement('span');
newLabel.innerText = `${event.target.value.length}/255`; // output: 5/255...n/255
mainDiv.appendChild(newLabel);
}
If I add a log in the onInput function it prints and does not return any error, but is not updating the webcomponent. Why?
There must be something else at play, your code works:
<script>
function onInput(event) {
const mainDiv = document.getElementById("mydiv");
const newLabel = document.createElement('div');
newLabel.innerText = `${event.target.value.length}/255`; // output: 5/255...n/255
mainDiv.appendChild(newLabel);
}
customElements.define("my-element", class extends HTMLElement {
connectedCallback() {
this.innerHTML = '<div id="mydiv"><input type="text" oninput="onInput(event)"/></div>'
}
});
</script>
<my-element></my-element>
Note
constructor() {
super();
}
Is not required, it says run the constructor from my parent, which is also done when you leave out the constructor in your Component.
Only when you do more in the constructor is it required.

Uncaught TypeError: xyz is not a function (when calling it from inside string)

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?
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:
document.createElement will create the node
.setAttribute() can add attributes
.classList and .add() any classes you need.
instead of setting the onblur attribute, .addEventListener() to add a proper handler as the direct attribute but note that sometimes you might prefer the attribute.
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;

Get event target inside a web component

Anchor elements (<a>) are created when the user interacts with a web component. The problem is, that I cannot get the anchor element returned from the "outside" of the web component when an anchor is clicked.
I add an event listener to document listening for click events. When an element somewhere in the DOM is clicked I expect the e.target to be the clicked element.
In the case of a click somewhere inside the web component the custom element (<fancy-list></fancy-list>) will be returned - not the clicked element.
When the mode of the shadow DOM is set to open the DOM should be accessible.
class Fancylist extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const wrapper = document.createElement('div');
wrapper.innerHTML = `<ul></ul><button>Add item</button>`;
shadow.appendChild(wrapper);
this.on_root_click = this.on_root_click.bind(this);
}
connectedCallback() {
this.ul_elm = this.shadowRoot.querySelector('ul');
this.shadowRoot.addEventListener('click', this.on_root_click, false);
}
on_root_click(e){
switch(e.target.nodeName){
case 'BUTTON':
this.ul_elm.innerHTML += '<li>List item</li>';
break;
case 'A':
e.preventDefault();
console.log('You clicked a link!');
break;
}
}
}
customElements.define('fancy-list', Fancylist);
<!DOCTYPE html>
<html>
<head>
<title>List</title>
<meta charset="utf-8" />
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', e => {
document.body.addEventListener('click', e => {
//console.log(e.composedPath());
console.log(e.target); // why is this not returning an anchor element when an anchor is clickend inside the <fancy-list>?
}, false);
}, false);
</script>
</head>
<body>
<h1>List</h1>
<fancy-list></fancy-list>
</body>
</html>
The purpose of the Shadow DOM is precisely to mask the HTML content the Shadow DOM from the containter point of view.
That's also why inner events are retargeted in order to expose the Shadow DOM host.
However, you can still get the real target by getting the first item of the Event.path Array property.
event.path[0]
NB: of course it will work only with open Shadow DOM.
class Fancylist extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const wrapper = document.createElement('div');
wrapper.innerHTML = `<ul></ul><button>Add item</button>`;
shadow.appendChild(wrapper);
this.on_root_click = this.on_root_click.bind(this);
}
connectedCallback() {
this.ul_elm = this.shadowRoot.querySelector('ul');
this.shadowRoot.addEventListener('click', this.on_root_click, false);
}
on_root_click(e){
switch(e.target.nodeName){
case 'BUTTON':
this.ul_elm.innerHTML += '<li>List item</li>';
break;
case 'A':
e.preventDefault();
break;
}
}
}
customElements.define('fancy-list', Fancylist);
<!DOCTYPE html>
<html>
<head>
<title>List</title>
<meta charset="utf-8" />
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', e => {
document.body.addEventListener('click', e => {
console.log(e.path[0]);
}, false);
}, false);
</script>
</head>
<body>
<h1>List</h1>
<fancy-list></fancy-list>
</body>
</html>
Update 2021
As commented now you should use event.composedPath().

Call class function with addEventListener

I want to access the doSomething function when the button that the render function creates is clicked? I get the following error instead.
Uncaught ReferenceError: getMethod is not defined
main.js
class Div {
constructor(div){
this.div = div;
}
init(){
this.getMethod();
}
getMethod(){
var div = this.div;
var createButton = {
render: function(){
let button = `
<button
onclick="getMethod.doSomething(this)"
type="button">
Click Me!
</button>
`;
div.innerHTML = button;
},
doSomething: function(){
// do something
}
}
createButton.render();
}
}
const div = new Div(document.querySelector('div'));
div.init();
There are multiple problems with your code.
Event handlers in html attributes (e.g.onclick) get run in global scope, yet getMethod is defined in a local scope, the scope of your class.
getMethod does not have a property doSomething, I think you meant to write createButton.doSomething
To fix the first issue you have to define your button as an object, not just as text. Since text doesn't know anything about scope. Then you can addEventListener to add a handler for the click event. In the handler callback function's you will have access to all variables in your local scope (the scope of the getMethod function)
class Div {
constructor(div){
this.div = div;
}
init(){
this.getMethod();
}
getMethod(){
var div = this.div;
var createButton = {
render: function(){
const button = document.createElement('button');
button.type = "button";
button.textContent = "Click Me!";
button.addEventListener('click', () => {
createButton.doSomething(this)
})
//clear inner html / delete all children
div.innerHTML = '';
div.appendChild(button);
},
doSomething: function(){
console.log("do something");
}
}
createButton.render();
}
}
const divElem = document.getElementById("mydiv");
const div = new Div(divElem);
div.init();
<div id="mydiv"></div>

How to add event listener with function callback in html inside a class?

Imagine having a class that generates content on the page. Part of the content should have event listener attached in html such as onclick=function().
How can I make sure to call the function from within the class that constructed the html?
class Container {
constructor(hook) {
this.hook = "#" + hook;
this.addDiv = this.addDiv.bind(this);
this.fireMe = this.fireMe.bind(this);
this.init = this.init.bind(this);
this.init();
}
addDiv() {
const div = `<div onclick="fireMe()">FIRE ME</div>`;
document.querySelector(this.hook).innerHTML = div;
}
fireMe() {
console.log("hello!");
}
init() {
this.addDiv();
}
}
let div = new Container("app");
now getting error that fireMe is undefined (which is right because it is not available in global scope).
I know I can add event listener by rendering the div first and than adding the event listener, but is there a way of adding event listener from within <div> tag to actually reach Container.fireMe() method?
You have to create the element -> something like this
class Container {
constructor (hook) {
this.hook = '#' + hook;
this.addDiv = this.addDiv.bind(this);
this.fireMe = this.fireMe.bind(this);
this.init = this.init.bind(this);
this.init();
}
addDiv () {
const div = document.createElement('div');
div.textContent = 'FIRE ME';
div.addEventListener('click', this.fireMe );
document.querySelector(this.hook).innerHTML = div;
}
fireMe () {
console.log('hello!');
}
init () {
this.addDiv();
}
}
const div = new Container('app');
Never use inline event handlers as there are many reasons to avoid this 20+ year old technique that just will not die.
Instead, use modern, standards-based code with .addEventListener(). If you do this along with making the new HTML using the DOM API, you'll be able to more easily accomplish your goal:
addDiv() {
const div = document.createElement("div");
div.textConent = "FIRE ME";
div.addEventListener("click", this.fireMe);
document.querySelector(this.hook).innerHTML = div;
}
You should create elements use document.createElement() rather than using string
class Container {
constructor(hook) {
this.hook = "#" + hook;
this.addDiv = this.addDiv.bind(this);
this.fireMe = this.fireMe.bind(this);
this.init = this.init.bind(this);
this.init();
}
addDiv(){
const div = document.createElement('div');
div.innerHTML = "Fire Me";
div.addEventListener("click",this.fireMe);
document.querySelector(this.hook).appendChild(div);
}
fireMe() {
console.log("hello!");
}
init() {
this.addDiv();
}
}
let div = new Container("app");

Categories