glimmer-dsl-web 0.8.1 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1c41a710ea68b7c1e82d5a11f4a3199ad7a13e5bcbe360dcca1e329d14b4290e
4
- data.tar.gz: b99ec986f4538d9b1808bdc0d117e7cf5dbd793897197d46aae0dc9d65212290
3
+ metadata.gz: c0a02a921d8cd4c8a63d77cca2780e2a117e5291ad3243915e5591174e5ebd26
4
+ data.tar.gz: adc5e55869e5996a41bff34b71725167e458a7d25d6e2dd10bca437502c7b34c
5
5
  SHA512:
6
- metadata.gz: 1ec419eb0abf5490a56576256facbfc18a02a5743ef9bb1344186a95df5cf014535e962a5d88f1598c23243e07e691d264545a906aba761c88d93245564e08aa
7
- data.tar.gz: 52005399abe94f026d634fc5e73bca0e6d508dddce246e858adaa8e8c798a7f4c90e751695f1c2fdce707c77d49a5b0916a09987607fa78d4835f6069038ca69
6
+ metadata.gz: 6b84989eb95043c9f239d23f8605322ec8b96f0e19f32bbd327c9f1e8ea33571d755ba4764c758e6ee2471a5c01026374da185e57abbe1316acc2f8a1c19ff4d
7
+ data.tar.gz: a8c0153c7959af52e7fd8d4e98cad02eb7b9daa32bf2b0f1772d0b54cbefe54e6ee5e63f85c9ce837d337bee8b683836d1193947b04aa7fdb45d8a09fe74a58e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Change Log
2
2
 
3
+ ## 0.8.2
4
+
5
+ - Support value-less boolean attributes in HTML elements (e.g. input(:required, :autofocus, id: 'name-field') / p('Hello', :hidden, id: 'product-description')), by passing as symbols before the attributes hash, but after content string if any
6
+ - Fix issue in Hello, Component Slots! that was causing multiple renders of address_footer
7
+
3
8
  ## 0.8.1
4
9
 
5
10
  - Implement `Glimmer::Web::ElementProxy#inspect` method that includes element ID, keyword, args, and parent and prevents recursing through multiple parent levels to improve readability for troubleshooting.
@@ -8,7 +13,7 @@
8
13
  ## 0.8.0
9
14
 
10
15
  - Support formatting-paragraph-elements (e.g. 'br', 'strong', 'em') as stand-alone elements (not inside p)
11
- - Render void tags (<br>, <hr>, <img>, <input>, <meta>, <link>) as a single tag without generating a closing tag (e.g. NOT <br></br>).
16
+ - Render void tags (\<br\>, \<hr\>, \<img\>, \<input\>, \<meta\>, \<link\>) as a single tag without generating a closing tag (e.g. NOT \<br\>\</br\>).
12
17
 
13
18
  ## 0.7.3
14
19
 
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.8.1 (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.8.2 (Beta)
2
2
  ## Ruby-in-the-Browser Web Frontend Framework
3
3
  ### The "Rails" of Frontend Frameworks!!! ([Fukuoka Award Winning](https://andymaleh.blogspot.com/2025/01/glimmer-dsl-for-web-wins-in-fukuoka.html))
4
4
  #### Finally, Ruby Developer Productivity, Happiness, and Fun in the Frontend!!!
@@ -9,7 +9,7 @@
9
9
 
10
10
  **([Fukuoka Prefecture Future IT Initiative 2025 Money Forward Award Winner](https://andymaleh.blogspot.com/2025/01/glimmer-dsl-for-web-wins-in-fukuoka.html)) [(Award Announcement)](https://digitalfukuoka.jp/news/info/528/)**
11
11
 
12
- **(Talk Videos: [Intro to Ruby in the Browser](https://youtu.be/4AdcfbI6A4c?si=MmxOrkhIXTDHQoYi) / [Frontend Ruby with Glimmer DSL for Web \[Montreal.rb\]](https://youtu.be/rIZ-ILUv9ME?si=raygUXVPd_7ypWuE) / [Frontend Ruby with Glimmer DSL for Web \[/dev/mtl 2024\]](https://www.youtube.com/watch?v=J2VIY9DMJo4))**
12
+ **(Talk Videos: [Intro to Ruby in the Browser](https://youtu.be/4AdcfbI6A4c?si=MmxOrkhIXTDHQoYi) / [Frontend Ruby with Glimmer DSL for Web \[Montreal.rb\]](https://youtu.be/rIZ-ILUv9ME?si=raygUXVPd_7ypWuE) / [Frontend Ruby with Glimmer DSL for Web \[/dev/mtl 2024\]](https://www.youtube.com/watch?v=J2VIY9DMJo4)) / [Frontend Ruby with Glimmer DSL for Web at Ruby on Rio 2025-06-06 Talk](https://www.youtube.com/watch?v=LY6ulYICuzE)**
13
13
 
14
14
  **(Ruby Rogues Podcast: [Building Better Ruby Apps: Glimmer Component Slots and More](https://topenddevs.com/podcasts/ruby-rogues/episodes/building-better-ruby-apps-glimmer-s-component-slots-and-more-ruby-653))**
15
15
 
@@ -121,12 +121,12 @@ Document.ready? do
121
121
  form {
122
122
  div {
123
123
  label('Name: ', for: 'name-field')
124
- @name_input = input(type: 'text', id: 'name-field', required: true, autofocus: true)
124
+ @name_input = input(:required, :autofocus, type: 'text', id: 'name-field')
125
125
  }
126
126
 
127
127
  div {
128
128
  label('Email: ', for: 'email-field')
129
- @email_input = input(type: 'email', id: 'email-field', required: true)
129
+ @email_input = input(:required, type: 'email', id: 'email-field')
130
130
  }
131
131
 
132
132
  div {
@@ -1418,7 +1418,7 @@ rails new glimmer_app_server
1418
1418
  Add the following to `Gemfile`:
1419
1419
 
1420
1420
  ```
1421
- gem 'glimmer-dsl-web', '~> 0.8.1'
1421
+ gem 'glimmer-dsl-web', '~> 0.8.2'
1422
1422
  ```
1423
1423
 
1424
1424
  Run:
@@ -1642,9 +1642,9 @@ Under the hood, HTML element DSL keywords are invoked as Ruby methods.
1642
1642
 
1643
1643
  2- **Arguments (HTML Attributes + Text Content)**
1644
1644
 
1645
- You can set any HTML element attributes by passing as keyword arguments to element methods like `div(id: 'container', class: 'stack')` or `input(type: 'email', required: true)`
1645
+ You can set any HTML element attributes by passing as keyword arguments to element methods like `div(id: 'container', class: 'stack')` or `input(:required, type: 'email')`
1646
1646
 
1647
- Also, if the element has a little bit of text content that can fit in one line, it can be passed as the 1st argument like `label('Name: ', for: 'name_field')`, `button('Calculate', class: 'round-button')`, or `span('Mr')`
1647
+ Also, if the element has a little bit of text content that can fit in one line, it can be passed as the 1st argument like `label('Name: ', for: 'name_field')`, `button('Calculate', :disabled, class: 'round-button')`, or `span('Mr')`
1648
1648
 
1649
1649
  3- **Content Block (Properties + Listeners + Nested Elements + Text Content)**
1650
1650
 
@@ -1714,8 +1714,8 @@ the `Rails::ResourceService` class source code to find out what its API is. It c
1714
1714
  if Developers would rather not write the Backend by hand.
1715
1715
 
1716
1716
  `Rails::ResourceService` API:
1717
- - `index(resource: nil, resource_class: nil, singular_resource_name: nil, plural_resource_name: nil, index_resource_url: nil, params: nil) { |response| ... }`
1718
- - `show(resource: nil, resource_class: nil, resource_id: nil, singular_resource_name: nil, plural_resource_name: nil, show_resource_url: nil, params: nil) { |response| ... }`
1717
+ - `index(resource: nil, resource_class: nil, singular_resource_name: nil, plural_resource_name: nil, index_resource_url: nil, params: nil) { |response, resources| ... }`
1718
+ - `show(resource: nil, resource_class: nil, resource_id: nil, singular_resource_name: nil, plural_resource_name: nil, show_resource_url: nil, params: nil) { |response, resource| ... }`
1719
1719
  - `create(resource: nil, resource_class: nil, resource_attributes: nil, singular_resource_name: nil, plural_resource_name: nil, create_resource_url: nil, params: nil) { |response, created_resource, errors| ... }`
1720
1720
  - `update(resource: nil, resource_class: nil, resource_id: nil, resource_attributes: nil, singular_resource_name: nil, plural_resource_name: nil, update_resource_url: nil, params: nil) { |response, updated_resource, errors| ... }`
1721
1721
  - `destroy(resource: nil, resource_class: nil, resource_id: nil, singular_resource_name: nil, plural_resource_name: nil, destroy_resource_url: nil, params: nil) { |response| ... }`
@@ -1896,12 +1896,12 @@ Document.ready? do
1896
1896
  form {
1897
1897
  div {
1898
1898
  label('Name: ', for: 'name-field')
1899
- @name_input = input(type: 'text', id: 'name-field', required: true, autofocus: true)
1899
+ @name_input = input(:required, :autofocus, type: 'text', id: 'name-field')
1900
1900
  }
1901
1901
 
1902
1902
  div {
1903
1903
  label('Email: ', for: 'email-field')
1904
- @email_input = input(type: 'email', id: 'email-field', required: true)
1904
+ @email_input = input(:required, type: 'email', id: 'email-field')
1905
1905
  }
1906
1906
 
1907
1907
  div {
@@ -4807,6 +4807,10 @@ Learn more by reading the [GPG](https://github.com/AndyObtiva/glimmer/blob/maste
4807
4807
 
4808
4808
  F.A.Q. (Frequently Asked Questions):
4809
4809
 
4810
+ #### Can I build a modern UI with a modern style/look/feel using Glimmer DSL for Web?
4811
+
4812
+ Yes, 100%. Glimmer DSL for Web enables building highly advanced interactions with the simplest code possible. And, styling is just CSS, which is an orthogonal concern. You can apply any CSS style to Glimmer elements given that Glimmer produces real HTML, meaning 100% of what's possible in CSS outside of Glimmer is also possible within Glimmer. Glimmer DSL for Web supports applying CSS using CSS/SCSS/SASS files, embedding CSS inside Glimmer Web Components directly with a Ruby CSS DSL, and a hybrid approach when it is useful to combine multiple styling options (like keeping general styles in CSS files and adding specific styles in Glimmer Web Components). Also, you can integrate with any CSS framework (e.g. Tailwind/Bootstrap) if needed. As a result, you can create any modern UI with any modern style/look/feel when using Glimmer DSL for Web.
4813
+
4810
4814
  #### Can I reuse JavaScript libraries from Glimmer DSL for Web in Ruby?
4811
4815
 
4812
4816
  Absolutely. Glimmer DSL for Web can integrate with any JavaScript libraries. You can either load the JavaScript libraries in advance by linking to them in the Rails View/Layout (e.g. linking to JS library CDN URLs) or by including JavaScript files in the lookup directories of Opal Ruby, and adding a Ruby `require('path_to_js_lib')` call in the code. In Ruby, the `$$` global variable gives access to the top-level JavaScript global scope, which enables invocations on any JavaScript objects. For example, `$$.hljs` gives access to the loaded `window.hljs` object for the Highlight.js library, and that enables invoking any functions from that library as needed, like `$$.hljs.highlightAll` to activate code syntax highlighting.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.8.1
1
+ 0.8.2
@@ -2,11 +2,11 @@
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.8.1 ruby lib
5
+ # stub: glimmer-dsl-web 0.8.2 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "glimmer-dsl-web".freeze
9
- s.version = "0.8.1".freeze
9
+ s.version = "0.8.2".freeze
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]
@@ -9,6 +9,8 @@ module Glimmer
9
9
  class ElementExpression < Expression
10
10
  include GeneralElementExpression
11
11
 
12
+ REGEXP_ARG_TYPE_STRING = /(String)*(Hash)?/
13
+
12
14
  def can_interpret?(parent, keyword, *args, &block)
13
15
  slot = keyword.to_s.to_sym
14
16
  Glimmer::Web::ElementProxy.keyword_supported?(keyword) &&
@@ -21,9 +23,7 @@ module Glimmer
21
23
  !(parent.slot_elements.keys.include?(slot) || parent.slot_elements.keys.include?(slot.to_s))
22
24
  )
23
25
  ) ||
24
- args.size == 1 && args.first.is_a?(String) ||
25
- args.size == 1 && args.first.is_a?(Hash) ||
26
- args.size == 2 && args.first.is_a?(String) && args.last.is_a?(Hash)
26
+ valid_element_args?(args)
27
27
  ) &&
28
28
  (
29
29
  keyword != 'title' ||
@@ -36,6 +36,20 @@ module Glimmer
36
36
  parent.find_ancestor(include_self: true) { |ancestor| ancestor.keyword == 'svg' }
37
37
  )
38
38
  end
39
+
40
+ def valid_element_args?(args)
41
+ arg_types = args.map do |arg|
42
+ if arg.is_a?(String)
43
+ 'String'
44
+ elsif arg.is_a?(Hash)
45
+ 'Hash'
46
+ else
47
+ 'Unsupported'
48
+ end
49
+ end
50
+ arg_type_string = arg_types.join
51
+ arg_type_string.match(REGEXP_ARG_TYPE_STRING)
52
+ end
39
53
  end
40
54
  end
41
55
  end
@@ -64,6 +64,13 @@ module Glimmer
64
64
  Glimmer::Web::ElementProxy
65
65
  end
66
66
 
67
+ def element_boolean_attribute?(keyword, attribute)
68
+ attribute = attribute.to_s
69
+ keyword = keyword.to_s
70
+ HTML_ELEMENT_GLOBAL_BOOLEAN_ATTRIBUTES.include?(attribute) ||
71
+ HTML_ELEMENT_BOOLEAN_ATTRIBUTES[keyword]&.include?(attribute)
72
+ end
73
+
67
74
  def next_id_number_for(name)
68
75
  @max_id_numbers[name] = max_id_number_for(name) + 1
69
76
  end
@@ -88,16 +95,17 @@ module Glimmer
88
95
  @@widget_handling_listener
89
96
  end
90
97
 
91
- def render_html(element, attributes, content = nil)
92
- attributes = attributes.reduce('') do |output, option_pair|
98
+ def render_html(element, attributes:, boolean_attributes: [], content: nil)
99
+ attributes_string = attributes.reduce('') do |output, option_pair|
93
100
  attribute, value = option_pair
94
101
  value = value.to_s.sub('"', '&quot;').sub("'", '&apos;')
95
102
  output += " #{attribute}=\"#{value}\""
96
103
  end
104
+ boolean_attributes_string = boolean_attributes.to_a.map(&:to_s).join(' ')
97
105
  if html_void_keyword?(element)
98
- "<#{element}#{attributes}>"
106
+ "<#{element}#{attributes_string} #{boolean_attributes_string}>"
99
107
  else
100
- "<#{element}#{attributes}>#{content}</#{element}>"
108
+ "<#{element}#{attributes_string} #{boolean_attributes_string}>#{content}</#{element}>"
101
109
  end
102
110
  end
103
111
 
@@ -148,7 +156,29 @@ module Glimmer
148
156
 
149
157
  # title is a special attribute because it matches an element tag name (needs special treatment)
150
158
  HTML_ELEMENT_SPECIAL_ATTRIBUTES = ['title']
151
-
159
+
160
+ HTML_ELEMENT_GLOBAL_BOOLEAN_ATTRIBUTES = ['hidden', 'inert', 'contenteditable', 'spellcheck']
161
+ HTML_ELEMENT_BOOLEAN_ATTRIBUTES = {
162
+ 'audio' => ['autoplay', 'controls', 'loop', 'muted'],
163
+ 'button' => ['autofocus', 'disabled', 'formnovalidate'],
164
+ 'details' => ['open'],
165
+ 'dialog' => ['open'],
166
+ 'fieldset' => ['disabled'],
167
+ 'form' => ['novalidate'],
168
+ 'iframe' => ['allowfullscreen'],
169
+ 'img' => ['ismap'],
170
+ 'input' => ['alpha', 'autofocus', 'checked', 'disabled', 'formnovalidate', 'multiple', 'readonly', 'required'],
171
+ 'ol' => ['reversed'],
172
+ 'optgroup' => ['disabled'],
173
+ 'option' => ['disabled', 'selected'],
174
+ 'script' => ['async', 'defer', 'nomodule'],
175
+ 'select' => ['autofocus', 'disabled', 'multiple', 'required'],
176
+ 'template' => ['shadowrootclonable', 'shadowrootdelegatesfocus', 'shadowrootserializable', 'shadowrootcustomelementregistry'],
177
+ 'textarea' => ['autofocus', 'disabled', 'readonly', 'required'],
178
+ 'track' => ['default'],
179
+ 'video' => ['autoplay', 'controls', 'loop', 'muted', 'playsinline']
180
+ }
181
+
152
182
  GLIMMER_ATTRIBUTES = [:parent]
153
183
  PROPERTY_ALIASES = {
154
184
  'inner_html' => 'innerHTML',
@@ -489,9 +519,23 @@ module Glimmer
489
519
  # TODO auto-convert known glimmer attributes like parent to data attributes like data-parent
490
520
  # TODO check if we need to avoid rendering content block if no content is available
491
521
  @dom ||= begin
492
- content = args.first.is_a?(String) ? args.first : ''
522
+ if args.first.is_a?(String)
523
+ if !Glimmer::Web::ElementProxy.element_boolean_attribute?(keyword, args.first)
524
+ content = args.first
525
+ remaining_args = args[1, args.size - 1]
526
+ else
527
+ content = ''
528
+ remaining_args = args
529
+ end
530
+ else
531
+ content = ''
532
+ remaining_args = args
533
+ end
534
+ remaining_args = remaining_args.to_a
493
535
  content += children_dom_content if bulk_render?
494
- ElementProxy.render_html(keyword, html_options, content)
536
+ remaining_args = remaining_args[0...-1] if remaining_args.last.is_a?(Hash)
537
+ boolean_attributes = remaining_args
538
+ ElementProxy.render_html(keyword, attributes: html_options, boolean_attributes:, content:)
495
539
  end
496
540
  end
497
541
 
@@ -44,13 +44,21 @@ module Glimmer
44
44
 
45
45
  def format(keyword, *args, &block)
46
46
  content = nil
47
+ boolean_attributes = []
47
48
  if block_given?
48
49
  content = block.call.to_s
49
- elsif args.any? && !args.first.is_a?(Hash)
50
+ elsif args.any? && !args.first.is_a?(Hash) && !Glimmer::Web::ElementProxy.element_boolean_attribute?(keyword, args.first)
50
51
  content = args.first.to_s
52
+ args = args[1, args.size - 1]
51
53
  end
52
- attribute_hash = args.last.is_a?(Hash) ? args.last : {}
53
- ElementProxy.render_html(keyword, attribute_hash, content)
54
+ if args.last.is_a?(Hash)
55
+ attribute_hash = args.last
56
+ boolean_attributes = args[0, args.size - 1]
57
+ else
58
+ attribute_hash = {}
59
+ boolean_attributes = args
60
+ end
61
+ ElementProxy.render_html(keyword, attributes: attribute_hash, boolean_attributes:, content:)
54
62
  end
55
63
  end
56
64
 
@@ -165,7 +165,9 @@ class HelloComponentSlots
165
165
  legend('This is the address that is used for shipping your purchase.', style: {margin_bottom: 10})
166
166
  }
167
167
  address_footer { # contribute elements to the address_footer component slot
168
- p(sub("#{strong('Note:')} #{em('Purchase will be returned if the Shipping Address does not accept it in one week.')}"))
168
+ p {
169
+ sub("#{strong('Note:')} #{em('Purchase will be returned if the Shipping Address does not accept it in one week.')}")
170
+ }
169
171
  }
170
172
  }
171
173
 
@@ -175,7 +177,9 @@ class HelloComponentSlots
175
177
  legend('This is the address that is used for your billing method (e.g. credit card).', style: {margin_bottom: 10})
176
178
  }
177
179
  address_footer { # contribute elements to the address_footer component slot
178
- p(sub("#{strong('Note:')} #{em('Payment will fail if payment method does not match the Billing Address.')}"))
180
+ p {
181
+ sub("#{strong('Note:')} #{em('Payment will fail if payment method does not match the Billing Address.')}")
182
+ }
179
183
  }
180
184
  }
181
185
  }
@@ -32,12 +32,12 @@ Document.ready? do
32
32
  form {
33
33
  div {
34
34
  label('Name: ', for: 'name-field')
35
- @name_input = input(type: 'text', id: 'name-field', required: true, autofocus: true)
35
+ @name_input = input(:required, :autofocus, type: 'text', id: 'name-field')
36
36
  }
37
37
 
38
38
  div {
39
39
  label('Email: ', for: 'email-field')
40
- @email_input = input(type: 'email', id: 'email-field', required: true)
40
+ @email_input = input(:required, type: 'email', id: 'email-field')
41
41
  }
42
42
 
43
43
  div {
@@ -7,14 +7,14 @@ class ContactForm
7
7
  form {
8
8
  div {
9
9
  label('Name: ', for: 'name-field')
10
- @name_input = input(type: 'text', id: 'name-field', required: true, autofocus: true) {
10
+ @name_input = input(:required, :autofocus, type: 'text', id: 'name-field') {
11
11
  value <=> [presenter.new_contact, :name]
12
12
  }
13
13
  }
14
14
 
15
15
  div {
16
16
  label('Email: ', for: 'email-field')
17
- @email_input = input(type: 'email', id: 'email-field', required: true) {
17
+ @email_input = input(:required, type: 'email', id: 'email-field') {
18
18
  value <=> [presenter.new_contact, :email]
19
19
  }
20
20
  }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: glimmer-dsl-web
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.1
4
+ version: 0.8.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Maleh