Skip to content

Instantly share code, notes, and snippets.

@lbguilherme
Created March 3, 2022 12:14
Show Gist options
  • Save lbguilherme/12062c1102e9616329048d3e2ad26006 to your computer and use it in GitHub Desktop.
Save lbguilherme/12062c1102e9616329048d3e2ad26006 to your computer and use it in GitHub Desktop.
Crystal front-end app, what would it look like?
# This is a sample of what a front-end Crystal application could look like, for code review.
# The design was inspired by React and Flutter and it should be very performing and compact.
# If this is ever implemented, it will use WebAssembly to run Crystal code, together with
# some JavaScript bindings to access the DOM.
class TodoData
property text : String
property? done = false
def initialize(@text)
end
end
# A component can have immutable properties, a mutable state and events. It represents
# an UI element on the HTML DOM. Only itself can change its own state, the changes will
# trigger a re-render. Rendering uses a virtual DOM and a diffing algorithm. Children are
# only rendered if their properties changed, always preserving the state.
struct App < Component
# The component state is declared using the state macro. It is internally an inner class
# that can have properties and methods. Nothing special here except for the `notify_change`
# that must be called whenever the state changes. It will schedule a re-render of the
# component on the next UI tick.
state do
property todos = [] of TodoData
def add(todo_data)
todos << todo_data
notify_change
end
end
# This method should be cheap to run and it will produce a virtual DOM. It can access
# instance variables from the component and any data from the state. Running it should
# never cause the state to change. The `render(Component)` method lazyly creates a child
# component: If the created object is the same, it won't cause a inner render. If it changed
# then the child state will be preserved unchanged and the child will be re-rendered. It
# optionally takes a block to register reactions to events.
def render
div(class: "app") do
h1 "Todos"
ul do
state.todos.each do |todo|
li do
render Todo.new(todo) do
on_done_changed do |done|
todo.done = done
state.notify_change
end
end
end
end
end
render AddTodoForm.new do
on_new_todo do |todo|
state.add(todo)
end
end
end
end
end
# Represents a single Todo item. It is stateless.
struct Todo < Component
# Events are pretty much just a Proc in a instance var, but this helper macro makes thing easier.
# The parent can listen to the event and trigger actions (like updating the state).
event on_done_changed(done : Bool)
# The initialize method should be cheap, as it will run every time the parent needs to render.
# Expensive initialization should happen in the state's `initialize` method.
def initialize(@todo : TodoData)
end
def render
span(class: "todo") do
input(type: "checkbox", checked: @todo.done?) do
on_click do
# Calls the event, invoking a reaction on the parent component.
on_done_changed([email protected]?)
end
end
text @todo.text
end
end
end
# This is a form, it uses the common pattern of keeping a state variable in sync with the
# form input value. Probably a macro should be created to make it less repetitive.
struct AddTodoForm < Component
# Events can carry any data to the parent.
event on_new_todo(todo_data : TodoData)
state do
property new_todo_text = ""
end
def render
form do
input(type: "text", name: "todo_text", placeholder: "new todo", value: state.new_todo_text) do
on_change do |e|
state.new_todo_text = e.target.value
end
end
button(type: "submit") do
text "Add"
end
on_submit do
on_new_todo(TodoData.new(state.new_todo_text))
end
end
end
end
# Bootstraps the application, rendering the App component inside the <body> tag.
bootstrap App.new
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment