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

Partial-based alternative to proxies - incl async components - RFC #3111

Merged
merged 8 commits into from
Oct 23, 2017

Conversation

evs-chris
Copy link
Contributor

@evs-chris evs-chris commented Oct 19, 2017

Description:

This is an alternative version of #3106 that builds on top of partials rather than introducing a new vdom construct. I have dubbed the updated partial items super partials or macro partials. There aren't too terribly many functional differences between this an the proxy PR, notably

  • proxy is replaced with macro e.g. Ractive.macro( ( handle, attrs ) => ..., { options } )
  • macro partials live alongside regular partials e.g. new Ractive({ partials: { macro: Ractive.macro(...) } })
  • rather than modifying a common template member of the intermediate object, this makes template updates a bit more explicit by giving the handle a setTemplate method that can be called at any time

This also adds support for async components - components that are initially set as a promise for a component or a function that returns a promise for a component - by using an inline macro partial that resets when the component constructor becomes available. You can even specify a loading placeholder by passing a special inline partial e.g.

<SomeComponentThatIsAsync>
  {{#partial async-loading}}Loading...{{/partial}}
</SomeComponentThatIsAsync>

It might make sense to have an async-loaded partial too, to allow cross-fade transitions when the component loads in.

Note that an external dynamic async plugin would still be easily available if you prefer the <Async component="{{name}}" />-style usage.

Status

This isn't exactly complete, as there are still quite a few tests that need to be added, most notably regarding plain partial syntax that resolves to a macro partial. I think a few hooks would be nice to, like render and unrender. There's a teardown, but in hindsight, it should probably be moved to unbind rather than unrender on the host partial. There are now tests, and I think the hooks are appropriately set up.

I will be pushing out a demo build to npm once I've covered a few more bases. I'll add some more description and examples once that's done. Until then, if you're intereseted, you can take a look at the included tests to kinda get an idea of what you can do with this.

See this comment for published info and a playground.

Fixes #3099

@fskreuz
Copy link
Contributor

fskreuz commented Oct 20, 2017

So this means reserved partial names, yes?

…p turn

also fills out remaining bits of partial test suite along with a few other random places
@evs-chris
Copy link
Contributor Author

For those wondering about the reserved partial names, yes, this introduces three new special names that only apply if a component happens to be async:

  1. async-loading - if supplied, is rendered while the promise remains unresolved.
  2. async-loaded - if supplied, is used to render the actual component rather than just dumping it into the template directly. This is handy for setting up a transition in without having to have custom code on the component to handle it being async, and it requires -
  3. component - if using async-loaded this is the template that would be dumped straight into the parent were you not using async-loaded.

Only the first two are supplied by the user e.g.

<cmp>
  {{#partial async-loading}}loading...{{/partial}}
  {{#partial async-loaded}}<div burst-like-an-alien-in>{{>component}}</div>{{/partial}}
</cmp>

Notice that the component partial is automatically provided.

I chose those names because there's not a lot of wiggle room for mistaking what they do. I'm certainly open to suggestions on any naming if there are any other opinions 😄

@evs-chris
Copy link
Contributor Author

Here's the full skinny on async components support in this PR:

  1. You can set a component in a registry as a promise e.g. components: { cmp: import('./cmp.ractive.html').then(m => m.default) } in webpack and any instances of <cmp /> that try to render before the promise resovles will get a placeholder. This is where the async-loading partial passed into the component body comes into play. If there is one, the placeholder will consist of that template.

  2. When the promise resolves, the placeholder will set the component on the current ractive instance so that future renders are fully sync. It will also take the original template e.g. <cmp some="{{mapping}}">content</cmp> and dump it into the placeholder, replacing the async-loading template if applicable.

    1. This is where the async-loaded template comes into play. If supplied, the async-loaded template is dropped into the placeholder, replacing the async-loading template if applicable. The original component template is the supplied as {{>component}} for use within the async-loaded partial.
  3. It's probably not very helpful to shove a promise directly into the components hash because promises kick off immediately. This is where ractive's function support in most registries comes in handy: you just register a function that spins up the promise and returns that. This way, the component load doesn't start until absolutely necessary. components: { cmp () => import('./cmp.ractive.html') }

  4. As it stands, the promise for the component is not considered by the promise that is returned by the current operation, if any. This means that if the component is in something like {{#if foo}}<cmp />{{/if}}, then the promise returned by r.toggle('foo') won't include the load of the component or any associated transitions. I thought about tying that in, but it seemed YAGNI for the amount of extra code and general overhead that would require.

  5. All of this is implemented in one fairly small chunk of code right here. It's a macro partial that's built in because it's slightly more coupled to the ractive guts than would be possible to implement as a plugin. This is mostly because it steps in when a component is found to make sure that component constructor is not a promise (or thenable, as it were).

…g fragment

this allows a method to set a local context alias for the macro that isn't shared by any other nodes that happen to be in the same parent fragment
@evs-chris
Copy link
Contributor Author

I have pushed out a demo build to npm as @evs-chris/ractive@1.0.0-macro-002. Here's a little demo with a macro partial and an async component if you want to play around a bit. I added an aliasLocal method to the macro partial handle so that macros can have local data without mucking about in the context.

It also occurred to me that there should probably be an async-failed partial for async components in case the promise rejects for some reason, no?

…n rejected async component

also add async-failed special partial for async components
@evs-chris
Copy link
Contributor Author

There's now an async-failed special partial for async components, to be used should the component promise be rejected. The error is set on the macro-local model with an alias made available within the macro as error, so with a typical error, you can display a message with {{error.message}} if you need to.

I feel like this is pretty well done and is much better than the proxy version. The only thing that seems like it would be a near-necessity would be having the ability to block same-named partials from being loaded within the nested template by allowing a partial registry on the macro.

Any objections to using the partial registry for macro partials rather than some other registry? Either way, the partial vdom item will be responsible for them.

Any objections to going ahead and merging this as experimental to be made available in the next 0.9 release?

@fskreuz
Copy link
Contributor

fskreuz commented Oct 22, 2017

LGTM 👍

@evs-chris evs-chris merged commit 388dec0 into dev Oct 23, 2017
@evs-chris evs-chris deleted the super-partial branch October 23, 2017 02:36
@ceremcem
Copy link

When something fails, we might want to show something via async-failed, but we would probably retry or do something else via a handler (function).

@evs-chris
Copy link
Contributor Author

That's a bit beyond the scope of what ractive can handle. You'd need to cover that in the promise that you hand to ractive e.g.

MyComponent: () => import('MyComponent.html').then(module => module.default, error => {
  // here's your opportunity to do advanced error handling/retry
});

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

Successfully merging this pull request may close these issues.

Question: Async/lazy/dynamic components/elements
3 participants