I'm working on new web-components for my site, which work fine just with html/css. However, I was not able to add some kind of javascript functionality to my shadow DOM.
In my case, it's about a button inside the component, which should trigger a function handling the event. Nevertheless, I always get an error that the function is not defined. I know that the shadow DOM should protect the inside but I do not know how to implement my js properly. Hopefully you can help :)
class InfoSmall extends HTMLElement {
// attributes
constructor() {
super();
// not important
}
// component attributes
static get observedAttributes() {
return [''];
}
// attribute change
attributeChangedCallback(property, oldValue, newValue) {
if (oldValue == newValue) return;
this[ property ] = newValue;
};
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<div class="container>
<button id="copy"></button>
</div>`
shadow.querySelector('#copy').addEventListener("click", function () {
// functionality
});
}
}
I also tried the onclick-attribute but it was the same for me: no function defined. Or I also tried writing the script inside the innerHTML with an HTML-tag...
You are creating invalid HTML because you are missing a double quote on class="container>
Your code can be condensed to:
<script>
customElements.define("my-element", class extends HTMLElement {
constructor() {
super() // sets AND returns 'this' scope
.attachShadow({mode: 'open'}) // sets AND returns this.shadowRoot
.innerHTML = `<div class="container">
<button>Click Me!</button>
</div>`;
this.shadowRoot
.querySelector('button')
.onclick = () => {
alert("YEAH!")
};
}
});
</script>
<my-element></my-element>
Related
Under my Angular App, I'm using some 3rd library widget which is rendering in my component.
My template is:
<div>
<myWidGet></myWidGet>
</div>
Inside myWidGet there some button element that I want handle their events.
The button have those classes : .dx-edit-row .dx-command-edit .dx-link-save
so i i do that :
export class myClass AfterViewInit {
constructor(private elRef: ElementRef){}
ngAfterViewInit() {
this.elRef.nativeElement.querySelector('.dx-edit-row .dx-command-edit .dx-
link-save').on('click', (e) => {
alert('test');
});
}
}
My purpose is to get reference to my button and handle the click event from it.
Suggestions?
Normally the 3rd party widget should provide a click handler like so:
<myWidGet (click)="myFunction($event)"></myWidGet>
and in the controller:
myFunction(evt) {
const target = evt.target
console.log('test')
}
However, if they do not expose click handlers then I would seriously consider not using the widget.
If want to use the widget anyway then do this using jQuery:
ngAfterViewInit() {
$('.dx-edit-row.dx-command-edit.dx-link-save').on('click', (evt) => {
const target = evt.target
console.log('test')
});
}
The above assumes ALL these classes are present on the same button.
Or just use vanilla JS.
If the buttons are not available on ngAfterViewInit() then you could do this:
ngAfterViewInit() {
const interval = setInterval(() => {
const button = $('.dx-edit-row.dx-command-edit.dx-link-save')
// if button is ready
if (button) {
// add click handlers
button.on('click', (evt) => {
const target = evt.target
console.log('test')
});
// stop polling
clearInterval(interval)
}
}, 100)
}
Accessing DOM elements using jQuery is not really a good practice. Use ElementRef with Renderer2 instead. Also, there's nothing like ngOnViewInit in Angular. It's ngAfterViewInit.
Once the View loads, inside the ngAfterViewInit, you can get access to the HTMLElement using the nativeElement on ElementRef instance. You should explicitly typecast it into HTMLElement so as to get intellisence.
You can then call querySelector on it and pass it the classes. This will give you the button element.
Now you use Renderer2's instances' listen method. This takes three args:
The element you want to listen to events on(btnElement).
The Name of the event(click).
The callback function.
This would translate to code like:
constructor(
private el: ElementRef,
private renderer: Renderer2
) {}
ngAfterViewInit() {
const btnElement = (<HTMLElement>this.el.nativeElement)
.querySelector('.dx-edit-row.dx-command-edit.dx-link-save');
this.renderer.listen(btnElement, 'click', () => {
alert('Buton was clicked');
});
}
Here's a Working StackBlitz for your ref.
You can customize the third party widget using Angular Directive. It will allow to access DOM element and attach listeners using renderer2.
<myWidGet customEvents></myWidGet>
Directive:
#Directive({
selector: '[customEvents]'
})
export class CustomEventsDirective implements OnInit {
private element: any;
constructor(private el: ElementRef, private renderer: Renderer2) {
this.element = el.nativeElement;
}
ngOnInit() {
const btnElement = this.element.querySelector('.dx-edit-row.dx-command-edit.dx-link-save');
this.renderer.listen(btnElement, 'click', () => {
alert('Buton was clicked');
});
}
}
If I call a Polymer debouncer from a button click it works perfectly. I click 5 times in less than 2 seconds, prints only one timestamp:
myProofOfConcept(){
this.__debouncer = Polymer.Debouncer.debounce(
.__debouncer,
Polymer.Async.timeOut.after(2000),
() => {
console.log("HEY " + Date.now());
});
}
But if I call the exact same method from a Polymer properties change observer, it will wait the required 2 second timeout, and then console print as many times as the observer calls it, even if only 1 millisecond apart.
Is there some external factor that I don’t know about, that is driving this difference in behavior?
Here's something to look for if this ever happens to you.
If you debounce a method within a web component...
and then you instantiate multiple instances of that component within your web app... it may appear to you that the debouncer is not working, when in fact it may be working, rather it is just working in multiple instances of the same web component.
I had a stray previous instance of the web component inside an entirely different page. Hadn't been used, or even noticed. I had just forgotten to remove it when I migrated it to a different page.
EDIT: Control for debouncing across all instances of the element and for property changed from multiple places (button clicks and setInterval).
Usage seems to be fine when applied like:
<dom-module id="my-element">
<template>
<style>
:host {
display: block;
}
</style>
[[prop]]
<button on-click="add">Test</button>
</template>
<script>
(function() {
let DEBOUNCED_METHOD;
HTMLImports.whenReady(function() {
class MyElement extends Polymer.Element {
static get is() { return 'my-element'; }
static get properties() {
return {
prop: {
value: 0,
type: Number,
observer: 'myProofOfConcept'
}
}
}
constructor() {
super();
let test = setInterval(() => {
this.add();
}, 400);
setTimeout(() => {
clearInterval(test);
}, 4500)
}
add() {
this.prop += 1;
}
myProofOfConcept(){
DEBOUNCED_METHOD = Polymer.Debouncer.debounce(
DEBOUNCED_METHOD,
Polymer.Async.timeOut.after(2000),
this.log);
}
log() {
console.log("HEY " + Date.now());
}
}
customElements.define(MyElement.is, MyElement);
});
}())
</script>
Hope that helps!
I need to remove the wheel eventlistener immediately after it fired. I tried the following but its not removing the eventlistener.
export class HomeComponent implements OnInit {
constructor() {}
ngOnInit() {
document.querySelector("#section-one").addEventListener("wheel", () => this.myFunction1(), true);
}
myFunction1() {
alert();
document.querySelector("#section-one").removeEventListener("wheel", this.myFunction1, true);
console.log("Done!");
}
}
Any suggestions?
According to the docs:
Calling removeEventListener() with arguments that do not identify any
currently registered EventListener on the EventTarget has no effect.
your code shouldn't work.
Possible fix can be as follows:
wheelHandler: any;
ngOnInit() {
this.wheelHandler = this.myFunction1.bind(this);
document.querySelector("#section-one").addEventListener("wheel", this.wheelHandler, true);
}
myFunction1() {
alert();
document.querySelector("#section-one").removeEventListener("wheel", this.wheelHandler, true);
console.log("Done!");
}
where wheelHandler is a function referring to the same instance of handler
For more angular way solution see
How to listen for mousemove event on Document object in Angular
But AFAIK useCapture parameter is not supported yet. So it's always false
You could use the HostListener decorator to bind a event listener, but this only works for the host element. If you want to add and remove a listener for a child element you have to use the Renderer2.listen method. Which returns a function to remove the event listener.
#Component( {
template: '<div #sectionOne></div>'
})
export class myComponent {
private _listeners = [];
#ViewChild('sectionOne')
public section: ElementRef<any>;
constructor(private _renderer: Renderer2) {}
ngAfterViewInit() {
this._listeners.push(
this._renderer.listen(this.section.nativeElement, 'click', this.handler.bind(this))
);
}
ngOnDestroy() {
this._listeners.forEach(fn => fn());
}
public handler() {
}
}
The useCapture parameter isn't supported by angular by now. For more information, see this issue.
The problem is probably that when you use a class method as a callback function, this no longer points to the class in the callback function.
Use this code instead to add your event listener:
document.querySelector("#section-one")
.addEventListener("wheel", () => this.myFunction1(), true);
Note that this.myFunction1 has become () => this.myFunction1(). In other words, I wrapped the name of the callback function inside a lambda.
The code to remove the listener stays the same:
document.querySelector("#section-one").removeEventListener("wheel", this. myFunction1, true);
Finally, and most importantly, why are you using event listeners like that? This is definitely NOT the Angular way. 😅
I want to call the method inside the same class. For example, when I click a button, it will trigger the method handleLoginBtnClicked(). I expect it will call the method checkInputValidation() in the same class. What is the proper way to do this?
export default class LoginCard extends React.Component {
//If I click a button, this method will be called.
handleLoginBtnClicked() {
this.checkInputValidation();
}
checkInputValidation() {
alert("clicked");
}
...
...
...
render() {
...
<LoginBtn onClick={this.handleLoginBtnClicked}/>
...
}
}
Error Message:
Uncaught TypeError: this.checkInputValidation is not a function
You will need to bind those functions to the context of the component. Inside constructor you will need to do this:
export default class LoginCard extends React.Component {
constructor(props) {
super(props);
this.handleLoginBtnClicked = this.handleLoginBtnClicked.bind(this);
this.checkInputValidation = this.checkInputValidation.bind(this);
}
//This is the method handleLoginBtnClicked
handleLoginBtnClicked() {
...
}
//This is the method checkInputValidation
checkInputValidation() {
...
}
...
..
.
}
Where are you binding the handleLoginBtnClicked? You may be losing the functions context and losing the meaning of the special variable this. React will handle and trigger the onClick event, calling the function from a different context which is why it has been lost.
You should use the following syntax to create a new bound function to add as the event listener for the onClick event. This will ensure that handleLoginBtnClicked's context is not lost.
<element onClick={this.handleLoginBtnClicked.bind(this)}>
jQuery seems to be working fine in react component however, when I try to apply styling using jquery in react component its not working. In the below code console.log(eachVisitedTopic) within each loop is returning proper result as expected.
topicsVisited(arr){
$(function() {
$.each(arr, function(key, eachVisitedTopic) {
console.log(eachVisitedTopic);
$('.single-topic[data-topic-id="' + eachVisitedTopic + '"]').css({
'background-color': 'red'
});
});
});
};
Markup
import {React, ReactDOM} from '../../../../build/react';
export default class SingleTopicBox extends React.Component {
render() {
return (
<div>
<div className="col-sm-2">
<div className="single-topic" data-topic-id={this.props.topicID} onClick={() => this.props.onClick(this.props.topicID)}>
{this.props.label}
{this.props.topicID}
</div>
</div>
</div>
);
}
};
React should handle all the render, it checks the dirty dom and render only things that changed.
You can achieve what you want, just use a react state.
When you trigger a setState change react will look into the DOM and find what has changed and then render it.
Ref: https://facebook.github.io/react/docs/component-api.html#setstate
constructor() {
super();
this.state = {
bgDisplayColor: "blue"
};
}
Then you can do something like this in yout component:
$('.single-topic[data-topic-id="' + eachVisitedTopic + '"]').css({
'background-color': this.state.bgDisplayColor
});
And to update it you simply use:
this.setState({bgDisplayColor: "red"});
EDIT
To workaround the undefined variable error, you have to store the scope of "this" inside your function and use instead of "this", because inside the jquery .css "this" refers to Jquery and not the "this" scope of your actual class.
Example:
topicsVisited(arr){
var self = this;
$(function(){
$.each(arr, function(key, eachVisitedTopic){
console.log(eachVisitedTopic);
//self here is the global scope of your class
//Inside jQuery.css this refers to Jquery and not to your class.
$('.single-topic[data-topic-id="' + eachVisitedTopic + '"]').css({
'background-color': self.state.bgDisplayColor
});
});
});
});
};
Try it to by putting all jQuery code inside the componentDidMount
E.g :
componentDidMount() {
//Your jQuery function goes here
}