Skip to content

Instantly share code, notes, and snippets.

@lbguilherme
Last active March 4, 2022 11:08
Show Gist options
  • Save lbguilherme/cebd904a385f6dc880a04bd8ca8100f1 to your computer and use it in GitHub Desktop.
Save lbguilherme/cebd904a385f6dc880a04bd8ca8100f1 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, Angular 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 `mutate`
# that must be used whenever the state changes. It will schedule a re-render of the
# component on the next UI tick. Property setters handle it automatically.
state do
property todos = [] of TodoData
def add(todo_data)
mutate do
todos << todo_data
end
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 `html` macro takes a string interpolation as argument
# and will correctly parse the HTML and replace the values in a safe way. Interpolation can
# only happen in some known locations. The attributes of each element can be defined with
# interpolation too. Event attributes (starting with "on_") register handlers to be invoked
# when the event fires.
def view
html "
<div class='app'>
<h1>Todos</h1>
<ul>
#{state.todos.map { |todo|
html "
<li>
<Todo todo=#{todo} on_done_changed=#{state.mutate { todo.done = done }}/>
</li>
"
}}
</ul>
<AddTodoForm on_new_todo=#{state.add(todo)}/>
</div>
"
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 things 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 view
html "
<span class='todo'>
<input type='checkbox' checked=#{@todo.done?} on_click=#{on_done_changed([email protected]?)}/>
#{@todo.text}
</span>
"
end
end
# This is a form, it uses the common pattern of keeping a state variable in sync with the
# form input value.
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 view
html "
<form on_submit=#{on_new_todo(TodoData.new(state.new_todo_text))}>
<input type='text' placeholder='new todo' value=#{state.new_todo_text} on_change=#{state.new_todo_text = e.target.value}/>
<button type='submit'>Add</button>
</form>
"
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