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');
});
}
}
Related
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>
I have just come accross with an issue related to event listening in Vue directives.
I have a component which holds following code inside:
function setHeaderWrapperHeight() { ... }
function scrollEventHandler() { ... }
export default {
...
directives: {
fox: {
inserted(el, binding, vnode) {
setHeaderWrapperHeight(el);
el.classList.add('header__unfixed');
window.addEventListener(
'scroll',
scrollEventListener.bind(null, el, binding.arg)
);
window.addEventListener(
'resize',
setHeaderWrapperHeight.bind(null, el)
);
},
unbind(el, binding) {
console.log('Unbound');
window.removeEventListener('scroll', scrollEventListener);
window.removeEventListener('resize', setHeaderWrapperHeight);
}
}
}
...
}
And this component is re-rendered everytime I change router path, I achieved this behaviour by assigning current route path to :key prop so whenever path changes it gets re-rendered. But the propblem is though event listeners are not being removed/destroyed causing terrible performance issues. So how do I remove event listeners?
Calling bind on a function creates a new function. The listeners aren't being removed because the function you're passing to removeEventListener is not the same function you passed to addEventListener.
Communicating between hooks in directives is not particularly easy. The official documentation recommends using the element's dataset, though that seems clumsy in this case:
https://v2.vuejs.org/v2/guide/custom-directive.html#Directive-Hook-Arguments
You could just store the listeners on the element directly as properties so that they're available in the unbind hook.
The code below takes a slightly different approach. It uses an array to hold all of the elements that are currently bound to the directive. The listener on window is only ever registered once, no matter how many times the directive is used. If the directive isn't currently being used then that listener is removed:
let foxElements = []
function onClick () {
console.log('click triggered')
for (const entry of foxElements) {
clickHandler(entry.el, entry.arg)
}
}
function clickHandler (el, arg) {
console.log('clicked', el, arg)
}
new Vue({
el: '#app',
data () {
return {
items: [0]
}
},
directives: {
fox: {
inserted (el, binding) {
console.log('inserted')
if (foxElements.length === 0) {
console.log('adding window listener')
window.addEventListener('click', onClick)
}
foxElements.push({
el,
arg: binding.arg
})
},
unbind (el, binding) {
console.log('unbind')
foxElements = foxElements.filter(element => element.el !== el)
if (foxElements.length === 0) {
console.log('removing window listener')
window.removeEventListener('click', onClick)
}
}
}
}
})
<script src="https://unpkg.com/vue#2.6.11/dist/vue.js"></script>
<div id="app">
<button #click="items.push(Math.floor(Math.random() * 1000))">Add</button>
<hr>
<button
v-for="(item, index) in items"
v-fox:example
#click="items.splice(index, 1)"
>Remove {{ item }}</button>
</div>
However, all of this assumes that a directive is even the right way to go. If you can just do this at the component level then it may get a lot simpler because you have the component instance available to store things. Just remember that calling bind creates a new function, so you'll need to keep a reference to that function somewhere so you can pass it to removeEventListener.
Just for the records, and to help whoever passes through here as there is already an accepted answer, what one could do in this case (on Vue 3 at least, not tested on Vue 2) is to use binding.dir (which is a reference to the directive's own object) to host the function for adding the event listener on the directive object and take it back later when there's a need to remove this listener.
One simple example (not related to the original question) for binding one focus event:
export default {
...
directives: {
fox: {
handleFocus: () => { /* a placeholder to rewrite later */ },
mounted(el, binding) {
binding.dir.handleFocus = () => { /* do whatever */ }
el.addEventListener('focus', binding.dir.handleFocus);
},
beforeUnmount(el, binding) {
el.removeEventListener('focus', binding.dir.handleFocus);
}
}
}
...
}
A practical example of what I'm doing with this, in my case, is to have a focus/blur notifier for any input or textarea tag. I made a Gist here of this, it is on a project built on Vue 3 with TypeScript.
I am using an external library on my app, I noticed that when I change the route then return back the library stop working.
The libraries are imported like this:
import videojs from "video.js"
import "videojs-markers"
....
ngOnInit() {
this.video = videojs('demo');
this.video.on('pause', function () {
this.onPauseVideo(this.video.currentTime())
}.bind(this));
this.video.on('play', function () {
this.onPlayVideo(this.video.currentTime())
}.bind(this));
this.setUpMarkers(this.video);
this.subscription = this.commentService.commentsChange.subscribe((newComments:VideoComment[])=>{
this.video.markers.add(newComments.map(el=>{
return {time:el.time,text:el.text,overlayText:el.overlayText}})
);
})
}
....
any suggestions.
remove .bind(this) from your code. arrow functional should help
you.
This line move to constructor(){}
this.video = videojs('demo');
remove all event listeners like this ngOnDestroy(){ this.video.off('pause')
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've added a tag to some of the components in my template like this.
<div>...</div>
<div #blopp>...</div>
<div>...</div>
<div #blopp>...</div>
<div #blopp>...</div>
<div>...</div>
I also do this in the class definition.
export class NavBar {
#ViewChildren("blopp", { read: ElementRef }) blopps: QueryList<ElementRef>;
constructor() { console.log("NavBar created"); }
ngAfterViewInit() { debugger; }
}
I can iterate over the elements in the debugger using the following script. However, I can't just use on(event,action) as I get the error that such method doesn't exist there.
blopps.forEach((element)=>{...});
Googling gave me something about blopp.listener(...) but it seems that my query list doesn't have that method neither. At the moment I feel less than well-oriented so it might be something rather obvious. What am I missing and how can I add events to my template's controls?
Field blopp has type QueryList, it means that it should be processed as in this example:
constructor(private renderer:Renderer){}
//AfterViewInit interface
ngAfterViewInit() {
blopps.forEach(elementRef => {
this.renderer.listen(elementRef.nativeElement, 'click', (e) => console.log(e));
});
}
You need to subscribe to changes and specify a type. If your element is native you can do it like this:
this.blopps.changes.subscribe(children => {
//note that children is a collection so you can do foreach here
children.last.nativeElement.focus();
// console.log(children.first['_results'][0].nativeElement);
console.log(children.first);
// Do Stuff or wire with referenced element here...
});
You need to call toArray() to get an array that you can iterate over:
blopps.toArray().forEach((element)=>{...});