I want to create a custom input with the Shadow DOM
class TextBox extends HTMLElement {
constructor() {
super();
var shadow = this.attachShadow({ mode: 'open' });
let textbox = document.createElement("input");
shadow.appendChild(textbox);
textbox.addEventListener("change", validate);
function validate(event) {
console.log("input can be validated");
}
}
get value() {
console.log("get");
let textbox = this.shadowRoot.querySelector("input");
return textbox.value;
}
set value(newValue) {
console.log("set");
let textbox = this.shadowRoot.querySelector("input");
textbox.value = newValue;
}
}
customElements.define('test-textbox', TextBox);
It should be possible to change the value of the displayed textbox via js. If I change the .value property of the textbox the setter of value don't get called? Am i missing something?
Later on I want to include the textbox via a template in my solution and be able to set the value of the textbox via textbox.value ="Peter"
The internal <input> field dispatches the input event every time its value changes. This event can be captured either in your component or by the code that uses your component.
The change event only happens in certain situations so the input event is a better choice.
The code below shows how the component listens for the input event and so does the external code.
function validate(event) {
console.log("input can be validated");
}
class TextBox extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
input {
width: 300px;
}
</style>
`;
const textbox = document.createElement("input");
shadow.appendChild(textbox);
textbox.addEventListener("input", validate);
textbox.focus();
}
get value() {
console.log("get");
let textbox = this.shadowRoot.querySelector("input");
return textbox.value;
}
set value(newValue) {
console.log("set");
let textbox = this.shadowRoot.querySelector("input");
textbox.value = newValue;
}
}
customElements.define('test-textbox', TextBox);
const el = document.querySelector('test-textbox');
el.addEventListener("input", (evt) => {
console.log('input event from the outside.');
});
<test-textbox></test-textbox>
Related
I am trying to toggle the visibility of a div element based on the values from two different select form fields.
The HTML select form fields have ids: #team and #country respectively.
I have so far tried:
let teamVal;
document.getElementById("team").addEventListener("change", function (e) {
teamVal= e.target.value;
});
let countryVal;
document.getElementById("country").addEventListener("change", function (e) {
countryVal = e.target.value;
});
// a helper function that should toggle the div element with an id, #myDiv
function toggleDivVisibility(teamVal, countryVal) {
console.log("Toggle method called!");
const myDiv = document.getElementById("myDiv");
if (teamVal === "firmino" && countryVal === "brazil") {
myDiv.style.display = "none";
} else {
myDiv.style.display = "block";
}
}
toggleDivVisibility(teamVal, countryVal);
Thus far, the method seems to be called on document load, however the implementation doesn't work.
Any assistance will be highly appreciated.
I need to enable the submit button as soon as all input fields has value enterred. I have two input fields type text and type password and a button which is disabled (I set its class as "disabled" than use CSS to change color etc..), I would like to remove that class whenever the above condition is met. I added 'change' and 'input' event listeners to all field like below:
const inputs = [...document.querySelectorAll('input[type="text"], input[type="password"]')];
const continueBtn = document.querySelector('continuebtn');
const signinForm = document.querySelector('#sign-in-form');
inputs.forEach((input) => {
input.addEventListener('input', function(e){
if (input.value !== '') {
continueBtn.classList.remove('disabled');
}else{
continueBtn.classList.add('disabled');
}
}
});
Tried with e.target.value.trim() === '' as well
I guess the above would be applied to all inputs and check if they're empty when the user is typing, but I'm not able to make it work: the button is being activated no matter what I do.
I would need some help in plain Javascript as this is what I'm currently learning. no jQuery. Thanks
Use the every() method to check all the inputs, not just the one that the user is currently editing.
const inputs = [...document.querySelectorAll('input[type="text"], input[type="password"]')];
const continueBtn = document.querySelector('#continuebtn');
const signinForm = document.querySelector('#sign-in-form');
inputs.forEach((input) => {
input.addEventListener('input', function(e) {
if (inputs.every(input => input.value.trim())) {
continueBtn.classList.remove('disabled');
} else {
continueBtn.classList.add('disabled');
}
});
});
#continuebtn.disabled {
background-color: grey;
}
<input type="text">
<input type="password">
<button id="continuebtn" class="disabled">Continue</button>
I have a form with one input field for the emailaddress. Now I want to add a class to the <form> when the input has value, but I can't figure out how to do that.
I'm using this code to add a class to the label when the input has value, but I can't make it work for the also:
function checkForInputFooter(element) {
const $label = $(element).siblings('.raven-field-label');
if ($(element).val().length > 0) {
$label.addClass('input-has-value');
} else {
$label.removeClass('input-has-value');
}
}
// The lines below are executed on page load
$('input.raven-field').each(function() {
checkForInputFooter(this);
});
// The lines below (inside) are executed on change & keyup
$('input.raven-field').on('change keyup', function() {
checkForInputFooter(this);
});
Pen: https://codepen.io/mdia/pen/gOrOWMN
This is the solution using jQuery:
function checkForInputFooter(element) {
// element is passed to the function ^
const $label = $(element).siblings('.raven-field-label');
var $element = $(element);
if ($element.val().length > 0) {
$label.addClass('input-has-value');
$element.closest('form').addClass('input-has-value');
} else {
$label.removeClass('input-has-value');
$element.closest('form').removeClass('input-has-value');
}
}
// The lines below are executed on page load
$('input.raven-field').each(function() {
checkForInputFooter(this);
});
// The lines below (inside) are executed on change & keyup
$('input.raven-field').on('change keyup', function() {
checkForInputFooter(this);
});
I've updated your pen here.
Here it is, using javascript vanilla. I selected the label tag ad form tag and added/removed the class accoring to the element value, but first you should add id="myForm" to your form html tag. Good luck.
function checkForInputFooter(element) {
// element is passed to the function ^
let label = element.parentNode.querySelector('.raven-field-label');
let myForm = document.getElementById("myform");
let inputValue = element.value;
if(inputValue != "" && inputValue != null){
label.classList.add('input-has-value');
myForm.classList.add('input-has-value');
}
else{
label.classList.remove('input-has-value');
myForm.classList.remove('input-has-value');
}
}
You can listen to the 'input' event of the input element and use .closest(<selector>) to add or remove the class
$('input').on('input', function () {
if (!this.value) {
$(this).closest('form').removeClass('has-value');
} else {
$(this).closest('form').addClass('has-value');
}
})
Edit: https://codepen.io/KlumperN/pen/xxVxdzy
I have two components where one is the parent component and the other is the child component. Now the parent has two children within it. which child click for add a style in it, i would require to remove the style in other children. so at a time only one child will keep the style. how to do this?
LiveDemo - click on the button. I am not able to remove the style back.
here is my code :
class Parent extends HTMLElement {
shadowRoot;
constructor(){
super();
this.shadowRoot = this.attachShadow({mode: 'open'});
}
connectedCallback(){
this.render();
}
render() {
this.shadowRoot.innerHTML = `<div>
<children-holder></children-holder>
<children-holder></children-holder>
<children-holder></children-holder>
</div>`
}
}
customElements.define('parent-holder', Parent);
class Children extends HTMLElement {
shadowRoot;
constructor(){
super()
this.shadowRoot = this.attachShadow({mode: 'open'});
}
connectedCallback(){
this.render();
this.shadowRoot.querySelector('button').addEventListener('click', () => {
this.shadowRoot.querySelector('button').style.border = "";
this.shadowRoot.querySelector('button').style.border = "3px solid red";
})
}
render() {
this.shadowRoot.innerHTML = `
<div><button class="button">Click me!</button></div>`
}
}
customElements.define('children-holder', Children);
a long answer for (eventually) 3 lines of code...
If you make Custom Element children access a parentNode, and loop its
DOM elements..
You are creating a dependency between components
Event Driven solution:
The click on a button bubbles up the DOM
so the parent can capture that click event
The evt.target will be the button clicked
The parent then emits a custom event
The Children are listening for that Event, there is NO dependency on the parent
Since the Event contains the button clicked,
each listening element can do its select/unselect code
And it is less and clearer code
class Parent extends HTMLElement {
constructor() {
super()
.attachShadow({mode: 'open'})
.shadowRoot.innerHTML = `<div>` +
`<children-holder></children-holder>`.repeat(3) +
`</div>`
}
connectedCallback() {
this.shadowRoot.addEventListener('click', evt => {
if (evt.target.nodeName === 'CHILDREN-HOLDER')
document.dispatchEvent(new CustomEvent('myStateEvent', {
detail: evt.target // THE BUTTON CLICKED
}));
});
}
}
customElements.define('parent-holder', Parent);
class Children extends HTMLElement {
constructor() {
super()
this.attachShadow({mode: 'open'});
}
connectedCallback() {
this.shadowRoot.innerHTML = `<div><button class="button">Click me!</button></div>`;
document.addEventListener('myStateEvent', evt => {
let IwasClicked = evt.detail === this;
this.shadowRoot.querySelector('button').style.border = IwasClicked ? "3px solid red" : ""
});
}
}
customElements.define('children-holder', Children);
Notes
dispatch and listen are both on the document, you can attach them anywhere
events bubble UP, not down
the default events like click bubble out of shadow DOM
Custom Events require composed:true
read: https://developer.mozilla.org/en-US/docs/Web/API/Event/Event
I did the dispatch in the Parent for clearity (A DEPENDENCY!)
It might be better to make the Child do the dispatchEvent,
So it becomes:
Yo! everyone listening! I was clicked, WE ALL do whatever WE need to do
And keep all logic in one component:
connectedCallback() {
let root = this.shadowRoot;
let eventName = "myStateEvent";
root.innerHTML = `<div><button class="button">Click me!</button></div>`;
document.addEventListener(eventName, evt => {
let button = root.querySelector("button");
button.style.border = evt.detail === button ? "3px solid red" : "";
});
root.addEventListener("click", evt =>
document.dispatchEvent(
new CustomEvent(eventName, {
detail: evt.target // THE BUTTON CLICKED
})
)
);
}
Now you understand Event driven solutions
And you might now ask: Why not use the click event?
That is possible once you understand that event.target is NOT what you might think it is.
When events originate from shadow DOM, the event.target value is the last shadowDOM it pierced
So your button click sets different event.target values:
Listener on <children-holder> event.target = button
Listener on <parent-holder> event.target = <children-holder>
Listener on document event.target = <parent-holder>
To solve your Button-Select-Color use-case with one click event
the button click is the dispatcher, sending a click event UP the DOM,
through all shadowDOM boundaries
You have to check the event.composedPath() function which retuns an Array of ALL DOM elements the Event passed.
(note: event.path is Chrome only!!)
So all code required for your style question is:
connectedCallback() {
let root = this.shadowRoot;
root.innerHTML = `<div><button>Click me!</button></div>`;
root.host.getRootNode().addEventListener("click", evt => {
let button = root.querySelector("button");
button.style.border = evt.composedPath().includes(button) ? "3px solid red" : "";
});
}
Notes
root.host.getRootNode() allows one selected button per parent Component
change to document and it is one button per page
evt.composedPath().includes(root) identifies the child-component
Working Fiddle: https://jsfiddle.net/WebComponents/bc9tw1qa/
You could achieve your desired behavior in a couple of ways, I'll describe 2 of them:
CSS-only:
When you click a button, it will receive the CSS focus state. So using the css
button:focus {
border: 3px solid red;
}
Will give only the most recently clicked button a border. The focus state will be removed when you click anywhere else on the screen.
JS solution
The separate shadow-roots make it a bit hard to traverse all the buttons using JS in an elegant way, but this should do the trick:
const button = this.shadowRoot.querySelector('button');
button.addEventListener('click', () => {
const parentShadowRoot = this.shadowRoot.host.getRootNode();
const childrenHolders = parentShadowRoot.querySelectorAll('children-holder');
childrenHolders.forEach(holder => {
const button = holder.shadowRoot.querySelector('button');
button.style.border = "";
})
button.style.border = "3px solid red";
})
You could simplify your two classes, check the example snippet.
class Parent extends HTMLElement {
constructor() {
super();
this.sroot = this.attachShadow({
mode: 'open'
});
this.render();
}
render() {
this.sroot.innerHTML = `<div>
<children-holder></children-holder>
<children-holder></children-holder>
<children-holder></children-holder>
</div>`
}
}
customElements.define('parent-holder', Parent);
class Children extends HTMLElement {
constructor() {
super();
this.sroot = this.attachShadow({
mode: 'open'
});
let style = document.createElement("style");
style.append('button:focus {border: 3px solid red;}');
this.sroot.append(style);
this.render();
}
render() {
let button = document.createElement("button");
button.innerHTML = "Click me!";
button.classList.add("button");
let div = document.createElement("div");
div.append(button);
this.sroot.append(div);
}
}
customElements.define('children-holder', Children);
<parent-holder></parent-holder>
You can first retrieve all the button's siblings as well as the button itself then you can remove the border from all of them and finally, add the red border to the button that was clicked.
Retrieve the clicked button as well as it's siblings by using parentNode.children.
You will get an HTMLCollection of the buttons on which you can now use Array.from to get a new, shallow-copied Array of your HTMLCollection which you can now iterate over.
Finally, you can now just remove the border from all the buttons and then add the border to the clicked button.
this.shadowRoot.querySelector('button').addEventListener('click', () => {
let x = this.parentNode.children;
Array.from(x).forEach((e) => {
e.shadowRoot.querySelector('button').style.border = "";
});
this.shadowRoot.querySelector('button').style.border = "3px solid red";
});
Here is a live example of the above in JSFiddle: https://jsfiddle.net/AndrewL64/Lrbn7d8t/18/
I have the following React component:
var $ = require('jquery');
module.exports = {
trigger: '.form-trigger',
form: '.financial-form',
init: function() {
$(this.trigger).click(this.toggleForm.bind(this));
},
toggleForm: function(e) {
// define currentTarget
var currentTarget = $(event.currentTarget);
// define current Text
var currentText = currentTarget.html();
// Prevent anchor click default
e.preventDefault();
// Remove any active classes
currentTarget.next(form).slideToggle("fast");
// console.log(currentText);
if ($(this.form).is(":visible")){
// $(element).is(":visible")
currentTarget.html("Close Form");
} else {
currentTarget.html(currentText);
}
}
};
There are multiple 'triggers' on the page which open up their adjacent forms. Inside the toggleForm function, the 'if' statement determines the text of the button. I store the buttons current Text so that if the form is open, I can change to "Close Form" or else return the button to its original text. However the currentText variable is storing the "Close Form" Text as it becomes the currentTarget. How can I make sure the button's text returns to its original state?
You can store the original text in jQuery's data()
var $ = require('jquery');
module.exports = {
trigger : '.form-trigger',
form : '.financial-form',
init : function() {
$(this.trigger).click(this.toggleForm.bind(this));
},
toggleForm: function(e) {
e.preventDefault();
var currentTarget = $(event.currentTarget),
flag = $(this.form).data('flag');
currentTarget.next(form).slideToggle("fast");
if ( !currentTarget.data('text') ) {
currentTarget.data('text', currentTarget.text());
}
currentTarget.text(flag ? currentTarget.data('text') : "Close Form");
$(this.form).data('flag', !flag);
}
};