Created
March 3, 2022 12:14
-
-
Save lbguilherme/12062c1102e9616329048d3e2ad26006 to your computer and use it in GitHub Desktop.
Crystal front-end app, what would it look like?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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