glimmer-dsl-web 0.0.4 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +475 -219
- data/VERSION +1 -1
- data/glimmer-dsl-web.gemspec +12 -5
- data/lib/glimmer/data_binding/element_binding.rb +3 -3
- data/lib/glimmer/dsl/web/bind_expression.rb +36 -0
- data/lib/glimmer/dsl/web/data_binding_expression.rb +30 -0
- data/lib/glimmer/dsl/web/dsl.rb +7 -0
- data/lib/glimmer/dsl/web/element_expression.rb +3 -21
- data/lib/glimmer/dsl/web/general_element_expression.rb +29 -0
- data/lib/glimmer/dsl/web/p_expression.rb +12 -0
- data/lib/glimmer/dsl/web/select_expression.rb +12 -0
- data/lib/glimmer/dsl/web/shine_data_binding_expression.rb +42 -0
- data/lib/glimmer/web/element_proxy.rb +31 -156
- data/lib/glimmer/web/event_proxy.rb +59 -0
- data/lib/glimmer/web/listener_proxy.rb +36 -14
- data/lib/glimmer-dsl-web/samples/hello/hello_button.rb +4 -69
- data/lib/glimmer-dsl-web/samples/hello/hello_data_binding.rb +166 -0
- data/lib/glimmer-dsl-web/samples/hello/hello_form.rb +102 -0
- data/lib/glimmer-dsl-web.rb +1 -7
- metadata +11 -23
- data/lib/glimmer/data_binding/observable_element.rb +0 -14
data/README.md
CHANGED
@@ -1,26 +1,11 @@
|
|
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.0.
|
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.0.6 (Early Alpha)
|
2
2
|
## Ruby in the Browser Web GUI Frontend Library
|
3
3
|
[![Gem Version](https://badge.fury.io/rb/glimmer-dsl-web.svg)](http://badge.fury.io/rb/glimmer-dsl-web)
|
4
4
|
[![Join the chat at https://gitter.im/AndyObtiva/glimmer](https://badges.gitter.im/AndyObtiva/glimmer.svg)](https://gitter.im/AndyObtiva/glimmer?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
5
5
|
|
6
|
-
|
6
|
+
[Glimmer](https://github.com/AndyObtiva/glimmer) DSL for Web enables building Web GUI frontends using [Ruby in the Browser](https://www.youtube.com/watch?v=4AdcfbI6A4c), as per [Matz's recommendation in his RubyConf 2022 keynote speech to replace JavaScript with Ruby](https://youtu.be/knutsgHTrfQ?t=789). It aims at providing the simplest frontend library in existence. The library follows the Ruby way (with [DSLs](https://martinfowler.com/books/dsl.html) and [TIMTOWTDI](https://en.wiktionary.org/wiki/TMTOWTDI#English)) and the Rails way ([Convention over Configuration](https://rubyonrails.org/doctrine)) while supporting both Unidirectional (One-Way) Data-Binding (using `<=`) and Bidirectional (Two-Way) Data-Binding (using `<=>`). You can finally live in pure Rubyland on the Web in both the frontend and backend with [Glimmer DSL for Web](https://rubygems.org/gems/glimmer-dsl-web)!
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
### You can finally live in pure Rubyland on the web!
|
11
|
-
|
12
|
-
[Glimmer](https://github.com/AndyObtiva/glimmer) DSL for Web is an upcoming **pre-alpha** [gem](https://rubygems.org/gems/glimmer-dsl-web) that enables building web GUI in pure Ruby via [Opal](https://opalrb.com/) on [Rails](https://rubyonrails.org/) (and potentially [Ruby WASM](https://github.com/ruby/ruby.wasm) in the future).
|
13
|
-
|
14
|
-
**Sample**
|
15
|
-
|
16
|
-
Initial HTML Markup:
|
17
|
-
|
18
|
-
```html
|
19
|
-
...
|
20
|
-
<div id="app-container">
|
21
|
-
</div>
|
22
|
-
...
|
23
|
-
```
|
8
|
+
**Hello, World! Sample**
|
24
9
|
|
25
10
|
Glimmer GUI code:
|
26
11
|
|
@@ -30,32 +15,25 @@ require 'glimmer-dsl-web'
|
|
30
15
|
include Glimmer
|
31
16
|
|
32
17
|
Document.ready? do
|
33
|
-
|
34
|
-
|
35
|
-
label(class: 'greeting') {
|
36
|
-
'Hello, World!'
|
37
|
-
}
|
18
|
+
div {
|
19
|
+
'Hello, World!'
|
38
20
|
}.render
|
39
21
|
end
|
40
22
|
```
|
41
23
|
|
42
|
-
That produces
|
24
|
+
That produces the following under `<body></body>`:
|
43
25
|
|
44
26
|
```html
|
45
|
-
|
46
|
-
|
47
|
-
<div data-parent="#app-container" class="element element-1">
|
48
|
-
<label class="greeting element element-2">
|
49
|
-
Hello, World!
|
50
|
-
</label>
|
51
|
-
</div>
|
27
|
+
<div data-parent="body" class="element element-1">
|
28
|
+
Hello, World!
|
52
29
|
</div>
|
53
|
-
...
|
54
30
|
```
|
55
31
|
|
56
32
|
![setup is working](/images/glimmer-dsl-web-setup-example-working.png)
|
57
33
|
|
58
|
-
**Hello,
|
34
|
+
**Hello, Button!**
|
35
|
+
|
36
|
+
Event listeners can be setup on any element using the same event names used in HTML (e.g. `onclick`) while passing in a standard Ruby block to handle behavior. `$$` gives access to `window` to invoke functions like `alert`.
|
59
37
|
|
60
38
|
Glimmer GUI code:
|
61
39
|
|
@@ -66,7 +44,11 @@ include Glimmer
|
|
66
44
|
|
67
45
|
Document.ready? do
|
68
46
|
div {
|
69
|
-
'
|
47
|
+
button('Greet') {
|
48
|
+
onclick do
|
49
|
+
$$.alert('Hello, Button!')
|
50
|
+
end
|
51
|
+
}
|
70
52
|
}.render
|
71
53
|
end
|
72
54
|
```
|
@@ -75,13 +57,17 @@ That produces the following under `<body></body>`:
|
|
75
57
|
|
76
58
|
```html
|
77
59
|
<div data-parent="body" class="element element-1">
|
78
|
-
|
60
|
+
<button class="element element-2">Greet</button>
|
79
61
|
</div>
|
80
62
|
```
|
81
63
|
|
82
|
-
|
64
|
+
Screenshot:
|
83
65
|
|
84
|
-
|
66
|
+
![Hello, Button!](/images/glimmer-dsl-web-samples-hello-hello-button.gif)
|
67
|
+
|
68
|
+
**Hello, Form!**
|
69
|
+
|
70
|
+
[Glimmer DSL for Web](https://rubygems.org/gems/glimmer-dsl-web) gives access to all Web Browser built-in features like HTML form validations, input focus, events, and element functions from a very terse and productive Ruby GUI DSL.
|
85
71
|
|
86
72
|
Glimmer GUI code:
|
87
73
|
|
@@ -93,44 +79,50 @@ include Glimmer
|
|
93
79
|
Document.ready? do
|
94
80
|
div {
|
95
81
|
h1('Contact Form')
|
82
|
+
|
96
83
|
form {
|
97
|
-
div
|
84
|
+
div {
|
98
85
|
label('Name: ', for: 'name-field')
|
99
|
-
@name_input = input(
|
86
|
+
@name_input = input(type: 'text', id: 'name-field', required: true, autofocus: true)
|
100
87
|
}
|
101
|
-
|
88
|
+
|
89
|
+
div {
|
102
90
|
label('Email: ', for: 'email-field')
|
103
|
-
@email_input = input(
|
91
|
+
@email_input = input(type: 'email', id: 'email-field', required: true)
|
104
92
|
}
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
93
|
+
|
94
|
+
div {
|
95
|
+
input(type: 'submit', value: 'Add Contact') {
|
96
|
+
onclick do |event|
|
97
|
+
if ([@name_input, @email_input].all? {|input| input.check_validity })
|
98
|
+
# re-open table content and add row
|
99
|
+
@table.content {
|
100
|
+
tr {
|
101
|
+
td { @name_input.value }
|
102
|
+
td { @email_input.value }
|
103
|
+
}
|
112
104
|
}
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
error_messages = []
|
117
|
-
error_messages << "Name is not valid! Make sure it is filled." if !@name_input.check_validity
|
118
|
-
error_messages << "Email is not valid! Make sure it is filled and has a valid format." if !@email_input.check_validity
|
119
|
-
$$.alert(error_messages.join("\n"))
|
105
|
+
@email_input.value = @name_input.value = ''
|
106
|
+
@name_input.focus
|
107
|
+
end
|
120
108
|
end
|
121
|
-
|
109
|
+
}
|
122
110
|
}
|
123
111
|
}
|
112
|
+
|
124
113
|
h1('Contacts Table')
|
114
|
+
|
125
115
|
@table = table {
|
126
116
|
tr {
|
127
117
|
th('Name')
|
128
118
|
th('Email')
|
129
119
|
}
|
120
|
+
|
130
121
|
tr {
|
131
122
|
td('John Doe')
|
132
123
|
td('johndoe@example.com')
|
133
124
|
}
|
125
|
+
|
134
126
|
tr {
|
135
127
|
td('Jane Doe')
|
136
128
|
td('janedoe@example.com')
|
@@ -140,15 +132,11 @@ Document.ready? do
|
|
140
132
|
# CSS Styles
|
141
133
|
style {
|
142
134
|
<<~CSS
|
143
|
-
|
144
|
-
margin:
|
145
|
-
}
|
146
|
-
.field {
|
147
|
-
margin-left: 5px;
|
135
|
+
input {
|
136
|
+
margin: 5px;
|
148
137
|
}
|
149
|
-
|
150
|
-
|
151
|
-
margin: 10px 5px;
|
138
|
+
input[type=submit] {
|
139
|
+
margin: 5px 0;
|
152
140
|
}
|
153
141
|
table {
|
154
142
|
border:1px solid grey;
|
@@ -171,83 +159,166 @@ That produces the following under `<body></body>`:
|
|
171
159
|
```html
|
172
160
|
<div data-parent="body" class="element element-1">
|
173
161
|
<h1 class="element element-2">Contact Form</h1>
|
162
|
+
|
174
163
|
<form class="element element-3">
|
175
|
-
<div class="
|
164
|
+
<div class="element element-4">
|
176
165
|
<label for="name-field" class="element element-5">Name: </label>
|
177
|
-
<input id="name-field"
|
166
|
+
<input type="text" id="name-field" required="true" autofocus="true" class="element element-6">
|
178
167
|
</div>
|
179
|
-
|
168
|
+
|
169
|
+
<div class="element element-7">
|
180
170
|
<label for="email-field" class="element element-8">Email: </label>
|
181
|
-
<input id="email-field" class="
|
171
|
+
<input type="email" id="email-field" required="true" class="element element-9">
|
172
|
+
</div>
|
173
|
+
|
174
|
+
<div class="element element-10">
|
175
|
+
<input type="submit" value="Add Contact" class="element element-11">
|
182
176
|
</div>
|
183
|
-
<button class="submit-button element element-10">Add Contact</button>
|
184
177
|
</form>
|
185
|
-
|
186
|
-
<
|
187
|
-
|
188
|
-
|
189
|
-
|
178
|
+
|
179
|
+
<h1 class="element element-12">Contacts Table</h1>
|
180
|
+
|
181
|
+
<table class="element element-13">
|
182
|
+
<tr class="element element-14">
|
183
|
+
<th class="element element-15">Name</th>
|
184
|
+
<th class="element element-16">Email</th>
|
190
185
|
</tr>
|
191
|
-
|
192
|
-
|
193
|
-
<td class="element element-18">
|
186
|
+
|
187
|
+
<tr class="element element-17">
|
188
|
+
<td class="element element-18">John Doe</td>
|
189
|
+
<td class="element element-19">johndoe@example.com</td>
|
194
190
|
</tr>
|
195
|
-
|
196
|
-
|
197
|
-
<td class="element element-21">
|
191
|
+
|
192
|
+
<tr class="element element-20">
|
193
|
+
<td class="element element-21">Jane Doe</td>
|
194
|
+
<td class="element element-22">janedoe@example.com</td>
|
198
195
|
</tr>
|
199
196
|
</table>
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
background: #ccc;
|
219
|
-
}
|
197
|
+
|
198
|
+
<style class="element element-23">
|
199
|
+
input {
|
200
|
+
margin: 5px;
|
201
|
+
}
|
202
|
+
input[type=submit] {
|
203
|
+
margin: 5px 0;
|
204
|
+
}
|
205
|
+
table {
|
206
|
+
border:1px solid grey;
|
207
|
+
border-spacing: 0;
|
208
|
+
}
|
209
|
+
table tr td, table tr th {
|
210
|
+
padding: 5px;
|
211
|
+
}
|
212
|
+
table tr:nth-child(even) {
|
213
|
+
background: #ccc;
|
214
|
+
}
|
220
215
|
</style>
|
221
216
|
</div>
|
222
217
|
```
|
223
218
|
|
224
|
-
|
225
|
-
|
226
|
-
---
|
227
|
-
|
228
|
-
***Hello, Button!***
|
219
|
+
Screenshot:
|
229
220
|
|
230
|
-
![Hello,
|
221
|
+
![Hello, Form!](/images/glimmer-dsl-web-samples-hello-hello-form.gif)
|
231
222
|
|
232
|
-
|
223
|
+
**Hello, Data-Binding!**
|
233
224
|
|
234
|
-
|
225
|
+
Glimmer GUI code:
|
235
226
|
|
236
|
-
|
227
|
+
```ruby
|
228
|
+
require 'glimmer-dsl-web'
|
237
229
|
|
238
|
-
|
230
|
+
Address = Struct.new(:street, :street2, :city, :state, :zip_code, keyword_init: true) do
|
231
|
+
STATES = {...} # contains US States
|
232
|
+
|
233
|
+
def state_code
|
234
|
+
STATES.invert[state]
|
235
|
+
end
|
236
|
+
|
237
|
+
def state_code=(value)
|
238
|
+
self.state = STATES[value]
|
239
|
+
end
|
239
240
|
|
240
|
-
|
241
|
+
def summary
|
242
|
+
values.map(&:to_s).reject(&:empty?).join(', ')
|
243
|
+
end
|
244
|
+
end
|
241
245
|
|
242
|
-
|
246
|
+
@address = Address.new(
|
247
|
+
street: '123 Main St',
|
248
|
+
street2: 'Apartment 3C, 2nd door to the right',
|
249
|
+
city: 'San Diego',
|
250
|
+
state: 'California',
|
251
|
+
zip_code: '91911'
|
252
|
+
)
|
243
253
|
|
244
|
-
|
254
|
+
include Glimmer
|
245
255
|
|
246
|
-
|
256
|
+
Document.ready? do
|
257
|
+
div {
|
258
|
+
form(style: 'display: grid; grid-auto-columns: 80px 200px;') { |address_form|
|
259
|
+
label('Street: ', for: 'street-field')
|
260
|
+
input(id: 'street-field') {
|
261
|
+
# Bidirectional Data-Binding with <=> ensures input.value and @address.street
|
262
|
+
# automatically stay in sync when either side changes
|
263
|
+
value <=> [@address, :street]
|
264
|
+
}
|
265
|
+
|
266
|
+
label('Street 2: ', for: 'street2-field')
|
267
|
+
textarea(id: 'street2-field') {
|
268
|
+
value <=> [@address, :street2]
|
269
|
+
}
|
270
|
+
|
271
|
+
label('City: ', for: 'city-field')
|
272
|
+
input(id: 'city-field') {
|
273
|
+
value <=> [@address, :city]
|
274
|
+
}
|
275
|
+
|
276
|
+
label('State: ', for: 'state-field')
|
277
|
+
select(id: 'state-field') {
|
278
|
+
Address::STATES.each do |state_code, state|
|
279
|
+
option(value: state_code) { state }
|
280
|
+
end
|
281
|
+
|
282
|
+
value <=> [@address, :state_code]
|
283
|
+
}
|
284
|
+
|
285
|
+
label('Zip Code: ', for: 'zip-code-field')
|
286
|
+
input(id: 'zip-code-field', type: 'number', min: '0', max: '99999') {
|
287
|
+
# Bidirectional Data-Binding with <=> ensures input.value and @address.zip_code
|
288
|
+
# automatically stay in sync when either side changes
|
289
|
+
# on_write option specifies :to_s method to invoke on value before writing to model attribute
|
290
|
+
# to ensure the numeric zip code value is stored as a String
|
291
|
+
value <=> [@address, :zip_code,
|
292
|
+
on_write: :to_s
|
293
|
+
]
|
294
|
+
}
|
295
|
+
|
296
|
+
style {
|
297
|
+
<<~CSS
|
298
|
+
.#{address_form.element_id} * {
|
299
|
+
margin: 5px;
|
300
|
+
}
|
301
|
+
.#{address_form.element_id} input, .#{address_form.element_id} select {
|
302
|
+
grid-column: 2;
|
303
|
+
}
|
304
|
+
CSS
|
305
|
+
}
|
306
|
+
}
|
307
|
+
|
308
|
+
div(style: 'margin: 5px') {
|
309
|
+
# Unidirectional Data-Binding is done with <= to ensure @address.summary changes update div.inner_text
|
310
|
+
# as computed by changes to the address member attributes + state_code address custom attribute
|
311
|
+
inner_text <= [@address, :summary,
|
312
|
+
computed_by: @address.members + ['state_code']
|
313
|
+
]
|
314
|
+
}
|
315
|
+
}.render
|
316
|
+
end
|
317
|
+
```
|
247
318
|
|
248
|
-
|
319
|
+
Screenshot:
|
249
320
|
|
250
|
-
|
321
|
+
![Hello, Data-Binding!](/images/glimmer-dsl-web-samples-hello-hello-data-binding.gif)
|
251
322
|
|
252
323
|
**Button Counter Sample**
|
253
324
|
|
@@ -279,7 +350,7 @@ class HelloButton
|
|
279
350
|
|
280
351
|
markup {
|
281
352
|
# This will hook into element #app-container and then build HTML inside it using Ruby DSL code
|
282
|
-
div(
|
353
|
+
div(parent: parent_selector) {
|
283
354
|
text 'Button Counter'
|
284
355
|
|
285
356
|
button {
|
@@ -288,7 +359,7 @@ class HelloButton
|
|
288
359
|
# copied to button innerText (content) to display to the user
|
289
360
|
inner_text <= [@counter, :count, on_read: ->(value) { "Click To Increment: #{value} " }]
|
290
361
|
|
291
|
-
|
362
|
+
onclick {
|
292
363
|
@counter.increment!
|
293
364
|
}
|
294
365
|
}
|
@@ -331,7 +402,7 @@ When clicked 7 times:
|
|
331
402
|
|
332
403
|
|
333
404
|
|
334
|
-
NOTE: Glimmer DSL for Web is
|
405
|
+
NOTE: Glimmer DSL for Web is an Early Alpha project. If you want it developed faster, please [open an issue report](https://github.com/AndyObtiva/glimmer-dsl-web/issues/new). I have completed some GitHub project features much faster before due to [issue reports](https://github.com/AndyObtiva/glimmer-dsl-web/issues) and [pull requests](https://github.com/AndyObtiva/glimmer-dsl-web/pulls). Please help make better by contributing, adopting for small or low risk projects, and providing feedback. It is still an early alpha, so the more feedback and issues you report the better.
|
335
406
|
|
336
407
|
Learn more about the differences between various [Glimmer](https://github.com/AndyObtiva/glimmer) DSLs by looking at:
|
337
408
|
|
@@ -346,10 +417,13 @@ Learn more about the differences between various [Glimmer](https://github.com/An
|
|
346
417
|
- [Setup](#setup)
|
347
418
|
- [Usage](#usage)
|
348
419
|
- [Supported Glimmer DSL Keywords](#supported-glimmer-dsl-keywords)
|
420
|
+
- [Coming from Glimmer DSL for Opal](#coming-from-glimmer-dsl-for-opal)
|
349
421
|
- [Samples](#samples)
|
350
422
|
- [Hello Samples](#hello-samples)
|
351
423
|
- [Hello, World!](#hello-world)
|
352
424
|
- [Hello, Button!](#hello-button)
|
425
|
+
- [Hello, Form!](#hello-form)
|
426
|
+
- [Hello, Data-Binding!](#hello-data-binding)
|
353
427
|
- [Button Counter](#button-counter)
|
354
428
|
- [Glimmer Process](#glimmer-process)
|
355
429
|
- [Help](#help)
|
@@ -363,11 +437,12 @@ Learn more about the differences between various [Glimmer](https://github.com/An
|
|
363
437
|
|
364
438
|
## Prerequisites
|
365
439
|
|
440
|
+
[Glimmer DSL for Web](https://rubygems.org/gems/glimmer-dsl-web) will begin by supporting [Opal Ruby](https://opalrb.com/) on [Rails](https://rubyonrails.org/). [Opal](https://opalrb.com/) is a lightweight Ruby to JavaScript transpiler that results in small downloadables compared to WASM. In the future, the project might grow to support [Ruby WASM](https://github.com/ruby/ruby.wasm) as an alternative to [Opal Ruby](https://opalrb.com/) that could be switched to with a simple configuration change.
|
441
|
+
|
366
442
|
- Ruby 3.0 (newer Ruby versions are not supported at this time)
|
367
443
|
- Rails 6-7: [https://github.com/rails/rails](https://github.com/rails/rails)
|
368
|
-
- Opal 1.4.1 for Rails 6-7
|
369
|
-
- Opal-Rails 2.0.2 for Rails 6-7
|
370
|
-
- jQuery 3 (included): [https://code.jquery.com/](https://code.jquery.com/) (jQuery 3.6.0 is included in the [glimmer-dsl-web](https://rubygems.org/gems/glimmer-dsl-web) gem)
|
444
|
+
- Opal 1.4.1 for Rails 6-7: [https://github.com/opal/opal](https://github.com/opal/opal)
|
445
|
+
- Opal-Rails 2.0.2 for Rails 6-7: [https://github.com/opal/opal-rails](https://github.com/opal/opal-rails)
|
371
446
|
|
372
447
|
## Setup
|
373
448
|
|
@@ -398,7 +473,7 @@ gem 'opal', '1.4.1'
|
|
398
473
|
gem 'opal-rails', '2.0.2'
|
399
474
|
gem 'opal-async', '~> 1.4.0'
|
400
475
|
gem 'opal-jquery', '~> 0.4.6'
|
401
|
-
gem 'glimmer-dsl-web', '~> 0.0.
|
476
|
+
gem 'glimmer-dsl-web', '~> 0.0.6'
|
402
477
|
gem 'glimmer-dsl-xml', '~> 1.3.1', require: false
|
403
478
|
gem 'glimmer-dsl-css', '~> 1.2.1', require: false
|
404
479
|
```
|
@@ -543,7 +618,7 @@ gem 'opal', '1.4.1'
|
|
543
618
|
gem 'opal-rails', '2.0.2'
|
544
619
|
gem 'opal-async', '~> 1.4.0'
|
545
620
|
gem 'opal-jquery', '~> 0.4.6'
|
546
|
-
gem 'glimmer-dsl-web', '~> 0.0.
|
621
|
+
gem 'glimmer-dsl-web', '~> 0.0.6'
|
547
622
|
gem 'glimmer-dsl-xml', '~> 1.3.1', require: false
|
548
623
|
gem 'glimmer-dsl-css', '~> 1.2.1', require: false
|
549
624
|
```
|
@@ -669,21 +744,21 @@ Otherwise, if you still cannot setup successfully (even with the help of the sam
|
|
669
744
|
|
670
745
|
Glimmer DSL for Web offers a GUI DSL for building HTML Web User Interfaces declaratively in Ruby.
|
671
746
|
|
672
|
-
1- Keywords (HTML Elements)
|
747
|
+
1- **Keywords (HTML Elements)**
|
673
748
|
|
674
749
|
You can declare any HTML element by simply using the lowercase underscored version of its name (Ruby convention for method names) like `div`, `span`, `form`, `input`, `button`, `table`, `tr`, `th`, and `td`.
|
675
750
|
|
676
751
|
Under the hood, HTML element DSL keywords are invoked as Ruby methods.
|
677
752
|
|
678
|
-
2- Arguments (HTML Attributes + Text Content)
|
753
|
+
2- **Arguments (HTML Attributes + Text Content)**
|
679
754
|
|
680
755
|
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)`
|
681
756
|
|
682
757
|
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')`
|
683
758
|
|
684
|
-
3- Content Block (Properties + Listeners + Nested Elements + Text Content)
|
759
|
+
3- **Content Block (Properties + Listeners + Nested Elements + Text Content)**
|
685
760
|
|
686
|
-
Element methods can accept a Ruby content block. It intentionally has a `{...}` style even as a multi-line block to indicate that the code is declarative GUI structure code.
|
761
|
+
Element methods can accept a Ruby content block. It intentionally has a `{...}` style even as a multi-line block to indicate that the code is declarative GUI structure code (intentionally breaking away from Ruby imperative code conventions given this is a declarative GUI DSL, meaning a different language that has its own conventions, embedded within Ruby).
|
687
762
|
|
688
763
|
You can nest HTML element properties under an element like:
|
689
764
|
|
@@ -693,11 +768,11 @@ input(type: 'text') {
|
|
693
768
|
}
|
694
769
|
```
|
695
770
|
|
696
|
-
You can nest HTML event listeners under an element by using
|
771
|
+
You can nest HTML event listeners under an element by using the HTML event listener name (e.g. `onclick`, `onchange`, `onblur`):
|
697
772
|
|
698
773
|
```ruby
|
699
774
|
button('Add') {
|
700
|
-
|
775
|
+
onclick do
|
701
776
|
@model.add_selected_element
|
702
777
|
end
|
703
778
|
}
|
@@ -718,26 +793,36 @@ form {
|
|
718
793
|
input(id: 'email-field', class: 'field', type: 'email', required: true)
|
719
794
|
}
|
720
795
|
button('Add Contact', class: 'submit-button') {
|
721
|
-
|
796
|
+
onclick do
|
722
797
|
...
|
723
798
|
end
|
724
799
|
}
|
725
800
|
}
|
726
801
|
```
|
727
802
|
|
728
|
-
You can nest text content underneath an element's Ruby block, like:
|
803
|
+
You can nest text content underneath an element's Ruby block provided it is the return value of the block (last declared value), like:
|
729
804
|
|
730
805
|
```ruby
|
731
|
-
|
806
|
+
p(class: 'summary') {
|
732
807
|
'This text content is going into the body of the span element'
|
733
808
|
}
|
734
809
|
```
|
735
810
|
|
811
|
+
4- **Operations (Properties + Functions)**
|
812
|
+
|
813
|
+
You can get/set any element property or invoke any element function by simply calling the lowercase underscored version of their name in Ruby like `input.check_validity`, `input.value`, and `input.id`.
|
814
|
+
|
736
815
|
## Supported Glimmer DSL Keywords
|
737
816
|
|
738
|
-
[All HTML elements](https://developer.mozilla.org/en-US/docs/Web/HTML/Element).
|
817
|
+
[All HTML elements](https://developer.mozilla.org/en-US/docs/Web/HTML/Element), following the Ruby method name standard of lowercase and underscored names.
|
739
818
|
|
740
|
-
[All HTML attributes](https://www.w3schools.com/html/html_attributes.asp).
|
819
|
+
[All HTML attributes](https://www.w3schools.com/html/html_attributes.asp), following the Ruby method name standard of lowercase and underscored names.
|
820
|
+
|
821
|
+
[All HTML events](https://www.w3schools.com/tags/ref_eventattributes.asp), same event attribute names as in HTML.
|
822
|
+
|
823
|
+
## Coming from Glimmer DSL for Opal
|
824
|
+
|
825
|
+
This project is inspired by [Glimmer DSL for Opal](https://github.com/AndyObtiva/glimmer-dsl-opal) and is similar in enabling frontend GUI development with Ruby. [Glimmer DSL for Web](https://rubygems.org/gems/glimmer-dsl-web) mainly differs from Glimmer DSL for Opal by adopting a DSL that follows web-like HTML syntax in Ruby to facilitate leveraging existing HTML/CSS/JS skills instead of adopting a desktop GUI DSL that is webified. As a result, applications written in Glimmer DSL for Opal are not compatible with Glimmer DSL for Web.
|
741
826
|
|
742
827
|
## Samples
|
743
828
|
|
@@ -745,8 +830,6 @@ This external sample app contains all the samples mentioned below configured ins
|
|
745
830
|
|
746
831
|
https://github.com/AndyObtiva/sample-glimmer-dsl-web-rails7-app
|
747
832
|
|
748
|
-
**[NOT RELEASED OR SUPPORTED YET]** https://github.com/AndyObtiva/sample-glimmer-dsl-web-rails-app
|
749
|
-
|
750
833
|
### Hello Samples
|
751
834
|
|
752
835
|
#### Hello, World!
|
@@ -775,7 +858,7 @@ That produces the following under `<body></body>`:
|
|
775
858
|
|
776
859
|
![setup is working](/images/glimmer-dsl-web-setup-example-working.png)
|
777
860
|
|
778
|
-
Alternative syntax when an element
|
861
|
+
Alternative syntax (useful when an element has text content that fits in one line):
|
779
862
|
|
780
863
|
```ruby
|
781
864
|
require 'glimmer-dsl-web'
|
@@ -806,47 +889,85 @@ require 'glimmer-dsl-web'
|
|
806
889
|
|
807
890
|
include Glimmer
|
808
891
|
|
892
|
+
Document.ready? do
|
893
|
+
div {
|
894
|
+
button('Greet') {
|
895
|
+
onclick do
|
896
|
+
$$.alert('Hello, Button!')
|
897
|
+
end
|
898
|
+
}
|
899
|
+
}.render
|
900
|
+
end
|
901
|
+
```
|
902
|
+
|
903
|
+
That produces the following under `<body></body>`:
|
904
|
+
|
905
|
+
```html
|
906
|
+
<div data-parent="body" class="element element-1">
|
907
|
+
<button class="element element-2">Greet</button>
|
908
|
+
</div>
|
909
|
+
```
|
910
|
+
|
911
|
+
Screenshot:
|
912
|
+
|
913
|
+
![Hello, Button!](/images/glimmer-dsl-web-samples-hello-hello-button.gif)
|
914
|
+
|
915
|
+
#### Hello, Form!
|
916
|
+
|
917
|
+
Glimmer GUI code:
|
918
|
+
|
919
|
+
```ruby
|
920
|
+
require 'glimmer-dsl-web'
|
921
|
+
|
922
|
+
include Glimmer
|
923
|
+
|
809
924
|
Document.ready? do
|
810
925
|
div {
|
811
926
|
h1('Contact Form')
|
927
|
+
|
812
928
|
form {
|
813
|
-
div
|
929
|
+
div {
|
814
930
|
label('Name: ', for: 'name-field')
|
815
|
-
@name_input = input(
|
931
|
+
@name_input = input(type: 'text', id: 'name-field', required: true, autofocus: true)
|
816
932
|
}
|
817
|
-
|
933
|
+
|
934
|
+
div {
|
818
935
|
label('Email: ', for: 'email-field')
|
819
|
-
@email_input = input(
|
936
|
+
@email_input = input(type: 'email', id: 'email-field', required: true)
|
820
937
|
}
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
|
825
|
-
|
826
|
-
|
827
|
-
|
938
|
+
|
939
|
+
div {
|
940
|
+
input(type: 'submit', value: 'Add Contact') {
|
941
|
+
onclick do |event|
|
942
|
+
if ([@name_input, @email_input].all? {|input| input.check_validity })
|
943
|
+
# re-open table content and add row
|
944
|
+
@table.content {
|
945
|
+
tr {
|
946
|
+
td { @name_input.value }
|
947
|
+
td { @email_input.value }
|
948
|
+
}
|
828
949
|
}
|
829
|
-
|
830
|
-
|
831
|
-
|
832
|
-
error_messages = []
|
833
|
-
error_messages << "Name is not valid! Make sure it is filled." if !@name_input.check_validity
|
834
|
-
error_messages << "Email is not valid! Make sure it is filled and has a valid format." if !@email_input.check_validity
|
835
|
-
$$.alert(error_messages.join("\n"))
|
950
|
+
@email_input.value = @name_input.value = ''
|
951
|
+
@name_input.focus
|
952
|
+
end
|
836
953
|
end
|
837
|
-
|
954
|
+
}
|
838
955
|
}
|
839
956
|
}
|
957
|
+
|
840
958
|
h1('Contacts Table')
|
959
|
+
|
841
960
|
@table = table {
|
842
961
|
tr {
|
843
962
|
th('Name')
|
844
963
|
th('Email')
|
845
964
|
}
|
965
|
+
|
846
966
|
tr {
|
847
967
|
td('John Doe')
|
848
968
|
td('johndoe@example.com')
|
849
969
|
}
|
970
|
+
|
850
971
|
tr {
|
851
972
|
td('Jane Doe')
|
852
973
|
td('janedoe@example.com')
|
@@ -856,15 +977,11 @@ Document.ready? do
|
|
856
977
|
# CSS Styles
|
857
978
|
style {
|
858
979
|
<<~CSS
|
859
|
-
|
860
|
-
margin:
|
980
|
+
input {
|
981
|
+
margin: 5px;
|
861
982
|
}
|
862
|
-
|
863
|
-
margin
|
864
|
-
}
|
865
|
-
.submit-button {
|
866
|
-
display: block;
|
867
|
-
margin: 10px 5px;
|
983
|
+
input[type=submit] {
|
984
|
+
margin: 5px 0;
|
868
985
|
}
|
869
986
|
table {
|
870
987
|
border:1px solid grey;
|
@@ -887,83 +1004,222 @@ That produces the following under `<body></body>`:
|
|
887
1004
|
```html
|
888
1005
|
<div data-parent="body" class="element element-1">
|
889
1006
|
<h1 class="element element-2">Contact Form</h1>
|
1007
|
+
|
890
1008
|
<form class="element element-3">
|
891
|
-
<div class="
|
1009
|
+
<div class="element element-4">
|
892
1010
|
<label for="name-field" class="element element-5">Name: </label>
|
893
|
-
<input id="name-field"
|
1011
|
+
<input type="text" id="name-field" required="true" autofocus="true" class="element element-6">
|
894
1012
|
</div>
|
895
|
-
|
1013
|
+
|
1014
|
+
<div class="element element-7">
|
896
1015
|
<label for="email-field" class="element element-8">Email: </label>
|
897
|
-
<input id="email-field" class="
|
1016
|
+
<input type="email" id="email-field" required="true" class="element element-9">
|
1017
|
+
</div>
|
1018
|
+
|
1019
|
+
<div class="element element-10">
|
1020
|
+
<input type="submit" value="Add Contact" class="element element-11">
|
898
1021
|
</div>
|
899
|
-
<button class="submit-button element element-10">Add Contact</button>
|
900
1022
|
</form>
|
901
|
-
|
902
|
-
<
|
903
|
-
|
904
|
-
|
905
|
-
|
1023
|
+
|
1024
|
+
<h1 class="element element-12">Contacts Table</h1>
|
1025
|
+
|
1026
|
+
<table class="element element-13">
|
1027
|
+
<tr class="element element-14">
|
1028
|
+
<th class="element element-15">Name</th>
|
1029
|
+
<th class="element element-16">Email</th>
|
906
1030
|
</tr>
|
907
|
-
|
908
|
-
|
909
|
-
<td class="element element-18">
|
1031
|
+
|
1032
|
+
<tr class="element element-17">
|
1033
|
+
<td class="element element-18">John Doe</td>
|
1034
|
+
<td class="element element-19">johndoe@example.com</td>
|
910
1035
|
</tr>
|
911
|
-
|
912
|
-
|
913
|
-
<td class="element element-21">
|
1036
|
+
|
1037
|
+
<tr class="element element-20">
|
1038
|
+
<td class="element element-21">Jane Doe</td>
|
1039
|
+
<td class="element element-22">janedoe@example.com</td>
|
914
1040
|
</tr>
|
915
1041
|
</table>
|
916
|
-
|
917
|
-
|
918
|
-
|
919
|
-
|
920
|
-
|
921
|
-
|
922
|
-
|
923
|
-
|
924
|
-
|
925
|
-
|
926
|
-
|
927
|
-
|
928
|
-
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
background: #ccc;
|
935
|
-
}
|
1042
|
+
|
1043
|
+
<style class="element element-23">
|
1044
|
+
input {
|
1045
|
+
margin: 5px;
|
1046
|
+
}
|
1047
|
+
input[type=submit] {
|
1048
|
+
margin: 5px 0;
|
1049
|
+
}
|
1050
|
+
table {
|
1051
|
+
border:1px solid grey;
|
1052
|
+
border-spacing: 0;
|
1053
|
+
}
|
1054
|
+
table tr td, table tr th {
|
1055
|
+
padding: 5px;
|
1056
|
+
}
|
1057
|
+
table tr:nth-child(even) {
|
1058
|
+
background: #ccc;
|
1059
|
+
}
|
936
1060
|
</style>
|
937
1061
|
</div>
|
938
1062
|
```
|
939
1063
|
|
940
|
-
|
941
|
-
|
942
|
-
---
|
1064
|
+
Screenshot:
|
943
1065
|
|
944
|
-
|
1066
|
+
![Hello, Form!](/images/glimmer-dsl-web-samples-hello-hello-form.gif)
|
945
1067
|
|
946
|
-
|
1068
|
+
#### Hello, Data-Binding!
|
947
1069
|
|
948
|
-
|
949
|
-
|
950
|
-
***Hello, Button! Submitted Invalid Data***
|
1070
|
+
Glimmer GUI code:
|
951
1071
|
|
952
|
-
|
1072
|
+
```ruby
|
1073
|
+
require 'glimmer-dsl-web'
|
953
1074
|
|
954
|
-
|
1075
|
+
Address = Struct.new(:street, :street2, :city, :state, :zip_code, keyword_init: true) do
|
1076
|
+
STATES = {
|
1077
|
+
"AK"=>"Alaska",
|
1078
|
+
"AL"=>"Alabama",
|
1079
|
+
"AR"=>"Arkansas",
|
1080
|
+
"AS"=>"American Samoa",
|
1081
|
+
"AZ"=>"Arizona",
|
1082
|
+
"CA"=>"California",
|
1083
|
+
"CO"=>"Colorado",
|
1084
|
+
"CT"=>"Connecticut",
|
1085
|
+
"DC"=>"District of Columbia",
|
1086
|
+
"DE"=>"Delaware",
|
1087
|
+
"FL"=>"Florida",
|
1088
|
+
"GA"=>"Georgia",
|
1089
|
+
"GU"=>"Guam",
|
1090
|
+
"HI"=>"Hawaii",
|
1091
|
+
"IA"=>"Iowa",
|
1092
|
+
"ID"=>"Idaho",
|
1093
|
+
"IL"=>"Illinois",
|
1094
|
+
"IN"=>"Indiana",
|
1095
|
+
"KS"=>"Kansas",
|
1096
|
+
"KY"=>"Kentucky",
|
1097
|
+
"LA"=>"Louisiana",
|
1098
|
+
"MA"=>"Massachusetts",
|
1099
|
+
"MD"=>"Maryland",
|
1100
|
+
"ME"=>"Maine",
|
1101
|
+
"MI"=>"Michigan",
|
1102
|
+
"MN"=>"Minnesota",
|
1103
|
+
"MO"=>"Missouri",
|
1104
|
+
"MS"=>"Mississippi",
|
1105
|
+
"MT"=>"Montana",
|
1106
|
+
"NC"=>"North Carolina",
|
1107
|
+
"ND"=>"North Dakota",
|
1108
|
+
"NE"=>"Nebraska",
|
1109
|
+
"NH"=>"New Hampshire",
|
1110
|
+
"NJ"=>"New Jersey",
|
1111
|
+
"NM"=>"New Mexico",
|
1112
|
+
"NV"=>"Nevada",
|
1113
|
+
"NY"=>"New York",
|
1114
|
+
"OH"=>"Ohio",
|
1115
|
+
"OK"=>"Oklahoma",
|
1116
|
+
"OR"=>"Oregon",
|
1117
|
+
"PA"=>"Pennsylvania",
|
1118
|
+
"PR"=>"Puerto Rico",
|
1119
|
+
"RI"=>"Rhode Island",
|
1120
|
+
"SC"=>"South Carolina",
|
1121
|
+
"SD"=>"South Dakota",
|
1122
|
+
"TN"=>"Tennessee",
|
1123
|
+
"TX"=>"Texas",
|
1124
|
+
"UT"=>"Utah",
|
1125
|
+
"VA"=>"Virginia",
|
1126
|
+
"VI"=>"Virgin Islands",
|
1127
|
+
"VT"=>"Vermont",
|
1128
|
+
"WA"=>"Washington",
|
1129
|
+
"WI"=>"Wisconsin",
|
1130
|
+
"WV"=>"West Virginia",
|
1131
|
+
"WY"=>"Wyoming"
|
1132
|
+
}
|
1133
|
+
|
1134
|
+
def state_code
|
1135
|
+
STATES.invert[state]
|
1136
|
+
end
|
1137
|
+
|
1138
|
+
def state_code=(value)
|
1139
|
+
self.state = STATES[value]
|
1140
|
+
end
|
955
1141
|
|
956
|
-
|
1142
|
+
def summary
|
1143
|
+
values.map(&:to_s).reject(&:empty?).join(', ')
|
1144
|
+
end
|
1145
|
+
end
|
957
1146
|
|
958
|
-
|
1147
|
+
@address = Address.new(
|
1148
|
+
street: '123 Main St',
|
1149
|
+
street2: 'Apartment 3C, 2nd door to the right',
|
1150
|
+
city: 'San Diego',
|
1151
|
+
state: 'California',
|
1152
|
+
zip_code: '91911'
|
1153
|
+
)
|
959
1154
|
|
960
|
-
|
1155
|
+
include Glimmer
|
961
1156
|
|
962
|
-
|
1157
|
+
Document.ready? do
|
1158
|
+
div {
|
1159
|
+
form(style: 'display: grid; grid-auto-columns: 80px 200px;') { |address_form|
|
1160
|
+
label('Street: ', for: 'street-field')
|
1161
|
+
input(id: 'street-field') {
|
1162
|
+
# Bidirectional Data-Binding with <=> ensures input.value and @address.street
|
1163
|
+
# automatically stay in sync when either side changes
|
1164
|
+
value <=> [@address, :street]
|
1165
|
+
}
|
1166
|
+
|
1167
|
+
label('Street 2: ', for: 'street2-field')
|
1168
|
+
textarea(id: 'street2-field') {
|
1169
|
+
value <=> [@address, :street2]
|
1170
|
+
}
|
1171
|
+
|
1172
|
+
label('City: ', for: 'city-field')
|
1173
|
+
input(id: 'city-field') {
|
1174
|
+
value <=> [@address, :city]
|
1175
|
+
}
|
1176
|
+
|
1177
|
+
label('State: ', for: 'state-field')
|
1178
|
+
select(id: 'state-field') {
|
1179
|
+
Address::STATES.each do |state_code, state|
|
1180
|
+
option(value: state_code) { state }
|
1181
|
+
end
|
1182
|
+
|
1183
|
+
value <=> [@address, :state_code]
|
1184
|
+
}
|
1185
|
+
|
1186
|
+
label('Zip Code: ', for: 'zip-code-field')
|
1187
|
+
input(id: 'zip-code-field', type: 'number', min: '0', max: '99999') {
|
1188
|
+
# Bidirectional Data-Binding with <=> ensures input.value and @address.zip_code
|
1189
|
+
# automatically stay in sync when either side changes
|
1190
|
+
# on_write option specifies :to_s method to invoke on value before writing to model attribute
|
1191
|
+
# to ensure the numeric zip code value is stored as a String
|
1192
|
+
value <=> [@address, :zip_code,
|
1193
|
+
on_write: :to_s
|
1194
|
+
]
|
1195
|
+
}
|
1196
|
+
|
1197
|
+
style {
|
1198
|
+
<<~CSS
|
1199
|
+
.#{address_form.element_id} * {
|
1200
|
+
margin: 5px;
|
1201
|
+
}
|
1202
|
+
.#{address_form.element_id} input, .#{address_form.element_id} select {
|
1203
|
+
grid-column: 2;
|
1204
|
+
}
|
1205
|
+
CSS
|
1206
|
+
}
|
1207
|
+
}
|
1208
|
+
|
1209
|
+
div(style: 'margin: 5px') {
|
1210
|
+
# Unidirectional Data-Binding is done with <= to ensure @address.summary changes update div.inner_text
|
1211
|
+
# as computed by changes to the address member attributes + state_code address custom attribute
|
1212
|
+
inner_text <= [@address, :summary,
|
1213
|
+
computed_by: @address.members + ['state_code']
|
1214
|
+
]
|
1215
|
+
}
|
1216
|
+
}.render
|
1217
|
+
end
|
1218
|
+
```
|
963
1219
|
|
964
|
-
|
1220
|
+
Screenshot:
|
965
1221
|
|
966
|
-
|
1222
|
+
![Hello, Data-Binding!](/images/glimmer-dsl-web-samples-hello-hello-data-binding.gif)
|
967
1223
|
|
968
1224
|
#### Button Counter
|
969
1225
|
|
@@ -995,7 +1251,7 @@ class HelloButton
|
|
995
1251
|
|
996
1252
|
markup {
|
997
1253
|
# This will hook into element #app-container and then build HTML inside it using Ruby DSL code
|
998
|
-
div(
|
1254
|
+
div(parent: parent_selector) {
|
999
1255
|
text 'Button Counter'
|
1000
1256
|
|
1001
1257
|
button {
|
@@ -1004,7 +1260,7 @@ class HelloButton
|
|
1004
1260
|
# copied to button innerText (content) to display to the user
|
1005
1261
|
inner_text <= [@counter, :count, on_read: ->(value) { "Click To Increment: #{value} " }]
|
1006
1262
|
|
1007
|
-
|
1263
|
+
onclick {
|
1008
1264
|
@counter.increment!
|
1009
1265
|
}
|
1010
1266
|
}
|