Closed Olyno closed 1 year ago
Hey there @Olyno - valid point - npm package is a thing on the wishlist.
Wow awesome, can't wait for it to be available!
@Olyno Here you go... https://www.npmjs.com/package/ionic-svelte
Please provide feedback if this works for you.
It seems to work. However, I would have preferred to have custom components exported. Something like:
<script>
import { Avatar } from 'ionic-svelte';
</script>
<Avatar src="/assets/img/ionic/avatar.svg" />
I can contribute by doing most of the components. Anything internal to Ionic (e.g. controllers.ts
) will be out of my reach.
Hi @Olyno - I understand.
How would you like to see that happening in the code and it makes the bundle probably a bit smaller? I think for this to work each component needs to get a separate svelte file, which also registers itself as webcomponent? And then index.ts needs to import as default and re-export? (export { default as AccordionGroup } from './components/Accordion/AccordionGroup.svelte';
)
I had thought of this,...maybe this can be generated? Because it is a pretty tedious job mirroring the input properties as well as the event bindings for output - for all components. Quite a job with Ionic's api changing (not frequently).
I do believe the Vue integration has something like this so maybe copy from there...
https://github.com/ionic-team/ionic-framework/blob/main/packages/vue/src/proxies.ts
Maybe you can give a code example on how you see this?
The controllers don't need any changes, as they are tree-shakable in this way.
I think for this to work each component needs to get a separate svelte file, which also registers itself as webcomponent? And then index.ts needs to import as default and re-export? (export { default as AccordionGroup } from './components/Accordion/AccordionGroup.svelte';)
That's what I thought. Nevertheless you are right that it is a huge job. I just looked more closely at trying to convert a first component, and it took me much longer than I thought.
I don't know if the proxy system works with Svelte. I'm thinking about the typing of the components which would be more than interesting. Importing custom components that would be just a simple conversion of a web component would not be interesting, better to use web components directly in this case. If this conversion includes typing, then it becomes more than interesting to use this way.
Svelte contains a svelte:element
element, could we use it?
I don't know if the proxy system works with Svelte. I'm thinking about the typing of the components which would be more than interesting. Importing custom components that would be just a simple conversion of a web component would not be interesting, better to use web components directly in this case.
The Vue and React implementations also wrap the webcomponent in a "native" component to create type safety and also some sort of tree shaking. The current implementation loads the whole Ionic library.
Ionic without webcomponents requires a rebuild from the stencil source into something different. That is a task beyond huge, imho. @ionic/core is webcomponents.
Svelte:element - maybe, but won't solve the problem of having to list (some) props and exposing events. Next, there still needs to be a mechanism to register the webcomponent before it can be used in the template.
My conclusion remains that in order to achieve tree shaking via the import you describe (which is preferrable) we need to wrap them....
Thx for trying though
Alternative path - after a good night of sleep - is to provide for dynamic import of components in ionicSvelteSetup (or separate method) - not sure if this saves bundle size but will speed up a bit the initialisation as it would only register the required webcomponents
You know if dynamic imports affect bundles positively and how?
I'm not sure what you want to do. Do you have an example in mind?
Yes, I was thinking like
setupIonicSvelte(['IonSelect','IonSlide')
which then only registers these components.
Problem with this structure is that the array contains strings requiring then a dynamic import in the method. This in turn does not help reducing the bundle - what I've read somewhere. Next, a short test with performance timing shows that the actual time the current proces takes, does not justify these changes.
Alternative is to pass the imported classes to setupIonicSvelte
which in turn uses that to register the component, but for this we need the name in kebab-case (IonSlide vs ion-slide). This makes the whole registration process per component a bit tedious imho.
Third option is taking the defineCustomElement function every ionic component exports, create a wrapper factory function that exports a svelte component, which you then import in the svelte. So that is the Proxies approach (https://github.com/ionic-team/ionic-framework/blob/5bb1414f7fa04ea07954cb3f68883ee2f162586a/packages/react/src/components/proxies.ts). I think this may be the most viable way - just need to figure out the factory function - what needs to be the output to make something a svelte component.
Edit - after researching the react proxy - I am not sure the last one can work. React and Vue use VDOM and the createElement/h function they use gets the tag-label ('ion-slide') as input to create the element. The input props are attached to it and then in parallel de webcomponent registered so it actually renders properly. Svelte does not have VDOM. So how to do this? Let me try svelte:elelement as it accepts tag like 'ion-slide'... :)
Edit2: This seems to work well for passing the props as well as the event handlers - there does not seem to be a generic thing to pass all events of the component to its consumers - so this might require listing all event of a component in the component. This almost calls for a generator script that generates all the components - instead of a factory function generating the Svelte component. But then again, this only will cover ionic events. Other events like click etc will not be covered.....
<script>
let tag = 'ion-button';
</script>
<svelte:element this={tag} {...$$props} on:click><slot /></svelte:element>
And the consumer:
import Stuff from '$lib/components/Stuff.svelte';
<Stuff
expand="full"
color="danger"
on:click={() => {
console.log('ss');}}>STUFF</Stuff>
So this needs a generic way to bubble up all events from svelte:element to the parent - in a simple way.
Hi @Olyno
Just curious - why do you want to do the import {IonCard} stuff? For tree shaking? Or you have issues using kebab-notation (ion-card)?
Reason asking - ionic elements are webcomponents. Once registered they can be used throughout the app using kebab. So, no import statement needed anymore.
The current way I am doing this registers all components realising a bundle of 900kb - which is big. If this is a problemn, then I reckon we can also do partially registration using some sort of import statement at certain places, only once and then leave like that.
like import 'ionic-svelte/components/ion-card';
which registers that one in a layout.svelte and then we are good to go using ion-card everywhere.
I was thinking this, because I did finish the generation of svelte-files for all components, but I wondered what the point is for dx and bundle size.
And I checked the Vue/React implementation and this also makes me wonder why they do this - also they register the webcomponent and then insert the kebab tag in the VDOM. That imho is a bit of overhead - even though maybe consistent code?
I am confused..
Hi 👋🏻 Sorry for the late reply. I really like the method with the svelte:element
. It seems to me to be the best compromise.
To answer your question, web components are good, but in my opinion are not adapted everywhere. Most of the time they lack autocompletion or suggestion, may not work with some frameworks....
The reason why I don't support web components in Svelte is the fact that it doesn't feel natural to me. Most Svelte projects are components that we can import, and without having to do much setup. Using web components can seem confusing because we lose that habit. Add to that the lack of autocompletion in the IDE and it gives you a global view.
Of course, my point of view will change the day we will have a very good autocompletion in our projects with web components (note that some frameworks already support this, including Angular).
Hi - no problem
I can generate the svelte ionic wrappers following from the core code. Then I will be getting the following - see below - would that work? It does not expose the properties of the component explicitly- only the events are exposed (ionBlur etc). Would that solve the autocompletion or do you need the props also included? (export let expand
and then <svelte:element {expand} ....
.
<script>
import { IonButton } from "@ionic/core/components/ion-button";
import { defineComponent } from "ionic-svelte";
let tag = "ion-button";
defineComponent(tag, IonButton);
</script>
<svelte:element
this={tag}
{...$$props}
on:ionFocus
on:ionBlur
on:focuson
on:bluron
on:fullscreenchangeon
on:fullscreenerroron
on:scrollon
on:cuton
on:copyon
on:pasteon
on:keydownon
on:keypresson
on:keyupon
on:auxclickon
on:clickon
on:contextmenuon
on:dblclickon
on:mousedownon
on:mouseenteron
on:mouseleaveon
on:mousemoveon
on:mouseoveron
on:mouseouton
on:mouseupon
on:pointerlockchangeon
on:pointerlockerroron
on:selecton
on:wheelon
on:dragon
on:dragendon
on:dragenteron
on:dragstarton
on:dragleaveon
on:dragoveron
on:dropon
on:touchcancelon
on:touchendon
on:touchmoveon
on:touchstarton
on:pointeroveron
on:pointerenteron
on:pointerdownon
on:pointermoveon
on:pointerupon
on:pointercancelon
on:pointerouton
on:pointerleaveon
on:gotpointercaptureon
on:lostpointercapture
on:click><slot /></svelte:element
>
ps. I see the default events have "on" attached at the end. Will remove those in the script.
If the props are implicit, then it is indeed better to add exports, this seems to me to be the best to do for autocompletion
So - this can still become a bit of work - the generator script can deliver this from the Stencil source:
<script lang="ts">
import type { Color, RouterDirection, AnimationBuilder } from '@ionic/core';
import { IonButton } from '@ionic/core/components/ion-button';
import { defineComponent } from 'ionic-svelte';
const tag = 'ion-button';
export let color: Color;
export let buttonType = 'button';
export let disabled = false;
export let expand: 'full' | 'block' = 'full';
export let fill: 'clear' | 'outline' | 'solid' | 'default' = 'clear';
export let routerDirection: RouterDirection = 'forward';
export let routerAnimation: AnimationBuilder | undefined = undefined;
export let download: string | undefined = undefined;
export let href: string | undefined = undefined;
export let rel: string | undefined = undefined;
export let shape: 'round' = 'round';
export let size: 'small' | 'default' | 'large' = 'small';
export let strong = false;
export let target: string | undefined = undefined;
export let type: 'submit' | 'reset' | 'button' = 'button';
export let form: string | HTMLFormElement;
defineComponent('ion-button', IonButton);
</script>
<svelte:element
this={tag}
{color}
{buttonType}
{disabled}
{expand}
{fill}
{routerDirection}
{routerAnimation}
{download}
{href}
{rel}
{shape}
{size}
{strong}
{target}
{type}
{form}
{...$$props}
on:ionFocus
on:ionBlur
on:focus
on:blur
on:fullscreenchange
on:fullscreenerror
on:scroll
on:cut
on:copy
on:paste
on:keydown
on:keypress
on:keyup
on:auxclick
on:click
on:contextmenu
on:dblclick
on:mousedown
on:mouseenter
on:mouseleave
on:mousemove
on:mouseover
on:mouseout
on:mouseup
on:pointerlockchange
on:pointerlockerror
on:select
on:wheel
on:drag
on:dragend
on:dragenter
on:dragstart
on:dragleave
on:dragover
on:drop
on:touchcancel
on:touchend
on:touchmove
on:touchstart
on:pointerover
on:pointerenter
on:pointerdown
on:pointermove
on:pointerup
on:pointercancel
on:pointerout
on:pointerleave
on:gotpointercapture
on:lostpointercapture
on:click><slot /></svelte:element
>
While this gives tapesafety, it does make at least two props mandatory while they arent: color and form.
The Stencil code does not give explicit default values, and I expect more of these, requiring manual search & replace.
Edit - I think I may want to use Ionic docs as well - as they document defaults better - more explicit.
That sounds really good to me!
Not sure how to pull it off though - ionic docs arent that easy to parse - so maybe later...
Importable code available through experimental folder in the npm package. Haven't tried it in a project yet, as I focussed on the generation of type-safe svelte wrappers - https://github.com/Tommertom/svelte-ionic-npm
So moving from <ion-button>A great button</ion-button>
to
import { IonButton } from 'ionic-svelte/experimental/components/IonButton.svelte';
<IonButton>A great button</IonButton>
Experimental also has version of setupIonicSvelte
.
So if you want to use this, change imports from ... from 'ionic-svelte
to ... from 'ionic-svelte/experimental
hi @Olyno - looking for an opinion
I have been working on component library and it was great fun, getting to solve all sorts of smaller and bigger things.
So, basically the route was to create .svelte wrappers for each element, supporting slots and so on - see example https://github.com/Tommertom/svelte-ionic-npm/blob/main/experimental/components/IonItem.svelte
But now I wonder if this is the way to go, having the issue of applying styles in the svelte way to IonComponents.
This won't work (pseudo code):
import {IonButton} from 'ionic-svelte/experimental';
<IonButton>Click me</IonButton>
<style>
IonButton {
--background:red;
}
neither will work:
ion-button {
--background:red;
}
<style>
Here the encapsulation kicks in and avoids style spilling.
The way to deal can be to use global
in the parent component (or worse - at root level), but this is cumbersome and from all the issues I studied, it seems pretty clear they want it this way.
I am fine with that, I like the thinking behind encapsulation - and that made building the demo app also really big fun.
So then I believe I need to abandon the native component way.
Question what is the alternative? You have any ideas?
The option I haven't looked at, but might work is creating typings for the kebab-components, and provide some sort of import per component that registers the component. Hoping this creates type safety in the IDE and autocompletion. And some shaking.
So something like this:
MyStuff.svelte (pseudo)
import 'ionic-svelte/components/ion-button';
<ion-button> CLick me </ion-button>
<style>
ion-button {
--background:red;
}
</style>
Hi @Tommertom,
I think we should use css class for that. Using css, it should work. For example:
import {IonButton} from 'ionic-svelte/experimental';
<IonButton class="my-custom-button">Click me</IonButton>
<style>
.my-custom-button {
--background:red;
}
<style>
You can do that using
<script>
let clazz = ''
export { clazz as class }
</script>
<ion-button class="{clazz}">
<slot />
</ion-button>
Not sure if it will solve the issue, but this is a possible way
Hi @Tommertom,
I think we should use css class for that. Using css, it should work. For example:
import {IonButton} from 'ionic-svelte/experimental'; <IonButton class="my-custom-button">Click me</IonButton> <style> .my-custom-button { --background:red; } <style>
You can do that using
<script> let clazz = '' export { clazz as class } </script> <ion-button class="{clazz}"> <slot /> </ion-button>
Not sure if it will solve the issue, but this is a possible way
Interesting syntax - as a prop to the component
Then the prop needs to be filled in the parent as js object, not in the style definition. That won't fly
Then the syntax could be like
<IonButton style={--background:"red"}> click me <...>
If it is a prop then name style might be better than class
https://dev.to/dakmor/type-safe-web-components-with-jsdoc-4icf
https://paulallies.medium.com/crafting-js-applications-with-jsdoc-and-typescript-8c463b7fb605
This is the other way using jsdoc type annotations
To try out - https://github.com/ionic-team/ionic-docs/issues/2658#issuecomment-1329762596 (typings and component info)
in combination with
https://github.com/sveltejs/language-tools/blob/master/packages/svelte2tsx/svelte-jsx.d.ts
This as *.d.ts file does the trick (typings and completion in IDE) for ion-button
declare namespace svelte.JSX {
interface IntrinsicElements {
'ion-button':
{
/**
* The type of button.
*/
"buttonType"?: string;
/**
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
*/
"color"?: Color;
/**
* If `true`, the user cannot interact with the button.
*/
"disabled"?: boolean;
/**
* This attribute instructs browsers to download a URL instead of navigating to it, so the user will be prompted to save it as a local file. If the attribute has a value, it is used as the pre-filled file name in the Save prompt (the user can still change the file name if they want).
*/
"download"?: string | undefined;
/**
* Set to `"block"` for a full-width button or to `"full"` for a full-width button with square corners and no left or right borders.
*/
"expand"?: 'full' | 'block';
/**
* Set to `"clear"` for a transparent button that resembles a flat button, to `"outline"` for a transparent button with a border, or to `"solid"` for a button with a filled background. The default fill is `"solid"` except inside of a toolbar, where the default is `"clear"`.
*/
"fill"?: 'clear' | 'outline' | 'solid' | 'default';
/**
* The HTML form element or form element id. Used to submit a form when the button is not a child of the form.
*/
"form"?: string | HTMLFormElement;
/**
* Contains a URL or a URL fragment that the hyperlink points to. If this property is set, an anchor tag will be rendered.
*/
"href"?: string | undefined;
/**
* The mode determines which platform styles to use.
*/
"mode"?: "ios" | "md";
/**
* Specifies the relationship of the target object to the link object. The value is a space-separated list of [link types](https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types).
*/
"rel"?: string | undefined;
/**
* When using a router, it specifies the transition animation when navigating to another page using `href`.
*/
"routerAnimation"?: AnimationBuilder | undefined;
/**
* When using a router, it specifies the transition direction when navigating to another page using `href`.
*/
"routerDirection"?: RouterDirection;
/**
* Set to `"round"` for a button with more rounded corners.
*/
"shape"?: 'round';
/**
* Set to `"small"` for a button with less height and padding, to `"default"` for a button with the default height and padding, or to `"large"` for a button with more height and padding. By default the size is unset, unless the button is inside of an item, where the size is `"small"` by default. Set the size to `"default"` inside of an item to make it a standard size button.
*/
"size"?: 'small' | 'default' | 'large';
/**
* If `true`, activates a button with a heavier font weight.
*/
"strong"?: boolean;
/**
* Specifies where to display the linked URL. Only applies when an `href` is provided. Special keywords: `"_blank"`, `"_self"`, `"_parent"`, `"_top"`.
*/
"target"?: string | undefined;
/**
* The type of the button.
*/
"type"?: 'submit' | 'reset' | 'button';
}
}
}
And besides the typings, I could provide for imports like these, but I believe this is a bit old-fashioned? Benefit of these imports, is that you need to do them only once, then webcomponents are registered.
This way, you can manage bundle size a bit better compared to all at once...
<script lang="ts">
import 'ionic-svelte/ion-card';
</script>
<ion-card>
<ion-card-header>
<ion-card-subtitle>Great success!!</ion-card-subtitle>
<ion-card-title>Welcome to your app!</ion-card-title>
</ion-card-header>
<ion-card-content> Some input</ion-card-content>
</ion-card>
So, independent component library:
We ready like this?
This looks perfect, thank you so much for your work!
Hey there - so what I see is that this thing is now finished.
npm create ionic-svelte-app@latest
gives you a barebone starter with ionic-svelte
as npm package, typescript support on html, including type-ahead and code-splitting via commonjs modules - which imho seems perfectly fine as we only need to register/import a component once and then it works everywhere.
<script lang='ts'>
import { setupIonicBase } from 'ionic-svelte';
/* Call Ionic's setup routine */
setupIonicBase();
/* Theme variables */
import '../theme/variables.css';
/*
The next command loads and registers all Ionic Webcomponents for you to use.
This adds at least >800kb (uncompressed) to your bundle - 80 components (so do your math!!)
You can also choose to import each component you want to use separately.
It is recommended to do this in this file, as you only need to do such once. But you are free
to do this elsewhere if you like to code-split differently.
Example:
import 'ionic-svelte/components/ion-app';
import 'ionic-svelte/components/ion-card';
import 'ionic-svelte/components/ion-card-title';
import 'ionic-svelte/components/ion-card-subtitle';
import 'ionic-svelte/components/ion-card-header';
import 'ionic-svelte/components/ion-card-content';
import 'ionic-svelte/components/ion-chip';
import 'ionic-svelte/components/ion-button';
Click the ionic-svelte-components-all-import below to go to the full list of possible imports.
Please don't forget to import ion-app in this file when you decide to code-split:
>>>>>> import 'ionic-svelte/components/ion-app';
You can report issues here - https://github.com/Tommertom/svelte-ionic-npm/issues
Want to know what is happening more - follow me on Twitter - https://twitter.com/Tommertomm
*/
import 'ionic-svelte/components/all';
</script>
<ion-app>
<slot />
</ion-app>
Closing this issue for now - if there are bugs or other things, please raise anohter issue. And thx @Olyno for bringing this here. Please let me know how you using this project - I'd love to get some feedback on what is working and what brings a bit of DX friction.
Thank you for your work! I'll look into it later!
Hi 👋🏻
I would like to be able to install the different components from a single package to install, and not to copy and paste the components manually.
Would it be possible to create a specific package for the components please?