Crystal + Svelte = :zap:
Celestite allows you to use the full power of Svelte reactive components in your Crystal web apps. It's a drop-in replacement for your view layer -- no more need for intermediate .ecr
templates. With celestite, you write your backend server code in Crystal, your frontend client code in JavaScript & HTML, and everything works together seamlessly...and fast.
Read the full introductory blog post here.
The render server was built using node 10.15.3 (in particular it uses the WHATWG URL Standard, which was added in Node 7+.) It doesn't need to do this, strictly-speaking, and if there's a compelling reason to support earlier versions of Node I'm happy to make this change.)
This is not much more than a proof-of-concept at the moment, but it does work! Standard warnings apply - it will likely break/crash in spectacular and ill-timed glory, so don't poke it, feed it past midnight, or use it for anything mission-critical (yet).
Celestite has been developed / tested with the Amber web framework, but designed to work standalone as well. There's also no reason it won't work with Lucky, Kemal, Athena, etc. (but no work integrating with those has been done yet.) The steps below assume you'll be working with Amber.
shard.yml
and run shards install
dependencies:
celestite:
github: noahlh/celestite
version: ~> 0.1.3
Celestite::Adapter::Amber
in your application_controller.cr
This adds the celestite_render
macro.
# application_controller.cr
require "jasper_helpers"
class ApplicationController < Amber::Controller::Base
include JasperHelpers
+ include Celestite::Adapter::Amber
end
celestite_amber_init.cr
to /config/initializers
An example is provided. You can name this file whatever you want, just so long as it gets called upon initialization.
_error.svelte
to your views directoryThis is required for the time being because Sapper expects it. If it's missing, your app will still work, but there will be some weirdness with CSS rendering (trust me - this cost me an evening ;)
<script>
export let status;
export let error;
</script>
<h1>{status}</h1>
<p>{error.message}</p>
This is because of a slight hitch with how Svelte works behind the scenes (via Sapper), but essentially: the client needs to be able to access the relevant JS files in /client, yet Sapper needs complete control over that directory (it wipes it with each new build). So we simultaneously give it its own directory and also make it available via the root path.
# routes.cr
pipeline :static do
plug Amber::Pipe::Error.new
plug Amber::Pipe::Static.new("./public")
+ plug Amber::Pipe::Static.new("./public/celestite")
end
.svelte
files and start building!A note on naming: make sure you follow Sapper's file naming rules. Hint: the root component should be named index.svelte
(all lowercase).
celestite_render
(context : Celestite::Context = nil, path : String? = nil, template : String? = nil)
Performs the render. This is to be called where you'd normally call render
in your controllers. It doesn't need any parameters by default (it automatically extracts the path of the method calling it based on your Amber routes), but you may use the following optional parameters:
context : Celestite::Context
Celestite uses a Hash literal called Celestite::Context
. Any variables you'd like available in your components go in here, using a Symbol key of the desired name.
So if you want to access example_crystal_data
in your vue component, assign the relevant value to my_context[:example_crystal_data]
. See example below for details
This is acheived using Sapper's session-seeding mechanism.
path : String?
If you need to manually specify which path you're rending (i.e. you're not in Amber), you can pass in a string parameter. In Amber this will be assigned a default value equal to the current Amber route the controller method is handling.
template : String?
(Not implemented yet) Which layout/template you'd like to render the component in. Will use a default template specified in the init file if none specified on render.
# home_controller.cr
class HomeController < ApplicationController
def index
data = 1 + 1
context = Celestite::Context{:data => data}
celestite_render(context)
end
end
Your .svelte
components will automatically be rendered server-side before being sent to the client. This is usually seamless, but you'll need to be aware of any code (or libraries) that rely on browser-specific APIs (such as document
or window
). This will throw an error when the components are rendered in the context of node (vs the browser).
To get around this, you can import Sapper's onMount()
function. Any function wrapped in onMount()
will be rendered in the (browser) client only.
You can read more about server-side rendering considerations here.
My goal/philosophy is to release early, release often, and get as much user feedback as early in the process as possible, so even though the perfectionist in me would like to spend another 6 years improving this, by then it'll be 2024 and who knows we might all be living underwater. No time like the present.
Short-term goals:
Longer-term goals:
This has been a solo project of mine and I would love nothing more than to get feedback on the code / improvements / contributions. I've found by far the best way to learn and level-up development skills is to have others review code that you've wrestled with.
That is to say, don't hold back. Report things that are broken, help improve some of the code, or even just fix some typos. Everyone (at all skill levels) is welcome.
git checkout -b omg-this-fixed-so-many-bugs
)git commit -am 'Made a fix!'
)git push origin omg-this-fixed-so-many-bugs
)