Update 2024-06-29: I am sorry for missing the fact that the Hyperstack Todo MVC, written in Frontend Ruby using the Hyperstack framework, already made the accomplishment of being the one Todo MVC to rule them all (it was not listed on the Todo MVC website, which is why I missed it), albeit the project is out of date and has not had a release in 3 years (2021) as of the time of writing. But, I was still right about Todo MVC Ruby Edition being the One Todo MVC To Rule Them All! And, I suppose this Todo MVC takes its place given that the other one's project is not actively maintained.
Todo MVC is a popular sample that is built in all Web Frontend Frameworks to provide a comprehensive SPA (Single Page Application) example covering all basic Frontend features. As such, it provides Software Engineers with a very good idea of how day-to-day Frontend Development would be with every Frontend Framework, whether in JavaScript, using React, Angular, Ember, or Vue, or in a Compile-to-JavaScript language, using something like Elm, GWT, or Scala.js. I am happy to announce that Todo MVC has just been built using Glimmer DSL for Web with 100% Frontend Ruby code (included in Glimmer DSL for Web version 0.2.7). And, this version's code is easily simpler, more minimalistic, more readable, and more maintainable than all 45+ versions on the Todo MVC website! I checked all of them myself and every one of them fell in one of multiple ways, like by requiring developers to manage code in multiple languages, which adds mental friction and hinders productivity (for example JavaScript and some form of ugly XML/HTML/JSX), having over-engineered overly-complicated code that is not readable at a glance or is mostly imperative instead of being declarative, requiring developers to pollute the code or mix concerns with weird sometimes too-low-level concepts that do not map to any real world domain model concepts (like state hooks and effects, which make code maintainability a lot more complicated than it needs to be and often an absolute hell), or adding a compilation step to do premature optimization when in fact even plain JavaScript performance is good enough for most business apps' simple interaction use cases nowadays. I can confidently say Todo MVC Ruby Edition is the one Todo MVC to rule them all! Thanks to the awesomeness of Ruby in the Browser!!!
You can try Todo MVC Ruby Edition over here (keep in mind it is hosted on a slow inexpensive Backend, but once the page loads up, the Frontend is fast) and find out how fast Ruby code is in the Frontend (hint: fast enough!):
https://sample-glimmer-dsl-web-rails7-app-black-sound-6793.fly.dev/
One of the most incredible things about the Frontend Ruby version of Todo MVC is that it is the only version that allows writing all code in Ruby instead of being forced to split mental resources between HTML, CSS, and JavaScript (while still having the option to manage CSS in CSS/SCSS files if preferred). That has the huge advantage of making the code a lot more readable and programmable in the minimalistic Ruby syntax than every other Todo MVC version as there is no need to muck with ugly XML/HTML/JSX syntax anymore (no more redundant repetitions of every element's name to open and close). Instead, Glimmer DSL for Web provides a Ruby HTML DSL for the HTML part, a Ruby CSS DSL for the CSS part (although it is also possible to embed basic CSS into components, but I chose to use the CSS DSL to demonstrate it even if CSS programmability is not needed for this sample), and just plain Ruby code for the JavaScript part. And, the code flows very nicely from structure to logic to styling in one language (no need to add `<% %>` or `{ }` anymore) with excellent Object Oriented Design and separation of concerns, thanks to the MVP Architectural Pattern (Model-View-Presenter is a variation on MVC). That pattern includes clean Presenter and Model layers that offload presentation and business domain concerns from the View layer, making the View layer very slim and minimalistic, meaning Glimmer Web Components can get as small as 1/10 the size of React Components. Colocating CSS styling with each component's HTML structure code enables higher productivity and better style maintainability with a similar approach to that of React Styled Components, albeit in much more readable Ruby code. Also, the Fronend-Ruby-to-JavaScript-transpiler that is used by Glimmer DSL for Web supports Ruby 3.1 features (like the Shorthand Hash Syntax), which is totally awesome! This provides the added advantage of being able to reuse Backend Ruby logic in the Frontend as is if needed (which is not possible in any JavaScript framework).
I generated most of the Ruby HTML DSL and CSS DSL code automatically from the HTML/CSS of other Todo MVC versions by using the HTML to Glimmer Converter and CSS to Glimmer Converter. This facilitates rewriting any existing SPA (Single Page Application) in Ruby very quickly without having to do much manual work. But, of course, after the code has been converted into Ruby code, it becomes programmable (e.g. with `if/else` statements and `.each` iterators) and can leverage event listeners (e.g. `onclick`) and advanced data-binding features like Bidirectional/Unidirectional/Content Data-Binding, which are super-cool, unique, and expressive in Ruby given its support for operator overload of `<=>` (for Bidirectional Data-Binding) and `<=` (for Unidirectional Data-Binding).
Check out the Todo MVC Ruby Edition code below, starting with the View layer, moving into the Presenter layer, and ending with the Model layer; and then read the conclusion of the blog post afterwards.
Update: the code has been improved and simplified significantly in newer versions of Glimmer DSL for Web. Read the follow-up blog post "Todo MVC Ruby Edition w/ Component Style Blocks, Inline-Style Data-Binding, and Class-Inclusion Data-Binding" for more details. Click these links to see the updated Todo MVC entry file and models/presenters/views.
Todo MVC Code
# Source: https://github.com/AndyObtiva/glimmer-dsl-web/blob/master/lib/glimmer-dsl-web/samples/regular/todo_mvc.rb | |
require 'glimmer-dsl-web' | |
require_relative 'todo_mvc/presenters/todo_presenter' | |
require_relative 'todo_mvc/views/new_todo_form' | |
require_relative 'todo_mvc/views/todo_list' | |
require_relative 'todo_mvc/views/todo_filters' | |
require_relative 'todo_mvc/views/todo_mvc_footer' | |
class TodoMvc | |
include Glimmer::Web::Component | |
before_render do | |
@presenter = TodoPresenter.new | |
end | |
after_render do | |
@presenter.setup_filter_routes | |
end | |
markup { | |
div(class: 'todomvc') { | |
section(class: 'todoapp') { | |
new_todo_form(presenter: @presenter) | |
todo_list(presenter: @presenter) | |
todo_filters(presenter: @presenter) | |
style { | |
todo_mvc_styles | |
} | |
} | |
todo_mvc_footer | |
on_remove do | |
@presenter.unsetup_filter_routes | |
end | |
} | |
} | |
def todo_mvc_styles | |
rule('body, button, html') { | |
margin '0' | |
padding '0' | |
} | |
rule('button') { | |
_webkit_font_smoothing 'antialiased' | |
_webkit_appearance 'none' | |
appearance 'none' | |
background 'none' | |
border '0' | |
color 'inherit' | |
font_family 'inherit' | |
font_size '100%' | |
font_weight 'inherit' | |
vertical_align 'baseline' | |
} | |
rule('.todoapp') { | |
background '#fff' | |
margin '130px 0 40px 0' | |
position 'relative' | |
box_shadow '0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1)' | |
} | |
media('screen and (-webkit-min-device-pixel-ratio:0)') { | |
rule('body') { | |
font "14px 'Helvetica Neue', Helvetica, Arial, sans-serif" | |
line_height '1.4em' | |
background '#f5f5f5' | |
color '#111111' | |
min_width '230px' | |
max_width '550px' | |
margin '0 auto' | |
_webkit_font_smoothing 'antialiased' | |
font_weight '300' | |
} | |
} | |
end | |
end | |
Document.ready? do | |
TodoMvc.render | |
end |
# Source: https://github.com/AndyObtiva/glimmer-dsl-web/blob/master/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/new_todo_form.rb | |
require_relative 'new_todo_input' | |
class NewTodoForm | |
include Glimmer::Web::Component | |
option :presenter | |
markup { | |
header(class: 'header') { | |
h1('todos') | |
new_todo_input(presenter: presenter) | |
style { | |
rule('.header h1') { | |
color '#b83f45' | |
font_size '80px' | |
font_weight '200' | |
position 'absolute' | |
text_align 'center' | |
_webkit_text_rendering 'optimizeLegibility' | |
_moz_text_rendering 'optimizeLegibility' | |
text_rendering 'optimizeLegibility' | |
top '-140px' | |
width '100%' | |
} | |
} | |
} | |
} | |
end |
# Source: https://github.com/AndyObtiva/glimmer-dsl-web/blob/master/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/new_todo_input.rb | |
require_relative 'todo_input' | |
class NewTodoInput < TodoInput | |
option :presenter | |
markup { | |
input(class: todo_input_class, placeholder: "What needs to be done?", autofocus: "") { | |
# Data-bind `input` `value` property bidirectionally to `presenter.new_todo` `task` attribute | |
# meaning make any changes to the new todo task automatically update the input value | |
# and any changes to the input value by the user automatically update the new todo task value | |
value <=> [presenter.new_todo, :task] | |
onkeyup do |event| | |
presenter.create_todo if event.key == 'Enter' || event.keyCode == "\r" | |
end | |
style { | |
todo_input_styles | |
} | |
} | |
} | |
def todo_input_class | |
'new-todo' | |
end | |
def todo_input_styles | |
super | |
rule(".#{todo_input_class}") { | |
padding '16px 16px 16px 60px' | |
height '65px' | |
border 'none' | |
background 'rgba(0, 0, 0, 0.003)' | |
box_shadow 'inset 0 -2px 1px rgba(0,0,0,0.03)' | |
} | |
rule(".#{todo_input_class}::placeholder") { | |
font_style 'italic' | |
font_weight '400' | |
color 'rgba(0, 0, 0, 0.4)' | |
} | |
end | |
end |
# Source: https://github.com/AndyObtiva/glimmer-dsl-web/blob/master/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/todo_input.rb | |
# Superclass for NewTodoInput and EditTodoInput with common styles | |
class TodoInput | |
include Glimmer::Web::Component | |
def todo_input_class | |
'todo-input' | |
end | |
def todo_input_styles | |
rule(".#{todo_input_class}") { | |
position 'relative' | |
margin '0' | |
width '100%' | |
font_size '24px' | |
font_family 'inherit' | |
font_weight 'inherit' | |
line_height '1.4em' | |
color 'inherit' | |
padding '6px' | |
border '1px solid #999' | |
box_shadow 'inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2)' | |
box_sizing 'border-box' | |
_webkit_font_smoothing 'antialiased' | |
} | |
rule(".#{todo_input_class}::selection") { | |
background 'red' | |
} | |
end | |
end |
# Source: https://github.com/AndyObtiva/glimmer-dsl-web/blob/master/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/todo_list.rb | |
require_relative 'todo_list_item' | |
class TodoList | |
include Glimmer::Web::Component | |
option :presenter | |
markup { | |
main(class: 'main') { | |
# Data-bind main element style property unidirectionally to `Todo.all` value (`Array` of `Todo` objects) | |
# meaning make any changes to `Todo.all` automatically update the style of the main element | |
# on_read option is a converter that is invoked upon reading data from `Todo.all` to update the main style | |
# with the converted value (e.g. 'display: none;') | |
style <= [ Todo, :all, | |
on_read: ->(todos) { todos.empty? ? 'display: none;' : '' } | |
] | |
div(class: 'toggle-all-container') { | |
input(class: 'toggle-all', type: 'checkbox') | |
label('Mark all as complete', class: 'toggle-all-label', for: 'toggle-all') { | |
onclick do |event| | |
presenter.toggle_all_completed | |
end | |
} | |
} | |
ul(class: 'todo-list') { | |
# Content data-binding for ul automatically re-renders its content block every time `presenter` `todos` change. | |
content(presenter, :todos) { | |
presenter.todos.each do |todo| | |
todo_list_item(presenter:, todo:) | |
end | |
} | |
} | |
style { | |
todo_list_styles | |
} | |
} | |
} | |
def todo_list_styles | |
rule('.main') { | |
border_top '1px solid #e6e6e6' | |
position 'relative' | |
z_index '2' | |
} | |
rule('.toggle-all') { | |
border 'none' | |
bottom '100%' | |
height '1px' | |
opacity '0' | |
position 'absolute' | |
right '100%' | |
width '1px' | |
} | |
rule('.toggle-all+label') { | |
align_items 'center' | |
display 'flex' | |
font_size '0' | |
height '65px' | |
justify_content 'center' | |
left '0' | |
position 'absolute' | |
top '-65px' | |
width '45px' | |
} | |
rule('.toggle-all+label:before') { | |
color '#949494' | |
content '"❯"' | |
display 'inline-block' | |
font_size '22px' | |
padding '10px 27px' | |
_webkit_transform 'rotate(90deg)' | |
transform 'rotate(90deg)' | |
} | |
rule('.toggle-all:focus+label, .toggle:focus+label, :focus') { | |
box_shadow '0 0 2px 2px #cf7d7d' | |
outline '0' | |
} | |
rule('.todo-list') { | |
list_style 'none' | |
margin '0' | |
padding '0' | |
} | |
end | |
end |
# Source: https://github.com/AndyObtiva/glimmer-dsl-web/blob/master/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/todo_list_item.rb | |
require_relative 'edit_todo_input' | |
class TodoListItem | |
include Glimmer::Web::Component | |
option :presenter | |
option :todo | |
markup { | |
li { | |
# It is possible to configure multiple data-bindings as done with class_name below | |
# Data-bind li class unidirectionally to `todo.completed` using `on_read` converter | |
# to convert `todo.completed` value and produce final value to update li class with | |
# using li_class_name method defined below. | |
class_name <= [ todo, :completed, | |
on_read: -> (completed) { li_class_name(todo) } | |
] | |
# Data-bind li class unidirectionally to `todo.editing` using `on_read` converter | |
# to convert `todo.editing` value and produce final value to update li class with | |
# using li_class_name method defined below. | |
class_name <= [ todo, :editing, | |
on_read: -> (editing) { li_class_name(todo) } | |
] | |
div(class: 'view') { | |
input(class: 'toggle', type: 'checkbox') { | |
# Data-bind `input` `checked` property bidirectionally to `todo` `completed` attribute | |
# meaning make any changes to the `todo` `completed` attribute value automatically update the `input` `checked` property | |
# and any changes to the `input` `checked` property by the user automatically update the `todo` `completed` attribute value. | |
# `after_write` hook is invoked after writing a new value to the model attribute (`todo` `completed` attribute) | |
checked <=> [ todo, :completed, | |
after_write: -> (_) { presenter.refresh_todos_with_filter if presenter.filter != :all } | |
] | |
} | |
label { | |
# Data-bind `label` inner HTML unidirectionally to `todo.task` (`String` value), | |
# meaning make changes to `todo` `task` attribute automatically update `label` inner HTML. | |
inner_html <= [todo, :task] | |
ondblclick do |event| | |
todo.start_editing | |
end | |
} | |
button(class: 'destroy') { | |
onclick do |event| | |
presenter.destroy(todo) | |
end | |
} | |
} | |
edit_todo_input(presenter:, todo:) | |
if todo == presenter.todos.first | |
style { | |
todo_list_item_styles | |
} | |
end | |
} | |
} | |
def li_class_name(todo) | |
classes = [] | |
classes << 'completed' if todo.completed? | |
classes << 'editing' if todo.editing? | |
classes.join(' ') | |
end | |
def todo_list_item_styles | |
rule('.todo-list li.completed label') { | |
color '#949494' | |
text_decoration 'line-through' | |
} | |
rule('.todo-list li') { | |
border_bottom '1px solid #ededed' | |
font_size '24px' | |
position 'relative' | |
} | |
rule('.todo-list li .toggle') { | |
_webkit_appearance 'none' | |
appearance 'none' | |
border 'none' | |
bottom '0' | |
height 'auto' | |
margin 'auto 0' | |
opacity '0' | |
position 'absolute' | |
text_align 'center' | |
top '0' | |
width '40px' | |
} | |
rule('.todo-list li label') { | |
color '#484848' | |
display 'block' | |
font_weight '400' | |
line_height '1.2' | |
min_height '40px' | |
padding '15px 15px 15px 60px' | |
transition 'color .4s' | |
word_break 'break-all' | |
} | |
rule('.todo-list li .toggle+label') { | |
background_image 'url(data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23949494%22%20stroke-width%3D%223%22/%3E%3C/svg%3E)' | |
background_position '0' | |
background_repeat 'no-repeat' | |
} | |
rule('.todo-list li.completed label') { | |
color '#949494' | |
text_decoration 'line-through' | |
} | |
rule('.todo-list li .toggle:checked+label') { | |
background_image 'url(data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%2359A193%22%20stroke-width%3D%223%22%2F%3E%3Cpath%20fill%3D%22%233EA390%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22%2F%3E%3C%2Fsvg%3E)' | |
} | |
rule('.todo-list li.editing') { | |
border_bottom 'none' | |
padding '0' | |
} | |
rule('.todo-list li.editing input[type=checkbox], .todo-list li.editing label') { | |
opacity '0' | |
} | |
rule('.todo-list li .destroy') { | |
bottom '0' | |
color '#949494' | |
display 'none' | |
font_size '30px' | |
height '40px' | |
margin 'auto 0' | |
position 'absolute' | |
right '10px' | |
top '0' | |
transition 'color .2s ease-out' | |
width '40px' | |
} | |
rule('.todo-list li:focus .destroy, .todo-list li:hover .destroy') { | |
display 'block' | |
} | |
rule('.todo-list li .destroy:focus, .todo-list li .destroy:hover') { | |
color '#c18585' | |
} | |
rule('.todo-list li .destroy:after') { | |
content '"×"' | |
display 'block' | |
height '100%' | |
line_height '1.1' | |
} | |
media ('screen and (-webkit-min-device-pixel-ratio: 0)') { | |
rule('.todo-list li .toggle, .toggle-all') { | |
background 'none' | |
} | |
rule('.todo-list li .toggle') { | |
height '40px' | |
} | |
} | |
end | |
end | |
# Source: https://github.com/AndyObtiva/glimmer-dsl-web/blob/master/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/edit_todo_input.rb | |
require_relative 'todo_input' | |
class EditTodoInput < TodoInput | |
option :presenter | |
option :todo | |
markup { | |
input(class: todo_input_class) { |edit_input| | |
# Data-bind `input` `style` property unidirectionally to `todo` `editing` attribute, | |
# using `on_read` converter to convert `todo.editing` value and produce final value to update `input` `style` with, | |
# and using `after_read` hook to have `input` grab keyboard focus when editing todo. | |
style <= [ todo, :editing, | |
on_read: ->(editing) { editing ? '' : 'display: none;' }, | |
after_read: ->(_) { edit_input.focus if todo.editing? } | |
] | |
# Data-bind `input` `value` property bidirectionally to `todo` `task` attribute | |
# meaning make any changes to the `todo` `task` attribute value automatically update the `input` `value` property | |
# and any changes to the `input` `value` property by the user automatically update the `todo` `task` attribute value. | |
value <=> [todo, :task] | |
onkeyup do |event| | |
if event.key == 'Enter' || event.keyCode == "\r" | |
todo.save_editing | |
presenter.destroy(todo) if todo.task.strip.empty? | |
elsif event.key == 'Escape' || event.keyCode == 27 | |
todo.cancel_editing | |
end | |
end | |
onblur do |event| | |
todo.save_editing | |
end | |
style { | |
todo_input_styles | |
} | |
} | |
} | |
def todo_input_class | |
'edit-todo' | |
end | |
def todo_input_styles | |
super | |
rule("*:has(> .#{todo_input_class})") { | |
position 'relative' | |
} | |
rule(".#{todo_input_class}") { | |
position 'absolute' | |
display 'block' | |
width 'calc(100% - 43px)' | |
padding '12px 16px' | |
margin '0 0 0 43px' | |
top '0' | |
} | |
end | |
end |
# Source: https://github.com/AndyObtiva/glimmer-dsl-web/blob/master/lib/glimmer-dsl-web/samples/regular/todo_mvc/presenters/todo_presenter.rb | |
require 'glimmer/data_binding/observer' | |
require_relative '../models/todo' | |
class TodoPresenter | |
FILTER_ROUTE_REGEXP = /\#\/([^\/]*)$/ | |
attr_accessor :todos, :can_clear_completed, :active_todo_count | |
attr_reader :new_todo, :filter | |
def initialize | |
@todos = Todo.all.clone | |
@new_todo = Todo.new(task: '') | |
@filter = :all | |
refresh_todo_stats | |
end | |
def refresh_todo_stats | |
refresh_can_clear_completed | |
refresh_active_todo_count | |
end | |
def create_todo | |
todo = new_todo.clone | |
Todo.all << todo # indirectly adds todo to todo list in the View through data-binding | |
new_todo.task = '' # indirectly clears new todo form in the View through data-binding | |
observe_todo_completion_to_update_todo_stats(todo) | |
refresh_todos_with_filter | |
refresh_todo_stats | |
end | |
def refresh_todos_with_filter | |
self.todos = Todo.send(filter).clone | |
end | |
def filter=(filter) | |
return if filter == @filter | |
@filter = filter | |
refresh_todos_with_filter | |
end | |
def destroy(todo) | |
delete(todo) | |
refresh_todos_with_filter | |
refresh_todo_stats | |
end | |
def clear_completed | |
Todo.completed.each { |todo| delete(todo) } | |
refresh_todos_with_filter | |
refresh_todo_stats | |
end | |
def toggle_all_completed | |
target_completed_value = Todo.active.any? | |
todos_to_update = target_completed_value ? Todo.active : Todo.completed | |
todos_to_update.each { |todo| todo.completed = target_completed_value } | |
end | |
def setup_filter_routes | |
@filter_router_function = -> (event) { apply_route_filter } | |
$$.addEventListener('popstate', &@filter_router_function) | |
apply_route_filter | |
end | |
def apply_route_filter | |
route_filter_match = $$.document.location.href.to_s.match(FILTER_ROUTE_REGEXP) | |
return if route_filter_match.nil? | |
route_filter = route_filter_match[1] | |
route_filter = 'all' if route_filter == '' | |
self.filter = route_filter | |
end | |
def unsetup_filter_routes | |
$$.removeEventListener('popstate', &@filter_router_function) | |
@filter_router_function = nil | |
end | |
private | |
def delete(todo) | |
Todo.all.delete(todo) | |
observer_registration = observers_for_todo_stats.delete(todo.object_id) | |
observer_registration&.deregister | |
end | |
def observers_for_todo_stats | |
@observers_for_todo_stats = {} | |
end | |
def observe_todo_completion_to_update_todo_stats(todo) | |
# save todo observer to deregister when destroying todo | |
observers_for_todo_stats[todo.object_id] = todo_stat_observer.observe(todo, :completed) | |
end | |
def todo_stat_observer | |
@todo_stat_observer ||= Glimmer::DataBinding::Observer.proc { refresh_todo_stats } | |
end | |
def refresh_can_clear_completed | |
self.can_clear_completed = Todo.completed.any? | |
end | |
def refresh_active_todo_count | |
self.active_todo_count = Todo.active.count | |
end | |
end |
# Source: https://github.com/AndyObtiva/glimmer-dsl-web/blob/master/lib/glimmer-dsl-web/samples/regular/todo_mvc/models/todo.rb | |
Todo = Struct.new(:task, :completed, :editing, keyword_init: true) do | |
class << self | |
attr_writer :all | |
def all | |
@all ||= [] | |
end | |
def active | |
all.select(&:active?) | |
end | |
def completed | |
all.select(&:completed?) | |
end | |
end | |
FILTERS = [:all, :active, :completed] | |
alias completed? completed | |
alias editing? editing | |
def active | |
!completed | |
end | |
alias active? active | |
def start_editing | |
return if editing? | |
@original_task = task | |
self.editing = true | |
end | |
def cancel_editing | |
return unless editing? | |
self.task = @original_task | |
self.editing = false | |
end | |
def save_editing | |
return unless editing? | |
self.editing = false | |
end | |
end |
This version of Todo MVC is a very good start, but like most pieces of software, it does have room for improvement. For example, in the future, Glimmer DSL for Web could support a Frontend Routing API natively, it can avoid embedding style elements in body by having Glimmer automatically shove them in head, it can provide simpler syntax for data-binding CSS class inclusion and inline style properties, and more View responsibilities could be pushed down to the Presenter and Model layers by taking more advantage of the Observer Pattern. CSS Rules are verbosely created with the `rule` keyword, but they could have been created with just `r` or `_` instead if Software Engineers would rather avoid the repetition of the `rule` keyword. Additionally, if no programmability is needed (there is no logic in the style code that depends on component variables), then the CSS could be embedded as pure CSS too within the components. There are many other ideas for improvement, so if I update Todo MVC in the future, I will report on that in future blog posts.
Ruby in the Browser enables handling all kinds of interactions that are not possible in Hotwire without JavaScript, albeit with a simpler mental model. After all, it does not require that developers write ugly verbose HTML code (nobody should in 2024 just like nobody writes XML anymore) nor worry too much about element IDs while breaking MVC by making controllers push data to Views (which causes bad coupling that degrades maintainability). Instead, developers could just build smart Views that pull their own data automatically with Data-Binding observers just as per the proper MVC Pattern and they could work with variables/attributes in Ruby the normal way instead of relying on element IDs, simplifying maintainability immensely. A lot of developers avoid Hotwire and default to JavaScript Frameworks like React because Hotwire cannot do everything. They don't have to be stuck with inferior JavaScript technologies like React anymore if they avoid Hotwire. They can write their Frontend code in Ruby much more effectively (200%-10,000% more) in 2024! Meaning, 12 months of work in React take about 3-6 months in Ruby. That's a massive difference that will make and break companies depending on whether they stick with outdated inferior JavaScript technologies or upgrade to awesome superior Ruby technologies. This is the same exponential jump in productivity that happened in the mid-2000 with Rails, but now happening in the Web Frontend.
I think this is easily the future of productive Frontend Development in Rails for smart Ruby Software Engineers as it's not just a 10% or 20% improvement in productivity/maintainability over JavaScript options like React, yet an exponential improvement, meaning 200%, 400%, or even 10,000% better depending on the Frontend app being built. And, it removes the need for Rails shops to hire extra JavaScript developers. Backend Ruby Software Engineers can now do Frontend work in Ruby, thus enabling businesses to cut their hiring budgets in half and scale better (on top of cutting development cost and time-to-delivery in half by writing half the code of JavaScript in Ruby)!!!
1 comment:
Very impressive. Ruby has depth and subtlety that is showing itself in exercises like this.
Post a Comment