storybook_rails 1.0.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/LICENSE.txt +21 -0
- data/README.md +223 -0
- data/app/controllers/action_view/storybook/stories_controller.rb +44 -0
- data/app/views/action_view/storybook/stories/show.html.erb +1 -0
- data/lib/action_view/storybook.rb +33 -0
- data/lib/action_view/storybook/controls.rb +21 -0
- data/lib/action_view/storybook/controls/array_config.rb +36 -0
- data/lib/action_view/storybook/controls/boolean_config.rb +30 -0
- data/lib/action_view/storybook/controls/color_config.rb +27 -0
- data/lib/action_view/storybook/controls/control_config.rb +43 -0
- data/lib/action_view/storybook/controls/date_config.rb +34 -0
- data/lib/action_view/storybook/controls/number_config.rb +39 -0
- data/lib/action_view/storybook/controls/object_config.rb +21 -0
- data/lib/action_view/storybook/controls/options_config.rb +50 -0
- data/lib/action_view/storybook/controls/text_config.rb +13 -0
- data/lib/action_view/storybook/dsl.rb +14 -0
- data/lib/action_view/storybook/dsl/controls_dsl.rb +101 -0
- data/lib/action_view/storybook/dsl/story_dsl.rb +39 -0
- data/lib/action_view/storybook/engine.rb +46 -0
- data/lib/action_view/storybook/helpers.rb +13 -0
- data/lib/action_view/storybook/helpers/story_params.rb +5 -0
- data/lib/action_view/storybook/stories.rb +135 -0
- data/lib/action_view/storybook/story_config.rb +74 -0
- data/lib/action_view/storybook/tasks/write_stories_json.rake +13 -0
- data/lib/action_view/storybook/version.rb +7 -0
- metadata +225 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionView
|
4
|
+
module Storybook
|
5
|
+
module Controls
|
6
|
+
class ControlConfig
|
7
|
+
include ActiveModel::Validations
|
8
|
+
|
9
|
+
attr_reader :param, :value, :name
|
10
|
+
|
11
|
+
validates :param, presence: true
|
12
|
+
|
13
|
+
def initialize(param, value, name: nil)
|
14
|
+
@param = param
|
15
|
+
@value = value
|
16
|
+
@name = name || param.to_s.humanize.titlecase
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_csf_params
|
20
|
+
{
|
21
|
+
args: { param => csf_value },
|
22
|
+
argTypes: { param => { control: csf_control_params, name: name } }
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def value_from_param(param)
|
27
|
+
param
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# provide extension points for subclasses to vary the value
|
33
|
+
def csf_value
|
34
|
+
value
|
35
|
+
end
|
36
|
+
|
37
|
+
def csf_control_params
|
38
|
+
{ type: type }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionView
|
4
|
+
module Storybook
|
5
|
+
module Controls
|
6
|
+
class DateConfig < ControlConfig
|
7
|
+
def initialize(param, value, name: nil)
|
8
|
+
super(param, value, name: name)
|
9
|
+
end
|
10
|
+
|
11
|
+
def type
|
12
|
+
:date
|
13
|
+
end
|
14
|
+
|
15
|
+
def value_from_param(param)
|
16
|
+
if param.is_a?(String)
|
17
|
+
DateTime.iso8601(param)
|
18
|
+
else
|
19
|
+
super(param)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def csf_value
|
26
|
+
params_value = value
|
27
|
+
params_value = params_value.in_time_zone if params_value.is_a?(Date)
|
28
|
+
params_value = params_value.iso8601 if params_value.is_a?(Time)
|
29
|
+
params_value
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionView
|
4
|
+
module Storybook
|
5
|
+
module Controls
|
6
|
+
class NumberConfig < ControlConfig
|
7
|
+
TYPES = %i[number range].freeze
|
8
|
+
|
9
|
+
attr_reader :type, :min, :max, :step
|
10
|
+
|
11
|
+
validates :type, presence: true
|
12
|
+
validates :type, inclusion: { in: TYPES }, unless: -> { type.nil? }
|
13
|
+
|
14
|
+
def initialize(type, param, value, min: nil, max: nil, step: nil, name: nil)
|
15
|
+
super(param, value, name: name)
|
16
|
+
@type = type
|
17
|
+
@min = min
|
18
|
+
@max = max
|
19
|
+
@step = step
|
20
|
+
end
|
21
|
+
|
22
|
+
def value_from_param(param)
|
23
|
+
if param.is_a?(String) && param.present?
|
24
|
+
(param.to_f % 1) > 0 ? param.to_f : param.to_i
|
25
|
+
else
|
26
|
+
super(param)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def csf_control_params
|
33
|
+
params = super
|
34
|
+
params.merge(min: min, max: max, step: step).compact
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionView
|
4
|
+
module Storybook
|
5
|
+
module Controls
|
6
|
+
class ObjectConfig < ControlConfig
|
7
|
+
def type
|
8
|
+
:object
|
9
|
+
end
|
10
|
+
|
11
|
+
def value_from_param(param)
|
12
|
+
if param.is_a?(String)
|
13
|
+
JSON.parse(param).deep_symbolize_keys
|
14
|
+
else
|
15
|
+
super(param)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionView
|
4
|
+
module Storybook
|
5
|
+
module Controls
|
6
|
+
class OptionsConfig < ControlConfig
|
7
|
+
class << self
|
8
|
+
# support the options being a Hash or an Array. Storybook supports either.
|
9
|
+
def inclusion_in(config)
|
10
|
+
case config.options
|
11
|
+
when Hash
|
12
|
+
config.options.values
|
13
|
+
when Array
|
14
|
+
config.options
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
TYPES = %i[select multi-select radio inline-radio check inline-check].freeze
|
20
|
+
|
21
|
+
attr_reader :type, :options, :symbol_value
|
22
|
+
|
23
|
+
validates :type, :options, presence: true
|
24
|
+
validates :type, inclusion: { in: TYPES }, unless: -> { type.nil? }
|
25
|
+
validates :value, inclusion: { in: method(:inclusion_in) }, unless: -> { options.nil? || value.nil? }
|
26
|
+
|
27
|
+
def initialize(type, param, options, default_value, name: nil)
|
28
|
+
super(param, default_value, name: name)
|
29
|
+
@type = type
|
30
|
+
@options = options
|
31
|
+
@symbol_value = default_value.is_a?(Symbol)
|
32
|
+
end
|
33
|
+
|
34
|
+
def value_from_param(param)
|
35
|
+
if param.is_a?(String) && symbol_value
|
36
|
+
param.to_sym
|
37
|
+
else
|
38
|
+
super(param)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def csf_control_params
|
45
|
+
super.merge(options: options)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionView
|
4
|
+
module Storybook
|
5
|
+
module Dsl
|
6
|
+
class ControlsDsl
|
7
|
+
attr_reader :controls
|
8
|
+
|
9
|
+
def initialize(story_config)
|
10
|
+
@story_config = story_config
|
11
|
+
@controls = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def text(param, value, name: nil)
|
15
|
+
controls << Controls::TextConfig.new(param, value, name: name)
|
16
|
+
end
|
17
|
+
|
18
|
+
def boolean(param, value, name: nil)
|
19
|
+
controls << Controls::BooleanConfig.new(param, value, name: name)
|
20
|
+
end
|
21
|
+
|
22
|
+
def number(param, value, name: nil, min: nil, max: nil, step: nil)
|
23
|
+
controls << Controls::NumberConfig.new(:number, param, value, name: name, min: min, max: max, step: step)
|
24
|
+
end
|
25
|
+
|
26
|
+
def range(param, value, name: nil, min: nil, max: nil, step: nil)
|
27
|
+
controls << Controls::NumberConfig.new(:range, param, value, name: name, min: min, max: max, step: step)
|
28
|
+
end
|
29
|
+
|
30
|
+
def color(param, value, name: nil, preset_colors: nil)
|
31
|
+
controls << Controls::ColorConfig.new(param, value, name: name, preset_colors: preset_colors)
|
32
|
+
end
|
33
|
+
|
34
|
+
def object(param, value, name: nil)
|
35
|
+
controls << Controls::ObjectConfig.new(param, value, name: name)
|
36
|
+
end
|
37
|
+
|
38
|
+
def select(param, options, value, name: nil)
|
39
|
+
controls << Controls::OptionsConfig.new(:select, param, options, value, name: name)
|
40
|
+
end
|
41
|
+
|
42
|
+
def multi_select(param, options, value, name: nil)
|
43
|
+
controls << Controls::OptionsConfig.new(:'multi-select', param, options, value, name: name)
|
44
|
+
end
|
45
|
+
|
46
|
+
def radio(param, options, value, name: nil)
|
47
|
+
controls << Controls::OptionsConfig.new(:radio, param, options, value, name: name)
|
48
|
+
end
|
49
|
+
|
50
|
+
def inline_radio(param, options, value, name: nil)
|
51
|
+
controls << Controls::OptionsConfig.new(:'inline-radio', param, options, value, name: name)
|
52
|
+
end
|
53
|
+
|
54
|
+
def check(param, options, value, name: nil)
|
55
|
+
controls << Controls::OptionsConfig.new(:check, param, options, value, name: name)
|
56
|
+
end
|
57
|
+
|
58
|
+
def inline_check(param, options, value, name: nil)
|
59
|
+
controls << Controls::OptionsConfig.new(:'inline-check', param, options, value, name: name)
|
60
|
+
end
|
61
|
+
|
62
|
+
def array(param, value, separator = ",", name: nil)
|
63
|
+
controls << Controls::ArrayConfig.new(param, value, separator, name: name)
|
64
|
+
end
|
65
|
+
|
66
|
+
def date(param, value, name: nil)
|
67
|
+
controls << Controls::DateConfig.new(param, value, name: name)
|
68
|
+
end
|
69
|
+
|
70
|
+
def respond_to_missing?(_method, *)
|
71
|
+
true
|
72
|
+
end
|
73
|
+
|
74
|
+
def method_missing(method, *args)
|
75
|
+
value = args.first
|
76
|
+
control_method = case value
|
77
|
+
when Date
|
78
|
+
:date
|
79
|
+
when Array
|
80
|
+
:array
|
81
|
+
when Hash
|
82
|
+
:object
|
83
|
+
when Numeric
|
84
|
+
:number
|
85
|
+
when TrueClass, FalseClass
|
86
|
+
:boolean
|
87
|
+
when String
|
88
|
+
:text
|
89
|
+
end
|
90
|
+
if control_method
|
91
|
+
send(control_method, method, *args)
|
92
|
+
else
|
93
|
+
super
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
Controls = ActionView::Storybook::Controls
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionView
|
4
|
+
module Storybook
|
5
|
+
module Dsl
|
6
|
+
class StoryDsl
|
7
|
+
def self.evaluate!(story_config, &block)
|
8
|
+
new(story_config).instance_eval(&block)
|
9
|
+
end
|
10
|
+
|
11
|
+
def parameters(**params)
|
12
|
+
@story_config.parameters = params
|
13
|
+
end
|
14
|
+
|
15
|
+
def controls(&block)
|
16
|
+
controls_dsl = ControlsDsl.new(story_config)
|
17
|
+
controls_dsl.instance_eval(&block)
|
18
|
+
@story_config.controls = controls_dsl.controls
|
19
|
+
end
|
20
|
+
|
21
|
+
def layout(layout)
|
22
|
+
@story_config.layout = layout
|
23
|
+
end
|
24
|
+
|
25
|
+
def content(&block)
|
26
|
+
@story_config.content_block = block
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_reader :story_config
|
32
|
+
|
33
|
+
def initialize(story_config)
|
34
|
+
@story_config = story_config
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails"
|
4
|
+
require "action_view/storybook"
|
5
|
+
|
6
|
+
module ActionView
|
7
|
+
module Storybook
|
8
|
+
class Engine < Rails::Engine
|
9
|
+
config.storybook_rails = ActiveSupport::OrderedOptions.new
|
10
|
+
|
11
|
+
initializer "storybook_rails.set_configs" do |app|
|
12
|
+
options = app.config.storybook_rails
|
13
|
+
|
14
|
+
options.show_stories = Rails.env.development? if options.show_stories.nil?
|
15
|
+
|
16
|
+
if options.show_stories
|
17
|
+
options.stories_path ||= defined?(Rails.root) ? Rails.root.join("test/components/stories") : nil
|
18
|
+
end
|
19
|
+
|
20
|
+
ActiveSupport.on_load(:storybook_rails) do
|
21
|
+
options.each { |k, v| send("#{k}=", v) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
initializer "storybook_rails.set_autoload_paths" do |app|
|
26
|
+
options = app.config.storybook_rails
|
27
|
+
|
28
|
+
ActiveSupport::Dependencies.autoload_paths << options.stories_path if options.show_stories && options.stories_path
|
29
|
+
end
|
30
|
+
|
31
|
+
config.after_initialize do |app|
|
32
|
+
options = app.config.storybook_rails
|
33
|
+
|
34
|
+
if options.show_stories
|
35
|
+
app.routes.prepend do
|
36
|
+
get "storybook/*stories/:story" => "action_view/storybook/stories#show"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
rake_tasks do
|
42
|
+
load File.join(__dir__, "tasks/write_stories_json.rake")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionView
|
4
|
+
module Storybook
|
5
|
+
class Stories
|
6
|
+
extend ActiveSupport::DescendantsTracker
|
7
|
+
include ActiveModel::Validations
|
8
|
+
|
9
|
+
class_attribute :story_configs, default: []
|
10
|
+
class_attribute :parameters, :title, :stories_layout
|
11
|
+
|
12
|
+
validate :valid_story_configs
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def story(name, template = default_template, &block)
|
16
|
+
story_config = StoryConfig.configure(story_id(name), name, layout, template, &block)
|
17
|
+
story_configs << story_config
|
18
|
+
story_config
|
19
|
+
end
|
20
|
+
|
21
|
+
def parameters(**params)
|
22
|
+
self.parameters = params
|
23
|
+
end
|
24
|
+
|
25
|
+
def layout(layout = nil)
|
26
|
+
# if no argument is passed act like a getter
|
27
|
+
self.stories_layout = layout unless layout.nil?
|
28
|
+
stories_layout
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_csf_params
|
32
|
+
validate!
|
33
|
+
csf_params = { title: title }
|
34
|
+
csf_params[:parameters] = parameters if parameters.present?
|
35
|
+
csf_params[:stories] = story_configs.map(&:to_csf_params)
|
36
|
+
csf_params
|
37
|
+
end
|
38
|
+
|
39
|
+
def write_csf_json
|
40
|
+
json_path = File.join(stories_path, "#{stories_name}.stories.json")
|
41
|
+
File.open(json_path, "w") do |f|
|
42
|
+
f.write(JSON.pretty_generate(to_csf_params))
|
43
|
+
end
|
44
|
+
json_path
|
45
|
+
end
|
46
|
+
|
47
|
+
def stories_name
|
48
|
+
name.chomp("Stories").underscore
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns all component stories classes.
|
52
|
+
def all
|
53
|
+
load_stories if descendants.empty?
|
54
|
+
descendants
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns +true+ if the stories exist.
|
58
|
+
def stories_exists?(stories_name)
|
59
|
+
all.any? { |stories| stories.stories_name == stories_name }
|
60
|
+
end
|
61
|
+
|
62
|
+
# Find a component stories by its underscored class name.
|
63
|
+
def find_stories(stories_name)
|
64
|
+
all.find { |stories| stories.stories_name == stories_name }
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns +true+ if the story of the component stories exists.
|
68
|
+
def story_exists?(name)
|
69
|
+
story_configs.map(&:name).include?(name.to_sym)
|
70
|
+
end
|
71
|
+
|
72
|
+
# find the story by name
|
73
|
+
def find_story(name)
|
74
|
+
story_configs.find { |config| config.name == name.to_sym }
|
75
|
+
end
|
76
|
+
|
77
|
+
# validation - ActiveModel::Validations like but on the class vs the instance
|
78
|
+
def valid?
|
79
|
+
# use an instance so we can enjoy the benefits of ActiveModel::Validations
|
80
|
+
@validation_instance = new
|
81
|
+
@validation_instance.valid?
|
82
|
+
end
|
83
|
+
|
84
|
+
delegate :errors, to: :@validation_instance
|
85
|
+
|
86
|
+
def validate!
|
87
|
+
valid? || raise(ActiveModel::ValidationError, @validation_instance)
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def inherited(other)
|
93
|
+
super(other)
|
94
|
+
# setup class defaults
|
95
|
+
other.title = other.stories_name.humanize.titlecase
|
96
|
+
other.story_configs = []
|
97
|
+
end
|
98
|
+
|
99
|
+
def default_template
|
100
|
+
"#{name.chomp("Stories").underscore}_stories"
|
101
|
+
end
|
102
|
+
|
103
|
+
def load_stories
|
104
|
+
if stories_path
|
105
|
+
Dir["#{stories_path}/**/*_stories.rb"].sort.each do |file|
|
106
|
+
require_dependency file
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def stories_path
|
112
|
+
Storybook.stories_path
|
113
|
+
end
|
114
|
+
|
115
|
+
def story_id(name)
|
116
|
+
"#{stories_name}/#{name.to_s.parameterize}".underscore
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
protected
|
121
|
+
|
122
|
+
def valid_story_configs
|
123
|
+
story_configs.reject(&:valid?).each do |story_config|
|
124
|
+
errors.add(:story_configs, :invalid, value: story_config)
|
125
|
+
end
|
126
|
+
|
127
|
+
story_names = story_configs.map(&:name)
|
128
|
+
duplicate_names = story_names.group_by(&:itself).map { |k, v| k if v.length > 1 }.compact
|
129
|
+
return if duplicate_names.empty?
|
130
|
+
|
131
|
+
errors.add(:story_configs, :invalid, message: "Stories #{'names'.pluralize(duplicate_names.count)} #{duplicate_names.to_sentence} are repeated")
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|