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
@@ -4,19 +4,31 @@ module ViewComponent
4
4
  module Storybook
5
5
  class Stories
6
6
  extend ActiveSupport::DescendantsTracker
7
+ include ActiveModel::Validations
7
8
 
8
9
  class_attribute :story_configs, default: []
9
- class_attribute :parameters, :title, :stories_layout
10
+ class_attribute :stories_parameters, :stories_title, :stories_layout
11
+
12
+ validate :validate_story_configs
10
13
 
11
14
  class << self
15
+ def title(title = nil)
16
+ # if no argument is passed act like a getter
17
+ self.stories_title = title unless title.nil?
18
+ stories_title
19
+ end
20
+
12
21
  def story(name, component = default_component, &block)
13
- story_config = StoryConfig.configure(story_id(name), name, component, layout, &block)
22
+ story_config = StoryConfig.new(story_id(name), name, component, layout, &block)
23
+ story_config.instance_eval(&block)
14
24
  story_configs << story_config
15
25
  story_config
16
26
  end
17
27
 
18
- def parameters(**params)
19
- self.parameters = params
28
+ def parameters(params = nil)
29
+ # if no argument is passed act like a getter
30
+ self.stories_parameters = params unless params.nil?
31
+ stories_parameters
20
32
  end
21
33
 
22
34
  def layout(layout = nil)
@@ -26,6 +38,7 @@ module ViewComponent
26
38
  end
27
39
 
28
40
  def to_csf_params
41
+ validate!
29
42
  csf_params = { title: title }
30
43
  csf_params[:parameters] = parameters if parameters.present?
31
44
  csf_params[:stories] = story_configs.map(&:to_csf_params)
@@ -33,7 +46,7 @@ module ViewComponent
33
46
  end
34
47
 
35
48
  def write_csf_json
36
- json_path = File.join(ViewComponent::Storybook.stories_path, "#{stories_name}.stories.json")
49
+ json_path = File.join(stories_path, "#{stories_name}.stories.json")
37
50
  File.open(json_path, "w") do |f|
38
51
  f.write(JSON.pretty_generate(to_csf_params))
39
52
  end
@@ -56,7 +69,7 @@ module ViewComponent
56
69
  end
57
70
 
58
71
  # Find a component stories by its underscored class name.
59
- def find_stories(stories_name)
72
+ def find_story_configs(stories_name)
60
73
  all.find { |stories| stories.stories_name == stories_name }
61
74
  end
62
75
 
@@ -66,23 +79,34 @@ module ViewComponent
66
79
  end
67
80
 
68
81
  # find the story by name
69
- def find_story(name)
82
+ def find_story_config(name)
70
83
  story_configs.find { |config| config.name == name.to_sym }
71
84
  end
72
85
 
86
+ # validation - ActiveModel::Validations like but on the class vs the instance
87
+ def valid?
88
+ # use an instance so we can enjoy the benefits of ActiveModel::Validations
89
+ @validation_instance = new
90
+ @validation_instance.valid?
91
+ end
92
+
93
+ delegate :errors, to: :@validation_instance
94
+
95
+ def validate!
96
+ valid? || raise(ValidationError, @validation_instance)
97
+ end
98
+
73
99
  private
74
100
 
75
101
  def inherited(other)
76
102
  super(other)
77
103
  # setup class defaults
78
- other.title = other.stories_name.humanize.titlecase
104
+ other.stories_title = other.stories_name.humanize.titlecase
79
105
  other.story_configs = []
80
106
  end
81
107
 
82
108
  def default_component
83
109
  name.chomp("Stories").constantize
84
- rescue StandardError
85
- nil
86
110
  end
87
111
 
88
112
  def load_stories
@@ -93,14 +117,36 @@ module ViewComponent
93
117
  Storybook.stories_path
94
118
  end
95
119
 
96
- def show_stories
97
- Storybook.show_stories
98
- end
99
-
100
120
  def story_id(name)
101
121
  "#{stories_name}/#{name.to_s.parameterize}".underscore
102
122
  end
103
123
  end
124
+
125
+ protected
126
+
127
+ def validate_story_configs
128
+ story_configs.reject(&:valid?).each do |story_config|
129
+ story_errors = story_config.errors.full_messages.join(', ')
130
+ errors.add(:story_configs, :invalid_story, story_name: story_config.name, story_errors: story_errors)
131
+ end
132
+
133
+ story_names = story_configs.map(&:name)
134
+ duplicate_names = story_names.group_by(&:itself).map { |k, v| k if v.length > 1 }.compact
135
+ return if duplicate_names.empty?
136
+
137
+ duplicate_name_sentence = duplicate_names.map { |name| "'#{name}'" }.to_sentence
138
+ errors.add(:story_configs, :duplicate_stories, count: duplicate_names.count, duplicate_names: duplicate_name_sentence)
139
+ end
140
+
141
+ class ValidationError < StandardError
142
+ attr_reader :stories
143
+
144
+ def initialize(stories)
145
+ @stories = stories
146
+
147
+ super("#{@stories.class.name} invalid: (#{@stories.errors.full_messages.join(', ')})")
148
+ end
149
+ end
104
150
  end
105
151
  end
106
152
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ class Story
6
+ include ActiveModel::Validations
7
+
8
+ attr_reader :component, :content_block, :slots, :layout
9
+
10
+ def initialize(component, content_block, slots, layout)
11
+ @component = component
12
+ @content_block = content_block
13
+ @slots = slots
14
+ @layout = layout
15
+ end
16
+ end
17
+ end
18
+ end
@@ -4,19 +4,73 @@ module ViewComponent
4
4
  module Storybook
5
5
  class StoryConfig
6
6
  include ActiveModel::Validations
7
+ include ContentConcern
8
+ include Controls::ControlsHelpers
7
9
 
8
- attr_reader :id, :name, :component
9
- attr_accessor :controls, :parameters, :layout, :content_block
10
+ attr_reader :id, :name, :component_class
10
11
 
11
- def initialize(id, name, component, layout)
12
+ validate :validate_constructor_args
13
+
14
+ def initialize(id, name, component_class, layout)
12
15
  @id = id
13
16
  @name = name
14
- @component = component
17
+ @component_class = component_class
15
18
  @layout = layout
16
- @controls = []
19
+ @slots ||= {}
20
+ end
21
+
22
+ def constructor(*args, **kwargs, &block)
23
+ @constructor_args = MethodArgs::ControlMethodArgs.new(
24
+ component_constructor,
25
+ *args,
26
+ **kwargs
27
+ )
28
+ content(nil, &block)
29
+
30
+ self
31
+ end
32
+
33
+ # Once deprecated block version is removed make this a private getter
34
+ def controls(&block)
35
+ if block_given?
36
+ ActiveSupport::Deprecation.warn("`controls` will be removed in v1.0.0. Use `#constructor` instead.")
37
+ controls_dsl = Dsl::LegacyControlsDsl.new
38
+ controls_dsl.instance_eval(&block)
39
+
40
+ controls_hash = controls_dsl.controls.index_by(&:param)
41
+ constructor(**controls_hash)
42
+ else
43
+ list = constructor_args.controls.dup
44
+ list << content_control if content_control
45
+ list += slots.flat_map(&:controls) if slots
46
+ list
47
+ end
48
+ end
49
+
50
+ def layout(layout = nil)
51
+ @layout = layout unless layout.nil?
52
+ @layout
53
+ end
54
+
55
+ def parameters(parameters = nil)
56
+ @parameters = parameters unless parameters.nil?
57
+ @parameters
58
+ end
59
+
60
+ def method_missing(method_name, *args, **kwargs, &block)
61
+ if component_class.slot_type(method_name)
62
+ slot(method_name, *args, **kwargs, &block)
63
+ else
64
+ super
65
+ end
66
+ end
67
+
68
+ def respond_to_missing?(method_name, _include_private = false)
69
+ component_class.slot_type(method_name).present?
17
70
  end
18
71
 
19
72
  def to_csf_params
73
+ validate!
20
74
  csf_params = { name: name, parameters: { server: { id: id } } }
21
75
  csf_params.deep_merge!(parameters: parameters) if parameters.present?
22
76
  controls.each do |control|
@@ -25,18 +79,89 @@ module ViewComponent
25
79
  csf_params
26
80
  end
27
81
 
28
- def values_from_params(params)
29
- controls.map do |control|
30
- value = control.value_from_param(params[control.param])
31
- value = control.value if value.nil? # nil only not falsey
32
- [control.param, value]
33
- end.to_h
82
+ def validate!
83
+ valid? || raise(ValidationError, self)
84
+ end
85
+
86
+ ##
87
+ # Build a Story from this config
88
+ # * Resolves the values of the constructor args from the params
89
+ # * constructs the component
90
+ # * resolve the content_control and content_block to a single block
91
+ # * builds a list of Slots by resolving their args from the params
92
+ def story(params)
93
+ # constructor_args.target_method is UnboundMethod so can't call it directly
94
+ component = constructor_args.call(params) do |*args, **kwargs|
95
+ component_class.new(*args, **kwargs)
96
+ end
97
+
98
+ story_content_block = resolve_content_block(params)
99
+
100
+ story_slots = slots.map do |slot_config|
101
+ slot_config.slot(component, params)
102
+ end
103
+
104
+ Storybook::Story.new(component, story_content_block, story_slots, layout)
105
+ end
106
+
107
+ class ValidationError < StandardError
108
+ attr_reader :story_config
109
+
110
+ def initialize(story_config)
111
+ @story_config = story_config
112
+
113
+ super("'#{@story_config.name}' invalid: (#{@story_config.errors.full_messages.join(', ')})")
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ def constructor_args
120
+ @constructor_args ||= MethodArgs::ControlMethodArgs.new(component_constructor)
121
+ end
122
+
123
+ def component_constructor
124
+ component_class.instance_method(:initialize)
125
+ end
126
+
127
+ def slots
128
+ @slots.values.flatten
34
129
  end
35
130
 
36
- def self.configure(id, name, component, layout, &configuration)
37
- config = new(id, name, component, layout)
38
- ViewComponent::Storybook::Dsl::StoryDsl.evaluate!(config, &configuration)
39
- config
131
+ def validate_constructor_args
132
+ return if constructor_args.valid?
133
+
134
+ constructor_args_errors = constructor_args.errors.full_messages.join(', ')
135
+ errors.add(:constructor_args, :invalid, errors: constructor_args_errors)
136
+ end
137
+
138
+ def slot(slot_name, *args, **kwargs, &block)
139
+ # if the name is a slot then build a SlotConfig with slot_name and param the same
140
+ if component_class.slot_type(slot_name) == :collection_item
141
+ # generate a unique param generated by the count of slots with this name already added
142
+ @slots[slot_name] ||= []
143
+ slot_index = @slots[slot_name].count + 1
144
+ slot_config = Slots::SlotConfig.from_component(
145
+ component_class,
146
+ slot_name,
147
+ "#{slot_name}#{slot_index}".to_sym,
148
+ *args,
149
+ **kwargs,
150
+ &block
151
+ )
152
+ @slots[slot_name] << slot_config
153
+ else
154
+ slot_config = Slots::SlotConfig.from_component(
155
+ component_class,
156
+ slot_name,
157
+ slot_name,
158
+ *args,
159
+ **kwargs,
160
+ &block
161
+ )
162
+ @slots[slot_name] = slot_config
163
+ end
164
+ slot_config
40
165
  end
41
166
  end
42
167
  end
@@ -4,10 +4,16 @@ namespace :view_component_storybook do
4
4
  desc "Write CSF JSON stories for all Stories"
5
5
  task write_stories_json: :environment do
6
6
  puts "Writing Stories JSON"
7
+ exceptions = []
7
8
  ViewComponent::Storybook::Stories.all.each do |stories|
8
9
  json_path = stories.write_csf_json
9
10
  puts "#{stories.name} => #{json_path}"
11
+ rescue StandardError => e
12
+ exceptions << e
10
13
  end
14
+
15
+ raise StandardError, exceptions.map(&:message).join(", ") if exceptions.present?
16
+
11
17
  puts "Done"
12
18
  end
13
19
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ViewComponent
4
4
  module Storybook
5
- VERSION = "0.5.0"
5
+ VERSION = "0.10.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: view_component_storybook
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jon Palmer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-09-19 00:00:00.000000000 Z
11
+ date: 2021-08-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: view_component
@@ -16,28 +16,42 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '2.2'
19
+ version: '2.36'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '2.2'
26
+ version: '2.36'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '1.14'
33
+ version: '2.2'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: capybara
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3'
34
48
  type: :development
35
49
  prerelease: false
36
50
  version_requirements: !ruby/object:Gem::Requirement
37
51
  requirements:
38
52
  - - "~>"
39
53
  - !ruby/object:Gem::Version
40
- version: '1.14'
54
+ version: '3'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: rake
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -72,84 +86,98 @@ dependencies:
72
86
  requirements:
73
87
  - - "~>"
74
88
  - !ruby/object:Gem::Version
75
- version: '3.9'
89
+ version: '3.10'
76
90
  type: :development
77
91
  prerelease: false
78
92
  version_requirements: !ruby/object:Gem::Requirement
79
93
  requirements:
80
94
  - - "~>"
81
95
  - !ruby/object:Gem::Version
82
- version: '3.9'
96
+ version: '3.10'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: rspec-rails
85
99
  requirement: !ruby/object:Gem::Requirement
86
100
  requirements:
87
101
  - - "~>"
88
102
  - !ruby/object:Gem::Version
89
- version: '3.9'
103
+ version: '5.0'
90
104
  type: :development
91
105
  prerelease: false
92
106
  version_requirements: !ruby/object:Gem::Requirement
93
107
  requirements:
94
108
  - - "~>"
95
109
  - !ruby/object:Gem::Version
96
- version: '3.9'
110
+ version: '5.0'
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: rubocop
99
113
  requirement: !ruby/object:Gem::Requirement
100
114
  requirements:
101
115
  - - "~>"
102
116
  - !ruby/object:Gem::Version
103
- version: '0.81'
117
+ version: '1.18'
104
118
  type: :development
105
119
  prerelease: false
106
120
  version_requirements: !ruby/object:Gem::Requirement
107
121
  requirements:
108
122
  - - "~>"
109
123
  - !ruby/object:Gem::Version
110
- version: '0.81'
124
+ version: '1.18'
111
125
  - !ruby/object:Gem::Dependency
112
126
  name: rubocop-rails
113
127
  requirement: !ruby/object:Gem::Requirement
114
128
  requirements:
115
129
  - - "~>"
116
130
  - !ruby/object:Gem::Version
117
- version: 2.4.2
131
+ version: '2.11'
118
132
  type: :development
119
133
  prerelease: false
120
134
  version_requirements: !ruby/object:Gem::Requirement
121
135
  requirements:
122
136
  - - "~>"
123
137
  - !ruby/object:Gem::Version
124
- version: 2.4.2
138
+ version: '2.11'
125
139
  - !ruby/object:Gem::Dependency
126
140
  name: rubocop-rspec
127
141
  requirement: !ruby/object:Gem::Requirement
128
142
  requirements:
129
143
  - - "~>"
130
144
  - !ruby/object:Gem::Version
131
- version: '1.38'
145
+ version: '2.1'
132
146
  type: :development
133
147
  prerelease: false
134
148
  version_requirements: !ruby/object:Gem::Requirement
135
149
  requirements:
136
150
  - - "~>"
137
151
  - !ruby/object:Gem::Version
138
- version: '1.38'
152
+ version: '2.1'
139
153
  - !ruby/object:Gem::Dependency
140
154
  name: simplecov
141
155
  requirement: !ruby/object:Gem::Requirement
142
156
  requirements:
143
157
  - - "~>"
144
158
  - !ruby/object:Gem::Version
145
- version: 0.18.5
159
+ version: 0.21.2
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: 0.21.2
167
+ - !ruby/object:Gem::Dependency
168
+ name: simplecov-console
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '0.9'
146
174
  type: :development
147
175
  prerelease: false
148
176
  version_requirements: !ruby/object:Gem::Requirement
149
177
  requirements:
150
178
  - - "~>"
151
179
  - !ruby/object:Gem::Version
152
- version: 0.18.5
180
+ version: '0.9'
153
181
  description: Generate Storybook CSF JSON for rendering Rails View Components in Storybook
154
182
  email:
155
183
  - 328224+jonspalmer@users.noreply.github.com
@@ -161,22 +189,35 @@ files:
161
189
  - README.md
162
190
  - app/controllers/view_component/storybook/stories_controller.rb
163
191
  - app/views/view_component/storybook/stories/show.html.erb
192
+ - config/locales/en.yml
164
193
  - lib/view_component/storybook.rb
194
+ - lib/view_component/storybook/content_concern.rb
165
195
  - lib/view_component/storybook/controls.rb
166
- - lib/view_component/storybook/controls/array_config.rb
196
+ - lib/view_component/storybook/controls/base_options_config.rb
167
197
  - lib/view_component/storybook/controls/boolean_config.rb
168
198
  - lib/view_component/storybook/controls/color_config.rb
169
199
  - lib/view_component/storybook/controls/control_config.rb
200
+ - lib/view_component/storybook/controls/controls_helpers.rb
201
+ - lib/view_component/storybook/controls/custom_config.rb
170
202
  - lib/view_component/storybook/controls/date_config.rb
203
+ - lib/view_component/storybook/controls/multi_options_config.rb
171
204
  - lib/view_component/storybook/controls/number_config.rb
172
205
  - lib/view_component/storybook/controls/object_config.rb
173
206
  - lib/view_component/storybook/controls/options_config.rb
207
+ - lib/view_component/storybook/controls/simple_control_config.rb
174
208
  - lib/view_component/storybook/controls/text_config.rb
175
209
  - lib/view_component/storybook/dsl.rb
176
- - lib/view_component/storybook/dsl/controls_dsl.rb
177
- - lib/view_component/storybook/dsl/story_dsl.rb
210
+ - lib/view_component/storybook/dsl/legacy_controls_dsl.rb
178
211
  - lib/view_component/storybook/engine.rb
212
+ - lib/view_component/storybook/method_args.rb
213
+ - lib/view_component/storybook/method_args/control_method_args.rb
214
+ - lib/view_component/storybook/method_args/method_args.rb
215
+ - lib/view_component/storybook/method_args/method_parameters_names.rb
216
+ - lib/view_component/storybook/slots.rb
217
+ - lib/view_component/storybook/slots/slot.rb
218
+ - lib/view_component/storybook/slots/slot_config.rb
179
219
  - lib/view_component/storybook/stories.rb
220
+ - lib/view_component/storybook/story.rb
180
221
  - lib/view_component/storybook/story_config.rb
181
222
  - lib/view_component/storybook/tasks/write_stories_json.rake
182
223
  - lib/view_component/storybook/version.rb
@@ -195,14 +236,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
195
236
  requirements:
196
237
  - - ">="
197
238
  - !ruby/object:Gem::Version
198
- version: 2.3.0
239
+ version: 2.5.0
199
240
  required_rubygems_version: !ruby/object:Gem::Requirement
200
241
  requirements:
201
242
  - - ">="
202
243
  - !ruby/object:Gem::Version
203
244
  version: '0'
204
245
  requirements: []
205
- rubygems_version: 3.0.6
246
+ rubygems_version: 3.1.6
206
247
  signing_key:
207
248
  specification_version: 4
208
249
  summary: Storybook for Rails View Components