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

SSR Actions #4375

Open
PatrickG opened this issue Feb 5, 2020 · 9 comments
Open

SSR Actions #4375

PatrickG opened this issue Feb 5, 2020 · 9 comments

Comments

@PatrickG
Copy link
Member

PatrickG commented Feb 5, 2020

Is your feature request related to a problem? Please describe.
It would be nice if actions could output attributes in SSR context.

Describe the solution you'd like

<script>
  function myAction(node, params) {
    // the normal action stuff for client side
  }
  myAction.SSR = function (params) {
    return {
      'data-some-key': params.someKey,
    };
  }

  export let someKey = 'someValue';
</script>

<div use:myAction={{ someKey }}>
<!-- would render <div data-some-key="someValue"></div> -->

How important is this feature to you?
Somewhat. It's just an idea i had, that could play well with something like svelte-css-vars. Imagine that svelte-css-vars could return { style: '--color: red' } when rendered on the server.

@PatrickG PatrickG changed the title SSR Action SSR Actions Feb 6, 2020
@bfanger
Copy link
Contributor

bfanger commented Oct 21, 2020

Sounded easy, but is complexer than I initially thought:

  1. Who is responsible for removing the properties?
    When running the action on the client does svelte's hydrate step remove the properties? Or should the action itself take into account that it could have properties from the ssr render.
  2. What if the property already exists?
    Should svelte know how to merge styles? Should hydrate unmerge properties?
  3. There is already an idiomatic way to set properties.
    <div style="--color: {color}"> works on both client and server (Although performance with a lot of styles might be a problem) for the data-* example the spread props could be a solution: REPL
    (Ps. if an attribute already exists it is overwritten, but which one depends)
  4. The main focus of actions is related to dom access, which you'll never have on the server. Maybe better solutions exists for the problems this tries to solve.

@madeleineostoja
Copy link

Just wanted to chime in and say I think there is a strong use-case for setting SSR attributes in an action. I'm not sure that the main focus for users of actions is dom access, but rather abstractions of element logic that can be shared, and that can certainly apply on the server. I know a bunch of my most commonly used actions would really benefit from being able to render certain attributes in an SSR context, outside of dom access.

For me at least the answers to the questions raised (1. and 2.) are simple — behave in the same way as if the attribute was written in markup. If some hydrated code clobbers the attribute the action set, so be it, that's up to both implementer and user to work around. I don't think it has to be very nuanced or complex in terms of behaviour.

I'd just love a way to be able to do something like this

function action(...) {
  // clientside only stuff

  return {
    // SSR-able attribute map
    attributes() {
	  return {
	    attr: val
	  }
    }
  }
}

And have those attributes rendered in an SSR context as well as clientside.

@armchair-traveller
Copy link

Highly relevant use case mentioned in another issue on spreading events, where this feature would've helped with:

I also looked into how to create renderless components such as React Aria and Headless UI in Svelte.
Since actions only run in the browser, aria-attributes would not be present on the server-side rendered HTML, making actions a non-starter. I guess you could use a combination of spread props for aria-attributes and actions for event listeners, but that complicates the API quite a bit. Being able to spread event props would make it a lot easier to write renderless components.

Originally posted by @LeanderG in #5112 (comment)

Event spreading seems to be achieved through actions in Svelte. For this use case I believe it would be a good in-between until spreadable events + props and dynamic elements (as/svelte:element) become available.

@adiguba
Copy link
Contributor

adiguba commented Mar 4, 2023

Hello,

I'd just love a way to be able to do something like this

function action(...) {
  // clientside only stuff

  return {
    // SSR-able attribute map
    attributes() {
	  return {
	    attr: val
	  }
    }
  }
}

I think it's not possible like that, because the function expects an DOM node as the first parameter.

But after some try, it seem possible to add a complementary SSR function in order to populate the attributes of the node.
Example :

function action(node, args) {
  // clientside only stuff
  
}

action.SRR = function(attrs, args) {
   // serverside stuff here
   // attrs is an object containing the tag attributes...
}

The only small problem is that I think the SSR function will be exported in the client code (even if it will not be used).
But I don't think it's very problematic.

I tried a quick prototype and it's seem to work pretty well.
Exemple, this component :

<script lang="ts">
    type ActionArgs = { title: string };

    function close(node: HTMLButtonElement, args: ActionArgs) {
        node.classList.add("close-button");
        node.setAttribute("aria-label", args.title);
        
        // other Client side stuff
    }

    close.SSR = function(attrs: Record<string,any>, args: ActionArgs) {
        attrs.class = (attrs.class||'') + ' close-button'
        attrs['aria-label'] = args.title;
    }

</script>

<button use:close={{title:"Close"}} class="btn">X</button>

Will generate the following HTML on SSR/prerendering :

<button class="btn close-button" aria-label="Close">X</button>

I think some things can still be improved :

  • Record<string,any> for the type of the first arg is pretty poor. It could at least define some fields more precisely (like class/style which are strings)
  • Maybe some utility methods could be provided to simplify the modification of class/styles...
  • Maybe there is a way to not bundle the SSR function in client code

My prototype is available here : https://github.com/adiguba/svelte/tree/ssr-actions

@DaveKeehl
Copy link

Interesting! Has there any update regarding this feature request?

@Rich-Harris
Copy link
Member

No, and I'm still not fully clear on the benefits to be honest. Can't we just use spread attributes here?

@armchair-traveller
Copy link

armchair-traveller commented Apr 2, 2024

I'm disconnected from this problem now and thus' have less of an opinion, but for me Svelte 5 ergonomics make this less of an issue since spreading events was solved. And probably other stuff too. I'd love to hear more about the issues others get solved by potentially having this implemented.

But this still serves as a good example:
https://melt-ui.com/docs/preprocessor

Ergonomics are so bad that it actually makes sense to make a preprocessor for an action lib's usecase, even if all the attributes can be controlled by the action.

P.S. Still love that if you build using an action lib, you don't lose access to directives e.g. animate and such. A big part of component libs in Svelte being a pain in the past was having to reinvent element directives and conveniences, especially when you're like a headless lib where almost all your components mirror an element 1:1. It seems with Svelte 5, things like event directives (modifers?) have been removed in favor of making them functions. It does bring into question if other directives should be given the same consideration though

@fcrozatier
Copy link
Contributor

No, and I'm still not fully clear on the benefits to be honest. Can't we just use spread attributes here?

For reusable state/behavior, we have 3 ways to go, and none is really satisfying:

  1. Spreading only. This hinders composability since eg. if an onclick behavior is set in this way any further onclick will override the behavior. So if we want to be able to add many listeners actions are preferred. REPL

Example. <button {...toggle} onclick={()=>"oops it's not toggling anymore"}> Toggle</button>

  1. Actions only. Then we can both set attributes and set event listeners in a composable way, but there is no SSR. For example this can lead to flashes of content if we rely on an action to set hidden. REPL

Example. <button use:toggle={{pressed: true}}> See that flash of unpressed?<button>

  1. Use both actions and spread attributes. In this case it becomes unergonomic to the point where people have made preprocessors to improve the DX as mentionned.

Example <button use:toggle.action {...toggle.attributes}>Not a way to live your life<button>

SSR actions would improve the situation greatly by allowing to set the attributes in ssr, set the behavior on mount, and have a great DX.

Bonus point if we can access some kind of virtual node in ssr allowing to grab and set attributes on the parent!

const createTab= (node: HTMLButtonElement, active:boolean)=>{
  if(node.ssr){
    node.role="tab";
    node.ariaSelected = `${active}`

   node.parent.role = "tablist"; <- that's cool! 
 } else {
  node.addEventListener("click", ()=>...)
} 
}

@vnphanquang
Copy link

vnphanquang commented Jul 4, 2024

Although I'd love to see something like this from Svelte core, I somewhat agree with Rich & bfanger in that the benefit of making action ssr-compatible is quite insignificant given it is an already solvable problem by explicitly spreading attributes. In Svelte 5 ergonomics around props and events have been improved so this is even less of an inconvenience:

<script lang="ts">
  import type { HTMLAttributes } from 'svelte/elements';
  let { class: cls, onmouseenter, onmouseleave, ...rest }: HTMLAttributes<HTMLDivElement> = $props();
  // `$props` is arguably much cleaner than `export let ...`
  // and events are now just regular attributes
</script>

<div class={cls} {onmouseenter} {onmouseleave} {...rest}></div>

Having action supports SSR will introduce not only complexity to the codebase but also ambiguity to ssr/client boundary and conflict between attributes added from multiple places. People will likely try to do client-only things in ssr-only area, and vice versa.


That being said, i often find myself asking the same question as OP did and want some way to extract reusable action coupled with a bit of static attribute setup. So i spawn up a preprocessor-based lib named @svelte-put/preaction as a proof-of-concept in user-land. Here is the idea:

<script>
  import { make, apply } from '@svelte-put/preaction';

  const doStuff = make((initialParam) => {
    // do some stuff, as if ran at top-level script tag
    return {
      action: (node, param) => {
        // regular Svelte action
      },
      attributes: {
        // "static" attribute setup
        'data-initial-param': initialParam,
      },
    };
  });
</script>

<div use:apply={doStuff('argument'))} class="my-class"></div>

Which will transform to this:

<script>
  // same stuff above
  const __preaction = doStuff('argument');
</script>
<div {...__preaction.attributes} use:__preaction.action={'argument'} class="my-class"></div>

CAUTION: proof-of-concept only do not use in production, and only tested with Svelte 5

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests