A Component's EventEmitter does fire in some cases but does not in other cases. Why?
I have a custom Date picker. You can change the date manually (<input>) or use ng2-bootstrap <datepicker> to select conveniently.
I have this template:
<input [(ngModel)]="dateString"
class="form-control"
(focus)="showPopup()"
(change)="inputChange()">
<datepicker class="popup" [ngClass]="{ 'popup-hidden': !showDatepicker }"
[(ngModel)]="dateModel"
[showWeeks]="true"
(ngModelChange)="hidePopup($event)">
</datepicker>
The component with the relevant parts:
export class CustomDatepickerComponent {
#Input()
dateModel: Date;
dateString: string;
showDatepicker = false;
#Output()
dateModelChange: EventEmitter<Date> = new EventEmitter();
showPopup() {
this.showDatepicker = true;
}
// Called when the date is changed manually
// It DOES fire the event
inputChange() {
let result = moment(this.dateString, 'YYYY-MM-DD');
this.update(result.toDate());
}
// Called through the DatePicker of ng-bootstrap
// It DOESN'T fire the event
// However, the showDatepicker binding takes effect (see template)
hidePopup(event: Date) {
showDatepicker = false;
this.update(event);
}
update(date: Date) {
this.dateModel = date;
this.dateString = moment(date).format('YYYY-MM-DD');
// This SHOULD fire the event
// It is called in BOTH cases!
// But it fires only when the date is changed through the <input>
this.dateModelChange.emit(this.dateModel);
}
I use this datepicker this way:
<custom-datepicker [dateModel]="testDate" (change)="change($event)"></custom-datepicker>
Here's the change() handler function.
testDate = new Date();
change($event) { console.info('CHANGE', $event); }
Any ideas?
Ok... Now this is somewhat embarassing.
This was the usage of this component:
custom-datepicker [dateModel]="testDate" (change)="change($event)"></custom-datepicker>
Which should be changed to:
custom-datepicker [dateModel]="testDate" (dateModelChange)="change($event)"></custom-datepicker>
The interesting part is that it seems that the changed event of the <input> element bubbled up from CustomDatepickerComponent. It was handled inside the component, it fired dateModelChange which was not handled, then it bubbled up to the outer component as change event -- and this way it was handled.
If I passed the event to inputChange() and called event.stopPropagation() then it was cancelled and did not propagate.
Once again: it was nothing to do with EventEmitter.
I had a similiar issue. I solved it by decoupling the #ainput date model (datemodel) from the date model used inside the datepicker ( localdate: Date;)
Then I used ngOnChange to keep these both variables synced. This way, the EventEmitter is working fine and sending a value for each date selection or update.
export class myDatepicker implements OnChanges{
localdate: Date;
#Input() datemodel: Date;
#Input() isOpen: boolean = false;
#Input() label: string;
#Input() disabled: boolean = false;
#Output() datemodelChange: EventEmitter<Date> = new EventEmitter<Date>();
// Date has been selected in Datepicker, emit event
public selectedDate(value: any) {
this.datemodelChange.emit(value);
}
// React on changes in the connected input property
ngOnChanges(changes: { [propName: string]: SimpleChange }) {
try {
if (changes["datemodel"] && changes["datemodel"].currentValue) {
this.localdate = moment(changes["datemodel"].currentValue).toDate(); // raw Input value
}
} catch (ex) {
console.log('myDatePicker: ' + ex);
}
}
}
Another solution for anyone stumbling across this page.
I had a similar issue which was also not the fault of EventEmitter. :-)
Initially it appeared my EventEmitter was intermittently not firing, however the culprit turned out to be the click handler responsible for firing the EventEmitter.
I had this in my html template:
<button mat-mini-fab color="primary">
<i class="fas fa-pencil-alt" (click)="onClickEdit()"></i>
</button>
and the click handler:
export class MyClass {
#Output() edit = new EventEmitter();
constructor() { }
onClickEdit() {
console.log('onClickEdit(), this.edit=' + this.edit);
this.edit.emit();
}
}
I added the console.log() to debug the problem, and I noticed onClickEdit() was not fired every time I clicked the fancy edit icon, which explains why the EventEmitter wasn't firing. :-)
I fixed this by moving the (click) attribute to the enclosing button (in hindsight it seems kind of obvious.)
<button mat-mini-fab color="primary" (click)="onClickEdit()">
<i class="fas fa-pencil-alt"></i>
</button>
Problem solved.
Related
I would like to know how to eliminate the mouseOver event from the template by using $ref. I want to control mouseOver behavior inside javascript instead.
Components component has myStats child component. myStats is supposed to be displayed only when I hover over Components.
I am not sure how make ref do what the commented out template code does.
Do I have to use a function of myStats (= onMouseEvent)? It would be easier if I can control mouseOver only inside Components
Components code:
<template>
<div class="pv-lookup" ref="myStats">
<!-- I would like to get rid of the commented out mouse hover event code below by using $ref: -->
<!-- <div #mouseover="mouseOver = true" #mouseout="mouseOver = false"> -->
<div>
<someComponent1 />
<someComponent2 />
<myStats v-if="mouseOver" #mouseMoved="onMouseMoved"/>
</div>
</div>
</template>
<script>
export default class Components extends Vue {
public mouseOver = false;
#Emit()
public onMouseMoved() {
this.mouseOver = !this.mouseOver;
}
mounted() {
this.$nextTick(() => {
if (this.isInhouseClient || this.isInhouse) {
// TODO: set value for this.mouseOver
// Approach 1:
// ERROR: Property 'onMouseEvent' does not exist on type 'Vue | Element | Vue[] | Element[]'.
this.$refs.myStats.onMouseEvent();
// Approach 2:
// ERROR: Property 'onMouseEvent' does not exist on type 'MouseEvent'. Did you mean 'initMouseEvent'?
const myStats= this.$refs.myStatsas HTMLElement;
myStats.addEventListener("mouseover", function( event ) {
event.onMouseEvent();
}, false);
// Approach 3:
// It's the simplest, but how do I connect with 'ref'?
this.mouseOver = !this.mouseOver;
}
});
}
}
</script>
myStats code:
<script>
export default class myStats extends Vue {
public mouseOver: boolean = false;
#Emit()
public mouseMoved(){}
public onMouseEvent() {
this.mouseMoved();
}
}
</script>
Thank you for your insights
You can simply add event listeners for the same events (mouseover and mouseout) to the DOM element. Beware though, these events bubble up from children. In your case it would be better to use mouseenter and mouseleave.
So your code will be:
mounted() {
const myStatsEl = this.$refs.myStats; // get DOM element of the component Components
myStatsEl.addEventListener("mouseenter", (event) => {
this.mouseOver = true;
});
myStatsEl.addEventListener("mouseleave", (event) => {
this.mouseOver = false;
});
},
No need to emit events from myStats component or use its functions.
That being said, the correct 'Vue-way' of doing this would be with #mouseenter and #mouseleave, which is much cleaner. I'm not sure why you wouldn't want to use that. I can't think of any cases where this way might be limiting.
I've got the following controller on my HTML page:
...
<div data-controller="parent">
<div data-target="parent.myDiv">
<div data-controller="child">
<span data-target="child.mySpan"></span>
</div>
</div>
</div>
...
This child controller is mapped to the following child_controller.js class:
export default class {
static targets = ["mySpan"];
connect() {
document.addEventListener("myEvent", (event) => this.handleMyEvent(event));
}
handleMyEvent(event) {
console.log(event);
this.mySpanTarget; // Manipulate the span. No problem.
}
}
As you can see, there is an event listener on the connect() of the Stimulus controller, and when it detects that the event was fired up, it logs the event and manipulates the span target.
The problem arises when I replace the contents of the target myDiv from my parent_controller.js:
...
let childControllerHTML = "<div data-controller="child">...</div>"
myDivTarget.innerHTML= childControllerHTML;
...
Now that the myEvent gets fired up, the event listener picks it not once, but twice (because the same event got logged twice). With every subsequent replacement of the child HTML, the event gets logged one more time than it did before.
I know that one can make use of document.removeEventListener to prevent the old controller from still listening to the events:
export default class {
static targets = ["mySpan"];
connect() {
this.myEventListener = document.addEventListener("myEvent", (event) => this.handleMyEvent(event));
}
disconnect() {
document.removeEventListener("myEvent", this.myEventListener);
}
handleMyEvent(event) {
console.log(event);
this.mySpanTarget; // FAILS. Can't find span.
}
}
But doing it like this makes the handleMyEvent method lose the context as it no longer finds the mySpanTarget under this.
How can I remove the listener from the child controller to which I already got no access as it is no longer in the DOM, while retaining the context?
I found the answer on StimulusJS's Discourse page.
One has to make use of the bind method when initializing the controller:
export default class {
static targets = ["mySpan"];
initialize() {
this.boundHandleMyEvent = this.handleMyEvent.bind(this);
}
connect() {
document.addEventListener("myEvent", this.boundHandleMyEvent);
}
disconnect() {
document.removeEventListener("myEvent", this.boundHandleMyEvent);
}
handleMyEvent(event) {
console.log(event);
this.mySpanTarget; // Manipulate the span. No problem.
}
...
}
Now, the event is only listened once, and the context is not lost inside the handleMyEvent method.
A button is only doing the function once, the video shows the problem:
https://vimeo.com/342491616
The logic goes like this:
When 'home' (inside footer.component) is clicked, sends function to parent(app.component), and then this one sends it to 'hamburguer' button (inside header.component).
I've read it may be something related to ngOnChanges but I tried it several times with it and it didn't work, but maybe I was using it wrong(?)
Code here:
footer.component.html
<a (click)="sendMessage()" routerLink="/page"><img src="assets/images/homebutton.png" class="footer_buttons" id="home_button"></a>
footer.component.ts (inside export class...)
message: string = "hamburguer";
#Output() messageEvent = new EventEmitter<string>();
sendMessage() {
this.messageEvent.emit(this.message);
//console.log(this.message);
}
/////////////////////////////////////////////////////////////
app.component.ts (inside export class...)
message = "";
receiveMessage($event){
this.message = $event;
//console.log($event);
}
/////////////////////////////////////////////////////////////
header.component.html
<div class="hamburguer">
<a (click)="getUrl();">
<img src="assets/images/{{ message }}.png" id="hamburguer">
</a>
</div>
header.component.ts (inside export class...)
#Input() message;
ngOnInit() {
if(window.location.pathname != "/settings"){
this.message = 'hamburguer';
}else{
this.message = 'cross';
}
}
This is happening because you fire the event only once.You need to remove the ngOninit Function and use your own function instead. Take a look at the following StackBlitz example
By the way,You rather work with variables instead working with the URL.URL is dynamic and could change, Pass a variable into your function and populate this.message according to that.
ngOnInit is a function that active on the first time, and invoked once.
you should add another "if" statement in the footer button, Home Button, in order to change the "message" variable.
In my application I have a global search field which is used to filter the data in the list , list will have multiple column. From other component setting the filter value (setting to input value) it's happening but I have to trigger the manual keyboard event (enter key) action on the input.
I tried with viewChild decorator.
component.html
<input #gb type="text" placeholder="Global search.." class="changeListComponent_inputSearch" [(ngModel)]="jiraRef" />
component.ts
#ViewChild('gb') gb:ElementRef;
this.jiraRef = jiraRef;
const event = new KeyboardEvent("keypress",{ "which ": "13"});
this.gb.nativeElement.focus();
this.gb.nativeElement.dispatchEvent(event);
using this I could set the value and make a focus but keyboard event is not triggering.
Triggered the key up event using the native js code.
Placed the id attribute to the input element along with element reference.
Created the new function from inside my component.
triggerEvent(el, type) {
if ('createEvent' in document) {
var e = document.createEvent('HTMLEvents');
e.initEvent(type, false, true);
el.dispatchEvent(e);
} else {
var e = document.createEventObject();
e.eventType = type;
el.fireEvent('on'+e.eventType, e);
}
}
var el=document.getElementById('gb-search');
this.triggerEvent(el,'keyup');
I'm not sure why you are making it complicated. why not just add a keypress event to the input field, and then also be able trigger that event through a viewChild?
<input id="text1" type=text (keyup)="enterPressed()">
and then also be able to access this same method through a view child:
this.viewChild.enterPressed();
In general your inputs can be like:
<input (keydown.enter)="doEnter()">
From other component simply call to the function doEnter. How?
If the search component is in the parent it's easy because you can use ViewChild
<search-component></search-component>
#ViewChild(SearchComponent) searchComponent
..whe we need..
this.searchComponent.doEnter()
Else, typical use a Subject to subscribe. You has a service,
#Injectable({
providedIn: 'root',
})
export class KeyBoardService {
keyboard:Subject<any>=new Subject<any>()
constructor() { }
}
In your component Search, inject the service in constructor and in ngOnInit
constructor(private keyboardService:KeyBoardService){}
ngOnInit(){
this.keyboardService.keyboard.subscribe(_=>{
this.doEnter()
})
}
And in the other component
constructor(private keyboardService:KeyBoardService){}
..in anywhere..
this.keyboardService.next(null);
Exist another option that is when the two components are at time in the screen -and in the same <router-outlet></router-outlet> that is use a template reference variable and a Input. So we has
<search-component #search></search-component>
<other-component [search]=search><other-component>
in other component
#Input('search') searchComponent
..in anywhere
this.searchComponent.doEnter
You can simply use
<input type=text (keypress)="eventHandler($event)">
eventHandler(event) {
console.log(event, event.keyCode, event.keyIdentifier);
}
Solution is based on this answer
Get which key pressed from (keypress) angular2
I'm using Ionic 3 that comes with Angular 4. I built a component that takes the original (click) event from current element and binds it to a new one (dinamically created in the component). What I'm trying to do, is to remove the original click event from current component.
HTML
<!-- imageZoom() is defined in my view controller: page.ts -->
<inline-spinner
[src]="http://..."
(click)="imageZoom($event)"></inline-spinner>
COMPONENT
export class InlineSpinnerComponent implements OnChanges {
public img: any;
#Output() click: EventEmitter<any> = new EventEmitter();
constructor() {
this.img = new Image();
// I want to unbind/remove "click" from here if possible.
// this.elementRef.nativeElement = <inline-spinner>
}
ngOnChanges() {
// Just to show what I'm doing with the original click event
this.img.addEventListener('click', () => { this.click.emit(this.img); });
}
}
Any help will be really appreciated. Thanks!
I guess your are looking for something like this. This should be in a directive, that you attach to the element on which you want neutralize the click.
It will check if the the click event comes from the concerned component only and in that case only do Nothing
#HostListener('document:click', ['$event', '$event.target'])
onClick(event: Event, targetElement: HTMLElement): void {
if (!targetElement) {
return;
}
if(this.elementRef.nativeElement ==== targetElement){
event.preventDefault();
}
}