Todo MVC Ruby Edition is a Rails sample app that was built with Glimmer DSL for Web using Frontend Ruby. It was initially documented in the blog post "Todo MVC Ruby Edition Is the One Todo MVC To Rule Them All". It has been refactored, simplified, and optimized significantly (all operations happen instantly now), relying on new Glimmer DSL for Web features, such as Component Style Blocks, Inline-Style Data-Binding, and Class-Inclusion Data-Binding.
You can access an online-hosted version of Todo MVC Ruby Edition here:
https://sample-glimmer-dsl-web-rails7-app-black-sound-6793.fly.dev/
style(:width) <= [button_model, :width]
style(:height) <= [button_model, :height]
style(:font_size) <= [button_model, :font_size]
2) Element Class-Inclusion Data-Binding: this enables simple data-binding of fine-grained CSS classes (e.g. `active`) to Model/Presenter boolean attributes (in the past, only the course-grained class attribute was data-bindable).
class_name(:pushed) <= [button_model, :pushed]
class_name(:pulled) <= [button_model, :pushed, on_read: :!]
3) Ability to specify element style attribute value as a hash of CSS properties instead of a plain String.
div(style: {display: :grid, grid_auto_columns: '80px 260px'})
4) Ability to specify CSS classes as an array of String's or Symbol's instead of a plain String.
li(class: [:completed, :editing, 'todo-item'])
5) Glimmer Web Component Style Block: a style block enables declaring common styles for a component that apply to all instances of the component, using Glimmer DSL for CSS. It is an optional feature that adopts the new recommendation to co-locate styles with components to speed up productivity and maintainability. It is similar to the Styled Components approach that React developers rely on, except it is implemented in Ruby, thus providing the unique benefit of not forcing developers to switch context between multiple languages, thus improving productivity/maintainability. But, if developers prefer relying on separate CSS/SCSS files, that is fully supported as well. When a style block is declared in a component, behind the scenes, Glimmer DSL for Web generates a <style> element that is automatically included in the <head> element of the page for better performance, and it is automatically removed from the head element upon removing the last instance of a component on a webpage. Since the style block applies to all instances of a component, it is evaluated against the component class, and can leverage class methods if needed.
Example:
class TodoMvc
include Glimmer::Web::Component
before_render do # evaluated against the component instance
@presenter = TodoPresenter.new
end
after_render do # evaluated against the component instance
@presenter.setup_filter_routes
end
markup { # evaluated against the component instance
div { # has CSS class 'todo-mvc', derived from component class name by convention
section(class: 'todoapp') {
new_todo_form(presenter: @presenter)
todo_list(presenter: @presenter)
todo_filters(presenter: @presenter)
}
todo_mvc_footer
on_remove do
@presenter.unsetup_filter_routes
end
}
}
style { # evaluated against the component class
r('body, button, html') {
margin 0
padding 0
}
r('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
}
r("#{component_element_selector} .todoapp") { # embeds .todo-mvc before .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)') {
r('body') {
font "14px 'Helvetica Neue', Helvetica, Arial, sans-serif"
line_height 1.4.em
background '#f5f5f5'
color '#111111'
min_width 230
max_width 550
margin '0 auto'
_webkit_font_smoothing :antialiased
font_weight '300'
}
}
}
end
Below is the updated code of Todo MVC Ruby Edition. As mentioned before, it has been refactored, simplified, and optimized significantly.
Happy learning!
# 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 { | |
section(class: 'todoapp') { | |
new_todo_form(presenter: @presenter) | |
todo_list(presenter: @presenter) | |
todo_filters(presenter: @presenter) | |
} | |
todo_mvc_footer | |
on_remove do | |
@presenter.unsetup_filter_routes | |
end | |
} | |
} | |
style { | |
r('body, button, html') { | |
margin 0 | |
padding 0 | |
} | |
r('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 | |
} | |
r('.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)') { | |
r('body') { | |
font "14px 'Helvetica Neue', Helvetica, Arial, sans-serif" | |
line_height 1.4.em | |
background '#f5f5f5' | |
color '#111111' | |
min_width 230 | |
max_width 550 | |
margin '0 auto' | |
_webkit_font_smoothing :antialiased | |
font_weight '300' | |
} | |
} | |
} | |
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 { | |
r('.header h1') { | |
color '#b83f45' | |
font_size 80 | |
font_weight '200' | |
position :absolute | |
text_align :center | |
_webkit_text_rendering :optimizeLegibility | |
_moz_text_rendering :optimizeLegibility | |
text_rendering :optimizeLegibility | |
top -140 | |
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 { # evaluated against instance as a smart convention | |
input(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 { # evaluated against class as a smart convention (common to all instances) | |
todo_input_styles | |
r(component_element_selector) { # NewTodoInput has component_element_class as 'new-todo-input' | |
padding '16px 16px 16px 60px' | |
height 65 | |
border :none | |
background 'rgba(0, 0, 0, 0.003)' | |
box_shadow 'inset 0 -2px 1px rgba(0,0,0,0.03)' | |
} | |
r("#{component_element_selector}::placeholder") { | |
font_style :italic | |
font_weight '400' | |
color 'rgba(0, 0, 0, 0.4)' | |
} | |
} | |
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 | |
class << self | |
def todo_input_styles | |
r(component_element_selector) { | |
position :relative | |
margin 0 | |
width '100%' | |
font_size 24 | |
font_family :inherit | |
font_weight :inherit | |
line_height 1.4.em | |
color :inherit | |
padding 6 | |
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 | |
} | |
r("#{component_element_selector}::selection") { | |
background :red | |
} | |
r("#{component_element_selector}:focus") { | |
box_shadow '0 0 2px 2px #cf7d7d' | |
outline 0 | |
} | |
end | |
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 | |
after_render do | |
observe(presenter, :created_todo) do |todo| | |
@todo_ul.content { # re-open todo ul content to add created todo | |
todo_list_item(presenter:, todo:) | |
} | |
end | |
end | |
markup { | |
main(class: 'main') { | |
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 | |
} | |
} | |
@todo_ul = ul { | |
# class name is data-bound unidirectionally to the presenter filter attribute, | |
# meaning it would automatically get set to its value whenever presenter.filter changes | |
class_name <= [presenter, :filter] | |
presenter.todos.each do |todo| | |
todo_list_item(presenter:, todo:) | |
end | |
} | |
} | |
} | |
style { | |
r('.main') { | |
border_top '1px solid #e6e6e6' | |
position :relative | |
z_index '2' | |
} | |
r('.toggle-all') { | |
border :none | |
bottom '100%' | |
height 1 | |
opacity 0 | |
position :absolute | |
right '100%' | |
width 1 | |
} | |
r('.toggle-all+label') { | |
align_items :center | |
display :flex | |
font_size 0 | |
height 65 | |
justify_content :center | |
left 0 | |
position :absolute | |
top -65 | |
width 45 | |
} | |
r('.toggle-all+label:before') { | |
color '#949494' | |
content '"❯"' | |
display 'inline-block' | |
font_size 22 | |
padding '10px 27px' | |
_webkit_transform 'rotate(90deg)' | |
transform 'rotate(90deg)' | |
} | |
r('.toggle-all:focus+label, .toggle:focus+label') { | |
box_shadow '0 0 2px 2px #cf7d7d' | |
outline 0 | |
} | |
r('.todo-list ul') { | |
list_style :none | |
margin 0 | |
padding 0 | |
} | |
r('.todo-list ul.active li.completed') { | |
display :none | |
} | |
r('.todo-list ul.completed li.active') { | |
display :none | |
} | |
} | |
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 | |
after_render do | |
# after rendering markup, observe todo deleted attribute and remove component when deleted | |
observe(todo, :deleted) do |deleted| | |
self.remove if deleted | |
end | |
end | |
markup { | |
li { | |
# Data-bind inclusion of `completed` in `li` `class` attribute unidirectionally to `todo` `completed` attribute, | |
# meaning inclusion/exclusion of `completed` class happens automatically when `todo.completed` boolean value changes. | |
class_name(:completed) <= [todo, :completed] | |
# Data-bind inclusion of `active` in `li` `class` attribute unidirectionally to `todo` `completed` attribute, negated, | |
# meaning inclusion/exclusion of `active` class happens automatically when `todo.completed` negated boolean value changes. | |
class_name(:active) <= [todo, :completed, on_read: :!] | |
# Data-bind inclusion of `editing` in `li` `class` attribute unidirectionally to `todo` `editing` attribute, | |
# meaning inclusion/exclusion of `editing` class happens automatically when `todo.editing` boolean value changes. | |
class_name(:editing) <= [todo, :editing] | |
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] | |
} | |
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| | |
# if the markup root (li) last child is not an input field, re-open content and add an edit input field | |
unless markup_root.children.last.keyword == 'input' | |
markup_root.content { | |
edit_todo_input(presenter:, todo:) | |
} | |
end | |
todo.start_editing | |
end | |
} | |
button(class: 'destroy') { | |
onclick do |event| | |
presenter.destroy(todo) | |
end | |
} | |
} | |
} | |
} | |
style { | |
r('.todo-list li.completed label') { | |
color '#949494' | |
text_decoration 'line-through' | |
} | |
r('.todo-list li') { | |
border_bottom '1px solid #ededed' | |
font_size 24 | |
position :relative | |
} | |
r('.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 40 | |
} | |
r('.todo-list li label') { | |
color '#484848' | |
display :block | |
font_weight '400' | |
line_height '1.2' | |
min_height 40 | |
padding '15px 15px 15px 60px' | |
transition 'color .4s' | |
word_break 'break-all' | |
} | |
r('.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' | |
} | |
r('.todo-list li.completed label') { | |
color '#949494' | |
text_decoration 'line-through' | |
} | |
r('.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)' | |
} | |
r('.todo-list li.editing') { | |
border_bottom :none | |
padding 0 | |
} | |
r('.todo-list li.editing input[type=checkbox], .todo-list li.editing label') { | |
opacity 0 | |
} | |
r('.todo-list li .destroy') { | |
bottom 0 | |
color '#949494' | |
display :none | |
font_size 30 | |
height 40 | |
margin 'auto 0' | |
position :absolute | |
right 10 | |
top 0 | |
transition 'color .2s ease-out' | |
width 40 | |
} | |
r('.todo-list li:focus .destroy, .todo-list li:hover .destroy') { | |
display :block | |
} | |
r('.todo-list li .destroy:focus, .todo-list li .destroy:hover') { | |
color '#c18585' | |
} | |
r('.todo-list li .destroy:after') { | |
content '"×"' | |
display :block | |
height '100%' | |
line_height '1.1' | |
} | |
media ('screen and (-webkit-min-device-pixel-ratio: 0)') { | |
r('.todo-list li .toggle, .toggle-all') { | |
background :none | |
} | |
r('.todo-list li .toggle') { | |
height 40 | |
} | |
} | |
} | |
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 { # evaluated against instance as a smart default convention | |
input { |edit_input| | |
# Data-bind inclusion of `li` `class` `editing` unidirectionally to todo editing attribute, | |
# meaning inclusion of editing class is determined by todo editing boolean attribute. | |
# `after_read` hook will have `input` grab keyboard focus when editing todo. | |
class_name(:editing) <= [ todo, :editing, | |
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 { # evaluated against class as a smart default convention (common to all instances) | |
todo_input_styles | |
r("*:has(> #{component_element_selector})") { | |
position :relative | |
} | |
r(component_element_selector) { | |
position :absolute | |
display :none | |
width 'calc(100% - 43px)' | |
padding '12px 16px' | |
margin '0 0 0 43px' | |
top 0 | |
} | |
r("#{component_element_selector}.editing") { | |
display :block | |
} | |
} | |
end |
# Source: https://github.com/AndyObtiva/glimmer-dsl-web/blob/master/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/todo_filters.rb | |
class TodoFilters | |
include Glimmer::Web::Component | |
option :presenter | |
markup { | |
footer { | |
# Data-bind `footer` `style` `display` unidirectionally to presenter todos, | |
# and on read, convert todos based on whether they are empty to 'none' or 'block' | |
style(:display) <= [ presenter, :todos, | |
on_read: ->(todos) { todos.empty? ? 'none' : 'block' } | |
] | |
span(class: 'todo-count') { | |
span('.strong') { | |
# Data-bind `span` `inner_text` unidirectionally to presenter active_todo_count | |
inner_text <= [presenter, :active_todo_count] | |
} | |
span { | |
# Data-bind `span` `inner_text` unidirectionally to presenter active_todo_count, | |
# and on read, convert active_todo_count to string that follows count number | |
inner_text <= [presenter, :active_todo_count, | |
on_read: -> (active_todo_count) { " item#{'s' if active_todo_count != 1} left" } | |
] | |
} | |
} | |
ul(class: 'filters') { | |
TodoPresenter::FILTERS.each do |filter| | |
li { | |
a(filter.to_s.capitalize, href: "#/#{filter unless filter == :all}") { | |
# Data-bind inclusion of `a` `class` `selected` unidirectionally to presenter filter attribute, | |
# and on read of presenter filter, convert to boolean value of whether selected class is included | |
class_name(:selected) <= [ presenter, :filter, | |
on_read: -> (presenter_filter) { presenter_filter == filter } | |
] | |
onclick do |event| | |
presenter.filter = filter | |
end | |
} | |
} | |
end | |
} | |
button('Clear completed', class: 'clear-completed') { | |
# Data-bind inclusion of `button` `class` `can-clear-completed` unidirectionally to presenter can_clear_completed attribute, | |
# meaning inclusion of can-clear-completed class is determined by presenter can_clear_completed boolean attribute. | |
class_name('can-clear-completed') <= [presenter, :can_clear_completed] | |
onclick do |event| | |
presenter.clear_completed | |
end | |
} | |
} | |
} | |
style { | |
r(component_element_selector) { | |
border_top '1px solid #e6e6e6' | |
font_size 15 | |
height 20 | |
padding '10px 15px' | |
text_align :center | |
display :none | |
} | |
r("#{component_element_selector}:before") { | |
bottom 0 | |
box_shadow '0 1px 1px rgba(0,0,0,.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0,0,0,.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0,0,0,.2)' | |
content '""' | |
height 50 | |
left 0 | |
overflow :hidden | |
position :absolute | |
right 0 | |
} | |
r('.todo-count') { | |
float :left | |
text_align :left | |
} | |
r('.todo-count .strong') { | |
font_weight '300' | |
} | |
r('.filters') { | |
left 0 | |
list_style :none | |
margin 0 | |
padding 0 | |
position :absolute | |
right 0 | |
} | |
r('.filters li') { | |
display :inline | |
} | |
r('.filters li a') { | |
border '1px solid transparent' | |
border_radius 3 | |
color :inherit | |
margin 3 | |
padding '3px 7px' | |
text_decoration :none | |
cursor :pointer | |
} | |
r('.filters li a.selected') { | |
border_color '#ce4646' | |
} | |
r('.clear-completed, html .clear-completed:active') { | |
cursor :pointer | |
float :right | |
line_height 19 | |
position :relative | |
text_decoration :none | |
display :none | |
} | |
r('.clear-completed.can-clear-completed, html .clear-completed.can-clear-completed:active') { | |
display :block | |
} | |
media('(max-width: 430px)') { | |
r(component_element_selector) { | |
height 50 | |
} | |
r('.filters') { | |
bottom 10 | |
} | |
} | |
} | |
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 | |
FILTERS = [:all, :active, :completed] | |
FILTER_ROUTE_REGEXP = /\#\/([^\/]*)$/ | |
attr_accessor :can_clear_completed, :active_todo_count, :created_todo | |
attr_reader :todos, :new_todo, :filter | |
def initialize | |
@todos = [] | |
@new_todo = Todo.new(task: '') | |
@filter = :all | |
@can_clear_completed = false | |
@active_todo_count = 0 | |
todo_stat_refresh_observer.observe(todos) # refresh stats if todos array adds/removes todo objects | |
end | |
def active_todos = todos.select(&:active?) | |
def completed_todos = todos.select(&:completed?) | |
def create_todo | |
todo = new_todo.clone | |
todos.append(todo) | |
observe_todo_completion_to_update_todo_stats(todo) | |
new_todo.task = '' | |
self.created_todo = todo # notifies View observer indirectly to add created todo to todo list | |
end | |
def filter=(filter) | |
return if filter == @filter | |
@filter = filter | |
end | |
def destroy(todo) | |
delete(todo) | |
end | |
def clear_completed | |
refresh_todo_stats do | |
completed_todos.each { |todo| delete(todo) } | |
end | |
end | |
def toggle_all_completed | |
target_completed_value = active_todos.any? | |
todos_to_update = target_completed_value ? active_todos : completed_todos | |
refresh_todo_stats do | |
todos_to_update.each { |todo| todo.completed = target_completed_value } | |
end | |
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 observe_todo_completion_to_update_todo_stats(todo) | |
# saving observer registration object to deregister when deleting todo | |
observers_for_todo_stats[todo.object_id] = todo_stat_refresh_observer.observe(todo, :completed) | |
end | |
def todo_stat_refresh_observer | |
@todo_stat_refresh_observer ||= Glimmer::DataBinding::Observer.proc { refresh_todo_stats } | |
end | |
def delete(todo) | |
todos.delete(todo) | |
observer_registration = observers_for_todo_stats.delete(todo.object_id) | |
observer_registration&.deregister | |
todo.deleted = true # notifies View observer indirectly to delete todo | |
end | |
def observers_for_todo_stats | |
@observers_for_todo_stats = {} | |
end | |
def refresh_todo_stats(&work_before_refresh) | |
if work_before_refresh | |
@do_not_refresh_todo_stats = true | |
work_before_refresh.call | |
@do_not_refresh_todo_stats = nil | |
end | |
return if @do_not_refresh_todo_stats | |
refresh_can_clear_completed | |
refresh_active_todo_count | |
end | |
def refresh_can_clear_completed | |
self.can_clear_completed = todos.any?(&:completed?) | |
end | |
def refresh_active_todo_count | |
self.active_todo_count = active_todos.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, :deleted, keyword_init: true) do | |
alias completed? completed | |
alias editing? editing | |
alias deleted? deleted | |
def active = !completed | |
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 |
No comments:
Post a Comment