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

Higher Order Components (HOC) #1648

Closed
ekhaled opened this issue Aug 13, 2018 · 10 comments
Closed

Higher Order Components (HOC) #1648

ekhaled opened this issue Aug 13, 2018 · 10 comments
Labels
awaiting submitter needs a reproduction, or clarification

Comments

@ekhaled
Copy link
Contributor

ekhaled commented Aug 13, 2018

WRT to a conversation we had on discord.

Currently, we can wrap components in 2 ways:

  • using <slot>
  • using dynamic components.

Using slots means we cannot pass data from the wrapping component. The data has to come from the parent component itself.

Using dynamic components, we cannot proxy events. Because events do not bubble by default in svelte.

I think we are almost there, with dynamic components.
Here is a contrived example:

<!-- HOC.html -->
<svelte:component this={comp} {...props} />

<script>
  export default {
    computed:{
      props: ({comp, ...props}) => props
    }
  }
</script>

And use the above component like so:

<!-- App.html -->
<HOC comp={AnotherComponent} {a} {b} />

<script>
  import HOC from './HOC.html';
  import AnotherComponent from './AnotherComponent.html';

  export default {
    components:{
      HOC
    },
   data: () => {AnotherComponent, a: 1, b: 2}
  }
</script>

The problem arises when AnotherComponent has events that we would like to listen for in App.html.

Ideally, if we can proxy events somehow, I think that will complete the HOC story.
Maybe something like this:

<Hoc comp={AnotherComponent} {a} {b} on:* />

This would mean any events in AnotherComponent will bubble up through the HOC. And you can use HOC transparently within another component, as if you are using AnotherComponent itself.

Here is how other frameworks approach this:
React: https://reactjs.org/docs/higher-order-components.html
Vue: https://medium.com/bethink-pl/higher-order-components-in-vue-js-a79951ac9176

cc. @TehShrike

@ekhaled
Copy link
Contributor Author

ekhaled commented Aug 13, 2018

I can foresee book-keeping nightmares when implementing this.
For example, what happens when the value comp changes to another component... how do we then get rid of old listeners and attach new listeners.. etc.

From the point of view of someone using higher order components properly, the inner component will never change at runtime.
So I am open to alternative syntax, which ensures the wrapped component never changes.
Maybe even something like: <svelte:hoc>

@Rich-Harris
Copy link
Member

Of course, since components are just JavaScript constructors, you can construct HOCs today:

https://svelte.technology/repl?version=2.10.1&gist=b42e56c57e390b85e7c3c16349205cc6

For me there are two questions:

  1. are there things we could use that pattern for that could be done more ergonomically?
  2. are there things that we'd want to do (that HOCs are used for in other frameworks) that are not possible with that pattern?

on:* seems like a good candidate for the first question — it's more ergonomic than the event portion of this function from that example:

function withDefaultData(Component, data) {
  return function(opts) {
    opts.data || (opts.data = {});
    Object.assign(opts.data, data);
    const comp = new Component(opts);
    
    comp.fire = (name, event) => {
      Component.prototype.fire.call(comp, name, event);
      Component.prototype.fire.call(comp, '*', {
        name,
        event
      });
    };
    
    return comp;
  }
}

For the second question, anything involving the equivalent of a child's render() method is prohibitively difficult in Svelte, but that's why we implement the standard <slot>. What things are we missing?

@ekhaled
Copy link
Contributor Author

ekhaled commented Aug 13, 2018

That REPL is brilliant!
Unless I'm mistaken.. to keep data in sync, we would need to add manual observers, right?

Now that we have support for spread props, I think the only things missing are:

  • on:*
  • forwarding custom methods

For me, a positive end result would be, if this...

<Section {foo} {bar} on:input />
<Footer {baz} {qux} on:hover />

and this...

<FadeIn comp={Section} {foo} {bar} on:input />
<FadeIn comp={Footer} {baz} {qux} on:hover />

...behaved identically.

FadeIn would:

  1. Keep data in sync
  2. Expose all the custom methods from the wrapped component
  3. Forward all events from wrapped component, without requiring you to know of the events in advance.
  4. Add extra functionality on top

@Rich-Harris
Copy link
Member

You can still use regular props and bindings! https://svelte.technology/repl?version=2.10.1&gist=773955ca320356b9b16848127b39970f

I think you could implement most of FadeIn like so:

export default {
  components: {
    FadeIn: opts => new opts.comp(opts)
  }
};

...except that it wouldn't, well, fade in.

@ekhaled
Copy link
Contributor Author

ekhaled commented Aug 13, 2018

wow!!! how does that data stay in sync????

I suppose we can do custom methods too using (or something similar to):

hoc.customMethod = function(){
  wrappedComp.customMethod.apply(wrappedComp, arguments)
}

after we have pulled all the custom methods from the wrapped components prototype.

Hmmm, maybe this is something that could be done external to svelte core.
Just some general purpose way to eliminate the boilerplate above.

@Rich-Harris
Copy link
Member

The code that's generated internally is basically just this:

const thing = new Thing({
  ...
});

// later
function update(changed, ctx) {
  if (changed.whatever) {
    thing.set({ whatever: ctx.whatever });
  }
}

And if options includes a _bind property, it will set up binding as well (though incidentally I want to move away from that magic API towards using on('state', ...) instead).

So it doesn't care what Thing is as long as it can create a new one of whatever it is. Similarly, you can add new methods inside the function wrapper, or even modify the component prototype:

import Foo from './Foo.html';
import someMethods from './some-methods.js';

Object.assign(Foo.prototype, someMethods);

export default {
  components: { Foo }
};

@ekhaled
Copy link
Contributor Author

ekhaled commented Aug 13, 2018

Here is a WIP REPL with what has been discussed so far (for anyone interested).

Working:

  1. Data syncing
  2. event forwarding
  3. proxying custom methods

Note: It shows an error on initial load due to SSR problems. Just add a line break anywhere in App.html and it renders fine.

I think this can be made more general purpose.
Ideally CenteredButton should be called Centered, and it should center anything you throw at it.
Right now it's tied to the Button component.

I'll give it another shot tomorrow.

@ekhaled
Copy link
Contributor Author

ekhaled commented Aug 15, 2018

@Rich-Harris

What I set out to achieve

A higher order component (HOC) that will wrap any other target component (TC), and behave as if it was the target component itself.

This means HOC has to:

  1. Keep data in sync between itself and the TC
  2. Forward custom events from TC
  3. Expose custom methods available in TC's prototype

To that end, I have prepared (with lot of help and guidance from you) this REPL
It shows an error on initial load due to SSR problems (see below).
Just add a line break anywhere in App.html and it renders fine.

What works

  • Data sync works (again, thanks to you)
  • Event forwarding works, but it needs a little bit of markup and wiring up (see below)
  • Exposing custom methods works

What the compiler could (maybe) help with

If this was natively supported by the compiler, we could avoid:

  • Writing markup and wiring, to fire an innerEvent
  • Enumerating private prototype methods of TC to find custom methods to forward. See builtinMethods and it's usage in hoc-tools.js.
  • Adding a ref just so we could expose custom methods
  • SSR, cos SSR fails completely

Please have a look and let us know if this is something that could be supported by the compiler in some form.

@charly22
Copy link

@Rich-Harris

Of course, since components are just JavaScript constructors, you can construct HOCs today:

https://svelte.technology/repl?version=2.10.1&gist=b42e56c57e390b85e7c3c16349205cc6

I can't find the way to make this approach work on SSR since svelte components can not have defaults export since the default it's the component itself. There is any way to let the compiler to allow this?

On the other side, in a more philosophical one, do you believe this approach goes ok with the statement of "write less code"? I'm wondering if you're proposing this approach just as a proof it's is possible or as in a definitive solution?

@antony antony added the proposal label Apr 9, 2020
@antony antony added the stale label Jul 4, 2020
@antony antony closed this as completed Jul 4, 2020
@mckravchyk
Copy link

mckravchyk commented Oct 31, 2020

I wanted to create a data wrapper for a specific component, so the component itself will be a "pure component" and completely separated from the app as a whole, and the wrapper will provide the integration by passing down the props and events.

I'm not sure if it's the right way to do it, but it seems to just work and Svelte handles it very nicely as no extra dom node is created for the wrapper component.

/**
  * App.svelte
  */
// Embed the whole component with data bundled in
<NotesDataWrapper />
/**
  * NotesDataWrapper.svelte
  * Essentially Notes.svelte + data
  */
// Notes data, will be synced with the app data
const notes = []; 
// ...
// Embedding the component directly - no extra dom nodes are created
<Notes notes={notes} />
/**
  * Notes.svelte
  * A pure component which just displays the data that is supplied
  */
<div class="notes">
  {#each notes as note, index}
    <Note note={note} />
  {/each}
</div>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
awaiting submitter needs a reproduction, or clarification
Projects
None yet
Development

No branches or pull requests

6 participants