outro_rails 0.1.4 → 0.1.6

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: b57460d8171563b6c440d80d129e02e0426b1ac3fcd6886dc40994a525e39abb
4
- data.tar.gz: 1c749d767f9c6b0680509069176ec0d454ea4dd2a8bfb0476e15f704a647e10b
3
+ metadata.gz: b9a61b44de4d831dd78c2ef49903677777792cfe812a6f77211dbf4e846ffb35
4
+ data.tar.gz: 60ee9af6996b28da00252dc296e40d5869c0f877ed8335e126d8650ae6b8d462
5
5
  SHA512:
6
- metadata.gz: 178b499ca5c588f4ad584a540ac3f2bd43188b6bdbaba8886db6c34d213d0c1210bc7c137caf90d5327cdd56fb94e8dbae6ee99a4b925000c10a7297c4b1a7a4
7
- data.tar.gz: 315e08ed8694798be6d319b8f55fcc5254853e5d7f254db938945b1aaa938f75cfe5f8e3ab4354d01741c6ed2ba846d2d9e8f2fbce0500d62a49335447ae394b
6
+ metadata.gz: 153b0d96d98f43b44c323a06fa7b3106b2ea06d0e92ca2c4a9e33f74bb3a6555b109bffbb880c3969d5147ca0aa3e43ba573060111b073c36e80f7958911c4ac
7
+ data.tar.gz: 91a29abfb552a60757fcdc0ded4883d921000fa9fb2c2a31e4a855e5cca628689b9def02844da3f88f736fba6c44ac4b9ff4e5280d50bb736b532bd6b5d67096
data/README.md CHANGED
@@ -50,7 +50,8 @@ quickest version is to add two tags to your layout:
50
50
  ```
51
51
 
52
52
  If you'd rather fold the engine's assets into your existing bundles, see
53
- [Assets](#assets) below.
53
+ [Assets](#assets) below. If you'd rather style the form controls with your
54
+ own CSS framework, see [Control classes](#control-classes).
54
55
 
55
56
  ## Assets
56
57
 
@@ -161,6 +162,61 @@ yarn build
161
162
  yarn build:css
162
163
  ```
163
164
 
165
+ ### Form Control Classes
166
+
167
+ Out of the box the engine's text fields, textareas, selects, and labels
168
+ carry its own classes, styled by the stylesheet above:
169
+
170
+ | Element | Setting | Default |
171
+ | ---------- | -------------------------- | ---------------- |
172
+ | `button` | `OutroRails.button_class` | `outro-button` |
173
+ | `input` | `OutroRails.input_class` | `outro-input` |
174
+ | `label` | `OutroRails.label_class` | `outro-label` |
175
+ | `select` | `OutroRails.select_class` | `outro-select` |
176
+ | `textarea` | `OutroRails.textarea_class` | `outro-textarea` |
177
+
178
+ If your app already has a form style (Bootstrap, Tailwind, your own classes)
179
+ point these at it in an initializer and the engine will render your classes
180
+ instead:
181
+
182
+ ```ruby
183
+ # config/initializers/outro_rails.rb
184
+ OutroRails.configure do |config|
185
+ config.button_class = "btn btn-sm"
186
+ config.input_class = "form-control"
187
+ config.label_class = "form-label"
188
+ config.select_class = "form-select"
189
+ config.textarea_class = "form-control"
190
+ end
191
+ ```
192
+
193
+ Each setting is a plain string and can also be assigned on its own
194
+ (`OutroRails.select_class = "form-select"`).
195
+
196
+ These apply everywhere the engine renders a form control.
197
+
198
+ #### Per render
199
+
200
+ The same four names are accepted as locals on the chart partial and as
201
+ options on `chord_chart_area` for a page that needs something other than
202
+ the application-wide setting:
203
+
204
+ ```erb
205
+ <%= form.chord_chart_area :chord_sheet,
206
+ button_class: "btn-primary",
207
+ input_class: "form-control",
208
+ label_class: "form-label",
209
+ select_class: "form-select",
210
+ textarea_class: "form-control font-monospace" %>
211
+ ```
212
+
213
+ ```erb
214
+ <%= render "outro_rails/chord_charts/chart",
215
+ chord_chart: @song.chord_sheet,
216
+ select_class: "form-select",
217
+ label_class: "form-label" %>
218
+ ```
219
+
164
220
  ## Usage
165
221
  To view the chord library and music theory tools, mount the engine in your
166
222
  routes.rb:
@@ -200,9 +256,38 @@ This gives the model `chord_sheet`/`chord_sheet=`/`chord_sheet?`, backed by a
200
256
  (title), `tuning`, `capo`, `key`, and `body`). A model can attach more
201
257
  than one chart under different names.
202
258
 
259
+ #### Required or Optional
260
+
261
+ By default the chart is required: `song.chord_sheet` always returns a chart
262
+ (building an unsaved one on first access), and saving the record with the
263
+ artist or song blank fails validation with errors.
264
+
265
+ Pass `required: false` to let the chart be left out entirely:
266
+
267
+ ```ruby
268
+ class Song < ApplicationRecord
269
+ has_chord_chart :chord_sheet, required: false
270
+ end
271
+ ```
272
+
273
+ With an optional chart:
274
+
275
+ * `song.chord_sheet` returns `nil` until someone fills it in, and
276
+ `song.chord_sheet?` reports whether one exists.
277
+ * A form submitted with the artist, song, and body all blank saves the
278
+ record with no chart. Filling in *some* of those fields still
279
+ validates as usual, so the user sees what's missing.
280
+ * Clearing every field on a record that already had a chart removes that
281
+ chart when the record saves. If the record itself fails validation,
282
+ nothing is deleted.
283
+ * `song.chord_sheet = nil` removes a chart; `song.chord_sheet = { ... }`
284
+ builds one if needed and assigns the given attributes.
285
+
286
+ `song.chord_sheet_required?` reports which way the model was declared.
287
+
203
288
  ### Forms: chord_chart_area
204
289
 
205
- In a form, pair it with the `chord_chart_area` form builder method:
290
+ In a form, use the `chord_chart_area` form builder method:
206
291
 
207
292
  ```erb
208
293
  <%= form_with model: @song do |form| %>
@@ -211,10 +296,29 @@ In a form, pair it with the `chord_chart_area` form builder method:
211
296
  <% end %>
212
297
  ```
213
298
 
214
- `chord_chart_area` renders artist/song/tuning/capo/key/body fields. Tuning
215
- options are pulled from `Instruments::Guitar::Tunings`, capo options are capped
216
- at fret 7 (`Instruments::Guitar::Capo::FRETS`), and key options are set from
217
- from `Theory::Key`.
299
+ `chord_chart_area` renders a fieldset with a heading and
300
+ artist/song/tuning/capo/key/body fields. Tuning options are pulled from
301
+ `Instruments::Guitar::Tunings`, capo options are capped at fret 7
302
+ (`Instruments::Guitar::Capo::FRETS`). Key options are set from
303
+ `Theory::Key`.
304
+
305
+ The heading defaults to "Chord Chart". Set `legend:` to change it, or pass
306
+ `false` when the surrounding page already has a heading for this section:
307
+
308
+ ```erb
309
+ <%= form.chord_chart_area :chord_sheet, legend: "Lead sheet" %>
310
+ <%= form.chord_chart_area :chord_sheet, legend: false %>
311
+ ```
312
+
313
+ When the model declares the chart with `required: false`, the fieldset is
314
+ labeled "Optional" and tells the user to leave the fields blank if there's
315
+ no chord chart.
316
+
317
+ The fields inside the fieldset use the engine's own control classes. Pass
318
+ `input_class:`, `textarea_class:`, `select_class:`, or `label_class:` to
319
+ render them with your app's classes for this form, or set them once
320
+ application-wide — see [Control classes](#control-classes). Any other
321
+ option is passed through to `fields_for`, as before.
218
322
 
219
323
  ### Controller: Permitting the Parameters
220
324
 
@@ -233,15 +337,13 @@ class SongsController < ApplicationController
233
337
  def song_params
234
338
  params.require(:song).permit(
235
339
  :description,
236
- chord_chart_chord_sheet_attributes: %i[artist body capo key song tuning]
340
+ chord_chart_chord_sheet_attributes: %i[id artist body capo key song tuning]
237
341
  )
238
342
  end
239
343
  end
240
344
  ```
241
345
 
242
- The six permitted attributes are exactly the fields `chord_chart_area`
243
- renders. `record` and `name` are managed by the engine and should not be
244
- permitted. If a model attaches more than one chart, each one needs its own
346
+ If a model attaches more than one chart, each one needs its own
245
347
  key: `has_chord_chart :lead_sheet` is permitted as
246
348
  `chord_chart_lead_sheet_attributes`.
247
349
 
@@ -264,6 +366,11 @@ This renders the chart's metadata, an instrument toggle, a transpose-to-key
264
366
  picker, the chart body with recognized chords highlighted, and a
265
367
  diagram for every chord the body references.
266
368
 
369
+ The partial accepts `chord_chart: nil`, so it can be handed an optional
370
+ chart that hasn't been filled in yet; it renders a short "No chord chart."
371
+ placeholder in that case. Check `@song.chord_sheet?` instead if you'd
372
+ rather show nothing at all.
373
+
267
374
  ### Using Your Own Layout
268
375
 
269
376
  By default the engine renders its pages in its own layout,
@@ -426,7 +533,6 @@ It seeds the chord library and four example songs (`spec/dummy/db/seeds.rb`).
426
533
  The example songs are listed on the home page. To illustrate permissions,
427
534
  the dummy app sets even record ids as editable and odd ones as view-only.
428
535
 
429
-
430
536
  ## Publishing a New Version
431
537
 
432
538
  **1. Bump the version.** Edit `lib/outro_rails/version.rb`
@@ -11,8 +11,7 @@
11
11
  from `currentColor`, so they adapt to light and dark host themes.
12
12
  ========================================================================== */
13
13
 
14
- .outro,
15
- .outro-page {
14
+ :root {
16
15
  --outro-container-width: 66rem;
17
16
  --outro-radius: 14px;
18
17
  --outro-radius-sm: 6px;
@@ -54,8 +53,7 @@
54
53
 
55
54
  @supports not (color: color-mix(in srgb, currentColor 50%, transparent)) {
56
55
 
57
- .outro,
58
- .outro-page {
56
+ :root {
59
57
  --outro-muted: rgba(128, 128, 128, 0.9);
60
58
  --outro-border: rgba(128, 128, 128, 0.3);
61
59
  --outro-border-strong: rgba(128, 128, 128, 0.5);
@@ -129,8 +127,6 @@
129
127
  align-items: center;
130
128
  gap: 0.4rem;
131
129
  min-width: 0;
132
- align-items: center;
133
- min-width: 0;
134
130
  margin: 0;
135
131
  padding: 0.3rem clamp(0.6rem, 2vw, 0.95rem);
136
132
  border: 1px solid var(--outro-border);
@@ -307,21 +303,6 @@
307
303
  font: inherit;
308
304
  }
309
305
 
310
- .outro-button {
311
- display: inline-flex;
312
- align-items: center;
313
- gap: 0.45rem;
314
- padding: 0.4rem 0.9rem;
315
- border: 1px solid var(--outro-border-strong);
316
- border-radius: var(--outro-radius-sm);
317
- background-color: var(--outro-surface, transparent);
318
- color: inherit;
319
- font: inherit;
320
- line-height: 1.2;
321
- text-decoration: none;
322
- cursor: pointer;
323
- }
324
-
325
306
  .outro-card--link {
326
307
  transition: transform 0.16s ease, box-shadow 0.16s ease,
327
308
  border-color 0.16s ease;
@@ -415,13 +396,6 @@
415
396
  }
416
397
  }
417
398
 
418
- .outro-panel {
419
- padding: var(--outro-card-padding);
420
- border: 1px solid var(--outro-border);
421
- border-radius: var(--outro-radius);
422
- background-color: var(--outro-surface, transparent);
423
- }
424
-
425
399
  .outro-empty {
426
400
  padding: clamp(1.25rem, 4vw, 2rem);
427
401
  border: 1px dashed var(--outro-border-strong);
@@ -438,17 +412,6 @@
438
412
  margin-bottom: 0;
439
413
  }
440
414
 
441
- .outro-select,
442
- .outro-input,
443
- .outro-textarea {
444
- padding: var(--outro-control-padding);
445
- border: 1px solid var(--outro-border-strong);
446
- border-radius: var(--outro-radius-sm);
447
- background-color: var(--outro-surface, transparent);
448
- color: inherit;
449
- font: inherit;
450
- }
451
-
452
415
  .outro-button {
453
416
  display: inline-flex;
454
417
  align-items: center;
@@ -727,7 +690,6 @@
727
690
  .outro-guitar {
728
691
  display: flex;
729
692
  flex-direction: column;
730
- align-items: center;
731
693
  gap: 14px;
732
694
  width: 100%;
733
695
  max-width: 100%;
@@ -735,7 +697,6 @@
735
697
  border-radius: var(--outro-instrument-radius);
736
698
  background-color: var(--outro-instrument-bg);
737
699
  overflow-x: auto;
738
- align-items: center;
739
700
  align-items: safe center;
740
701
  }
741
702
 
@@ -910,7 +871,6 @@
910
871
  .outro-piano {
911
872
  display: flex;
912
873
  flex-direction: column;
913
- align-items: center;
914
874
  gap: 14px;
915
875
  width: 100%;
916
876
  max-width: 100%;
@@ -918,7 +878,6 @@
918
878
  border-radius: var(--outro-instrument-radius);
919
879
  background-color: var(--outro-instrument-bg);
920
880
  overflow-x: auto;
921
- align-items: center;
922
881
  align-items: safe center;
923
882
  }
924
883
 
@@ -1207,6 +1166,15 @@
1207
1166
  position: relative;
1208
1167
  }
1209
1168
 
1169
+ .outro-chord-chart-fields__legend {
1170
+ float: none;
1171
+ }
1172
+
1173
+ legend.outro-chord-chart-fields__legend {
1174
+ float: none;
1175
+ margin-bottom: 0;
1176
+ }
1177
+
1210
1178
  .outro-chord-chart__body {
1211
1179
  margin: 0;
1212
1180
  padding: 0.85rem;
@@ -1259,6 +1227,12 @@
1259
1227
  border: 0;
1260
1228
  }
1261
1229
 
1230
+ .outro-chord-chart-fields__hint {
1231
+ margin: 0;
1232
+ font-size: 0.9rem;
1233
+ color: var(--outro-muted);
1234
+ }
1235
+
1262
1236
  .outro-inline-form {
1263
1237
  display: inline;
1264
1238
  }
@@ -3,6 +3,7 @@
3
3
  module OutroRails
4
4
  module ChordChartsHelper
5
5
  include CapoHelper
6
+ include ClassNamesHelper
6
7
  include InstrumentsHelper
7
8
  include TransposerHelper
8
9
 
@@ -24,13 +25,19 @@ module OutroRails
24
25
  Instruments::ALL.map { |instrument| [ instrument.titleize, instrument ] }
25
26
  end
26
27
 
27
- def chord_chart_area(form, method, **options)
28
- chord_chart = form.object.public_send(method)
28
+ def chord_chart_area(form, method, legend: "Chord chart", **options)
29
+ record = form.object
30
+ required = record.public_send(:"#{method}_required?")
31
+
32
+ chord_chart = record.public_send(method) ||
33
+ OutroRails::ChordChart.new(name: method.to_s)
29
34
 
30
35
  form.fields_for(:"chord_chart_#{method}",
31
36
  chord_chart, **options) do |chord_chart_form|
32
37
  render partial: "outro_rails/chord_charts/fields",
33
- locals: { f: chord_chart_form }
38
+ locals: { f: chord_chart_form,
39
+ required: required,
40
+ legend: legend }
34
41
  end
35
42
  end
36
43
 
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OutroRails
4
+ module ClassNamesHelper
5
+ def outro_button_class(*modifiers)
6
+ outro_element_class(:button, *modifiers)
7
+ end
8
+
9
+ def outro_input_class(*modifiers)
10
+ outro_element_class(:input, *modifiers)
11
+ end
12
+
13
+ def outro_label_class(*modifiers)
14
+ outro_element_class(:label, *modifiers)
15
+ end
16
+
17
+ def outro_select_class(*modifiers)
18
+ outro_element_class(:select, *modifiers)
19
+ end
20
+
21
+ def outro_textarea_class(*modifiers)
22
+ outro_element_class(:textarea, *modifiers)
23
+ end
24
+
25
+ def outro_element_class(element, *modifiers)
26
+ tokens = [ OutroRails.class_for(element), *modifiers ]
27
+ .flat_map { |token| token.to_s.split(/\s+/) }
28
+ .reject(&:empty?)
29
+ .uniq
30
+
31
+ tokens.join(" ").presence
32
+ end
33
+ end
34
+ end
@@ -9,6 +9,8 @@ module OutroRails
9
9
  Theory::Key.canonical(:minor).map(&:to_s)
10
10
  ).freeze
11
11
 
12
+ CONTENT_ATTRIBUTES = %w[artist song body].freeze
13
+
12
14
  belongs_to :record, polymorphic: true, touch: true
13
15
 
14
16
  has_many :voicings,
@@ -22,7 +24,6 @@ module OutroRails
22
24
  validates :name, presence: true
23
25
  validates :artist, presence: true
24
26
  validates :song, presence: true
25
- validates :body, presence: true
26
27
  validates :tuning,
27
28
  inclusion: { in: Instruments::Guitar::Tunings.slugs },
28
29
  allow_blank: true
@@ -31,6 +32,15 @@ module OutroRails
31
32
  allow_nil: true
32
33
  validates :key, inclusion: { in: KEY_OPTIONS }, allow_blank: true
33
34
 
35
+ def self.blank_attributes?(attributes)
36
+ return true if attributes.nil?
37
+
38
+ attributes = attributes.to_h if attributes.respond_to?(:permitted?)
39
+ attributes = attributes.to_h.with_indifferent_access
40
+
41
+ CONTENT_ATTRIBUTES.all? { |field| attributes[field].blank? }
42
+ end
43
+
34
44
  def to_key
35
45
  Theory::Key.parse(key.presence || "C")
36
46
  end
@@ -16,27 +16,27 @@
16
16
  <%= form_with url: caged_picker_path, method: :get,
17
17
  class: "outro-picker" do %>
18
18
  <label class="outro-picker__field">
19
- <span class="outro-label">Root</span>
19
+ <%= tag.span "Root", class: outro_label_class %>
20
20
  <%= select_tag :root,
21
21
  options_for_select(scale_root_options, @root.path_slug),
22
- class: "outro-select",
22
+ class: outro_select_class,
23
23
  onchange: "this.form.requestSubmit()" %>
24
24
  </label>
25
25
 
26
26
  <label class="outro-picker__field">
27
- <span class="outro-label">Shape</span>
27
+ <%= tag.span "Shape", class: outro_label_class %>
28
28
  <%= select_tag :shape,
29
29
  options_for_select(
30
30
  OutroRails::Instruments::Guitar::ChordShapes::CAGED_LETTERS
31
31
  .map { |l| [ "#{l} shape", l.downcase ] },
32
32
  @letter.downcase
33
33
  ),
34
- class: "outro-select",
34
+ class: outro_select_class,
35
35
  onchange: "this.form.requestSubmit()" %>
36
36
  </label>
37
37
 
38
38
  <%= button_tag type: :submit, name: nil,
39
- class: "outro-button",
39
+ class: outro_button_class,
40
40
  "aria-label": "Show Shapes" do %>
41
41
  <%= image_tag "outro_rails/shapes.svg", class: 'outro-button__icon' %>
42
42
  Show Shapes
@@ -14,7 +14,8 @@
14
14
  ] %>
15
15
 
16
16
  <div class="outro-panel outro-filters">
17
- <span class="outro-label" id="capo-position-label">Capo position</span>
17
+ <%= tag.span "Capo position", id: "capo-position-label",
18
+ class: outro_label_class %>
18
19
  <nav class="outro-chip-row outro-capo-frets"
19
20
  aria-labelledby="capo-position-label">
20
21
  <% OutroRails::Instruments::Guitar::Capo::FRETS.each do |fret| %>
@@ -88,7 +88,7 @@
88
88
  <span class="outro-label">Instrument</span>
89
89
  <%= select_tag :instrument,
90
90
  options_for_select(chord_chart_instrument_options, instrument),
91
- class: "outro-select",
91
+ class: outro_select_class,
92
92
  onchange: "this.form.requestSubmit()" %>
93
93
  <%= hidden_field_tag :to_key, to_key if to_key %>
94
94
  <% end %>
@@ -99,15 +99,18 @@
99
99
  <%= select_tag :to_key,
100
100
  grouped_options_for_select(chord_chart_key_options,
101
101
  to_key || chord_chart.key),
102
- class: "outro-select",
102
+ class: outro_select_class,
103
103
  onchange: "this.form.requestSubmit()" %>
104
104
  <%= hidden_field_tag :instrument, instrument %>
105
105
  <% end %>
106
106
 
107
107
  <% if transposed %>
108
- <%= link_to "Reset to #{chord_chart.key}",
109
- url_for(request.query_parameters.except("to_key")),
110
- class: "outro-chip outro-chip--link" %>
108
+ <%= link_to url_for(request.query_parameters.except("to_key")),
109
+ class: outro_button_class do %>
110
+ <%= image_tag "outro_rails/undo.svg",
111
+ class: "outro-button__icon" %>
112
+ <%= "Reset to #{chord_chart.key}" %>
113
+ <% end %>
111
114
  <% end %>
112
115
  </div>
113
116
  </section>
@@ -186,7 +189,7 @@
186
189
  <% if all_voicings.many? %>
187
190
  <div class="outro-voicing-cycler__nav">
188
191
  <button type="button" data-voicing-prev
189
- class="outro-button
192
+ class="<%= outro_button_class %>
190
193
  outro-voicing-cycler__arrow"
191
194
  aria-label=
192
195
  "Previous voicing for <%= symbol %>">
@@ -195,7 +198,7 @@
195
198
  <span class="outro-voicing-cycler__counter"
196
199
  data-voicing-counter aria-live="polite"></span>
197
200
  <button type="button" data-voicing-next
198
- class="outro-button
201
+ class="<%= outro_button_class %>
199
202
  outro-voicing-cycler__arrow"
200
203
  aria-label=
201
204
  "Next voicing for <%= symbol %>">
@@ -231,7 +234,7 @@
231
234
  )
232
235
  %>
233
236
  <%= button_to revert_path,
234
- method: :delete, class: "outro-button",
237
+ method: :delete, class: outro_button_class,
235
238
  form_class: "outro-inline-form" do %>
236
239
  <%= image_tag "outro_rails/undo.svg",
237
240
  class: "outro-button__icon" %>
@@ -259,7 +262,7 @@
259
262
  )
260
263
  %>
261
264
  <%= button_to revert_path,
262
- method: :delete, class: "outro-button",
265
+ method: :delete, class: outro_button_class,
263
266
  form_class: "outro-inline-form" do %>
264
267
  <%= image_tag "outro_rails/undo.svg",
265
268
  class: "outro-button__icon" %>
@@ -5,46 +5,80 @@
5
5
 
6
6
  form_with model: @song do |form|
7
7
  form.chord_chart_area :chord_sheet
8
+ form.chord_chart_area :chord_sheet, legend: "Lead sheet"
9
+ form.chord_chart_area :chord_sheet, legend: false
8
10
  end
11
+
12
+ Locals:
13
+ f - the nested form builder for the chord chart
14
+ required - whether the owner declared the chart with `required: true`
15
+ (the default). When false, leaving every field blank
16
+ saves the record without a chart.
17
+ legend - heading shown above the fields, default "Chord chart".
18
+ Pass false to leave it out (when the host page already has
19
+ its own heading for this section).
20
+ %>
21
+
22
+ <%
23
+ required = local_assigns.fetch(:required, true)
24
+ legend = local_assigns.fetch(:legend, "Chord Chart")
9
25
  %>
10
26
 
11
27
  <fieldset class="outro-chord-chart-fields">
28
+ <% if legend %>
29
+ <legend class="outro-chord-chart-fields__legend">
30
+ <span class="outro-section-heading">
31
+ <%= legend %>
32
+ <% unless required %>
33
+ <span class="outro-symbol-list__detail">Optional</span>
34
+ <% end %>
35
+ </span>
36
+ </legend>
37
+ <% end %>
38
+
39
+ <% unless required %>
40
+ <p class="outro-chord-chart-fields__hint">
41
+ Leave artist, song, and chords blank if there is no chord chart.
42
+ </p>
43
+ <% end %>
44
+
12
45
  <div class="outro-picker outro-picker--grid outro-picker--cols-2">
13
46
  <label class="outro-picker__field">
14
- <span class="outro-label">Artist</span>
15
- <%= f.text_field :artist, class: "outro-input" %>
47
+ <%= tag.span "Artist", class: outro_label_class %>
48
+ <%= f.text_field :artist, class: outro_input_class %>
16
49
  </label>
17
50
 
18
51
  <label class="outro-picker__field">
19
- <span class="outro-label">Song</span>
20
- <%= f.text_field :song, class: "outro-input" %>
52
+ <%= tag.span "Song", class: outro_label_class %>
53
+ <%= f.text_field :song, class: outro_input_class %>
21
54
  </label>
22
55
  </div>
23
56
 
24
57
  <div class="outro-picker outro-picker--grid outro-picker--cols-3">
25
58
  <label class="outro-picker__field">
26
- <span class="outro-label">Tuning</span>
59
+ <%= tag.span "Tuning", class: outro_label_class %>
27
60
  <%= f.select :tuning, chord_chart_tuning_options, {},
28
- class: "outro-select" %>
61
+ class: outro_select_class %>
29
62
  </label>
30
63
 
31
64
  <label class="outro-picker__field">
32
- <span class="outro-label">Capo</span>
65
+ <%= tag.span "Capo", class: outro_label_class %>
33
66
  <%= f.select :capo, chord_chart_capo_options,
34
- { include_blank: "None" }, class: "outro-select" %>
67
+ { include_blank: "None" }, class: outro_select_class %>
35
68
  </label>
36
69
 
37
70
  <label class="outro-picker__field">
38
- <span class="outro-label">Key</span>
39
- <%= f.select :key, chord_chart_key_options, {}, class: "outro-select" %>
71
+ <%= tag.span "Key", class: outro_label_class %>
72
+ <%= f.select :key, chord_chart_key_options, {},
73
+ class: outro_select_class %>
40
74
  </label>
41
75
  </div>
42
76
 
43
77
  <label class="outro-transposer__field">
44
- <span class="outro-label">Chords &amp; lyrics</span>
78
+ <%= tag.span "Chords & lyrics (Optional)", class: outro_label_class %>
45
79
  <%= f.text_area :body,
46
80
  rows: 16,
47
81
  placeholder: "Paste or type a chord chart here.",
48
- class: "outro-textarea outro-transposer__textarea" %>
82
+ class: outro_textarea_class("outro-transposer__textarea") %>
49
83
  </label>
50
84
  </fieldset>
@@ -5,9 +5,9 @@
5
5
  <%= search_field_tag :q, params[:missing],
6
6
  placeholder: "F#m7b5 or fsharpm7flat5",
7
7
  "aria-label": "Go to chord by symbol or slug",
8
- class: "outro-input outro-filters__search-input" %>
8
+ class: outro_input_class("outro-filters__search-input") %>
9
9
  <%= button_tag type: :submit, name: nil,
10
- class: "outro-button",
10
+ class: outro_button_class,
11
11
  "aria-label": "Search" do %>
12
12
  <%= image_tag "outro_rails/search.svg", class: 'outro-button__icon' %>
13
13
  Search
@@ -18,52 +18,52 @@
18
18
  <%= form_with url: chords_path, method: :get,
19
19
  class: "outro-picker outro-filters__facets" do %>
20
20
  <label class="outro-picker__field">
21
- <span class="outro-label">Root</span>
21
+ <%= tag.span "Root", class: outro_label_class %>
22
22
  <%= select_tag :root,
23
23
  options_for_select(chord_root_options, @filters[:root]),
24
24
  include_blank: "Any",
25
- class: "outro-select",
25
+ class: outro_select_class,
26
26
  onchange: "this.form.requestSubmit()" %>
27
27
  </label>
28
28
 
29
29
  <label class="outro-picker__field">
30
- <span class="outro-label">Quality</span>
30
+ <%= tag.span "Quality", class: outro_label_class %>
31
31
  <%= select_tag :quality,
32
32
  options_for_select(chord_quality_options, @filters[:quality]),
33
33
  include_blank: "Any",
34
- class: "outro-select",
34
+ class: outro_select_class,
35
35
  onchange: "this.form.requestSubmit()" %>
36
36
  </label>
37
37
 
38
38
  <label class="outro-picker__field">
39
- <span class="outro-label">Extension</span>
39
+ <%= tag.span "Extension", class: outro_label_class %>
40
40
  <%= select_tag :extension,
41
41
  options_for_select(chord_extension_options, @filters[:extension]),
42
42
  include_blank: "Any",
43
- class: "outro-select",
43
+ class: outro_select_class,
44
44
  onchange: "this.form.requestSubmit()" %>
45
45
  </label>
46
46
 
47
47
  <label class="outro-picker__field">
48
- <span class="outro-label">Alteration</span>
48
+ <%= tag.span "Alteration", class: outro_label_class %>
49
49
  <%= select_tag :alteration,
50
50
  options_for_select(chord_alteration_options, @filters[:alteration]),
51
51
  include_blank: "Any",
52
- class: "outro-select",
52
+ class: outro_select_class,
53
53
  onchange: "this.form.requestSubmit()" %>
54
54
  </label>
55
55
 
56
56
  <label class="outro-picker__field">
57
- <span class="outro-label">Added tone</span>
57
+ <%= tag.span "Added tone", class: outro_label_class %>
58
58
  <%= select_tag :added_tone,
59
59
  options_for_select(chord_added_tone_options, @filters[:added_tone]),
60
60
  include_blank: "Any",
61
- class: "outro-select",
61
+ class: outro_select_class,
62
62
  onchange: "this.form.requestSubmit()" %>
63
63
  </label>
64
64
 
65
65
  <%= button_tag type: :submit, name: nil,
66
- class: "outro-button",
66
+ class: outro_button_class,
67
67
  "aria-label": "Filter" do %>
68
68
  <%= image_tag "outro_rails/filter.svg", class: 'outro-button__icon' %>
69
69
  Filter
@@ -71,7 +71,7 @@
71
71
 
72
72
  <% if filters_active? %>
73
73
  <%= link_to chords_path,
74
- class: "outro-button",
74
+ class: outro_button_class,
75
75
  "aria-label": "Clear" do %>
76
76
  <%= image_tag "outro_rails/clear.svg", class: 'outro-button__icon' %>
77
77
  Clear
@@ -19,7 +19,7 @@
19
19
  <div class="outro-empty">
20
20
  <p>No chords match those filters.</p>
21
21
  <%= link_to chords_path,
22
- class: "outro-button",
22
+ class: outro_button_class,
23
23
  "aria-label": "Clear Filters" do %>
24
24
  <%= image_tag "outro_rails/clear.svg", class: 'outro-button__icon' %>
25
25
  Clear Filters
@@ -63,9 +63,8 @@
63
63
  </dl>
64
64
 
65
65
  <div class="outro-card__footer">
66
- <span class="outro-label" id="scale-instruments-label">
67
- See this scale on
68
- </span>
66
+ <%= tag.span "See this scale on", id: "scale-instruments-label",
67
+ class: outro_label_class %>
69
68
  <div class="outro-chip-row" role="group"
70
69
  aria-labelledby="scale-instruments-label">
71
70
  <% OutroRails::ScalesController::INSTRUMENTS.each do |instrument| %>
@@ -12,17 +12,17 @@
12
12
  <%= hidden_field_tag :instrument, @instrument %>
13
13
 
14
14
  <label class="outro-picker__field">
15
- <span class="outro-label">Tuning</span>
15
+ <%= tag.span "Tuning", class: outro_label_class %>
16
16
  <%= select_tag :tuning,
17
17
  options_for_select(guitar_tuning_options, tuning.slug),
18
- class: "outro-select",
18
+ class: outro_select_class,
19
19
  onchange: "this.form.requestSubmit()" %>
20
20
  </label>
21
21
 
22
22
  <%# Fallback for keyboard users / no JS; the select auto-submits
23
23
  on change. %>
24
24
  <%= button_tag type: :submit, name: nil,
25
- class: "outro-button",
25
+ class: outro_button_class,
26
26
  "aria-label": "Show Tuning" do %>
27
27
  <%= image_tag "outro_rails/guitar.svg", class: 'outro-button__icon' %>
28
28
  Show Tuning
@@ -3,33 +3,33 @@
3
3
  <%= hidden_field_tag :instrument, @instrument %>
4
4
 
5
5
  <label class="outro-picker__field">
6
- <span class="outro-label">Root</span>
6
+ <%= tag.span "Root", class: outro_label_class %>
7
7
  <%= select_tag :root,
8
8
  options_for_select(scale_root_options, @scale.root.path_slug),
9
- class: "outro-select",
9
+ class: outro_select_class,
10
10
  onchange: "this.form.requestSubmit()" %>
11
11
  </label>
12
12
 
13
13
  <label class="outro-picker__field">
14
- <span class="outro-label">Scale</span>
14
+ <%= tag.span "Scale", class: outro_label_class %>
15
15
  <%= select_tag :type,
16
16
  options_for_select(scale_type_options, @scale.slug),
17
- class: "outro-select",
17
+ class: outro_select_class,
18
18
  onchange: "this.form.requestSubmit()" %>
19
19
  </label>
20
20
 
21
21
  <% if @instrument == "guitar" %>
22
22
  <label class="outro-picker__field">
23
- <span class="outro-label">Tuning</span>
23
+ <%= tag.span "Tuning", class: outro_label_class %>
24
24
  <%= select_tag :tuning,
25
25
  options_for_select(guitar_tuning_options, @tuning&.slug),
26
- class: "outro-select",
26
+ class: outro_select_class,
27
27
  onchange: "this.form.requestSubmit()" %>
28
28
  </label>
29
29
  <% end %>
30
30
 
31
31
  <%= button_tag type: :submit, name: nil,
32
- class: "outro-button",
32
+ class: outro_button_class,
33
33
  "aria-label": "Show Scale" do %>
34
34
  <%= image_tag "outro_rails/scales.svg", class: 'outro-button__icon' %>
35
35
  Show Scale
@@ -77,9 +77,8 @@
77
77
  <% others = OutroRails::ScalesController::INSTRUMENTS - [ @instrument ] %>
78
78
  <% if others.any? %>
79
79
  <div class="outro-switch">
80
- <span class="outro-label" id="scale-switch-label">
81
- View this scale on
82
- </span>
80
+ <%= tag.span "View this scale on", id: "scale-switch-label",
81
+ class: outro_label_class %>
83
82
  <div class="outro-chip-row" role="group"
84
83
  aria-labelledby="scale-switch-label">
85
84
  <% others.each do |other| %>
@@ -21,39 +21,39 @@
21
21
  class: "outro-transposer" do %>
22
22
  <div class="outro-picker outro-picker--flush">
23
23
  <label class="outro-picker__field">
24
- <span class="outro-label">Convert</span>
24
+ <%= tag.span "Convert", class: outro_label_class %>
25
25
  <%= select_tag :mode,
26
26
  options_for_select(transposer_mode_options, @mode),
27
- class: "outro-select" %>
27
+ class: outro_select_class %>
28
28
  </label>
29
29
 
30
30
  <label class="outro-picker__field">
31
- <span class="outro-label">Original key</span>
31
+ <%= tag.span "Original key", class: outro_label_class %>
32
32
  <%= select_tag :from_key,
33
33
  grouped_options_for_select(transposer_key_options,
34
34
  @from_key.to_s),
35
- class: "outro-select" %>
35
+ class: outro_select_class %>
36
36
  </label>
37
37
 
38
38
  <label class="outro-picker__field">
39
- <span class="outro-label">New key</span>
39
+ <%= tag.span "New key", class: outro_label_class %>
40
40
  <%= select_tag :to_key,
41
41
  grouped_options_for_select(transposer_key_options,
42
42
  @to_key.to_s),
43
- class: "outro-select" %>
43
+ class: outro_select_class %>
44
44
  </label>
45
45
  </div>
46
46
 
47
47
  <label class="outro-transposer__field">
48
- <span class="outro-label">Chords and lyrics</span>
48
+ <%= tag.span "Chords and lyrics", class: outro_label_class %>
49
49
  <%= text_area_tag :text, @text,
50
50
  rows: 8,
51
51
  placeholder: "Paste a chord chart here.",
52
- class: "outro-textarea outro-transposer__textarea" %>
52
+ class: outro_textarea_class("outro-transposer__textarea") %>
53
53
  </label>
54
54
 
55
55
  <%= button_tag type: :submit, name: nil,
56
- class: "outro-button",
56
+ class: outro_button_class,
57
57
  "aria-label": "Transpose" do %>
58
58
  <%= image_tag "outro_rails/transposer.svg",
59
59
  class: 'outro-button__icon' %>
@@ -5,7 +5,7 @@ module OutroRails
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  class_methods do
8
- def has_chord_chart(name, dependent: :destroy)
8
+ def has_chord_chart(name, required: true, dependent: :destroy)
9
9
  association_name = :"chord_chart_#{name}"
10
10
 
11
11
  has_one association_name,
@@ -14,25 +14,77 @@ module OutroRails
14
14
  as: :record,
15
15
  inverse_of: :record,
16
16
  autosave: true,
17
+ validate: false,
17
18
  dependent: dependent
18
19
 
19
20
  accepts_nested_attributes_for association_name
20
21
 
22
+ validate do
23
+ chart = association(association_name).target
24
+ next if chart.nil? || chart.destroyed? || chart.marked_for_destruction?
25
+ next unless chart.changed_for_autosave?
26
+ next if chart.valid?
27
+
28
+ chart.errors.each do |error|
29
+ attribute = error.attribute == :base ? name : :"#{name}.#{error.attribute}"
30
+ errors.import(error, attribute: attribute)
31
+ end
32
+ end
33
+
34
+ fallback = if required
35
+ "build_#{association_name}(name: #{name.to_s.inspect})"
36
+ else
37
+ "nil"
38
+ end
39
+
21
40
  class_eval <<-CODE, __FILE__, __LINE__ + 1
22
41
  def #{name}
23
- #{association_name} ||
24
- build_#{association_name}(name: #{name.to_s.inspect})
42
+ chart = #{association_name}
43
+ return chart if chart && !chart.destroyed? &&
44
+ !chart.marked_for_destruction?
45
+
46
+ #{fallback}
25
47
  end
26
48
 
27
49
  def #{name}?
28
- #{association_name}.present?
50
+ chart = #{association_name}
51
+ !chart.nil? && !chart.destroyed? && !chart.marked_for_destruction?
29
52
  end
30
53
 
31
54
  def #{name}=(attributes)
32
- #{name}.assign_attributes(attributes)
55
+ if attributes.nil?
56
+ self.#{association_name} = nil
57
+ else
58
+ chart = #{association_name} ||
59
+ build_#{association_name}(name: #{name.to_s.inspect})
60
+ chart.assign_attributes(attributes)
61
+ end
62
+ end
63
+
64
+ def #{name}_required?
65
+ #{required}
33
66
  end
34
67
  CODE
35
68
 
69
+ unless required
70
+ class_eval <<-CODE, __FILE__, __LINE__ + 1
71
+ def #{association_name}_attributes=(attributes)
72
+ unless OutroRails::ChordChart.blank_attributes?(attributes)
73
+ return super
74
+ end
75
+
76
+ chart = #{association_name}
77
+ return unless chart
78
+
79
+ if chart.persisted?
80
+ chart.mark_for_destruction
81
+ else
82
+ self.#{association_name} = nil
83
+ end
84
+ end
85
+ CODE
86
+ end
87
+
36
88
  scope :"with_#{name}", -> { includes(association_name) }
37
89
  end
38
90
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OutroRails
4
- VERSION = "0.1.4"
4
+ VERSION = "0.1.6"
5
5
  end
data/lib/outro_rails.rb CHANGED
@@ -18,4 +18,35 @@ module OutroRails
18
18
  !!chord_chart_voicing_authorization.call(chord_chart: chord_chart,
19
19
  controller: controller)
20
20
  end
21
+
22
+ ELEMENT_CLASS_DEFAULTS = {
23
+ button: "outro-button",
24
+ input: "outro-input",
25
+ label: "outro-label",
26
+ select: "outro-select",
27
+ textarea: "outro-textarea"
28
+ }.freeze
29
+
30
+ mattr_accessor :button_class, default: ELEMENT_CLASS_DEFAULTS[:button]
31
+ mattr_accessor :input_class, default: ELEMENT_CLASS_DEFAULTS[:input]
32
+ mattr_accessor :label_class, default: ELEMENT_CLASS_DEFAULTS[:label]
33
+ mattr_accessor :select_class, default: ELEMENT_CLASS_DEFAULTS[:select]
34
+ mattr_accessor :textarea_class, default: ELEMENT_CLASS_DEFAULTS[:textarea]
35
+
36
+ def self.configure
37
+ yield self
38
+ self
39
+ end
40
+
41
+ def self.class_for(element)
42
+ key = element.to_sym
43
+
44
+ unless ELEMENT_CLASS_DEFAULTS.key?(key)
45
+ raise ArgumentError,
46
+ "unknown element #{element.inspect}; expected one of " \
47
+ "#{ELEMENT_CLASS_DEFAULTS.keys.map(&:inspect).join(', ')}"
48
+ end
49
+
50
+ public_send(:"#{key}_class")
51
+ end
21
52
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: outro_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - endtoendpaper
@@ -79,6 +79,7 @@ files:
79
79
  - app/helpers/outro_rails/chord_charts_helper.rb
80
80
  - app/helpers/outro_rails/chords_helper.rb
81
81
  - app/helpers/outro_rails/circle_of_fifths_helper.rb
82
+ - app/helpers/outro_rails/class_names_helper.rb
82
83
  - app/helpers/outro_rails/instruments_helper.rb
83
84
  - app/helpers/outro_rails/scales_helper.rb
84
85
  - app/helpers/outro_rails/transposer_helper.rb