Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bug: ionRefresh emits object typed as RefresherEventDetail but actually emits RefresherCustomEvent #26747

Closed
4 of 8 tasks
Tommertom opened this issue Feb 7, 2023 · 21 comments
Closed
4 of 8 tasks
Labels

Comments

@Tommertom
Copy link

Tommertom commented Feb 7, 2023

Prerequisites

Ionic Framework Version

  • v4.x
  • v5.x
  • v6.x
  • v7.x
  • Nightly

Current Behavior

The tsx to ion-refresher specifies it emits RefresherEventDetails:
@Event() ionRefresh!: EventEmitter<RefresherEventDetail>;

this exposes the complete method to the user to complete the refreshing.

So this would mean something like this needs to work:

(pseudo code)
on:ionRefresh={refresher} where refresher would be like function refresher(ev:RefresherEventDetails) {.. ev.complete() }

This code complies to typings, but will give a runtime error as the correct way is ev.target.complete().

But that that will give a typings error in the IDE.

Changing to
function refresher(ev: RefresherCustomEvent) {.. ev.target.complete() } will fix this, but gives IDE errors because of type mismatch.

The workaround is to remove typings or use any keyword.

Notably the docs state that you must use complete() where the code example say target.complete(). So if you take things very literally, it could become confusing and error prone.

image
(meaning you need to bind to the ion-refresher component and then call its complete() method)

image
(take the event object and call ev.target.complete().

So possibly a documentation improvement (happy to do PR) but maybe a code fix in refresher.tsx?

Expected Behavior

No runtime issue when calling ev.complete() or no typings error when using type on the ionRefresh event.

Steps to Reproduce

See above

Code Reproduction URL

None available - the code examples on ion-refresher have the issue

Ionic Info

Occurs in angular project:
Ionic:

Ionic CLI : 6.20.6 (C:\Users\xxxx\AppData\Roaming\npm\node_modules@ionic\cli)
Ionic Framework : @ionic/angular 6.5.2
@angular-devkit/build-angular : 15.1.4
@angular-devkit/schematics : 15.1.4
@angular/cli : 15.1.4
@ionic/angular-toolkit : 6.1.0

Capacitor:

Capacitor CLI : 4.6.3
@capacitor/android : not installed
@capacitor/core : 4.6.3
@capacitor/ios : not installed

Utility:

cordova-res : not installed globally
native-run : 1.7.1

System:

NodeJS : v16.14.2 (C:\Program Files\nodejs\node.exe)
npm : 8.10.0
OS : Windows 10

And also ionic svelte (no ionic info)

Additional Information

https://discordapp.com/channels/520266681499779082/1049388501629681675/1072243939635122187

#20221

@ionitron-bot ionitron-bot bot added the triage label Feb 7, 2023
@liamdebeasi liamdebeasi self-assigned this Feb 7, 2023
@liamdebeasi
Copy link
Contributor

liamdebeasi commented Feb 7, 2023

Thanks for the report. The correct type is RefresherCustomEvent because of how the custom event payload is formatted:

{
  target: ...,
  detail: {
    complete: () => { ... }
  },
  ...
}

Both ev.target.complete() and ev.detail.complete() are correct since ev.target is ion-refresher. We happen to also provide the complete method in the detail payload.

edit: RefresherEventDetail is the type for just the detail payload on the custom event, so doing ev.complete() will not work since ev is of type RefresherCustomEvent.

@liamdebeasi liamdebeasi added the needs: reply the issue needs a response from the user label Feb 7, 2023
@liamdebeasi liamdebeasi removed their assignment Feb 7, 2023
@ionitron-bot ionitron-bot bot removed the triage label Feb 7, 2023
@Tommertom
Copy link
Author

Tommertom commented Feb 7, 2023

So that means the refresher.tsx code needs to be changed?
https://github.com/ionic-team/ionic-framework/blob/main/core/src/components/refresher/refresher.tsx

from
@Event() ionRefresh!: EventEmitter<RefresherEventDetail>;
to
@Event() ionRefresh!: EventEmitter<RefresherCustomEvent> ;

possibly giving a type error on line 701?

 this.ionRefresh.emit({
      complete: this.complete.bind(this),
    });

At least this would solve the typings issue. (and help generating the right typings in ionic-svelte using core.json)

@ionitron-bot ionitron-bot bot added triage and removed needs: reply the issue needs a response from the user labels Feb 7, 2023
@liamdebeasi
Copy link
Contributor

liamdebeasi commented Feb 7, 2023

No, the code is correct. EventEmitter<RefresherEventDetail> means that there will be an event of type CustomEvent<RefresherEventDetail>.

We ship a RefresherCustomEvent interface which extends CustomEvent and explicitly types the target field. This was done so developers can do ev.target.complete(). If we did not add that explicit type then TypeScript would say that complete is not a method on HTMLElement.

@liamdebeasi liamdebeasi added the needs: reply the issue needs a response from the user label Feb 7, 2023
@ionitron-bot ionitron-bot bot removed the triage label Feb 7, 2023
@Tommertom
Copy link
Author

Tommertom commented Feb 7, 2023

Ok - will put two code examples here in coming minutes. Angular and Svelte - with the errors. For me to understand what I need to fix in the code and/or the ionic-svelte typings definition which now says. "on:ionRefresh"?: (event: RefresherEventDetail) => void;

@ionitron-bot ionitron-bot bot added triage and removed needs: reply the issue needs a response from the user labels Feb 7, 2023
@Tommertom
Copy link
Author

Tommertom commented Feb 7, 2023

Svelte issue:

image
Typescript error, but working runtime.

Type '(ev: RefresherCustomEvent) => void' is not assignable to type '(event: RefresherEventDetail) => void'.
  Types of parameters 'ev' and 'event' are incompatible.
    Type 'RefresherEventDetail' is missing the following properties from type 'RefresherCustomEvent': detail, target, initCustomEvent, bubbles, and 20 more.ts(2322)

Obivous error because definition of ionRefresh in ionic-svelte typings (generated from core.json): "on:ionRefresh"?: (event: RefresherEventDetail) => void;

image
No typescript error, but runtime error (ev.prompt() does not exist)

image
Typescript error different place, but no runtime error.

Angular:
Explicit typing in receiving function
image

Will not compile - Error: src/app/home/home.page.html:8:59 - error TS2345: Argument of type 'Event' is not assignable to parameter of type 'RefresherCustomEvent'. [ng] Type 'Event' is missing the following properties from type 'RefresherCustomEvent': detail, initCustomEvent [ng] [ng] 8 <ion-refresher slot="fixed" (ionRefresh)="handleRefresh($event)">

Changing typing gives typescript error in TS file:
image

And this also fails to compile:
image

All issues can be avoided by not typing the receiving function. As per example in documentation.

So question is how to get explicit typings working in the angular version. And maybe a suggestion for the svelte one?

@liamdebeasi
Copy link
Contributor

Do you have a sample app I can try? It's hard to understand what is going on here from only screenshots.

Will not compile - Error: src/app/home/home.page.html:8:59 - error TS2345: Argument of type 'Event' is not assignable to parameter of type 'RefresherCustomEvent'. [ng] Type 'Event' is missing the following properties from type 'RefresherCustomEvent': detail, initCustomEvent [ng] [ng] 8 <ion-refresher slot="fixed" (ionRefresh)="handleRefresh($event)">

This is an issue with our Angular compatibility: #24245. The reason is the event type is assumed to always be Event which is not the case here.

@liamdebeasi liamdebeasi added the needs: reply the issue needs a response from the user label Feb 7, 2023
@ionitron-bot ionitron-bot bot added triage and removed triage needs: reply the issue needs a response from the user labels Feb 7, 2023
@Tommertom
Copy link
Author

Tommertom commented Feb 7, 2023

Do you have a sample app I can try? It's hard to understand what is going on here from only screenshots.

Sorry about that - svelte I reckon, given that the angular part is a known issue

Svelte: https://github.com/Tommertom/ionicrefresher

+page.svelte in /route

@liamdebeasi
Copy link
Contributor

Thanks! The types that Ionic Svelte is generating appear to be wrong. In node_modules/ionic-svelte/index.d.ts there is the following:

interface IonRefresher {
  ...,
  "on:ionRefresh"?: (event: RefresherEventDetail) => void;
}

This should be (event: CustomEvent<RefresherEventDetail>) => void;. Ionic components emit CustomEvent. The type definition for CustomEvent has a generic that lets you specify the detail payload. In this case, that is RefresherEventDetail.

The RefresherCustomEvent is not part of our autogenerated API at the moment. However, RefresherCustomEvent extends CustomEvent, so you should be fine to use CustomEvent<RefresherEventDetail> as the type.

@liamdebeasi liamdebeasi added the needs: reply the issue needs a response from the user label Feb 7, 2023
@ionitron-bot ionitron-bot bot removed the triage label Feb 7, 2023
@Tommertom
Copy link
Author

Ok - thx for the debugging - just for my info - you expect this to be happening other places as well? It is the first type I am seeing this issue. Or as you say - it is happening everywhere so I need to wrap all events in CustomEvent<....>?

Now - I am quite bluntly using the core.json data to generate this type. So for this situation it was

  "events": [
                {
                    "event": "ionRefresh",
                    "detail": "RefresherEventDetail",
                    "bubbles": true,
                    "cancelable": true,
                    "composed": true,
                    "docs": "Emitted when the user lets go of the content and has pulled down\nfurther than the `pullMin` or pulls the content down and exceeds the pullMax.\nUpdates the refresher state to `refreshing`. The `complete()` method should be\ncalled when the async operation has completed.",
                    "docsTags": []
                },

Thx again - will close the issue.

@ionitron-bot ionitron-bot bot added triage and removed needs: reply the issue needs a response from the user labels Feb 7, 2023
@liamdebeasi
Copy link
Contributor

Yes, every event emitted from Ionic components is a CustomEvent. The "detail" key in the core.json snippet you post describes the detail payload of CustomEvent not the type of the event itself.

@liamdebeasi liamdebeasi added the needs: reply the issue needs a response from the user label Feb 7, 2023
@ionitron-bot ionitron-bot bot removed the triage label Feb 7, 2023
@Tommertom
Copy link
Author

Nice - thx- will change the script and generate new typings.

@ionitron-bot ionitron-bot bot added triage and removed needs: reply the issue needs a response from the user labels Feb 7, 2023
@liamdebeasi
Copy link
Contributor

liamdebeasi commented Feb 7, 2023

The team also looked at the Ionic Svelte source. The issue appears to be around here: https://github.com/Tommertom/svelte-ionic-npm/blob/main/scripts/generate.js#L242-L251

Should minimally be:
"on:${event.event}"?: CustomEvent<${event.detail}>;

But ideally you can do the following to get the target typing too:
"on:${event.event}"?: CustomEvent<${event.detail}> & { target: ${htmlElementType};

(This is psuedo-code and assumes Svelte does not do something different with events)

@Tommertom
Copy link
Author

ok - thx- that I will include in the generate script. And clean up the repo as there are a few experimental generator scripts. Appreciate all the help!

@Tommertom
Copy link
Author

Tommertom commented Feb 7, 2023

I am now generating for all events
"on:ionRefresh"?: (event: CustomEvent<RefresherEventDetail>) => void;

What is htmlElementType in this respect? Element type definitions are using HTMLBaseElement for all components so that would make:

"on:ionRefresh"?: (event: CustomEvent<RefresherEventDetail> & {target: HTMLBaseElement}) => void;

@Tommertom
Copy link
Author

new version published - and scripts updated - generator.js is the go-to script

@liamdebeasi
Copy link
Contributor

htmlElementType is a combination of HTML, the tag name as pascal case, and Element. Example: HTMLIonRefresherElement

@Tommertom
Copy link
Author

Sorry to bother again - but I am now a bit confused what to do in the demo repo:

This will not generate any typescript issue(as I am using RefresherCustomEvent), but will be friction for developers to find as it is not specified in the docs nor can be inferred from the type definition in the IDE or typings file:

function refresher(ev:RefresherCustomEvent) {
		console.log('Refreshed.', ev);
		setTimeout(() => {
			ev.target.complete();
		}, 2000);
	}

Whereas if you use ev:RefresherEventDetail, there is a typescript error on ev.target.complete() and ionRefresh in the template.,

	function refresher(ev:RefresherEventDetail) {
		console.log('Refreshed.', ev);
		setTimeout(() => {
			ev.target.complete();
		}, 2000);
	}

@liamdebeasi
Copy link
Contributor

RefresherCustomEvent is documented here: https://ionicframework.com/docs/api/refresher#refreshercustomevent. Unfortunately, RefresherCustomEvent is not included as part of our automatic core.json generation. However, devs do not have to use RefresherCustomEvent. They could use CustomEvent<RefresherEventDetail> and do ev.detail.complete().

@Tommertom
Copy link
Author

Tommertom commented Feb 7, 2023

CustomEvent<RefresherEventDetail> still gives typescript errors

	function refresher(ev: CustomEvent<RefresherEventDetail>) {
		console.log('Refreshed.', ev);
		setTimeout(() => {
			if (ev.target) ev.target.complete();
		}, 2000);
	}

Error- Property 'complete' does not exist on type 'EventTarget'.ts(2339)

The if is to avoid 'ev.target' is possibly 'null'.ts(18047)

If this is the only event where this is happening, I am happy to overrule RefresherEventDetail with RefresherCustomEvent so the developer uses this one which does not give any typescript errors.

@liamdebeasi
Copy link
Contributor

liamdebeasi commented Feb 7, 2023

Property 'complete' does not exist on type 'EventTarget'.ts(2339)

Yes, this is why RefresherCustomEvent exists as it correctly types target. As I noted in #26747 (comment), if you want to use CustomEvent<RefresherEventDetail> you will need to also use ev.detail.complete() since detail has the correct type information (RefresherEventDetail).

You don't necessarily need to use these custom event interfaces. As I mentioned in #26747 (comment), you could do something like & {target: HTMLIonRefresherElement} when creating the type. This would allow you to correctly type target so developers can continue to use ev.target.complete() and avoid the if check.

@ionitron-bot
Copy link

ionitron-bot bot commented Mar 9, 2023

Thanks for the issue! This issue is being locked to prevent comments that are not relevant to the original issue. If this is still an issue with the latest version of Ionic, please create a new issue and ensure the template is fully filled out.

@ionitron-bot ionitron-bot bot locked and limited conversation to collaborators Mar 9, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

2 participants