bulma_x 0.2.0
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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +22 -0
- data/CHANGELOG.md +30 -0
- data/CONTRIBUTING.md +50 -0
- data/LICENSE +661 -0
- data/README.md +315 -0
- data/Rakefile +12 -0
- data/lib/bulma_x/base_component.rb +125 -0
- data/lib/bulma_x/base_input.rb +37 -0
- data/lib/bulma_x/block.rb +7 -0
- data/lib/bulma_x/box.rb +7 -0
- data/lib/bulma_x/breadcrumbs.rb +35 -0
- data/lib/bulma_x/button.rb +42 -0
- data/lib/bulma_x/card.rb +55 -0
- data/lib/bulma_x/checkbox.rb +23 -0
- data/lib/bulma_x/columns.rb +81 -0
- data/lib/bulma_x/component_dsl.rb +19 -0
- data/lib/bulma_x/dropdown.rb +65 -0
- data/lib/bulma_x/dsl/options.rb +129 -0
- data/lib/bulma_x/dsl/slots.rb +234 -0
- data/lib/bulma_x/dsl/validations.rb +74 -0
- data/lib/bulma_x/field.rb +150 -0
- data/lib/bulma_x/figure.rb +27 -0
- data/lib/bulma_x/file.rb +54 -0
- data/lib/bulma_x/footer.rb +7 -0
- data/lib/bulma_x/form.rb +27 -0
- data/lib/bulma_x/grid.rb +90 -0
- data/lib/bulma_x/help.rb +7 -0
- data/lib/bulma_x/hero.rb +36 -0
- data/lib/bulma_x/icon.rb +66 -0
- data/lib/bulma_x/image.rb +42 -0
- data/lib/bulma_x/input.rb +53 -0
- data/lib/bulma_x/level.rb +43 -0
- data/lib/bulma_x/link.rb +44 -0
- data/lib/bulma_x/media.rb +19 -0
- data/lib/bulma_x/message.rb +27 -0
- data/lib/bulma_x/modal.rb +26 -0
- data/lib/bulma_x/navbar.rb +162 -0
- data/lib/bulma_x/notification.rb +15 -0
- data/lib/bulma_x/pagination.rb +86 -0
- data/lib/bulma_x/panel.rb +29 -0
- data/lib/bulma_x/paragraph.rb +7 -0
- data/lib/bulma_x/progress.rb +36 -0
- data/lib/bulma_x/radio.rb +33 -0
- data/lib/bulma_x/section.rb +35 -0
- data/lib/bulma_x/select.rb +57 -0
- data/lib/bulma_x/shared/aria_options.rb +19 -0
- data/lib/bulma_x/shared/data_options.rb +19 -0
- data/lib/bulma_x/shared/flex_options.rb +57 -0
- data/lib/bulma_x/shared/global_options.rb +49 -0
- data/lib/bulma_x/shared/spacing_options.rb +80 -0
- data/lib/bulma_x/shared/text_options.rb +31 -0
- data/lib/bulma_x/slot.rb +13 -0
- data/lib/bulma_x/subtitle.rb +9 -0
- data/lib/bulma_x/table.rb +78 -0
- data/lib/bulma_x/tabs.rb +43 -0
- data/lib/bulma_x/tag.rb +25 -0
- data/lib/bulma_x/textarea.rb +29 -0
- data/lib/bulma_x/title.rb +21 -0
- data/lib/bulma_x/version.rb +5 -0
- data/lib/bulma_x/vertical_menu.rb +71 -0
- data/lib/bulma_x.rb +9 -0
- metadata +123 -0
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BulmaX
|
4
|
+
class Columns < BaseComponent
|
5
|
+
option :mobile, values: BOOLEAN, default: false
|
6
|
+
option :desktop, values: BOOLEAN, default: false
|
7
|
+
option :gapless, values: BOOLEAN, default: false
|
8
|
+
option :multiline, values: BOOLEAN, default: false
|
9
|
+
option :vcentered, values: BOOLEAN, default: false
|
10
|
+
option :centered, values: BOOLEAN, default: false
|
11
|
+
|
12
|
+
option :gap, values: (0..8).to_a.push(nil), default: nil
|
13
|
+
option :gap_mobile, values: (0..8).to_a.push(nil), default: nil
|
14
|
+
option :gap_tablet, values: (0..8).to_a.push(nil), default: nil
|
15
|
+
option :gap_desktop, values: (0..8).to_a.push(nil), default: nil
|
16
|
+
option :gap_widescreen, values: (0..8).to_a.push(nil), default: nil
|
17
|
+
option :gap_fullhd, values: (0..8).to_a.push(nil), default: nil
|
18
|
+
|
19
|
+
slots :column, component: 'Column'
|
20
|
+
|
21
|
+
def view_template
|
22
|
+
super do
|
23
|
+
slots(:column).each { render it }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Column < BaseComponent
|
28
|
+
PROPORTIONS = %w[three-quarters two-thirds half one-third one-quarter full four-fifths three-fifths two-fifths
|
29
|
+
one-fifth].freeze
|
30
|
+
SPANS = (1..12).to_a.freeze
|
31
|
+
|
32
|
+
option :proportion, values: PROPORTIONS, default: nil
|
33
|
+
option :proportion_mobile, values: PROPORTIONS, default: nil
|
34
|
+
option :proportion_tablet, values: PROPORTIONS, default: nil
|
35
|
+
option :proportion_desktop, values: PROPORTIONS, default: nil
|
36
|
+
option :proportion_widescreen, values: PROPORTIONS, default: nil
|
37
|
+
option :proportion_fullhd, values: PROPORTIONS, default: nil
|
38
|
+
|
39
|
+
option :span, values: SPANS, default: nil
|
40
|
+
option :offset, values: PROPORTIONS + SPANS, default: nil
|
41
|
+
option :narrow, values: [nil, :always, :mobile, :table, :touch, :desktop, :widescreen, :fullhd], default: nil
|
42
|
+
|
43
|
+
# rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
|
44
|
+
def root_classes
|
45
|
+
super +
|
46
|
+
[
|
47
|
+
'column',
|
48
|
+
@proportion && "is-#{@proportion}",
|
49
|
+
@proportion_mobile && "is-#{@proportion_mobile}-mobile",
|
50
|
+
@proportion_tablet && "is-#{@proportion_tablet}-tablet",
|
51
|
+
@proportion_desktop && "is-#{@proportion_desktop}-desktop",
|
52
|
+
@proportion_widescreen && "is-#{@proportion_widescreen}-widescreen",
|
53
|
+
@proportion_fullhd && "is-#{@proportion_fullhd}-fullhd",
|
54
|
+
@span && "is-#{@span}",
|
55
|
+
@offset && "is-offset-#{@offset}",
|
56
|
+
@narrow && (@narrow == :always ? 'is-narrow' : "is-narrow-#{@narrow}")
|
57
|
+
]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def root_classes
|
62
|
+
super +
|
63
|
+
[
|
64
|
+
'columns',
|
65
|
+
@mobile && 'is-mobile',
|
66
|
+
@desktop && 'is-desktop',
|
67
|
+
@gapless && 'is-gapless',
|
68
|
+
@multiline && 'is-multiline',
|
69
|
+
@centered && 'is-centered',
|
70
|
+
@vcentered && 'is-vcentered',
|
71
|
+
@gap && "is-column-gap-#{@gap}",
|
72
|
+
@gap_mobile && "is-column-gap-#{@gap_mobile}-mobile",
|
73
|
+
@gap_tablet && "is-column-gap-#{@gap_tablet}-tablet",
|
74
|
+
@gap_desktop && "is-column-gap-#{@gap_desktop}-desktop",
|
75
|
+
@gap_widescreen && "is-column-gap-#{@gap_widescreen}-widescreen",
|
76
|
+
@gap_fullhd && "is-column-gap-#{@gap_fullhd}-fullhd"
|
77
|
+
]
|
78
|
+
end
|
79
|
+
# rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BulmaX
|
4
|
+
module ComponentDsl
|
5
|
+
def self.included(base)
|
6
|
+
base.include(Dsl::Options)
|
7
|
+
base.include(Dsl::Slots)
|
8
|
+
base.include(Dsl::Validations)
|
9
|
+
|
10
|
+
base.instance_eval do
|
11
|
+
def self.inherited(subclass)
|
12
|
+
super
|
13
|
+
inherit_options(subclass)
|
14
|
+
inherit_validations(subclass)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BulmaX
|
4
|
+
class Dropdown < BaseComponent
|
5
|
+
option :direction, values: %i[down up], default: 'down'
|
6
|
+
option :opened, values: BOOLEAN, default: false
|
7
|
+
|
8
|
+
slot :trigger, classes: ['dropdown-trigger']
|
9
|
+
slot :menu, classes: ['dropdown-menu'], attributes: { role: 'menu' }
|
10
|
+
slot :content, component: 'Content'
|
11
|
+
slots :item, component: 'Item'
|
12
|
+
|
13
|
+
def view_template
|
14
|
+
super do
|
15
|
+
slot(:trigger)
|
16
|
+
slot(:menu)
|
17
|
+
|
18
|
+
slot(:content)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def root_classes
|
23
|
+
super +
|
24
|
+
[
|
25
|
+
'dropdown is-hoverable',
|
26
|
+
@direction == :up && 'is-up',
|
27
|
+
@opened && 'is-active'
|
28
|
+
]
|
29
|
+
end
|
30
|
+
|
31
|
+
class Item < BaseComponent
|
32
|
+
option :active, values: BOOLEAN, default: false
|
33
|
+
option :disabled, values: BOOLEAN, default: false
|
34
|
+
option :divider, values: BOOLEAN, default: false
|
35
|
+
|
36
|
+
def root_classes
|
37
|
+
if @divider
|
38
|
+
super + [
|
39
|
+
'dropdown-divider'
|
40
|
+
]
|
41
|
+
else
|
42
|
+
super + [
|
43
|
+
'dropdown-item',
|
44
|
+
@active && 'is-active',
|
45
|
+
@disabled && 'is-disabled'
|
46
|
+
]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def root_tag = @divider ? :hr : :a
|
51
|
+
end
|
52
|
+
|
53
|
+
class Content < BaseComponent
|
54
|
+
root_slot classes: ['dropdown-content']
|
55
|
+
|
56
|
+
slots :item, component: Item # TODO: fix reference to component
|
57
|
+
|
58
|
+
def view_template
|
59
|
+
super do
|
60
|
+
slots(:item).each { render it }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BulmaX
|
4
|
+
module Dsl
|
5
|
+
module Options
|
6
|
+
module ClassMethods
|
7
|
+
attr_reader :options, :default_options
|
8
|
+
|
9
|
+
def option(attribute_name, values: nil, default: nil, override: false)
|
10
|
+
@options ||= Hash.new { |h, k| h[k] = [] }
|
11
|
+
@default_options ||= {}
|
12
|
+
|
13
|
+
if @options.key?(attribute_name) && !override
|
14
|
+
raise ArgumentError,
|
15
|
+
"Option :#{attribute_name} already defined on #{self}, use override: true to redefine it"
|
16
|
+
end
|
17
|
+
|
18
|
+
@options[attribute_name] = values
|
19
|
+
@default_options[attribute_name] = default.dup.freeze
|
20
|
+
|
21
|
+
define_prefixed_helpers(attribute_name, values)
|
22
|
+
define_values_helper(attribute_name, values)
|
23
|
+
end
|
24
|
+
|
25
|
+
def default_option(attribute_name, value)
|
26
|
+
@default_options ||= {}
|
27
|
+
|
28
|
+
raise ArgumentError, "Option :#{attribute_name} does not exist" unless @options.key?(attribute_name)
|
29
|
+
|
30
|
+
@default_options[attribute_name] = value.dup.freeze
|
31
|
+
end
|
32
|
+
|
33
|
+
def remove_option(attribute_name)
|
34
|
+
@options.delete(attribute_name)
|
35
|
+
@default_options.delete(attribute_name)
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
def inherit_options(subclass)
|
41
|
+
subclass.instance_variable_set(:@options, @options.dup)
|
42
|
+
subclass.instance_variable_set(:@default_options, @default_options.dup)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def define_prefixed_helpers(attribute_name, values)
|
48
|
+
case values
|
49
|
+
in [true, false] | [false, true]
|
50
|
+
define_boolean_helper(attribute_name)
|
51
|
+
in Array
|
52
|
+
define_helpers(attribute_name, values)
|
53
|
+
in Range | nil
|
54
|
+
# Do nothing
|
55
|
+
else
|
56
|
+
raise ArgumentError, '#options expect an array of values'
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def define_boolean_helper(attribute_name)
|
61
|
+
define_method(attribute_name) do
|
62
|
+
tap { instance_variable_set(:"@#{attribute_name}", true) }
|
63
|
+
end
|
64
|
+
define_method(:"#{attribute_name}?") do
|
65
|
+
instance_variable_get(:"@#{attribute_name}")
|
66
|
+
end
|
67
|
+
define_method(:"not_#{attribute_name}") do
|
68
|
+
tap { instance_variable_set(:"@#{attribute_name}", false) }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def define_helpers(attribute_name, values)
|
73
|
+
values.each do |value|
|
74
|
+
define_method(:"#{attribute_name}_#{value}") do
|
75
|
+
tap { instance_variable_set(:"@#{attribute_name}", value) }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def define_values_helper(attribute_name, values)
|
81
|
+
class_eval do
|
82
|
+
define_singleton_method(:"#{attribute_name}_values") do
|
83
|
+
values
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.included(base)
|
90
|
+
base.extend(ClassMethods)
|
91
|
+
end
|
92
|
+
|
93
|
+
def build_from_options(**build_options)
|
94
|
+
@build_options = build_options
|
95
|
+
return if class_options.nil?
|
96
|
+
return if build_options.nil?
|
97
|
+
|
98
|
+
assert_options_validity!(build_options)
|
99
|
+
|
100
|
+
class_options.each_key do |attribute|
|
101
|
+
value = if build_options.key?(attribute)
|
102
|
+
build_options[attribute]
|
103
|
+
else
|
104
|
+
class_default_options[attribute].dup
|
105
|
+
end
|
106
|
+
|
107
|
+
instance_variable_set(:"@#{attribute}", value)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def class_options = self.class.options
|
112
|
+
def class_default_options = self.class.default_options
|
113
|
+
def option_get(name) = instance_variable_get(:"@#{name}")
|
114
|
+
def option_set(name, value) = instance_variable_set(:"@#{name}", value)
|
115
|
+
|
116
|
+
def assert_options_validity!(build_options)
|
117
|
+
build_options.each do |key, value|
|
118
|
+
raise ArgumentError, "Unknown option :#{key} on #{self.class}" unless class_options.key?(key)
|
119
|
+
|
120
|
+
class_option = class_options[key]
|
121
|
+
next if class_option.nil? || class_option.include?(value)
|
122
|
+
|
123
|
+
raise ArgumentError,
|
124
|
+
"Option #{key.inspect} has invalid value #{value.inspect}, expected one of #{class_option.inspect}"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,234 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BulmaX
|
4
|
+
module Dsl
|
5
|
+
module Slots
|
6
|
+
module DeferredRender
|
7
|
+
def before_template(&)
|
8
|
+
@_vanishing = true
|
9
|
+
vanish do
|
10
|
+
prepare(&)
|
11
|
+
yield self if block_given?
|
12
|
+
end
|
13
|
+
@_vanishing = false
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
def prepare; end
|
18
|
+
|
19
|
+
def render(...)
|
20
|
+
return if @_vanishing
|
21
|
+
|
22
|
+
super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module ClassMethods
|
27
|
+
# slot defines a slot that will be accessible on the instance via slot
|
28
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize
|
29
|
+
def slot(slot_name, tag: :div, classes: [], attributes: {}, component: Slot)
|
30
|
+
include DeferredRender
|
31
|
+
|
32
|
+
if component != Slot && (tag != :div || classes.any? || attributes.any?)
|
33
|
+
raise ArgumentError, 'When using a custom component, tag, classes and attributes are ignored'
|
34
|
+
end
|
35
|
+
|
36
|
+
define_tag_helper(slot_name, tag)
|
37
|
+
define_classes_helper(slot_name, classes)
|
38
|
+
define_attributes_helper(slot_name, attributes)
|
39
|
+
|
40
|
+
# TODO: Replace method usage by class variable for validation
|
41
|
+
# @enabled_slots ||= {}
|
42
|
+
# @enabled_slots[slot_name] = { collection: false, ...}
|
43
|
+
|
44
|
+
define_method(:"with_#{slot_name}") do |**options, &block|
|
45
|
+
@slots[slot_name] = { component:, options:, block: }
|
46
|
+
|
47
|
+
nil
|
48
|
+
end
|
49
|
+
private :"with_#{slot_name}"
|
50
|
+
|
51
|
+
define_method(:"#{slot_name}_slot_component") do
|
52
|
+
if component.is_a?(Class)
|
53
|
+
component
|
54
|
+
elsif component.is_a?(String)
|
55
|
+
if component == 'self'
|
56
|
+
self.class
|
57
|
+
else
|
58
|
+
self.class.const_get(component)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize
|
64
|
+
|
65
|
+
# slots defines a collection of slot that will be accessible on the instance via slots
|
66
|
+
|
67
|
+
# rubocop:disable Metrics
|
68
|
+
def slots(slot_name, tag: :div, classes: [], attributes: {}, component: Slot)
|
69
|
+
include DeferredRender
|
70
|
+
|
71
|
+
if component != Slot && (tag != :div || classes.any? || attributes.any?)
|
72
|
+
raise ArgumentError, 'When using a custom component, tag, classes and attributes are ignored'
|
73
|
+
end
|
74
|
+
|
75
|
+
define_tag_helper(slot_name, tag)
|
76
|
+
define_classes_helper(slot_name, classes)
|
77
|
+
define_attributes_helper(slot_name, attributes)
|
78
|
+
|
79
|
+
define_method(:"with_#{slot_name}") do |**options, &block|
|
80
|
+
@slots[slot_name] ||= []
|
81
|
+
|
82
|
+
@slots[slot_name] << { component:, options:, block: }
|
83
|
+
|
84
|
+
nil
|
85
|
+
end
|
86
|
+
private :"with_#{slot_name}"
|
87
|
+
|
88
|
+
define_method(:"#{slot_name}_slot_component") do
|
89
|
+
if component.is_a?(Class)
|
90
|
+
component
|
91
|
+
elsif component.is_a?(String)
|
92
|
+
if component == 'self'
|
93
|
+
self.class
|
94
|
+
else
|
95
|
+
self.class.const_get(component)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
# rubocop:enable Metrics
|
101
|
+
|
102
|
+
# root_slot allows to redefine the root tag, classes and attributes in one go
|
103
|
+
def root_slot(**kwargs)
|
104
|
+
tag = kwargs[:tag] || :div
|
105
|
+
define_method(:root_tag) { tag }
|
106
|
+
|
107
|
+
classes = kwargs[:classes] || []
|
108
|
+
define_method(:root_classes) { base_classes + classes }
|
109
|
+
|
110
|
+
attributes = kwargs[:attributes] || {}
|
111
|
+
define_method(:root_attributes) { base_attributes.merge(attributes) }
|
112
|
+
end
|
113
|
+
|
114
|
+
def define_tag_helper(slot_name, tag) = define_method(:"#{slot_name}_tag") { tag }
|
115
|
+
def define_classes_helper(slot_name, classes) = define_method(:"#{slot_name}_classes") { classes }
|
116
|
+
def define_attributes_helper(slot_name, attributes) = define_method(:"#{slot_name}_attributes") { attributes }
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.included(base)
|
120
|
+
base.extend(ClassMethods)
|
121
|
+
end
|
122
|
+
|
123
|
+
def initialize(...)
|
124
|
+
super
|
125
|
+
@slots = {}
|
126
|
+
end
|
127
|
+
|
128
|
+
# content is used to store a slot content for later use (via slot or slots)
|
129
|
+
# It is meant to be used during the render context :
|
130
|
+
# render Component.new { it.content(:slot_name) { 'My slot content' } }
|
131
|
+
def content(slot_name, **, &)
|
132
|
+
if respond_to?(:"with_#{slot_name}", true)
|
133
|
+
send(:"with_#{slot_name}", **, &)
|
134
|
+
else
|
135
|
+
raise ArgumentError,
|
136
|
+
"Content was called for slot `#{slot_name}`, but this slot does not exist on #{self.class}"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def slot?(slot_name, idx = 0)
|
141
|
+
if @slots[slot_name].is_a?(Enumerable)
|
142
|
+
@slots[slot_name].size > idx
|
143
|
+
else
|
144
|
+
@slots.key?(slot_name)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
alias content? slot?
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
# slot as an instance methods displays a slot content. It has three usages:
|
152
|
+
# 1. In a deferred render context, it will fetch the stored slot info to render it. Note: content params have priority over view_template params
|
153
|
+
# 2. In a deferred render context without stored content, it uses the default rendering of the given component IF it differs from the default Slot
|
154
|
+
# 3. In a direct rendering context, we want to display the block content
|
155
|
+
# 4. In a legacy component that used to override slot method, we still support it for now
|
156
|
+
def slot(slot_name, **, &)
|
157
|
+
unless respond_to?(:"#{slot_name}_slot_component", true)
|
158
|
+
raise ArgumentError,
|
159
|
+
"Slot #{slot_name} is not defined on #{self.class}"
|
160
|
+
end
|
161
|
+
|
162
|
+
component = slot_name == :root ? Slot : public_send(:"#{slot_name}_slot_component")
|
163
|
+
|
164
|
+
if @slots.key?(slot_name)
|
165
|
+
render_content(slot_name, **, component:)
|
166
|
+
elsif component != Slot
|
167
|
+
render_custom_component(component, **, &)
|
168
|
+
elsif respond_to?(:"component_slot_#{slot_name}", true)
|
169
|
+
render_legacy_slot(slot_name, **, &)
|
170
|
+
else
|
171
|
+
render generic_slot(slot_name, **, &)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# slots as an instance method returns the list of the contents stored
|
176
|
+
# If no content is stored, it returns an empty array
|
177
|
+
# rubocop:disable Metrics/AbcSize
|
178
|
+
def slots(slot_name, **template_args)
|
179
|
+
if !@slots.key?(slot_name)
|
180
|
+
[] # no item given through with_<slot_name>
|
181
|
+
elsif !@slots[slot_name].is_a?(Enumerable)
|
182
|
+
raise ArgumentError, "Slot #{slot_name} is not a collection"
|
183
|
+
else
|
184
|
+
@slots[slot_name].map do |stored_slot|
|
185
|
+
stored_component_method_name = :"#{slot_name}_slot_component"
|
186
|
+
|
187
|
+
unless respond_to?(stored_component_method_name, true)
|
188
|
+
raise ArgumentError, "Slot #{slot_name} is not defined on #{self.class}"
|
189
|
+
end
|
190
|
+
|
191
|
+
stored_component = public_send(stored_component_method_name)
|
192
|
+
|
193
|
+
if stored_component == Slot
|
194
|
+
generic_slot(slot_name, **template_args, &stored_slot[:block])
|
195
|
+
else
|
196
|
+
stored_component.new(**template_args.merge(stored_slot[:options]), &stored_slot[:block])
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
# rubocop:enable Metrics/AbcSize
|
202
|
+
|
203
|
+
def render_content(slot_name, component:, **template_options)
|
204
|
+
slot_info = @slots[slot_name]
|
205
|
+
slot_options = slot_info[:options].merge(**template_options)
|
206
|
+
|
207
|
+
if component == Slot
|
208
|
+
render generic_slot(slot_name, **slot_options, &slot_info[:block])
|
209
|
+
else
|
210
|
+
render component.new(**slot_options, &slot_info[:block])
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def render_custom_component(component, **, &)
|
215
|
+
render component.new(**, &)
|
216
|
+
end
|
217
|
+
|
218
|
+
def render_legacy_slot(slot_name, **, &)
|
219
|
+
# Warning.warn('using component_slot_ is deprecated and will be removed soon') # Deprecation silenced for now
|
220
|
+
send(:"component_slot_#{slot_name}", **, &)
|
221
|
+
end
|
222
|
+
|
223
|
+
def generic_slot(slot_name, **, &)
|
224
|
+
Slot.new(
|
225
|
+
tag: send(:"#{slot_name}_tag"),
|
226
|
+
classes: send(:"#{slot_name}_classes"),
|
227
|
+
attributes: send(:"#{slot_name}_attributes"),
|
228
|
+
**,
|
229
|
+
&
|
230
|
+
)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BulmaX
|
4
|
+
module Dsl
|
5
|
+
module Validations
|
6
|
+
class Error < StandardError; end
|
7
|
+
class InvalidValidator < StandardError; end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
ValidatorResult = Data.define(:result, :source_location, :details, :validator)
|
11
|
+
Validator = Data.define(:option, :validator, :message) do
|
12
|
+
def valid?(component, value)
|
13
|
+
source_location = nil
|
14
|
+
result = case validator
|
15
|
+
in Symbol
|
16
|
+
source_location = component.method(validator).source_location
|
17
|
+
component.send(validator, value)
|
18
|
+
in Proc
|
19
|
+
source_location = validator.source_location
|
20
|
+
component.instance_exec(value, &validator)
|
21
|
+
end
|
22
|
+
|
23
|
+
ValidatorResult.new(result, source_location.join(':'), message, self)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def validate(*options, validator, message: nil)
|
28
|
+
options.each do |option|
|
29
|
+
if validator.is_a?(Proc) && validator.arity != 1
|
30
|
+
raise InvalidValidator,
|
31
|
+
'Your validator must accept exactly on argument (the option value received during initialization)'
|
32
|
+
end
|
33
|
+
|
34
|
+
validators << Validator.new(option, validator, message)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def remove_validations(option_name)
|
39
|
+
removed, @validators = validators.partition { _1.option == option_name }
|
40
|
+
|
41
|
+
removed
|
42
|
+
end
|
43
|
+
|
44
|
+
def validators = @validators ||= []
|
45
|
+
|
46
|
+
protected
|
47
|
+
|
48
|
+
def inherit_validations(subclass)
|
49
|
+
deep_dup_validators = validators.dup
|
50
|
+
subclass.instance_variable_set(:@validators, deep_dup_validators)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.included(base)
|
55
|
+
base.extend(ClassMethods)
|
56
|
+
end
|
57
|
+
|
58
|
+
def validate!(**options)
|
59
|
+
self.class.validators.none? do |validator|
|
60
|
+
value = options[validator.option]
|
61
|
+
validation = validator.valid?(self, value)
|
62
|
+
|
63
|
+
next if validation.result
|
64
|
+
|
65
|
+
raise Error, <<~ERROR
|
66
|
+
Option :#{validator.option} failed validation for value #{value.inspect}.
|
67
|
+
#{"Details: #{validation.details}" if validation.details}
|
68
|
+
Validator was defined at #{validation.source_location}
|
69
|
+
ERROR
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|