sveltejs / svelte

Cybernetically enhanced web apps
https://svelte.dev
MIT License
77.4k stars 4.03k forks source link

SSR Actions #4375

Open PatrickG opened 4 years ago

PatrickG commented 4 years ago

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.

bfanger commented 3 years ago

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 commented 3 years ago

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 commented 2 years ago

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 https://github.com/sveltejs/svelte/issues/5112#issuecomment-829594149

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

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 :

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

DaveKeehl commented 3 months ago

Interesting! Has there any update regarding this feature request?

Rich-Harris commented 3 months ago

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

armchair-traveller commented 3 months ago

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 commented 2 months ago

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", ()=>...)
} 
}