Svelte looks really promising in places where Stimulus needs a lot of dom manipulation.
Rails, webpacker with it isn't that good with svelte yet. But I can share with you a small improvement into loading the components. If someone wants to build a gem/make a pr over here at webpacker would be cool!
// in the pack application.js
document.addEventListener('turbolinks:load', () => {
context.keys().forEach((file) => {
const componentName = file.replace(/\.\/|\.svelte/g, '');
const targetId = `svelte-${componentName}-root`;
const root = document.getElementById(targetId);
if(!root){
return;
}
const requiredApp = require(`./svelte/${componentName}.svelte`);
const props = root.getAttribute('data-props');
let parsedProps = {};
if(props){
parsedProps = JSON.parse(props);
}
new requiredApp.default({
target: root,
props: parsedProps
});
});
});
# a helper
def svelte_compontent(name, props)
content_tag(:div, nil, id: "svelte-#{name.dasherize}-root", 'data-props': props.to_json)
end
render the component
<%= svelte_compontent("app", {name: "webpacker"}) %>
Here is just load the components if the mount point is on the page and assign the props.
It would be really cool if we will have __SSR__ for svelte in the future, I've tried sapper and it's really fast, which it has it enabled
The improvement, reduces the js bundled. You don't want to load the same library over and over if you have multiple components on the same page
Hey @Rich-Harris could you point me to an example where I can see how to render a svelte component from js? I will then try to integrate it either with https://github.com/airbnb/hypernova-ruby or https://github.com/rails/execjs
Maybe we can make svelte more popular with rails developers after
@sebyx07 i have figured out how to compile into a function using https://github.com/bordeeinc/svelte-ruby -- but the compiled code is too es6y for execjs. i got stumped there. happy to chat on this. i actively am trying it out on a project. rails, + svelte + ssr would truely be the ultimate stack.
Feel free to create a PR for a webpacker guide that includes this info.
This is my latest config, fully turbolinks compatible. Destroying, unmounting
context = require.context("../svelte", false, /.svelte/);
document.addEventListener('turbolinks:load', () => {
if(window.initializedSvelteComponents){
window.initializedSvelteComponents.forEach((component) => {
component.$destroy();
});
}
window.initializedSvelteComponents = [];
context.keys().forEach((file) => {
const componentName = file.replace(/\.\/|\.svelte/g, '');
const targetSvelteName = `svelte-${componentName}-root`;
$(`[svelte-root=${targetSvelteName}]`).each(function () {
const root = this;
if(root.childNodes.length > 0){
root.childNodes.forEach((el) => { el.remove() });
}
const requiredApp = require(`../svelte/${componentName}.svelte`);
const props = root.getAttribute('data-props');
let parsedProps = {};
if (props) {
parsedProps = JSON.parse(props);
}
const component = new requiredApp.default({
target: root,
props: parsedProps
});
window.initializedSvelteComponents.push(component);
});
});
});
and helper
module SvelteHelper
def svelte_component(name, props = {})
name = name.to_s
options = {}
options[:'data-props'] = props.to_json.html_safe if props.present?
options[:"svelte-root"] = "svelte-#{name.dasherize}-root"
content_tag(:div, nil, options)
end
Since this is a svelte thread, butting in. Does anyone have issues with onMount?
Getting: onMount Error: Function called outside component initialization which the error tree points up to turbolinks.
Since this is a svelte thread, butting in. Does anyone have issues with
onMount?Getting:
onMount Error: Function called outside component initializationwhich the error tree points up to turbolinks.
You might need to add await tick() before the next code execution as it waits for the DOM to finish it's job first before calling the next function.
Based on @sebyx07's great suggestions, this is the config I currently use, doesn't require jQuery and allows svelte root components and others to be in the same directory. I also join more context to see the complete setup.
// app/javascript/lib/svelte_config.js
document.addEventListener('turbolinks:load', () => {
if (window.initializedSvelteComponents) {
window.initializedSvelteComponents.forEach(component => component.$destroy())
}
window.initializedSvelteComponents = []
for (const domComponent of document.querySelectorAll("[svelte-root-component]")) {
domComponent.childNodes.forEach((el) => { el.remove() })
const componentName = domComponent.attributes['svelte-root-component'].value
const requiredApp = require(`../svelte/${componentName}.svelte`)
const props = domComponent.getAttribute('data-props') || '{}'
const svelteComponent = new requiredApp.default({
target: domComponent,
props: JSON.parse(props)
})
window.initializedSvelteComponents.push(svelteComponent)
}
})
// app/javascript/packs/svelte_app.js
import * as turbolinks from 'turbolinks'
import '../lib/svelte_config'
import '../styles/svelte_app.scss'
turbolinks.start()
<!-- app/views/layouts/svelte_app.html.erb -->
<!DOCTYPE html>
<html lang="<%= I18n.locale %>">
<head>
<!-- ... -->
<%= javascript_pack_tag "svelte_app" %>
<%= stylesheet_pack_tag "svelte_app" %>
</head>
<body>
<%= yield %>
</body>
</html>
# app/helpers/application_helper.rb
module ApplicationHelper
def svelte_component(name, props = {})
name = name.to_s
options = { "svelte-root-component" => name.dasherize }
options['data-props'] = props.to_json.html_safe if props.present?
content_tag(:div, nil, options)
end
end
# app/controllers/demo_controller.rb
class MessageController < ActionController::Base
layout "svelte_app"
def show
@message = "Hello from Rails!"
end
end
<!-- app/views/demo/show.html.erb -->
<%= svelte_component("Message", message: @message) %>
<!-- app/javascript/svelte/Message.svelte -->
<script>
export let message
</script>
<p>
{message}
</p>
@sedubois - solution worked for me. Thanks for sharing!
@sedubois - did you happen to get <svelte:head> working? (I'm assuming all the other svelte:XYZ don't work either).
I get this error when I try to go from one page to another:
index.mjs:289 Uncaught TypeError: Cannot read property 'removeChild' of null
at detach (index.mjs:289)
at Object.d (Users.svelte:17)
at destroy_component (index.mjs:1718)
at Users.$destroy (index.mjs:1866)
at svelte_config.js:6
at Array.forEach (<anonymous>)
at HTMLDocument.<anonymous> (svelte_config.js:3)
at Object../node_modules/turbolinks/dist/turbolinks.js.e.dispatch (turbolinks.js:75)
at r.notifyApplicationAfterPageLoad (turbolinks.js:994)
at r.visitCompleted (turbolinks.js:1007)
at r.complete (turbolinks.js:782)
at r.<anonymous> (turbolinks.js:812)
When I remove the svelte:head tags everything works fine.
@davidwparker I did not try that, but would be interested to know.
@sedubois - okay. I'll be playing around with it this week (hopefully if I find the time). It's not a huge priority at the moment though. I'll be sure to post here any results I discover.
@sedubois - update:
The following all worked:
<svelte:body on:mouseenter={handler}/>
<svelte:window on:keydown={handleKeydown}/>
etc...
It blows up on this specific line:
export function detach(node: Node) {
node.parentNode.removeChild(node);
}
here: https://github.com/sveltejs/svelte/blob/787ece66a46db72ff820d129c600fb8c963ed612/src/runtime/internal/dom.ts#L12
It's very specific to this meta tag not having a parentNode.
My code is this:
<svelte:head>
<meta charset="utf-8">
</svelte:head>
However, when I update svelte_config.js and remove the destroy section it works fine:
if (window.initializedSvelteComponents) {
window.initializedSvelteComponents.forEach((component) => {
// console.log(component);
if (component) {
component.$destroy()
}
});
}
^^ delete that code- just assume it all works.
I'm assuming that code is there to prevent memory leaks?
cc/ @sebyx07
I put together a POC that resembles react-rails and helps with server- and client-side rendering, and provides a view helper (svelte_component):
https://github.com/nning/svelte-rails
This demo app shows howto integrate svelte-rails:
https://github.com/nning/svelte-rails-demo/commits/master
Currently, it isn't more than a POC and I will probably integrate the following features in the next days:
Thanks a lot for your help! You are, of course, invited to contribute to svelte-rails!
Updated 2020-07-06
All of this is also implemented in:
https://github.com/will-wow/webpacker-svelte
It's also probably based on the OP's solution.
That's good to know, thanks! webpacker-svelte misses server-side rendering, though.
@nning How does server-side rendering work?
@itay-grudev There are two bundles - one for the server, one for the client. When the view helper is called to render a component with the prerender option activated, the server rendering bundle is loaded in node, HTML is rendered, and passed again to ruby (where it is cached) and sent back to the client. The client loads a site containing markup for the prerendered components and when the client side bundle is ready, the components get hydrated (meaning, Svelte adds dynamic functions like reactive properties and event handlers).
That is cool. I'll switch to your project when I have time.
Most helpful comment
I put together a POC that resembles
react-railsand helps with server- and client-side rendering, and provides a view helper (svelte_component):https://github.com/nning/svelte-rails
This demo app shows howto integrate
svelte-rails:https://github.com/nning/svelte-rails-demo/commits/master
Currently, it isn't more than a POC and I will probably integrate the following features in the next days:
Thanks a lot for your help! You are, of course, invited to contribute to
svelte-rails!Updated 2020-07-06