view_component_storybook 0.5.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -128
  3. data/app/controllers/view_component/storybook/stories_controller.rb +10 -11
  4. data/app/views/view_component/storybook/stories/show.html.erb +8 -1
  5. data/config/locales/en.yml +32 -0
  6. data/lib/view_component/storybook.rb +14 -0
  7. data/lib/view_component/storybook/content_concern.rb +42 -0
  8. data/lib/view_component/storybook/controls.rb +5 -1
  9. data/lib/view_component/storybook/controls/base_options_config.rb +41 -0
  10. data/lib/view_component/storybook/controls/boolean_config.rb +7 -6
  11. data/lib/view_component/storybook/controls/color_config.rb +4 -6
  12. data/lib/view_component/storybook/controls/control_config.rb +25 -25
  13. data/lib/view_component/storybook/controls/controls_helpers.rb +76 -0
  14. data/lib/view_component/storybook/controls/custom_config.rb +52 -0
  15. data/lib/view_component/storybook/controls/date_config.rb +14 -13
  16. data/lib/view_component/storybook/controls/multi_options_config.rb +46 -0
  17. data/lib/view_component/storybook/controls/number_config.rb +13 -10
  18. data/lib/view_component/storybook/controls/object_config.rb +11 -7
  19. data/lib/view_component/storybook/controls/options_config.rb +18 -25
  20. data/lib/view_component/storybook/controls/simple_control_config.rb +48 -0
  21. data/lib/view_component/storybook/controls/text_config.rb +1 -3
  22. data/lib/view_component/storybook/dsl.rb +1 -2
  23. data/lib/view_component/storybook/dsl/{controls_dsl.rb → legacy_controls_dsl.rb} +19 -22
  24. data/lib/view_component/storybook/engine.rb +2 -1
  25. data/lib/view_component/storybook/method_args.rb +16 -0
  26. data/lib/view_component/storybook/method_args/control_method_args.rb +91 -0
  27. data/lib/view_component/storybook/method_args/method_args.rb +52 -0
  28. data/lib/view_component/storybook/method_args/method_parameters_names.rb +97 -0
  29. data/lib/view_component/storybook/slots.rb +14 -0
  30. data/lib/view_component/storybook/slots/slot.rb +24 -0
  31. data/lib/view_component/storybook/slots/slot_config.rb +44 -0
  32. data/lib/view_component/storybook/stories.rb +60 -14
  33. data/lib/view_component/storybook/story.rb +18 -0
  34. data/lib/view_component/storybook/story_config.rb +140 -15
  35. data/lib/view_component/storybook/tasks/write_stories_json.rake +6 -0
  36. data/lib/view_component/storybook/version.rb +1 -1
  37. metadata +64 -23
  38. data/lib/view_component/storybook/controls/array_config.rb +0 -36
  39. data/lib/view_component/storybook/dsl/story_dsl.rb +0 -39
@@ -3,9 +3,7 @@
3
3
  module ViewComponent
4
4
  module Storybook
5
5
  module Controls
6
- class TextConfig < ControlConfig
7
- validates :value, presence: true
8
-
6
+ class TextConfig < SimpleControlConfig
9
7
  def type
10
8
  :text
11
9
  end
@@ -7,8 +7,7 @@ module ViewComponent
7
7
  module Dsl
8
8
  extend ActiveSupport::Autoload
9
9
 
10
- autoload :StoryDsl
11
- autoload :ControlsDsl
10
+ autoload :LegacyControlsDsl
12
11
  end
13
12
  end
14
13
  end
@@ -3,71 +3,68 @@
3
3
  module ViewComponent
4
4
  module Storybook
5
5
  module Dsl
6
- class ControlsDsl
7
- attr_reader :component, :controls
8
-
9
- def initialize(component)
10
- @component = component
11
- @controls = []
6
+ class LegacyControlsDsl
7
+ def controls
8
+ @controls ||= []
12
9
  end
13
10
 
14
11
  def text(param, value, name: nil)
15
- controls << Controls::TextConfig.new(component, param, value, name: name)
12
+ controls << Controls::TextConfig.new(value, param: param, name: name)
16
13
  end
17
14
 
18
15
  def boolean(param, value, name: nil)
19
- controls << Controls::BooleanConfig.new(component, param, value, name: name)
16
+ controls << Controls::BooleanConfig.new(value, param: param, name: name)
20
17
  end
21
18
 
22
19
  def number(param, value, name: nil, min: nil, max: nil, step: nil)
23
- controls << Controls::NumberConfig.new(:number, component, param, value, name: name, min: min, max: max, step: step)
20
+ controls << Controls::NumberConfig.new(:number, value, param: param, name: name, min: min, max: max, step: step)
24
21
  end
25
22
 
26
23
  def range(param, value, name: nil, min: nil, max: nil, step: nil)
27
- controls << Controls::NumberConfig.new(:range, component, param, value, name: name, min: min, max: max, step: step)
24
+ controls << Controls::NumberConfig.new(:range, value, param: param, name: name, min: min, max: max, step: step)
28
25
  end
29
26
 
30
27
  def color(param, value, name: nil, preset_colors: nil)
31
- controls << Controls::ColorConfig.new(component, param, value, name: name, preset_colors: preset_colors)
28
+ controls << Controls::ColorConfig.new(value, param: param, name: name, preset_colors: preset_colors)
32
29
  end
33
30
 
34
31
  def object(param, value, name: nil)
35
- controls << Controls::ObjectConfig.new(component, param, value, name: name)
32
+ controls << Controls::ObjectConfig.new(value, param: param, name: name)
36
33
  end
37
34
 
38
35
  def select(param, options, value, name: nil)
39
- controls << Controls::OptionsConfig.new(:select, component, param, options, value, name: name)
36
+ controls << Controls::OptionsConfig.new(:select, options, value, param: param, name: name)
40
37
  end
41
38
 
42
39
  def multi_select(param, options, value, name: nil)
43
- controls << Controls::OptionsConfig.new(:'multi-select', component, param, options, value, name: name)
40
+ controls << Controls::OptionsConfig.new(:'multi-select', options, value, param: param, name: name)
44
41
  end
45
42
 
46
43
  def radio(param, options, value, name: nil)
47
- controls << Controls::OptionsConfig.new(:radio, component, param, options, value, name: name)
44
+ controls << Controls::OptionsConfig.new(:radio, options, value, param: param, name: name)
48
45
  end
49
46
 
50
47
  def inline_radio(param, options, value, name: nil)
51
- controls << Controls::OptionsConfig.new(:'inline-radio', component, param, options, value, name: name)
48
+ controls << Controls::OptionsConfig.new(:'inline-radio', options, value, param: param, name: name)
52
49
  end
53
50
 
54
51
  def check(param, options, value, name: nil)
55
- controls << Controls::OptionsConfig.new(:check, component, param, options, value, name: name)
52
+ controls << Controls::OptionsConfig.new(:check, options, value, param: param, name: name)
56
53
  end
57
54
 
58
55
  def inline_check(param, options, value, name: nil)
59
- controls << Controls::OptionsConfig.new(:'inline-check', component, param, options, value, name: name)
56
+ controls << Controls::OptionsConfig.new(:'inline-check', options, value, param: param, name: name)
60
57
  end
61
58
 
62
- def array(param, value, separator = ",", name: nil)
63
- controls << Controls::ArrayConfig.new(component, param, value, separator, name: name)
59
+ def array(param, value, _separator = nil, name: nil)
60
+ controls << Controls::ObjectConfig.new(value, param: param, name: name)
64
61
  end
65
62
 
66
63
  def date(param, value, name: nil)
67
- controls << Controls::DateConfig.new(component, param, value, name: name)
64
+ controls << Controls::DateConfig.new(value, param: param, name: name)
68
65
  end
69
66
 
70
- def respond_to_missing?(_method)
67
+ def respond_to_missing?(_method, *)
71
68
  true
72
69
  end
73
70
 
@@ -12,6 +12,7 @@ module ViewComponent
12
12
  options = app.config.view_component_storybook
13
13
 
14
14
  options.show_stories = Rails.env.development? if options.show_stories.nil?
15
+ options.stories_route ||= ViewComponent::Storybook.stories_route
15
16
 
16
17
  if options.show_stories
17
18
  options.stories_path ||= defined?(Rails.root) ? Rails.root.join("test/components/stories") : nil
@@ -33,7 +34,7 @@ module ViewComponent
33
34
 
34
35
  if options.show_stories
35
36
  app.routes.prepend do
36
- get "/rails/stories/*stories/:story" => "view_component/storybook/stories#show", :internal => true
37
+ get "#{options.stories_route}/*stories/:story" => "view_component/storybook/stories#show", :internal => true
37
38
  end
38
39
  end
39
40
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/dependencies/autoload"
4
+
5
+ module ViewComponent
6
+ module Storybook
7
+ module MethodArgs
8
+ extend ActiveSupport::Autoload
9
+
10
+ autoload :MethodArgs
11
+ autoload :MethodParametersNames
12
+ autoload :ValidatableMethodArgs
13
+ autoload :ControlMethodArgs
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module MethodArgs
6
+ ##
7
+ # Class representing arguments passed to a method which can be validated
8
+ # against the args of the target method
9
+ # In addition the args and kwargs can contain Controls the values of which can
10
+ # be resolved from a params hash
11
+ class ControlMethodArgs < MethodArgs
12
+ include ActiveModel::Validations::Callbacks
13
+
14
+ attr_reader :param_prefix
15
+
16
+ validate :validate_controls
17
+ before_validation :assign_control_params
18
+
19
+ def with_param_prefix(prefix)
20
+ @param_prefix = prefix
21
+ self
22
+ end
23
+
24
+ ##
25
+ # resolve the controls values from the params
26
+ # call the target method or block with those values
27
+ def call(params, &target_block)
28
+ method_args = resolve_method_args(params)
29
+
30
+ (target_block || target_method).call(*method_args.args, **method_args.kwargs)
31
+ end
32
+
33
+ def resolve_method_args(params)
34
+ assign_control_params
35
+
36
+ args_from_params = args.map do |arg|
37
+ value_from_params(arg, params)
38
+ end
39
+ kwargs_from_params = kwargs.transform_values do |arg|
40
+ value_from_params(arg, params)
41
+ end
42
+
43
+ MethodArgs.new(target_method, *args_from_params, **kwargs_from_params)
44
+ end
45
+
46
+ def controls
47
+ @controls ||= (args + kwargs.values).select(&method(:control?))
48
+ end
49
+
50
+ private
51
+
52
+ def assign_control_params
53
+ return if @assigned_control_params
54
+
55
+ args.each_with_index do |arg, index|
56
+ add_param_if_control(arg, target_method_params_names.arg_name(index))
57
+ end
58
+
59
+ kwargs.each do |key, arg|
60
+ add_param_if_control(arg, key)
61
+ end
62
+
63
+ @assigned_control_params = true
64
+ end
65
+
66
+ def add_param_if_control(arg, param)
67
+ return unless control?(arg)
68
+
69
+ arg.param(param) if arg.param.nil? # don't overrite if set
70
+ # Always add prefix
71
+ arg.prefix_param(param_prefix) if param_prefix.present?
72
+ end
73
+
74
+ def value_from_params(arg, params)
75
+ control?(arg) ? arg.value_from_params(params) : arg
76
+ end
77
+
78
+ def control?(arg)
79
+ arg.is_a?(Controls::ControlConfig)
80
+ end
81
+
82
+ def validate_controls
83
+ controls.reject(&:valid?).each do |control|
84
+ control_errors = control.errors.full_messages.join(', ')
85
+ errors.add(:controls, :invalid_control, control_name: control.name, control_errors: control_errors)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module MethodArgs
6
+ ##
7
+ # Class representing arguments passed to a method which can be validated
8
+ # against the args of the target method
9
+ class MethodArgs
10
+ include ActiveModel::Validations
11
+
12
+ attr_reader :args, :kwargs, :target_method, :target_method_params_names
13
+
14
+ validate :validate_args, :validate_kwargs
15
+
16
+ def initialize(target_method, *args, **kwargs)
17
+ @target_method = target_method
18
+ @args = args
19
+ @kwargs = kwargs
20
+ @target_method_params_names = MethodParametersNames.new(target_method)
21
+ end
22
+
23
+ private
24
+
25
+ def validate_args
26
+ arg_count = args.count
27
+
28
+ if arg_count > target_method_params_names.max_arg_count
29
+ errors.add(:args, :too_many, max: target_method_params_names.max_arg_count, count: arg_count)
30
+ elsif arg_count < target_method_params_names.min_arg_count
31
+ errors.add(:args, :too_few, min: target_method_params_names.min_arg_count, count: arg_count)
32
+ end
33
+ end
34
+
35
+ def validate_kwargs
36
+ kwargs.each_key do |kwarg|
37
+ unless target_method_params_names.include_kwarg?(kwarg)
38
+ errors.add(:kwargs, :invalid_arg, kwarg: kwarg)
39
+ end
40
+ end
41
+
42
+ return if target_method_params_names.covers_required_kwargs?(kwargs.keys)
43
+
44
+ expected_keys = target_method_params_names.req_kwarg_names.join(', ')
45
+ actual_keys = kwargs.keys.join(', ')
46
+
47
+ errors.add(:kwargs, :invalid, expected_keys: expected_keys, actual_keys: actual_keys)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module MethodArgs
6
+ class MethodParametersNames
7
+ REQ_KWARG_TYPE = :keyreq
8
+ KWARG_TYPES = [REQ_KWARG_TYPE, :key].freeze
9
+ REQ_ARG_TYPE = :req
10
+ ARG_TYPES = [REQ_ARG_TYPE, :opt].freeze
11
+ KWARG_REST = :keyrest
12
+ REST = :rest
13
+
14
+ attr_reader :target_method
15
+
16
+ def initialize(target_method)
17
+ @target_method = target_method
18
+ end
19
+
20
+ def arg_name(pos)
21
+ if pos < named_arg_count
22
+ arg_names[pos]
23
+ else
24
+ offset_pos = pos - named_arg_count
25
+ "#{rest_arg_name}#{offset_pos}".to_sym
26
+ end
27
+ end
28
+
29
+ def include_kwarg?(kwarg_name)
30
+ supports_keyrest? || kwarg_names.include?(kwarg_name)
31
+ end
32
+
33
+ def covers_required_kwargs?(names)
34
+ names.to_set >= req_kwarg_names.to_set
35
+ end
36
+
37
+ def max_arg_count
38
+ supports_rest? ? Float::INFINITY : named_arg_count
39
+ end
40
+
41
+ def min_arg_count
42
+ req_arg_count
43
+ end
44
+
45
+ def req_kwarg_names
46
+ @req_kwarg_names ||= parameters.map do |type, name|
47
+ name if type == REQ_KWARG_TYPE
48
+ end.compact
49
+ end
50
+
51
+ private
52
+
53
+ def parameters
54
+ @parameters ||= target_method.parameters
55
+ end
56
+
57
+ def kwarg_names
58
+ @kwarg_names ||= parameters.map do |type, name|
59
+ name if KWARG_TYPES.include?(type)
60
+ end.compact.to_set
61
+ end
62
+
63
+ def arg_names
64
+ @arg_names ||= parameters.map do |type, name|
65
+ name if ARG_TYPES.include?(type)
66
+ end.compact
67
+ end
68
+
69
+ def req_arg_names
70
+ @req_arg_names ||= parameters.map do |type, name|
71
+ name if type == REQ_ARG_TYPE
72
+ end.compact
73
+ end
74
+
75
+ def named_arg_count
76
+ @named_arg_count ||= arg_names.count
77
+ end
78
+
79
+ def req_arg_count
80
+ @req_arg_count ||= req_arg_names.count
81
+ end
82
+
83
+ def rest_arg_name
84
+ @rest_arg_name ||= parameters.map { |type, name| name if type == REST }.first
85
+ end
86
+
87
+ def supports_keyrest?
88
+ @supports_keyrest ||= parameters.map(&:first).include?(KWARG_REST)
89
+ end
90
+
91
+ def supports_rest?
92
+ @supports_rest ||= parameters.map(&:first).include?(REST)
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/dependencies/autoload"
4
+
5
+ module ViewComponent
6
+ module Storybook
7
+ module Slots
8
+ extend ActiveSupport::Autoload
9
+
10
+ autoload :SlotConfig
11
+ autoload :Slot
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module Slots
6
+ class Slot
7
+ attr_reader :component, :slot_name, :slot_method_args, :content_block
8
+
9
+ # delegate :args, :kwargs, :controls, to: :slot_method_args
10
+
11
+ def initialize(component, slot_name, slot_method_args, content_block)
12
+ @component = component
13
+ @slot_name = slot_name
14
+ @slot_method_args = slot_method_args
15
+ @content_block = content_block
16
+ end
17
+
18
+ def call(&block)
19
+ component.send(slot_name, *slot_method_args.args, **slot_method_args.kwargs, &block)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module Slots
6
+ class SlotConfig
7
+ include ContentConcern
8
+
9
+ attr_reader :slot_name, :slot_method_args, :param, :content_block
10
+
11
+ def initialize(slot_name, slot_method_args, param, content_block)
12
+ @slot_name = slot_name
13
+ @slot_method_args = slot_method_args
14
+ @param = param
15
+ @content_block = content_block
16
+ end
17
+
18
+ def self.from_component(component_class, slot_name, param, *args, **kwargs, &block)
19
+ slot_method_args =
20
+ MethodArgs::ControlMethodArgs
21
+ .new(component_class.instance_method(slot_name), *args, **kwargs)
22
+ .with_param_prefix(param)
23
+ new(slot_name, slot_method_args, param, block)
24
+ end
25
+
26
+ def slot(componeont, params)
27
+ resolved_method_args = slot_method_args.resolve_method_args(params)
28
+ story_content_block = resolve_content_block(params)
29
+ Slot.new(componeont, slot_name, resolved_method_args, story_content_block)
30
+ end
31
+
32
+ def controls
33
+ list = slot_method_args.controls.dup
34
+ list << content_control if content_control
35
+ list
36
+ end
37
+
38
+ def content_param
39
+ "#{param}__content".to_sym
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end