Tommertom / svelte-ionic-app

Ionic UI showcase app - try Ionic UI and directly go to API or source code (Svelte, Angular, Vue, Vanilla and React)
https://ionic-svelte.firebaseapp.com
MIT License
774 stars 62 forks source link

Create an independent components package #39

Closed Olyno closed 1 year ago

Olyno commented 2 years ago

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?

Tommertom commented 2 years ago

Hey there @Olyno - valid point - npm package is a thing on the wishlist.

Olyno commented 1 year ago

Wow awesome, can't wait for it to be available!

Tommertom commented 1 year ago

@Olyno Here you go... https://www.npmjs.com/package/ionic-svelte

Please provide feedback if this works for you.

Olyno commented 1 year ago

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.

Tommertom commented 1 year ago

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.

Olyno commented 1 year ago

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?

Tommertom commented 1 year ago

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

Tommertom commented 1 year ago

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?

Olyno commented 1 year ago

I'm not sure what you want to do. Do you have an example in mind?

Tommertom commented 1 year ago

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.

Tommertom commented 1 year ago

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..

Olyno commented 1 year ago

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).

Tommertom commented 1 year ago

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.

Olyno commented 1 year ago

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

Tommertom commented 1 year ago

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.

Olyno commented 1 year ago

That sounds really good to me!

Tommertom commented 1 year ago

Not sure how to pull it off though - ionic docs arent that easy to parse - so maybe later...

Tommertom commented 1 year ago

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

Tommertom commented 1 year ago

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>
Olyno commented 1 year ago

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

Tommertom commented 1 year ago

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

Tommertom commented 1 year ago

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

Tommertom commented 1 year ago

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/docs/preprocessors/typescript.md#im-using-an-attributeevent-on-a-dom-element-and-it-throws-a-type-error

https://github.com/sveltejs/language-tools/blob/master/packages/svelte2tsx/svelte-jsx.d.ts

Tommertom commented 1 year ago

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';
    }
  }
}

image

Tommertom commented 1 year ago

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?

Olyno commented 1 year ago

This looks perfect, thank you so much for your work!

Tommertom commented 1 year ago

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.

Olyno commented 1 year ago

Thank you for your work! I'll look into it later!