glimmer-dsl-web 0.3.0 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6147bc2c609c3b5d7871c0d7f16b84f57c5b10c83c22a8ab63fb7e7120a69e65
4
- data.tar.gz: e469cc54afb68870470d1bb5ab4a265000d87901873119e01c68f7586ebd883e
3
+ metadata.gz: fda2e015b57a53d9281d9f04b7c222d8d42bf61bb33d670cb6ddb8cb0893d2c5
4
+ data.tar.gz: 327db5ff9687b0fb8f5ed96533c9c86dc71ca20876b8db39e28d7b156aed280b
5
5
  SHA512:
6
- metadata.gz: dadb81d22299adef771b31cc9152881a61f2cde0a93bd4a46223669d688fd78bae4fe33311875baa2cf6ee30f6f53d534ba2d4af2b5d57a49e3e902b00605dad
7
- data.tar.gz: a3426ea5baa49c13d0091bd1e3410f43ac168b1c3f04dbff5b7c7eb4c584b61bf9463b9ad59e89d3e7827abc6a48ad5c468219762a9b58332b46c8980a74397d
6
+ metadata.gz: 7b36ba84170c0a90ac89a28218275fa4da16b21972bb298cd59b675fb41670c367aec3524f8582a7181d91f2a723ea36f7d7e6b8e2a1d4e2733b634b7e8e808f
7
+ data.tar.gz: 4d88b75d775ca693fe50602f7d6b9fd136188f1d88d8d13dee8d7e281c9218a1fc81540e662b1428ff67ac1ee124a015919aec31b2853eb49378ff022003c225
data/CHANGELOG.md CHANGED
@@ -1,8 +1,23 @@
1
1
  # Change Log
2
2
 
3
+ ## 0.3.2
4
+
5
+ - Optimize performance (~248% faster) of rendering by changing DSL ordering to avoid component checks at the top
6
+ - Optimize performance of formatting html elements by adding Glimmer DSL shortcut methods
7
+ - Optimize performance of component expressions by indexing component keywords
8
+
9
+ ## 0.3.1
10
+
11
+ - Optimize Todo MVC performance for filtering between all, active, and completed (it happens instantly now)
12
+ - Append Todo MVC todos at the bottom instead of prepending them at the top (I copied the ES6 version initially which did things the opposite way from how Todo MVC behaves normally in other versions)
13
+ - Make Todo MVC "items left" text show "item left" if there is only 1 todo (I missed this detail before)
14
+ - Make Todo MVC footer links open a new tab/window (with `target: '_blank'` option)
15
+ - Refactor/Simplify Todo MVC sample code
16
+ - Upgrade to glimmer 2.7.8
17
+
3
18
  ## 0.3.0
4
19
 
5
- - Optimize performance (~170%-226% faster) by building GUI with a bulk_render call that assembles html as a string from all nested elements and mounts all HTML at once (instead of making many small DOM mount calls). The trade-off is not being able to interact with elements until rendering of the complete hierarchy is complete, which is acceptable because interactions do not happen till after everything is rendered anyways. Can be disabled by passing `bulk_render: false` option to the top-level element of a frontend app.
20
+ - Optimize performance (~170%-226% faster) of rendering by building GUI in bulk, assembling html as a string from all nested elements and mounting all HTML at once (instead of making many small DOM mount calls). The trade-off is not being able to interact with elements until rendering of the complete hierarchy is complete, which is acceptable because interactions do not happen till after everything is rendered anyways. Can be disabled by passing the `bulk_render: false` option to the top-level component/element of a frontend app.
6
21
  - Fix issue with not being able to add content to a custom control by opening a block that should add content inside its markup root element
7
22
 
8
23
  ## 0.2.8
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # [<img src="https://raw.githubusercontent.com/AndyObtiva/glimmer/master/images/glimmer-logo-hi-res.png" height=85 />](https://github.com/AndyObtiva/glimmer) Glimmer DSL for Web 0.3.0 (Beta)
1
+ # [<img src="https://raw.githubusercontent.com/AndyObtiva/glimmer/master/images/glimmer-logo-hi-res.png" height=85 />](https://github.com/AndyObtiva/glimmer) Glimmer DSL for Web 0.3.2 (Beta)
2
2
  ## Ruby in the Browser Web Frontend Framework
3
3
  ### Finally, Ruby Developer Productivity, Happiness, and Fun in the Frontend!!!
4
4
  [![Gem Version](https://badge.fury.io/rb/glimmer-dsl-web.svg)](http://badge.fury.io/rb/glimmer-dsl-web)
@@ -1154,6 +1154,10 @@ Screenshot:
1154
1154
 
1155
1155
  [Todo MVC Ruby Edition Is the One Todo MVC To Rule Them All!!!](https://andymaleh.blogspot.com/2024/06/todo-mvc-in-ruby-is-one-todo-mvc-to.html)
1156
1156
 
1157
+ [lib/glimmer-dsl-web/samples/regular/todo_mvc.rb](/lib/glimmer-dsl-web/samples/regular/todo_mvc.rb)
1158
+
1159
+ [lib/glimmer-dsl-web/samples/regular/todo_mvc](/lib/glimmer-dsl-web/samples/regular/todo_mvc)
1160
+
1157
1161
  ```ruby
1158
1162
  require 'glimmer-dsl-web'
1159
1163
 
@@ -1329,7 +1333,7 @@ rails new glimmer_app_server
1329
1333
  Add the following to `Gemfile`:
1330
1334
 
1331
1335
  ```
1332
- gem 'glimmer-dsl-web', '~> 0.3.0'
1336
+ gem 'glimmer-dsl-web', '~> 0.3.2'
1333
1337
  ```
1334
1338
 
1335
1339
  Run:
@@ -1521,6 +1525,8 @@ module ApplicationHelper
1521
1525
  end
1522
1526
  ```
1523
1527
 
1528
+ By default, elements are rendered in bulk for faster performance, meaning you cannot interact with element objects until rendering is done. This is a sensible default because most of the time, there is no need to interact with elements until the full frontend application is fully rendered. That said, if it is preferred every once in a while to render elements piecemeal instead of in bulk, this behavior can be adjusted by passing the option `bulk_render: false` to the top-level component or top-level element (if there is no component).
1529
+
1524
1530
  Note that Turbo is disabled on Glimmer elements/components. You can still use Turbo/Hotwire side by side with Glimmer DSL for Web by using one of the two technologies in every page. But, mixing them in the same pages is not recommended at the moment, so any pages loaded with Glimmer DSL for Web must be loaded without Turbo (e.g. by putting "data-turbo"="false" on anchor "a" tag links to Glimmer pages).
1525
1531
 
1526
1532
  If you run into any issues in setup, refer to the [Sample Glimmer DSL for Web Rails 7 App](https://github.com/AndyObtiva/sample-glimmer-dsl-web-rails7-app) project (in case I forgot to include some setup steps by mistake).
@@ -1556,7 +1562,7 @@ Disable the `webpacker` gem line in `Gemfile`:
1556
1562
  Add the following to `Gemfile`:
1557
1563
 
1558
1564
  ```ruby
1559
- gem 'glimmer-dsl-web', '~> 0.3.0'
1565
+ gem 'glimmer-dsl-web', '~> 0.3.2'
1560
1566
  ```
1561
1567
 
1562
1568
  Run:
@@ -3262,6 +3268,8 @@ Screenshot:
3262
3268
 
3263
3269
  [lib/glimmer-dsl-web/samples/regular/todo_mvc.rb](/lib/glimmer-dsl-web/samples/regular/todo_mvc.rb)
3264
3270
 
3271
+ [lib/glimmer-dsl-web/samples/regular/todo_mvc](/lib/glimmer-dsl-web/samples/regular/todo_mvc)
3272
+
3265
3273
  ```ruby
3266
3274
  require 'glimmer-dsl-web'
3267
3275
 
@@ -3354,10 +3362,6 @@ end
3354
3362
 
3355
3363
  ![Todo MVC](/images/glimmer-dsl-web-samples-regular-todo-mvc.gif)
3356
3364
 
3357
- The rest of the files are found at:
3358
-
3359
- [lib/glimmer-dsl-web/samples/regular/todo_mvc](/lib/glimmer-dsl-web/samples/regular/todo_mvc)
3360
-
3361
3365
  ## Design Principles
3362
3366
 
3363
3367
  - The Ruby Way (including TIMTOWTDI: There Is More Than One Way To Do It)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.0
1
+ 0.3.2
@@ -2,16 +2,16 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: glimmer-dsl-web 0.3.0 ruby lib
5
+ # stub: glimmer-dsl-web 0.3.2 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "glimmer-dsl-web".freeze
9
- s.version = "0.3.0"
9
+ s.version = "0.3.2"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib".freeze]
13
13
  s.authors = ["Andy Maleh".freeze]
14
- s.date = "2024-06-25"
14
+ s.date = "2024-07-02"
15
15
  s.description = "Glimmer DSL for Web (Ruby in the Browser Web Frontend Framework) enables building Web Frontends using Ruby in the Browser, as per Matz's recommendation in his RubyConf 2022 keynote speech to replace JavaScript with Ruby. It aims at providing the simplest, most intuitive, most straight-forward, and most productive frontend framework in existence. The framework follows the Ruby way (with DSLs and TIMTOWTDI) and the Rails way (Convention over Configuration) in building Isomorphic Ruby on Rails Applications. It provides a Ruby HTML DSL, which uniquely enables writing both structure code and logic code in one language. It supports both Unidirectional (One-Way) Data-Binding (using <=) and Bidirectional (Two-Way) Data-Binding (using <=>). Dynamic rendering (and re-rendering) of HTML content is also supported via Content Data-Binding. Modular design is supported with Glimmer Web Components. And, a Ruby CSS DSL is supported with the included Glimmer DSL for CSS. Many samples are demonstrated in the Rails sample app (there is a very minimal Standalone [No Rails] sample app too). You can finally live in pure Rubyland on the Web in both the frontend and backend with Glimmer DSL for Web! This gem relies on Opal Ruby.".freeze
16
16
  s.email = "andy.am@gmail.com".freeze
17
17
  s.extra_rdoc_files = [
@@ -93,7 +93,7 @@ Gem::Specification.new do |s|
93
93
 
94
94
  s.specification_version = 4
95
95
 
96
- s.add_runtime_dependency(%q<glimmer>.freeze, ["~> 2.7.6"])
96
+ s.add_runtime_dependency(%q<glimmer>.freeze, ["~> 2.7.9"])
97
97
  s.add_runtime_dependency(%q<glimmer-dsl-xml>.freeze, ["~> 1.4.0"])
98
98
  s.add_runtime_dependency(%q<glimmer-dsl-css>.freeze, ["~> 1.4.0"])
99
99
  s.add_runtime_dependency(%q<opal>.freeze, ["= 1.8.2"])
@@ -101,7 +101,7 @@ Gem::Specification.new do |s|
101
101
  s.add_runtime_dependency(%q<opal-async>.freeze, ["~> 1.4.1"])
102
102
  s.add_runtime_dependency(%q<opal-jquery>.freeze, ["~> 0.5.1"])
103
103
  s.add_runtime_dependency(%q<to_collection>.freeze, [">= 2.0.1", "< 3.0.0"])
104
- s.add_development_dependency(%q<puts_debuggerer>.freeze, [">= 1.0.0"])
104
+ s.add_development_dependency(%q<puts_debuggerer>.freeze, [">= 1.0.1"])
105
105
  s.add_development_dependency(%q<rake>.freeze, [">= 10.1.0", "< 14.0.0"])
106
106
  s.add_development_dependency(%q<rake-tui>.freeze, [">= 0"])
107
107
  s.add_development_dependency(%q<jeweler>.freeze, [">= 2.3.9", "< 3.0.0"])
@@ -4,7 +4,6 @@ require 'glimmer/data_binding/observer'
4
4
  module Glimmer
5
5
  module DataBinding
6
6
  class ElementBinding
7
- include Glimmer
8
7
  include Observable
9
8
  include Observer
10
9
 
@@ -12,7 +11,7 @@ module Glimmer
12
11
  def initialize(element, property, translator = nil)
13
12
  @element = element
14
13
  @property = property
15
- @translator = translator || proc {|value| value}
14
+ @translator = translator
16
15
 
17
16
  # TODO implement automatic cleanup upon calling element.remove
18
17
  # Alternatively, have this be built into ElementProxy and remove this code
@@ -24,8 +23,9 @@ module Glimmer
24
23
  end
25
24
 
26
25
  def call(value)
27
- converted_value = translated_value = @translator.call(value, evaluate_property)
28
- @element.send("#{@property}=", converted_value) unless evaluate_property == converted_value
26
+ evaluated_property_value = evaluate_property
27
+ converted_value = @translator&.call(value, evaluated_property_value) || value
28
+ @element.send("#{@property}=", converted_value) unless converted_value == evaluated_property_value
29
29
  end
30
30
 
31
31
  def evaluate_property
@@ -8,7 +8,7 @@ module Glimmer
8
8
  include ParentExpression
9
9
 
10
10
  def can_interpret?(parent, keyword, *args, &block)
11
- !!Glimmer::Web::Component.for(keyword)
11
+ Glimmer::Web::Component.for(keyword)
12
12
  end
13
13
 
14
14
  def interpret(parent, keyword, *args, &block)
@@ -40,14 +40,14 @@ module Glimmer
40
40
  Engine.add_dynamic_expressions(
41
41
  Web,
42
42
  %w[
43
- component
44
43
  listener
45
- data_binding
46
- property
47
- content_data_binding
48
- shine_data_binding
49
44
  style
45
+ content_data_binding
46
+ component
50
47
  formatting_element
48
+ data_binding
49
+ shine_data_binding
50
+ property
51
51
  ]
52
52
  )
53
53
  end
@@ -17,3 +17,14 @@ module Glimmer
17
17
  end
18
18
  end
19
19
  end
20
+
21
+ module Glimmer
22
+ # Optimize performance through shortcut methods for all HTML formatting elements that circumvent the DSL chain of responsibility
23
+ element_expression = Glimmer::DSL::Web::FormattingElementExpression.new
24
+ Glimmer::Web::FormattingElementProxy::FORMATTING_ELEMENT_KEYWORDS.each do |keyword|
25
+ Glimmer::DSL::Engine.static_expressions[keyword] ||= Concurrent::Hash.new
26
+ element_expression_dsl = element_expression.class.dsl
27
+ Glimmer::DSL::Engine.static_expressions[keyword][element_expression_dsl] = element_expression
28
+ Glimmer.send(:define_method, keyword, &Glimmer::DSL::Engine::STATIC_EXPRESSION_METHOD_FACTORY.call(keyword))
29
+ end
30
+ end
@@ -119,71 +119,54 @@ module Glimmer
119
119
  end
120
120
  end
121
121
 
122
+ ADD_COMPONENT_KEYWORDS_UPON_INHERITANCE = proc do
123
+ class << self
124
+ def inherited(subclass)
125
+ Glimmer::Web::Component.add_component_keyword_to_classes_map_for(subclass)
126
+ subclass.class_eval(&Glimmer::Web::Component::ADD_COMPONENT_KEYWORDS_UPON_INHERITANCE)
127
+ end
128
+ end
129
+ end
130
+
122
131
  class << self
123
132
  def included(klass)
124
133
  if !klass.ancestors.include?(GlimmerSupersedable)
125
134
  klass.extend(ClassMethods)
126
135
  klass.include(Glimmer)
127
136
  klass.prepend(GlimmerSupersedable)
128
- Glimmer::Web::Component.add_component_namespaces_for(klass)
137
+ Glimmer::Web::Component.add_component_keyword_to_classes_map_for(klass)
138
+ klass.class_eval(&Glimmer::Web::Component::ADD_COMPONENT_KEYWORDS_UPON_INHERITANCE)
129
139
  end
130
140
  end
131
141
 
132
142
  def for(underscored_component_name)
133
- extracted_namespaces = underscored_component_name.
134
- to_s.
135
- split(/__/).map do |namespace|
136
- namespace.camelcase(:upper)
137
- end
138
- Glimmer::Web::Component.component_namespaces.each do |base|
139
- extracted_namespaces.reduce(base) do |result, namespace|
140
- if !result.constants.include?(namespace)
141
- namespace = result.constants.detect {|c| c.to_s.upcase == namespace.to_s.upcase } || namespace
142
- end
143
- begin
144
- constant = result.const_get(namespace)
145
- return constant if constant&.respond_to?(:ancestors) &&
146
- (
147
- constant&.ancestors&.to_a.include?(Glimmer::Web::Component) ||
148
- # TODO checking GlimmerSupersedable as a hack because when a class is loaded twice (like when loading samples
149
- # by reloading ruby files), it loses its Glimmer::Web::Component ancestor as a bug in Opal
150
- # but somehow the prepend module remains
151
- constant&.ancestors&.to_a.include?(GlimmerSupersedable)
152
- )
153
- constant
154
- rescue => e
155
- # Glimmer::Config.logger.debug {"#{e.message}\n#{e.backtrace.join("\n")}"}
156
- result
157
- end
158
- end
143
+ component_classes = Glimmer::Web::Component.component_keyword_to_classes_map[underscored_component_name]
144
+ if component_classes.nil? || component_classes.empty?
145
+ Glimmer::Config.logger.debug {"#{underscored_component_name} has no Glimmer web component class!" }
146
+ nil
147
+ else
148
+ component_class = component_classes.first
159
149
  end
160
- raise "#{underscored_component_name} has no Glimmer web component class!"
161
- rescue => e
162
- Glimmer::Config.logger.debug {e.message}
163
- Glimmer::Config.logger.debug {"#{e.message}\n#{e.backtrace.join("\n")}"}
164
- nil
165
150
  end
166
151
 
167
- def add_component_namespaces_for(klass)
168
- Glimmer::Web::Component.namespaces_for_class(klass).drop(1).each do |namespace|
169
- Glimmer::Web::Component.component_namespaces << namespace
152
+ def add_component_keyword_to_classes_map_for(component_class)
153
+ keywords_for_class(component_class).each do |keyword|
154
+ Glimmer::Web::Component.component_keyword_to_classes_map[keyword] ||= []
155
+ Glimmer::Web::Component.component_keyword_to_classes_map[keyword] << component_class
170
156
  end
171
157
  end
172
158
 
173
- def namespaces_for_class(m)
174
- return [m] if m.name.nil?
175
- namespace_constants = m.name.split(/::/).map(&:to_sym)
176
- namespace_constants.reduce([Object]) do |output, namespace_constant|
177
- output += [output.last.const_get(namespace_constant)]
178
- end[1..-1].uniq.reverse
159
+ def keywords_for_class(component_class)
160
+ namespaces = component_class.to_s.split(/::/).map(&:underscore).reverse
161
+ namespaces.size.times.map { |n| namespaces[0..n].reverse.join('__') }
179
162
  end
180
163
 
181
- def component_namespaces
182
- @component_namespaces ||= reset_component_namespaces
164
+ def component_keyword_to_classes_map
165
+ @component_keyword_to_classes_map ||= reset_component_keyword_to_classes_map
183
166
  end
184
167
 
185
- def reset_component_namespaces
186
- @component_namespaces = Set[Object, Glimmer::Web]
168
+ def reset_component_keyword_to_classes_map
169
+ @component_keyword_to_classes_map = {}
187
170
  end
188
171
 
189
172
  def interpretation_stack
@@ -316,6 +299,18 @@ module Glimmer
316
299
  @markup_root&.render(parent: parent, custom_parent_dom_element: custom_parent_dom_element, brand_new: brand_new)
317
300
  end
318
301
 
302
+ def remove
303
+ @markup_root&.remove
304
+ end
305
+
306
+ def data_bind(property, model_binding)
307
+ @markup_root&.data_bind(property, model_binding)
308
+ end
309
+
310
+ def bind_content(*binding_args, &content_block)
311
+ @markup_root&.bind_content(*binding_args, &content_block)
312
+ end
313
+
319
314
  # Returns content block if used as an attribute reader (no args)
320
315
  # Otherwise, if a block is passed, it adds it as content to this Glimmer web component
321
316
  def content(*args, &block)
@@ -539,12 +539,12 @@ module Glimmer
539
539
  end
540
540
 
541
541
  def data_bind(property, model_binding)
542
- element_binding_translator = value_converters_for_input_type(type)[:model_to_view]
543
- element_binding_parameters = [self, property, element_binding_translator]
542
+ element_binding_read_translator = value_converters_for_input_type(type)&.[](:model_to_view)
543
+ element_binding_parameters = [self, property, element_binding_read_translator]
544
544
  element_binding = DataBinding::ElementBinding.new(*element_binding_parameters)
545
- element_binding.call(model_binding.evaluate_property)
546
545
  #TODO make this options observer dependent and all similar observers in element specific data binding handlers
547
546
  element_binding.observe(model_binding)
547
+ element_binding.call(model_binding.evaluate_property)
548
548
  data_bindings[element_binding] = model_binding
549
549
  unless model_binding.binding_options[:read_only]
550
550
  # TODO add guards against nil cases for hash below
@@ -552,7 +552,8 @@ module Glimmer
552
552
  if listener_keyword
553
553
  data_binding_read_listener = lambda do |event|
554
554
  view_property_value = send(property)
555
- converted_view_property_value = value_converters_for_input_type(type)[:view_to_model].call(view_property_value, model_binding.evaluate_property)
555
+ element_binding_write_translator = value_converters_for_input_type(type)&.[](:view_to_model)
556
+ converted_view_property_value = element_binding_write_translator&.call(view_property_value, model_binding.evaluate_property) || view_property_value
556
557
  model_binding.call(converted_view_property_value)
557
558
  end
558
559
  handle_observation_request(listener_keyword, data_binding_read_listener)
@@ -722,7 +723,7 @@ module Glimmer
722
723
  end
723
724
 
724
725
  def value_converters_for_input_type(input_type)
725
- input_value_converters[input_type] || {model_to_view: ->(value, old_value) {value}, view_to_model: ->(value, old_value) {value}}
726
+ input_value_converters[input_type]
726
727
  end
727
728
 
728
729
  def input_value_converters
@@ -1,28 +1,9 @@
1
- Todo = Struct.new(:task, :completed, :editing, keyword_init: true) do
2
- class << self
3
- attr_writer :all
4
-
5
- def all
6
- @all ||= []
7
- end
8
-
9
- def active
10
- all.select(&:active?)
11
- end
12
-
13
- def completed
14
- all.select(&:completed?)
15
- end
16
- end
17
-
18
- FILTERS = [:all, :active, :completed]
19
-
1
+ Todo = Struct.new(:task, :completed, :editing, :deleted, keyword_init: true) do
20
2
  alias completed? completed
21
3
  alias editing? editing
4
+ alias deleted? deleted
22
5
 
23
- def active
24
- !completed
25
- end
6
+ def active = !completed
26
7
  alias active? active
27
8
 
28
9
  def start_editing
@@ -3,53 +3,54 @@ require 'glimmer/data_binding/observer'
3
3
  require_relative '../models/todo'
4
4
 
5
5
  class TodoPresenter
6
+ FILTERS = [:all, :active, :completed]
6
7
  FILTER_ROUTE_REGEXP = /\#\/([^\/]*)$/
7
8
 
8
- attr_accessor :todos, :can_clear_completed, :active_todo_count
9
- attr_reader :new_todo, :filter
9
+ attr_accessor :can_clear_completed, :active_todo_count, :created_todo
10
+ attr_reader :todos, :new_todo, :filter
10
11
 
11
12
  def initialize
12
- @todos = Todo.all.clone
13
+ @todos = []
13
14
  @new_todo = Todo.new(task: '')
14
15
  @filter = :all
15
- refresh_todo_stats
16
+ @can_clear_completed = false
17
+ @active_todo_count = 0
18
+ todo_stat_refresh_observer.observe(todos) # refresh stats if todos array adds/removes todo objects
16
19
  end
17
20
 
18
- def create_todo(todo = nil)
19
- todo ||= new_todo.clone
20
- Todo.all.prepend(todo)
21
- observers_for_todo_stats[todo.object_id] = todo_stat_observer.observe(todo, :completed) unless observers_for_todo_stats.has_key?(todo.object_id)
22
- refresh_todos_with_filter
23
- refresh_todo_stats
24
- new_todo.task = ''
25
- end
21
+ def active_todos = todos.select(&:active?)
22
+
23
+ def completed_todos = todos.select(&:completed?)
26
24
 
27
- def refresh_todos_with_filter
28
- self.todos = Todo.send(filter).clone
25
+ def create_todo
26
+ todo = new_todo.clone
27
+ todos.append(todo)
28
+ observe_todo_completion_to_update_todo_stats(todo)
29
+ new_todo.task = ''
30
+ self.created_todo = todo # notifies View observer indirectly to add created todo to todo list
29
31
  end
30
32
 
31
33
  def filter=(filter)
32
34
  return if filter == @filter
33
35
  @filter = filter
34
- refresh_todos_with_filter
35
36
  end
36
37
 
37
38
  def destroy(todo)
38
39
  delete(todo)
39
- refresh_todos_with_filter
40
- refresh_todo_stats
41
40
  end
42
41
 
43
42
  def clear_completed
44
- Todo.completed.each { |todo| delete(todo) }
45
- refresh_todos_with_filter
46
- refresh_todo_stats
43
+ refresh_todo_stats do
44
+ completed_todos.each { |todo| delete(todo) }
45
+ end
47
46
  end
48
47
 
49
48
  def toggle_all_completed
50
- target_completed_value = Todo.active.any?
51
- todos_to_update = target_completed_value ? Todo.active : Todo.completed
52
- todos_to_update.each { |todo| todo.completed = target_completed_value }
49
+ target_completed_value = active_todos.any?
50
+ todos_to_update = target_completed_value ? active_todos : completed_todos
51
+ refresh_todo_stats do
52
+ todos_to_update.each { |todo| todo.completed = target_completed_value }
53
+ end
53
54
  end
54
55
 
55
56
  def setup_filter_routes
@@ -73,30 +74,42 @@ class TodoPresenter
73
74
 
74
75
  private
75
76
 
77
+ def observe_todo_completion_to_update_todo_stats(todo)
78
+ # saving observer registration object to deregister when deleting todo
79
+ observers_for_todo_stats[todo.object_id] = todo_stat_refresh_observer.observe(todo, :completed)
80
+ end
81
+
82
+ def todo_stat_refresh_observer
83
+ @todo_stat_refresh_observer ||= Glimmer::DataBinding::Observer.proc { refresh_todo_stats }
84
+ end
85
+
76
86
  def delete(todo)
77
- Todo.all.delete(todo)
87
+ todos.delete(todo)
78
88
  observer_registration = observers_for_todo_stats.delete(todo.object_id)
79
89
  observer_registration&.deregister
90
+ todo.deleted = true # notifies View observer indirectly to delete todo
80
91
  end
81
92
 
82
93
  def observers_for_todo_stats
83
94
  @observers_for_todo_stats = {}
84
95
  end
85
96
 
86
- def todo_stat_observer
87
- @todo_stat_observer ||= Glimmer::DataBinding::Observer.proc { refresh_todo_stats }
88
- end
89
-
90
- def refresh_todo_stats
97
+ def refresh_todo_stats(&work_before_refresh)
98
+ if work_before_refresh
99
+ @do_not_refresh_todo_stats = true
100
+ work_before_refresh.call
101
+ @do_not_refresh_todo_stats = nil
102
+ end
103
+ return if @do_not_refresh_todo_stats
91
104
  refresh_can_clear_completed
92
105
  refresh_active_todo_count
93
106
  end
94
107
 
95
108
  def refresh_can_clear_completed
96
- self.can_clear_completed = Todo.completed.any?
109
+ self.can_clear_completed = todos.any?(&:completed?)
97
110
  end
98
111
 
99
112
  def refresh_active_todo_count
100
- self.active_todo_count = Todo.active.count
113
+ self.active_todo_count = active_todos.count
101
114
  end
102
115
  end
@@ -5,10 +5,10 @@ class EditTodoInput < TodoInput
5
5
  option :todo
6
6
 
7
7
  markup {
8
- input(class: todo_input_class) { |edit_input|
8
+ input(class: self.class.todo_input_class) { |edit_input|
9
9
  style <= [ todo, :editing,
10
10
  on_read: ->(editing) { editing ? '' : 'display: none;' },
11
- after_read: ->(_) { edit_input.focus if todo.editing? }
11
+ after_read: -> { edit_input.focus if todo.editing? }
12
12
  ]
13
13
 
14
14
  value <=> [todo, :task]
@@ -25,31 +25,29 @@ class EditTodoInput < TodoInput
25
25
  onblur do |event|
26
26
  todo.save_editing
27
27
  end
28
-
29
- style {
30
- todo_input_styles
31
- }
32
28
  }
33
29
  }
34
30
 
35
- def todo_input_class
36
- 'edit-todo'
37
- end
38
-
39
- def todo_input_styles
40
- super
41
-
42
- rule("*:has(> .#{todo_input_class})") {
43
- position 'relative'
44
- }
31
+ class << self
32
+ def todo_input_class
33
+ 'edit-todo'
34
+ end
45
35
 
46
- rule(".#{todo_input_class}") {
47
- position 'absolute'
48
- display 'block'
49
- width 'calc(100% - 43px)'
50
- padding '12px 16px'
51
- margin '0 0 0 43px'
52
- top '0'
53
- }
36
+ def todo_input_styles
37
+ super
38
+
39
+ rule("*:has(> .#{todo_input_class})") {
40
+ position 'relative'
41
+ }
42
+
43
+ rule(".#{todo_input_class}") {
44
+ position 'absolute'
45
+ display 'block'
46
+ width 'calc(100% - 43px)'
47
+ padding '12px 16px'
48
+ margin '0 0 0 43px'
49
+ top '0'
50
+ }
51
+ end
54
52
  end
55
53
  end
@@ -4,7 +4,7 @@ class NewTodoInput < TodoInput
4
4
  option :presenter
5
5
 
6
6
  markup {
7
- input(class: todo_input_class, placeholder: "What needs to be done?", autofocus: "") {
7
+ input(class: self.class.todo_input_class, placeholder: "What needs to be done?", autofocus: "") {
8
8
  value <=> [presenter.new_todo, :task]
9
9
 
10
10
  onkeyup do |event|
@@ -12,30 +12,32 @@ class NewTodoInput < TodoInput
12
12
  end
13
13
 
14
14
  style {
15
- todo_input_styles
15
+ self.class.todo_input_styles
16
16
  }
17
17
  }
18
18
  }
19
19
 
20
- def todo_input_class
21
- 'new-todo'
22
- end
23
-
24
- def todo_input_styles
25
- super
26
-
27
- rule(".#{todo_input_class}") {
28
- padding '16px 16px 16px 60px'
29
- height '65px'
30
- border 'none'
31
- background 'rgba(0, 0, 0, 0.003)'
32
- box_shadow 'inset 0 -2px 1px rgba(0,0,0,0.03)'
33
- }
20
+ class << self
21
+ def todo_input_class
22
+ 'new-todo'
23
+ end
34
24
 
35
- rule(".#{todo_input_class}::placeholder") {
36
- font_style 'italic'
37
- font_weight '400'
38
- color 'rgba(0, 0, 0, 0.4)'
39
- }
25
+ def todo_input_styles
26
+ super
27
+
28
+ rule(".#{todo_input_class}") {
29
+ padding '16px 16px 16px 60px'
30
+ height '65px'
31
+ border 'none'
32
+ background 'rgba(0, 0, 0, 0.003)'
33
+ box_shadow 'inset 0 -2px 1px rgba(0,0,0,0.03)'
34
+ }
35
+
36
+ rule(".#{todo_input_class}::placeholder") {
37
+ font_style 'italic'
38
+ font_weight '400'
39
+ color 'rgba(0, 0, 0, 0.4)'
40
+ }
41
+ end
40
42
  end
41
43
  end
@@ -5,7 +5,7 @@ class TodoFilters
5
5
 
6
6
  markup {
7
7
  footer(class: 'todo-filters') {
8
- style <= [ Todo, :all,
8
+ style <= [ presenter, :todos,
9
9
  on_read: ->(todos) { todos.empty? ? 'display: none;' : '' }
10
10
  ]
11
11
 
@@ -14,12 +14,14 @@ class TodoFilters
14
14
  inner_text <= [presenter, :active_todo_count]
15
15
  }
16
16
  span {
17
- " items left"
17
+ inner_text <= [presenter, :active_todo_count,
18
+ on_read: -> (active_todo_count) { " item#{'s' if active_todo_count != 1} left" }
19
+ ]
18
20
  }
19
21
  }
20
22
 
21
23
  ul(class: 'filters') {
22
- Todo::FILTERS.each do |filter|
24
+ TodoPresenter::FILTERS.each do |filter|
23
25
  li {
24
26
  a(filter.to_s.capitalize, href: "#/#{filter unless filter == :all}") {
25
27
  class_name <= [ presenter, :filter,
@@ -2,29 +2,31 @@
2
2
  class TodoInput
3
3
  include Glimmer::Web::Component
4
4
 
5
- def todo_input_class
6
- 'todo-input'
7
- end
8
-
9
- def todo_input_styles
10
- rule(".#{todo_input_class}") {
11
- position 'relative'
12
- margin '0'
13
- width '100%'
14
- font_size '24px'
15
- font_family 'inherit'
16
- font_weight 'inherit'
17
- line_height '1.4em'
18
- color 'inherit'
19
- padding '6px'
20
- border '1px solid #999'
21
- box_shadow 'inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2)'
22
- box_sizing 'border-box'
23
- _webkit_font_smoothing 'antialiased'
24
- }
5
+ class << self
6
+ def todo_input_class
7
+ 'todo-input'
8
+ end
25
9
 
26
- rule(".#{todo_input_class}::selection") {
27
- background 'red'
28
- }
10
+ def todo_input_styles
11
+ rule(".#{todo_input_class}") {
12
+ position 'relative'
13
+ margin '0'
14
+ width '100%'
15
+ font_size '24px'
16
+ font_family 'inherit'
17
+ font_weight 'inherit'
18
+ line_height '1.4em'
19
+ color 'inherit'
20
+ padding '6px'
21
+ border '1px solid #999'
22
+ box_shadow 'inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2)'
23
+ box_sizing 'border-box'
24
+ _webkit_font_smoothing 'antialiased'
25
+ }
26
+
27
+ rule(".#{todo_input_class}::selection") {
28
+ background 'red'
29
+ }
30
+ end
29
31
  end
30
32
  end
@@ -5,9 +5,17 @@ class TodoList
5
5
 
6
6
  option :presenter
7
7
 
8
+ after_render do
9
+ observe(presenter, :created_todo) do |todo|
10
+ @todo_ul.content { # re-open todo ul content to add created todo
11
+ todo_list_item(presenter:, todo:)
12
+ }
13
+ end
14
+ end
15
+
8
16
  markup {
9
17
  main(class: 'main') {
10
- style <= [ Todo, :all,
18
+ style <= [ presenter, :todos,
11
19
  on_read: ->(todos) { todos.empty? ? 'display: none;' : '' }
12
20
  ]
13
21
 
@@ -21,12 +29,14 @@ class TodoList
21
29
  }
22
30
  }
23
31
 
24
- ul(class: 'todo-list') {
25
- content(presenter, :todos) {
26
- presenter.todos.each do |todo|
27
- todo_list_item(presenter:, todo:)
28
- end
29
- }
32
+ @todo_ul = ul {
33
+ class_name <= [presenter, :filter,
34
+ on_read: ->(filter) { "todo-list #{filter}" }
35
+ ]
36
+
37
+ presenter.todos.each do |todo|
38
+ todo_list_item(presenter:, todo:)
39
+ end
30
40
  }
31
41
 
32
42
  style {
@@ -36,6 +46,8 @@ class TodoList
36
46
  }
37
47
 
38
48
  def todo_list_styles
49
+ TodoListItem.todo_list_item_styles
50
+
39
51
  rule('.main') {
40
52
  border_top '1px solid #e6e6e6'
41
53
  position 'relative'
@@ -84,5 +96,13 @@ class TodoList
84
96
  margin '0'
85
97
  padding '0'
86
98
  }
99
+
100
+ rule('.todo-list.active li.completed') {
101
+ display 'none'
102
+ }
103
+
104
+ rule('.todo-list.completed li.active') {
105
+ display 'none'
106
+ }
87
107
  end
88
108
  end
@@ -6,20 +6,25 @@ class TodoListItem
6
6
  option :presenter
7
7
  option :todo
8
8
 
9
+ after_render do
10
+ # after rendering markup, observe todo deleted attribute and remove component when deleted
11
+ observe(todo, :deleted) do |deleted|
12
+ self.remove if deleted
13
+ end
14
+ end
15
+
9
16
  markup {
10
17
  li {
11
18
  class_name <= [ todo, :completed,
12
- on_read: -> (completed) { li_class_name(todo) }
19
+ on_read: -> { li_class_name(todo) }
13
20
  ]
14
21
  class_name <= [ todo, :editing,
15
- on_read: -> (editing) { li_class_name(todo) }
22
+ on_read: -> { li_class_name(todo) }
16
23
  ]
17
24
 
18
25
  div(class: 'view') {
19
26
  input(class: 'toggle', type: 'checkbox') {
20
- checked <=> [ todo, :completed,
21
- after_write: -> (_) { presenter.refresh_todos_with_filter if presenter.filter != :all }
22
- ]
27
+ checked <=> [todo, :completed]
23
28
  }
24
29
 
25
30
  label {
@@ -38,23 +43,20 @@ class TodoListItem
38
43
  }
39
44
 
40
45
  edit_todo_input(presenter:, todo:)
41
-
42
- if todo == presenter.todos.first
43
- style {
44
- todo_list_item_styles
45
- }
46
- end
47
46
  }
48
47
  }
49
48
 
50
49
  def li_class_name(todo)
51
50
  classes = []
52
51
  classes << 'completed' if todo.completed?
52
+ classes << 'active' if !todo.completed?
53
53
  classes << 'editing' if todo.editing?
54
54
  classes.join(' ')
55
55
  end
56
56
 
57
- def todo_list_item_styles
57
+ def self.todo_list_item_styles
58
+ EditTodoInput.todo_input_styles
59
+
58
60
  rule('.todo-list li.completed label') {
59
61
  color '#949494'
60
62
  text_decoration 'line-through'
@@ -7,10 +7,10 @@ class TodoMvcFooter
7
7
  "Double-click to edit a todo"
8
8
  }
9
9
  p {
10
- "Created by #{a('Andy Maleh', href: 'https://github.com/AndyObtiva')}"
10
+ "Created by #{a('Andy Maleh', href: 'https://github.com/AndyObtiva', target: '_blank')}"
11
11
  }
12
12
  p {
13
- "Part of #{a('TodoMVC', href: 'http://todomvc.com')}"
13
+ "Part of #{a('TodoMVC', href: 'http://todomvc.com', target: '_blank')}"
14
14
  }
15
15
 
16
16
  style {
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: glimmer-dsl-web
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Maleh
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-06-25 00:00:00.000000000 Z
11
+ date: 2024-07-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: glimmer
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 2.7.6
19
+ version: 2.7.9
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 2.7.6
26
+ version: 2.7.9
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: glimmer-dsl-xml
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -134,14 +134,14 @@ dependencies:
134
134
  requirements:
135
135
  - - ">="
136
136
  - !ruby/object:Gem::Version
137
- version: 1.0.0
137
+ version: 1.0.1
138
138
  type: :development
139
139
  prerelease: false
140
140
  version_requirements: !ruby/object:Gem::Requirement
141
141
  requirements:
142
142
  - - ">="
143
143
  - !ruby/object:Gem::Version
144
- version: 1.0.0
144
+ version: 1.0.1
145
145
  - !ruby/object:Gem::Dependency
146
146
  name: rake
147
147
  requirement: !ruby/object:Gem::Requirement