storybook_rails 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|