-
Notifications
You must be signed in to change notification settings - Fork 53
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
Support hydratable actions #237
Comments
The way we've handled more trivial cases is to use At this moment I can't commit time to developing this functionality out but am open to PRs for it if they include test and a documentation PR as well. My main concern stems from how these are perceived to interact with Svelte use:actions which I am not 100% up to speed on. |
They don't. In the proposal, the action function lives in a standalone JS file, which makes it impossible to use svelte reactive syntax. Therefore it is just a simple function receiving an element. After digging into Elderjs source code, I found that Elderjs doesn't strictly depend on svelte. Currently it works like this:
Only the first step is directly bound to svelte. The 2nd~4th steps can actually be applied to any component framework, even a native web component, and a simple function that getting a mount target. Here is the idea, abstract everything into a single html attribute: <!-- svelte component -->
<Foo hydrate-client={props} hydrate-options={options} />
<!-- after preprocess -->
<div class="foo-component" ejs-mount={JSON.stringify(["Foo", props, options])}></div> <!-- svelte action -->
<div class="container" hydrate-use:Foo> ... </div>
<!-- after preprocess -->
<div class="container" ejs-mount={JSON.stringify(["Foo", , {prerender: false}])}> ... </div>
<!-- don't prerender by default when the tag doesn't close immediately or the component doesn't support ssr --> The postprocessor should know nothing about the framework. It will collect <!-- rendered html -->
<div class="foo-component" ejs-mount="["e;...]"></div>
<!-- after postprocess -->
<div class="foo-component" ejs-id="fooXYZ"> ... prerendered content ...</div> <!-- rendered html -->
<div class="container" ejs-mount="["...]"> ... </div>
<!-- after postprocess -->
<div class="container" ejs-id="fooXYZ"> ... </div> You can even ssr a vue component in svelte template via Each framework should provide a factory function to decorate rollup/esbuild config to add their own plugin/preprocessor. Each framework should compile their components into a common interface: // client
export default (target, props) => void;
// ssr
export const render = (page, props) => {head, html, css}; As the result, the action component can be implemented as an empty framework. It just have to rollup |
Instead of using default export, it should probably use a private name so it won't change the signature of the component module? // client
export const __ejs_mount = (component, target, props) => void;
// ssr
export const __ejs_render = (component, page, props) => {head, html, css}; |
Yep, mainly concerned with the @eight04 You nailed it. The early versions of Elder.js were designed to be front end framework agnostic. I had long term visions of implementing Relevant history:Very early versions of Elder.js used plain string literals within a custom Then to mount a svelte component, you imported the // ... imports, etc
modules.export = ({helpers, props}) => html`
<body>
${svelte({props, for, the, component})}
</body>
` The helpers function included things like If we do implement the signatures you show above, I'd encourage us to also accept Pros/ConsPros: Writing sites with Cons: The biggest con was that early testers REALLY wanted to write in Why this context:If I had to do it all over again, I would have probably stuck with just plain JS functions or I share this because I am 100% open to this proposal and will fully support whoever is spearheading it. The biggest issues in my mind are:
Chances are you probably have better understandings of the implications of this. 👍 As you've probably seen from the code, I don't come from an engineering background. I just know what I wanted to build and have learned while writing Elder.js. 100% open to improvements and ideas. My biggest bottleneck at the moment is dev time. |
If those "actions" are implemented as empty components (vanilla component?), it might make more sense to re-use the <!-- Home.svelte -->
<script>
// use a special suffix to determine components from regular scripts?
import randomBGColor from "../../components/randomBGColor.vanilla.js";
</script>
<div class="container" hydrate-client:randomBGColor>
... lots of content ...
</div> |
@eight04 I'm good with this syntax. Few additional notes that hit me while reviewing #240 that should be considered should we want to allow other front end frameworks. This should probably be moved into a new ticket if we want to pursue it, but in the effort of keeping ideas together, here are some thoughts:
|
We can ask components to include their CSS via static
For example: // compiled Foo.svelte (ssr)
... compiled component code ...
import "path/to/foo.svelte.css";
import "other/css/file.css"; // after transform
... compiled component code ...
import _css0 from "path/to/foo.svelte.css";
import _css1 from "other/css/file.css";
export const _css = [_css0, _css1]; Add another transform function transforming CSS code to a default export: function transform(code, id) {
if (id.endsWith('.css')) {
// and maybe minify css here
return {code: `export default ${JSON.stringify(code)}};
}
} |
Does it mean that Elderjs collects CSS from all components? If a component is only used by a single page, will its CSS be injected to the whole site? That is, if we don't want to support |
This is roughly what the Svelte Rollup Plugin does and could work depending on our full implementation.
Yes, exactly. The
We still need a component to give us the CSS so we can compile it into a |
Here is a POC based on dev-html-parser: When working on the POC, I found that some changes in #240 (e.g. renaming Roadmap:
|
In the POC, we use a In esbuild, you can only generate contents in the
In either way, source map will be broken unless we use something like sorcery. |
@eight04 getting a chance to test this now. Carved out a couple hours.
Large PR is fine with me. |
When trying to run it on the template project. I’m getting a ‘frameworks’ is not an interable error but after some investigation (adding
Overall, amazing PoC. I can see how this can be extended to support other frameworks pretty easily. Pretty cool how you used the Super exciting. Notes on the PoC:
Regarding esbuild, I’m sure you saw the https://vitejs.dev/guide/api-plugin.html This could be a win. I also know that they support HMR for both the client and the server, so that could be a win for us. I haven’t looked at the implementation. I'll do some more reading tonight. |
The PoC doesn't set a default framework. That's probably why you got a
Yes I think it is possible. It is also the core feature to support "actions". For example: <!-- Layout.svelte -->
<script>
import Foo from "../components/Foo.component.js";
</script>
<div class="container" hydrate-client:Foo>
... contents ...
</div> You can also statically render other components from a different framework: <!-- Layout.svelte -->
<script>
import Foo from "../components/Foo.vue";
</script>
<div class="container">
<Foo hydrate-client={{a: "b"}} hydrate-options={{loading: "none"}}/>
</div> Technically, there is no need to import the component via
They are sorted by the import order like JS. I did see that we have some logic to sort CSS files, but personally I don't need this feature.
Yeah I forgot to modify the script loader i.e. For esbuild, I'm trying the 3rd approach i.e.
Since esbuild places bundled CSS along with the JS file with the same filename, we can easily component CSS by changing the extension e.g. |
100% right on both accounts. First time seeing CSS sorting: We want Investigating how SSR is done in |
Here is the PoC for esbuild: Esbuild sorts CSS by the import order too. It seems that it records input filenames inside bundled CSS when Notes:
The logic for watch mode is probably broken in the PoC. In my mind, we need:
|
Since HMR happens at client side, I guess we have to move the entire render process into client to support HMR. Or create some kinds of modules to communicate with the server and replace statically rendered HTML. |
Diving into the PoC now and have 4 hours chunked out to play with this and think about how to add other frameworks.
I think we could also possibly detect the changes on the server and clear the |
Needed to bump svelte to Exciting. As you can probably tell bundling is not my strong suit and it looks like you’ve got a solid grasp of what is needed so I spent my time:
Vite:I can’t seem to get a vite working with SSR. Our model really doesn’t match there. The amount of plumbing we’d need to do would be wild based on my current understanding. We would basically need to build in memory the entry file for each request, this alone isn’t too difficult, the hard part is with how Elder.js handles hydration and mounting the components. It just doesn’t seem to mesh with vite's model of how one builds a website. Even if we figured all of this out, it appears we’d need to write our own HMR code. This was a deep rabbit hole but I feel confident that we’re not losing anything, not going with vite. Other Frameworks:
Questions:
Things of note:
Super well done man. Amazed at your progress on this. I've got notes on the esbuild code, but honestly I think you've got it all under control based on your #fixme comments. |
Thinking more on this, if we moved to plain html templating as discussed above, maybe preprocessing isn’t required as it is only really a syntactic sugar. Basically this would leave us with a simple function to inline components (that matches the preprocessing). |
After checking their code, I found that there are two problems in our adapter:
// elder.config.js
const svelte = require("@elderjs/integration-svelte");
const vue = require("elderjs-integration-vue"); // 3rd-party plugins
module.exports = {
...
integrations: [svelte(), vue()]
}
There are three ways to apply postcss transform:
Something like this? <!-- Layout.svelte -->
<script>
import {hydrateComponent} from "@elderjs/elderjs/helper";
</script>
<div class="container">
{@html hydrateComponent({name: "Foo", props: {a: "b"}})}
</div>
<div class="container2" {...hydrateComponent({name: "Bar", type: "attrObject"})}>
... lots of contents ...
</div> One thing comes into my mind: in vue, we can only inline raw HTML inside a wrapper element via
I think we can provide a helper function (it can produce raw HTML |
I like
This is a good idea.
You've definitely got this under control. Whichever you think works best given the problem space I'm good with.
Great idea, we have Adapters: How would Plain JS Components Be Bundled?How would we support plain js like components in the bundling process? Below is an early In my mind, it highlights 2 questions:
// modified menu.js component
module.exports = (props) => {
return {
css: `
.menu-toggle-wrapper {
padding-right: 15px;
}
#menu-toggle {
font-size: 12px;
padding: 0.25rem 0.5rem;
font-weight: bold;
}
#menu-toggle[data-open='true'] {
background-color: $dark-blue;
color: $white;
}
`,
js: `
document.getElementById('menu-toggle').addEventListener('click', function(e){
if(e.target.dataset.open === 'false'){
document.getElementById('sub-nav').classList.remove('d-none');
e.target.dataset.open = true;
} else {
document.getElementById('sub-nav').classList.add('d-none');
e.target.dataset.open = false;
}
});
`,
html: `
<div class="menu-toggle-wrapper d-md-none">
<div id="menu-toggle" class="btn btn-small btn-outline-dark-blue" data-open="false">MENU</div>
</div>
<div id="sub-nav" class="row flex-nowrap justify-content-between align-items-center d-none d-md-flex flex-column flex-md-row m-md-0 p-md-0">
<ul>
${
(props.links
.map((l) => {
return `
<a href="${l.permalink}" ${l.permalink === page.permalink ? "active" : ""}>${l.label}</a>
`;
})
.reduce((out, cv) => out + cv),
"")
}
</ul>
</div>`,
};
}; It seems that we could just modify the export const __ejs_render = (comp, props) => {
const { head, html, css, js, footer } = (comp.default || comp).render(props);
// push css, js, footer to the respective stacks?
return { head, html, css, js, footer };
}; Upgrade Path: Server/Hydrated ComponentsBeyond bundling, one thing worth considering if we move to plain js for templating is what the upgrade path looks like for existing sites. My team and I have 5+ sites to upgrade so today I was working through how we communicate how to mount server side only templates/layouts. Here is what I had imagined migrating the default layout to: // layout.js
export default ({ request, data, helpers, settings, templateHtml }) => {
// a template is assumed to be HTML if it returns a string?
return { html:`
<div class="container ${request.route}">
<!-- hydrateOptions is quiet verbose... too big of a breaking change to move to 'options'? -->
${helpers.component({ name: 'nav', props: { a: 'b' }, hydrateOptions: { loading: 'eager' } })}
${templateHtml}
<!-- assumed to be server because no hydrate options? -->
${helpers.component({ name: 'footer', props: { links: [{ href: '/', text: 'home' }] } })}
</div>
`};
}; Then for routes: // ./src/routes/home/Home.js (I feel that this should probably be standardized into ./src/routes/home/template.js)
export default ({ request, data, helpers, settings }) => {
// assumed to return html if it returns a string?
return { html: `
<!-- The existing route structure would access "Home.svelte" by default...
...the only problem is that we should probably make the template name available on the "request" object in Elder.ts
as currently we allow templates to be explicitly defined in the routes... not just determined by the name.
This would be a server side only "Svelte Template" in current Elder.js lingo.
-->
${helpers.component({ name: request.route, props: { request, data, helpers, settings } })}
` };
}; Having played with the idea of This pattern will also demystify why "Svelte Templates" and "Svelte Layouts" in Elder.js get these magical helpers and why these magical helpers can't be hydrated. Absolutely love the progress. I'll drop you a note on your GitHub email to talk about getting you maintainer access. |
Another problem, our render function is sync instead of async. Async is required for vue: https://www.npmjs.com/package/@vue/server-renderer I think a vanilla component will look like this: // Menu.component.js
// vanilla components will have `.component.js` extension (.ts, .mjs, etc?)
// CSS can be written in CSS file
// also note that currently returning CSS in `render` function only works when elderConfig.css === 'inline'.
import "./Menu.css";
export const render = process.env.componentType === "server" && (props) =>
`... <ul>${props.link.map(...)}</ul> ...`;
export default function (node) {
// node is the mount target
node.getElementById('menu-toggle').addEventListener('click', function(e){
if(e.target.dataset.open === 'false'){
node.getElementById('sub-nav').classList.remove('d-none');
e.target.dataset.open = true;
} else {
node.getElementById('sub-nav').classList.add('d-none');
e.target.dataset.open = false;
}
});
} // Layout.component.js
import {render as footer} from "../components/Footer.component.js";
export const render = ({ request, data, helpers, settings, templateHtml }) => `
<div class="container ${request.route}">
${helpers.component({ name: 'Nav', props: { a: 'b' }, hydrateOptions: { loading: 'eager' } })}
${templateHtml}
<!-- if footer is also a vanilla component, we can use its render() directly, which will be faster -->
<!-- Footer.component.js won't be hydrated -->
${footer({links: [...]})}
</div>
`; // src/routes/home/Home.component.js
export const render = ({ request, data, helpers, settings }) => {
// this would probably resolve to ___ELDER___/compiled/components/Home.js
// which can be compiled from src/components/Home.svelte, src/components/Home.vue, etc.
const comp = require(helper.findComponent(request.route).ssr);
const result = comp.__ejs_render(comp, {request, data, helpers, settings});
return {
head: result.head,
html: `
<!-- note that we can't use helpers.component() in this case, because some props can't be JSON.stringify -->
${result.html}
`
};
}; Note that you can still use
SSR components can access I'm willing to be a maintainer. We will have some platform issues e.g. |
@eight04 anything I can do to help move the ball forward on this? |
I'm quite busy until May 15 so I may not be able to wrap the PoC into a PR. Stuff that still needs to be done:
|
Since a hydratable component can only be a leaf node, it is hard to add js code to parent without hydrating the entire sub-tree.
Workaround
Create an empty client component:
Proposal
Support hydratable actions:
I think it should similar to but simpler than hydratable components?
hydrate-use
with an ID and push the info to a stack.*.js
files in the component folder. (Or should we have a newactions
folder?).Maybe it is possible to implement it in a plugin?
The text was updated successfully, but these errors were encountered: