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

Proxy elements - or a marriage of decorators, parser transforms, and partials - covers lightweight, dynamic, and async components - RFC #3106

Closed
wants to merge 3 commits into from

Conversation

evs-chris
Copy link
Contributor

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

Description:

This adds a new VDOM construct tentatively called a proxy. A proxy is simply a placeholder that gets a context handle that is extended with a method, refresh that triggers the proxy to unrender and render again. Proxies look like regular elements in the template, kinda like components look like regular elements. They even automatically get access to a content partial that is the content of the proxied element. Since the template on the proxy is effectively dynamic, you can achieve two common requests for components - dynamic and async - with a proxy.

Also, parser transforms are removed.

Details

This is outdated... see 4th post below

For this PR Ractive has grown a new registry called proxies (open for suggestions on the name). It has also grown a new static method designed to create a proxy element, conveniently named proxy, which takes an init hash like a component would with extend. The only required param in the init has is a proxy function, which receives a handle object that is a context with a few bonus methods, notably refresh, which causes the proxy element to rerender. The proxy method is expected to return an intermediary instance that is used for communication between the proxy element and the proxy class, kinda like a decorator.

const thing = Ractive.proxy({
  proxy(handle) {
    return { template: '{{>content}}' };
  }
});

new Ractive({
  proxies: { thing },
  template: '<thing>I will just render this string</thing>'
});

That is a proxy that does nothing but render the content passed to the proxy. Not particularly useful, but it's about the simplest example of a proxy that could possibly exist. Since components are checked before proxies, here's what a simple async component proxy would look like:

const Async = Ractive.proxy({
  proxy(handle) {
    const middle = { template: 'loading...' };
    loadAComponentSomehow(handle.name).then(cmp => {
      handle.ractive.components[handle.name] = cmp;
      middle.template = handle.proxy.template;
      handle.refresh();
    });
    return middle;
  }
});

new Ractive({
  proxies: {
    ComponentA: Async,
    ComponentB: Async
  },
  template: '<ComponentA /><ComponentB>content...</ComponentB>'
});

CSS

Since I also want to target lightweight components as proxies, they also support Ractive's scoped CSS, including CSS functions. This means that you can use the ractive-bin to create lightweight components that are roughly as expensive as partials with scoped CSS in the single-file format. This is actually the main reason that there is a constructor function on the Ractive constructor - so that the styling can get set up and to grant access to thing.styleSet(...).

<div class="material-card"><!-- probably some more structure to this, but you get the point -->
  {{>content}}
</div>

<script>
  const template = $TEMPLATE;
  export default Ractive.proxy({
    noCssTransform: true,
    css: $CSS,
    proxy(handle) {
      return { template };
    }
  });
</script>

<style>
  .material-card {
    /* all of the material card styles... */
  }
</style>

Questions

Question 1: I was pondering an update method for the intermediary, kinda like decorators get. There's already an invalidate, which if supplied, is called any time the bit of VDOM containing the proxy is told to update. In order for update to work, the proxy would have to define attributes that it wants to keep to itself e.g. <Async name="ComponentA" /> could say attributes: ['name'] and get an additional attrs object in the proxy method and also any time those attributes updated, an optionally supplied update method in the intermediary would be called, allowing <Dynamic name="{{.type}}" />.

Note, since the handle is a context object, you can already set up context-aware observers and the like. You can't, however, use Ractive's nice expression support to allow just about any computation to be passed to the proxy e.g. <Something foo="{{.bar * 10 + 2}}" />.

So, should I add attributes and support for an update method?

Question 2: Should any unclaimed attributes be supplied in an additional {{>extra-attributes}} partial like the one provided to components?

Question 3: Along those same lines, the content of the proxy tag is supplied as the {{>content}} partial (and yielding means nothing in this context, because it's already in the same context as the surrounding template). Should the entire proxy template also be supplied as maybe {{>initial}} or {{>outer}} or {{>outer-content}} or something like that?

Question 4: When added to attributes and update support, any attributes claimed by the proxy would be removed from the outer template partial. Does that sound useful, or at that point are you going to be messing about with template AST anyway, so no real benefit?

Question 5: Any suggestions for better names? I've also considered placeholders, transformers, lightweights, and probably a few more, but I don't know that any were better. Proxies feels a bit... strange, I guess.

Fixes the following issues:

#3099 #3089 and probably some others.

Is breaking:

Yes, in that proxies and proxy are added to Ractive constructors. Parser transforms are also removed in this because proxies provide the same approximate service in a more flexible, runtime-friendly way.

TODO

  • Tests
  • Finalize the names
  • Advanced attribute support?
  • Allow for proxy registries?
  • Allow for proxy inheritance hierarchy?

@evs-chris
Copy link
Contributor Author

Oh yeah... the template on the intermediate uses partial resolution, so you can specify script tags by id or strings to be parsed as templates if you have runtime parsing - or you can directly reference parsed templates. It's generally best to avoid runtime parsing, and the ractive bin is currently the best way to do that in general - and the only way to do that with this construct, as I believe rcu/rvc and friends are tied to components only.

@ceremcem
Copy link

@evs-chris Is there any way to test it out via Playground or something?

@evs-chris
Copy link
Contributor Author

After sleeping on it, I have another question: should proxies also have the ability to contribute to registries e.g. provide decorators, transitions, and friends? My gut says yes, and we may even want to be able to create an inheritance hierarchy with proxies in a similar manner to components at some point.

To allow that at some point, I've adjusted the Ractive.proxy function to take a function as a first arg followed by an optional options hash. The simple example from the initial post then becomes:

const thing = Ractive.proxy(handle => ({ template: '{{>content}}' }));

new Ractive({
  proxies: { thing },
  template: '<thing>I will just render this string</thing>'
});

Any options like css would be supplied in a hash after the function e.g.

Ractive.proxy(
  handle => {
    // do stuff here
  },
  {
    cssId: 'my-proxy',
    css: '.my-styles { color: pistachio; }'
  }
);

Back around to registries, that would also open up the possibility of blocking proxy names from within the proxy to prevent infinite recursion e.g. with a a proxy that renders an a element. Having Ractive.proxy(..., { proxies: { a: false } }) would stop Ractive from using the a proxy in template contained by an a proxy in much the same way that components can prevent recursion.

@evs-chris
Copy link
Contributor Author

evs-chris commented Oct 11, 2017

If anyone wants to try out the current iteration of this PR in the playground or somewhere similar without having to build it, I've published it as @evs-chris/ractive@1.0.0-proxy-001, so you can pull it in from https://cdn.jsdelivr.net/npm/@evs-chris/ractive@1.0.0-proxy-001/ractive.js.

Note that while I have tested it in the sandbox, actual tests are still on my TODO list pending feedback.

@evs-chris
Copy link
Contributor Author

Here's a playground with a moderately featured async component proxy that pulls its loading message from an optionally supplied loading content element that is not passed through to the loaded component. Any attributes other than name are also passed through to create mappings on the target component.

There's also a synchronous component in place and commented out so you can see how the async proxy behaves if the component is already loaded.

Note: I found a few bugs while playing, so the version is now 1.0.0-proxy-003.

including excluding them from the main template, gathering them to be tracked for the intermediary, providing an extra-attributes partial, and firing them on the inital intermediary call and optionally on each update
@evs-chris
Copy link
Contributor Author

I've added claimed attribute support for proxies, which means you can specify that the proxy will consume certain attributes by name, and if they're dynamic, you can supply a callback to be called whenever they change. They are also passed in on the intermediary creation call.

const Proxy = Ractive.proxy((handle, args) => {
  // args is a map of attribute values that are claimed in attributes below
}, {
  attributes: ['name']
});
<Proxy name="{{someName}}" class="foo" />

There, name will be claimed by the proxy, so it won't be present in the template available on the handle or in the now-supplied extra-attributes partial. Everything else is though - in this case class="foo".

Here's the async component example updated to make the loaded component dynamic based on the async-name attribute. By changing the name in the input, you will cause a different component to be swapped in. The first time a new name appears, it will go through the mock loading cycle, and any subsequent appearances will be synchronous because it's already available.

@fskreuz
Copy link
Contributor

fskreuz commented Oct 13, 2017

Been off the grid for 3 days 🎉

Here's an example of the minimalistic API I was thinking of based on the comments from previous discussions (like #3089 (comment)). The gist is that an async component should just be like any other component, albeit loaded and rendered differently. Underlying machinations may be different, but at least authoring experience shouldn't cause a drastic change.

// AsyncComponent.js - write in vanilla or component file format
export default Ractive.extend({ ... })

// RegularComponent.js - write in vanilla or component file format
export default Ractive.extend({ ... })

// App.js
import RegularComponent from './RegularComponent'
import Loader from 'some-loader'

// Loader method that returns a function that
// 0. Is called by Ractive when it needs the component.
// 1. Returns a promise.
// 2. Loads the component (Ractive doesn't care how).
// 3. Does some pre-processing (Ractive doesn't care if it does).
// 4. Resolves the promise with the constructor.
// 5. Resolution causes any existing usages to refresh/toggle/magically render
// 6. Ractive caches the constructor, as if it were defined in `components`.
const AsyncComponent = Loader.load('AsyncComponent.js')

const App = Ractive.extend({
  components: { RegularComponent },
  asyncComponents: { AsyncComponent },
  template: `
    <RegularComponent />
    <AsyncComponent/> <!-- this would re-render if it was already rendered -->
  `
})

App({ target: '#app' })

should proxies also have the ability to contribute to registries e.g. provide decorators, transitions, and friends?

In the above case, since the async component is written just like any other component, I would expect that plugins would still be possible and follow existing rules (i.e. plugins defined in components are component-scoped), CSS would be re-evaluated (i.e. async component CSS appended), that sort of stuff.

My 2c

@evs-chris
Copy link
Contributor Author

evs-chris commented Oct 13, 2017

My main issue with async components as a built-in like that is that there is no way to show a loading placeholder while you wait on the promise to resolve. With <Async component="MyComponent" other="params">...</Async>, you get the opportunity to throw in whatever you want for a placeholder - including nothing if that works for you. A cherry on top - having css scoped for the proxy means that you can also style your placeholder independent of the loaded component :).

That's also ignoring the impl details, which would effectively be building an async proxy plugin directly into ractive to handle the impedance mismatch between the sync render and async load.

@fskreuz
Copy link
Contributor

fskreuz commented Oct 13, 2017

I was thinking of using the component innerHTML but I guess we have plans for using it for yielded content. Hmm... conditional based on the registry and the resolution? 😁

{{async MyAsyncComponent }}
  <AsyncComponent />
{{else}}
  <MyLoadingAnimationComponentThatIsAlreadyThere />
{{/async}}

(elegance just left the building, hahaha 😁 )

@monoblaine
Copy link
Contributor

What I'm going to say is probably almost off-topic, but I'm concerned about the naming. Will using the name "proxy" create confusion regarding the proxy events?

@evs-chris
Copy link
Contributor Author

evs-chris commented Oct 14, 2017

Definitely not off topic - that's one of the main questions to resolve before merging this 😀

@fskreuz
Copy link
Contributor

fskreuz commented Oct 16, 2017

Referencing related implementation of stuff:

evs-chris added a commit that referenced this pull request Oct 19, 2017
or - second swing at replacing parser transforms with a runtime construct
@evs-chris evs-chris closed this Oct 23, 2017
@evs-chris evs-chris deleted the proxy branch October 23, 2017 02:31
evs-chris added a commit that referenced this pull request Oct 23, 2017
or - second swing at replacing parser transforms with a runtime construct
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.

None yet

4 participants