view_component_storybook 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1021436306238303a42b8496a96765bf9c86728c8c9663560812f6c30c54617e
4
+ data.tar.gz: 16ba6b7654c9b0bc8aab1c7cd36cdb5bb81a8d9913446cdd0d8c603856d57a95
5
+ SHA512:
6
+ metadata.gz: ccf6ca47f3d3f381956edbdf97d5b962b53b61b843851271a1b038e116cbd6423c44b2e73bbf7959441041b7e7ec914447474c17480cbff8122917237ae7da62
7
+ data.tar.gz: 4bf051cdaf384dbadb873093920736293b1399e9767ef68d32d4ed6fa07d831b22d5dff397a6dd9e29ff89a99298c323ef5555787b55e102d072b156ab387aaf
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Jon Palmer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # ViewComponent::Storybook
2
+
3
+ The ViewComponent::Storybook gem provides Ruby api for writing stories describing [View Components](https://github.com/github/view_component) and allowing them to be previewed and tested in [Storybook](https://github.com/storybookjs/storybook/)
4
+
5
+ ## Features
6
+ * A Ruby DSL for writing Stories describing View Components
7
+ * A Rails controller backend for Storybook Server compatible with most Strobook Knobs Addon parameters
8
+ * Coming Soon: Rake tasks to watch View Components and Stories and trigger Storybook hot reloading
9
+
10
+ ## Installation
11
+
12
+ ### Gem Installation
13
+
14
+ 1. Add the `view_component_storybook` gem, to your Gemfile: `gem 'view_component_storybook'`
15
+ 2. Run `bundle install`.
16
+ 3. Add `require "view_component_storybook/engine"` to `config/application.rb`
17
+ 4. Add `**/*.stories.json` to `.gitignore`
18
+
19
+ ### Storybook Installation
20
+
21
+ 1. Add Storybook server as a dev dependedncy. The Storybook Knobs addon isn't needed but is strongly recommended
22
+ ```sh
23
+ yarn add @storybook/server @storybook/addon-knobs --dev
24
+ ```
25
+ 2. Add an NPM script to your package.json in order to start the storybook later in this guide
26
+ ```json
27
+ {
28
+ "scripts": {
29
+ "storybook": "start-storybook"
30
+ }
31
+ }
32
+ ```
33
+ 3. Create the .storybook/main.js file to configure Storybook to find the json stories the gem creates. Also configure the knobs addon:
34
+ ```javascript
35
+ module.exports = {
36
+ stories: ['../test/components/**/*.stories.json'],
37
+ addons: [
38
+ '@storybook/addon-knobs',
39
+ ],
40
+ };
41
+ ```
42
+ 4. Create the .storybook/preview.js file to configure Storybook with the Rails application url to call for the html content of the stories
43
+ ```javascript
44
+ import { addParameters } from '@storybook/server';
45
+
46
+ addParameters({
47
+ server: {
48
+ url: `http://localhost:3000/rails/stories`,
49
+ },
50
+ });
51
+ ```
52
+
53
+
54
+ Note: `@storybook/server` will be part of the upcoming Storybook 6.0 release. Until that is released you'll need to use an [alpha release](https://github.com/storybookjs/storybook/releases/tag/v6.0.0-alpha.32)
55
+
56
+ ## Usage
57
+
58
+ ### Writing Stories
59
+
60
+ `ViewComponent::Storybook::Stories` provides a way to preview components in Storybook.
61
+
62
+ Suppose our app has a `ButtonComponent` that takes a `button_text` parameter:
63
+
64
+ ```ruby
65
+ class ButtonComponent < ViewComponent::Base
66
+ def initialize(button_text:)
67
+ @button_text = button_text
68
+ end
69
+ end
70
+ ```
71
+
72
+ We can write a stories desecibing the `ButtonComponent`
73
+
74
+ ```ruby
75
+ class ButtonComponentStories < ViewComponent::Storybook::Stories
76
+ story(:with_short_text) do
77
+ knobs do
78
+ text(:button_text, 'OK')
79
+ end
80
+ end
81
+
82
+ story(:with_long_text) do
83
+ knobs do
84
+ text(:button_text, 'Push Me Please!')
85
+ end
86
+ end
87
+ end
88
+ ```
89
+
90
+ ### Generating Storybook Stories JSON
91
+
92
+ Generate the Storybook JSON stories by tunning the rake task:
93
+ ```sh
94
+ rake view_component_storybook:write_stories_json
95
+ ```
96
+
97
+ ### Start the Rails app and Storybook
98
+
99
+ In separate shells start the Rails app and Storybook
100
+
101
+ ```sh
102
+ rails s
103
+ ```
104
+ ```sh
105
+ yarn storybook
106
+ ```
107
+
108
+ Alternatively you can use tools like [Foreman](https://github.com/ddollar/foreman) to start both Rails and Storybook with one command.
109
+
110
+ ### Configuration
111
+
112
+ By Default ViewComponent::Storybook expects to find stories in the folder `test/components/stories`. This can be configured but setting `stories_path` in `config/applicaion.rb`. For example is you're using RSpec you might set the following configuration:
113
+
114
+ ```ruby
115
+ config.view_component_storybook.stories_path = Rails.root.join("spec/components/stories")
116
+ ```
117
+
118
+ ### The Story DSL
119
+
120
+ Coming Soon
121
+
122
+ #### Parameters
123
+ #### Layout
124
+ #### Knobs
125
+
126
+
127
+ ## Development
128
+
129
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
130
+
131
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
132
+
133
+ ## Contributing
134
+
135
+ Bug reports and pull requests are welcome on GitHub at https://github.com/jonspalmer/actionview-component-storybook. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
136
+
137
+ ## License
138
+
139
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
140
+
141
+ ## Code of Conduct
142
+
143
+ Everyone interacting in the ViewComponent::Storybook project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/jonspalmer/view_component_storybook/blob/master/CODE_OF_CONDUCT.md).
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/application_controller"
4
+
5
+ module ViewComponent
6
+ module Storybook
7
+ class StoriesController < Rails::ApplicationController
8
+ prepend_view_path File.expand_path("../../../views", __dir__)
9
+ prepend_view_path Rails.root.join("app/views") if defined?(Rails.root)
10
+
11
+ before_action :find_stories, :find_story, only: :show
12
+ before_action :require_local!, unless: :show_stories?
13
+
14
+ content_security_policy(false) if respond_to?(:content_security_policy)
15
+
16
+ def show
17
+ component_args = @story.values_from_params(params.permit!.to_h)
18
+
19
+ @content_block = @story.content_block
20
+
21
+ @component = @story.component.new(**component_args)
22
+
23
+ layout = @story.layout
24
+ render layout: layout unless layout.nil?
25
+ end
26
+
27
+ private
28
+
29
+ def show_stories?
30
+ ViewComponent::Storybook.show_stories
31
+ end
32
+
33
+ def find_stories
34
+ stories_name = params[:stories]
35
+ @stories = ViewComponent::Storybook::Stories.find_stories(stories_name)
36
+
37
+ head :not_found unless @stories
38
+ end
39
+
40
+ def find_story
41
+ story_name = params[:story]
42
+ @story = @stories.find_story(story_name)
43
+ head :not_found unless @story
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1 @@
1
+ <%= render(@component, &@content_block)%>
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+ require "active_support/dependencies/autoload"
5
+ require "view_component/storybook/engine"
6
+
7
+ module ViewComponent
8
+ module Storybook
9
+ extend ActiveSupport::Autoload
10
+
11
+ autoload :Knobs
12
+ autoload :Stories
13
+ autoload :StoryConfig
14
+ autoload :Dsl
15
+
16
+ include ActiveSupport::Configurable
17
+ # Set the location of component previews through app configuration:
18
+ #
19
+ # config.view_component_storybook.stories_path = Rails.root.join("lib/component_stories")
20
+ #
21
+ mattr_accessor :stories_path, instance_writer: false
22
+
23
+ # Enable or disable component previews through app configuration:
24
+ #
25
+ # config.view_component_storybook.show_stories = true
26
+ #
27
+ # Defaults to +true+ for development environment
28
+ #
29
+ mattr_accessor :show_stories, instance_writer: false
30
+
31
+ ActiveSupport.run_load_hooks(:view_component_storybook, self)
32
+ end
33
+ 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 Dsl
8
+ extend ActiveSupport::Autoload
9
+
10
+ autoload :StoryDsl
11
+ autoload :KnobsDsl
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module Dsl
6
+ class KnobsDsl
7
+ attr_reader :component, :knobs
8
+
9
+ def initialize(component)
10
+ @component = component
11
+ @knobs = []
12
+ end
13
+
14
+ def text(param, value, group_id: nil, name: nil)
15
+ knobs << Knobs::SimpleConfig.new(:text, component, param, value, group_id: group_id, name: name)
16
+ end
17
+
18
+ def boolean(param, value, group_id: nil, name: nil)
19
+ knobs << Knobs::SimpleConfig.new(:boolean, component, param, value, group_id: group_id, name: name)
20
+ end
21
+
22
+ def number(param, value, options = {}, group_id: nil, name: nil)
23
+ knobs << Knobs::NumberConfig.new(component, param, value, options, group_id: group_id, name: name)
24
+ end
25
+
26
+ def color(param, value, group_id: nil, name: nil)
27
+ knobs << Knobs::SimpleConfig.new(:color, component, param, value, group_id: group_id, name: name)
28
+ end
29
+
30
+ def object(param, value, group_id: nil, name: nil)
31
+ knobs << Knobs::ObjectConfig.new(component, param, value, group_id: group_id, name: name)
32
+ end
33
+
34
+ def select(param, options, value, group_id: nil, name: nil)
35
+ knobs << Knobs::OptionsConfig.new(:select, component, param, options, value, group_id: group_id, name: name)
36
+ end
37
+
38
+ def radios(param, options, value, group_id: nil, name: nil)
39
+ knobs << Knobs::OptionsConfig.new(:radios, component, param, options, value, group_id: group_id, name: name)
40
+ end
41
+
42
+ def array(param, value, separator = ",", group_id: nil, name: nil)
43
+ knobs << Knobs::ArrayConfig.new(component, param, value, separator, group_id: group_id, name: name)
44
+ end
45
+
46
+ def date(param, value, group_id: nil, name: nil)
47
+ knobs << Knobs::DateConfig.new(component, param, value, group_id: group_id, name: name)
48
+ end
49
+
50
+ def respond_to_missing?(_method)
51
+ true
52
+ end
53
+
54
+ def method_missing(method, *args)
55
+ value = args.first
56
+ knob_method = case value
57
+ when Date
58
+ :date
59
+ when Array
60
+ :array
61
+ when Hash
62
+ :object
63
+ when Numeric
64
+ :number
65
+ when TrueClass, FalseClass
66
+ :boolean
67
+ when String
68
+ :text
69
+ end
70
+ if knob_method
71
+ send(knob_method, method, *args)
72
+ else
73
+ super
74
+ end
75
+ end
76
+
77
+ Knobs = ViewComponent::Storybook::Knobs
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
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 knobs(&block)
16
+ knobs_dsl = KnobsDsl.new(story_config.component)
17
+ knobs_dsl.instance_eval(&block)
18
+ @story_config.knobs = knobs_dsl.knobs
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 "view_component/storybook"
5
+
6
+ module ViewComponent
7
+ module Storybook
8
+ class Engine < Rails::Engine
9
+ config.view_component_storybook = ActiveSupport::OrderedOptions.new
10
+
11
+ initializer "view_component_storybook.set_configs" do |app|
12
+ options = app.config.view_component_storybook
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(:view_component_storybook) do
21
+ options.each { |k, v| send("#{k}=", v) }
22
+ end
23
+ end
24
+
25
+ initializer "view_component.set_autoload_paths" do |app|
26
+ options = app.config.view_component_storybook
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.view_component_storybook
33
+
34
+ if options.show_stories
35
+ app.routes.prepend do
36
+ get "/rails/stories/*stories/:story" => "view_component/storybook/stories#show", :internal => true
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,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/dependencies/autoload"
4
+
5
+ module ViewComponent
6
+ module Storybook
7
+ module Knobs
8
+ extend ActiveSupport::Autoload
9
+
10
+ autoload :KnobConfig
11
+ autoload :SimpleConfig
12
+ autoload :NumberConfig
13
+ autoload :OptionsConfig
14
+ autoload :ArrayConfig
15
+ autoload :DateConfig
16
+ autoload :ObjectConfig
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module Knobs
6
+ class ArrayConfig < KnobConfig
7
+ attr_reader :separator
8
+
9
+ validates :value, :separator, presence: true
10
+
11
+ def initialize(component, param, value, separator = ",", name: nil, group_id: nil)
12
+ super(component, param, value, name: name, group_id: group_id)
13
+ @separator = separator
14
+ end
15
+
16
+ def to_csf_params
17
+ super.merge(value: value, separator: separator)
18
+ end
19
+
20
+ def type
21
+ :array
22
+ end
23
+
24
+ def value_from_param(param)
25
+ if param.is_a?(String)
26
+ param.split(separator)
27
+ else
28
+ super(param)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module Knobs
6
+ class DateConfig < KnobConfig
7
+ validates :value, presence: true
8
+
9
+ def initialize(component, param, value, name: nil, group_id: nil)
10
+ super(component, param, value, name: name, group_id: group_id)
11
+ end
12
+
13
+ def to_csf_params
14
+ csf_params = super
15
+ params_value = value
16
+ params_value = params_value.in_time_zone if params_value.is_a?(Date)
17
+ params_value = params_value.iso8601 if params_value.is_a?(Time)
18
+ csf_params[:value] = params_value
19
+ csf_params
20
+ end
21
+
22
+ def type
23
+ :date
24
+ end
25
+
26
+ def value_from_param(param)
27
+ if param.is_a?(String)
28
+ DateTime.iso8601(param)
29
+ else
30
+ super(param)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module Knobs
6
+ class KnobConfig
7
+ include ActiveModel::Validations
8
+
9
+ attr_reader :component, :param, :value, :name, :group_id
10
+
11
+ validates :component, :param, presence: true
12
+ validates :param, inclusion: { in: ->(knob_config) { knob_config.component_params } }, unless: -> { component.nil? }
13
+
14
+ def initialize(component, param, value, name: nil, group_id: nil)
15
+ @component = component
16
+ @param = param
17
+ @value = value
18
+ @name = name || param.to_s.humanize.titlecase
19
+ @group_id = group_id
20
+ end
21
+
22
+ def to_csf_params
23
+ validate!
24
+ params = { type: type, param: param, name: name, value: value }
25
+ params[:group_id] = group_id if group_id
26
+ params
27
+ end
28
+
29
+ def value_from_param(param)
30
+ param
31
+ end
32
+
33
+ def component_params
34
+ @component_params ||= component && component.instance_method(:initialize).parameters.map(&:last)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module Knobs
6
+ class NumberConfig < KnobConfig
7
+ attr_reader :options
8
+
9
+ validates :value, presence: true
10
+
11
+ def initialize(component, param, value, options = {}, name: nil, group_id: nil)
12
+ super(component, param, value, name: name, group_id: group_id)
13
+ @options = options
14
+ end
15
+
16
+ def type
17
+ :number
18
+ end
19
+
20
+ def to_csf_params
21
+ params = super
22
+ params[:options] = options unless options.empty?
23
+ params
24
+ end
25
+
26
+ def value_from_param(param)
27
+ if param.is_a?(String) && param.present?
28
+ (param.to_f % 1) > 0 ? param.to_f : param.to_i
29
+ else
30
+ super(param)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module Knobs
6
+ class ObjectConfig < KnobConfig
7
+ validates :value, presence: true
8
+
9
+ def type
10
+ :object
11
+ end
12
+
13
+ def value_from_param(param)
14
+ if param.is_a?(String)
15
+ JSON.parse(param).symbolize_keys
16
+ else
17
+ super(param)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module Knobs
6
+ class OptionsConfig < KnobConfig
7
+ TYPES = %i[select radios].freeze
8
+
9
+ attr_reader :type, :options
10
+
11
+ validates :value, :type, :options, presence: true
12
+ validates :type, inclusion: { in: TYPES }, unless: -> { type.nil? }
13
+ validates :value, inclusion: { in: ->(config) { config.options.values } }, unless: -> { options.nil? || value.nil? }
14
+
15
+ def initialize(type, component, param, options, default_value, name: nil, group_id: nil)
16
+ super(component, param, default_value, name: name, group_id: group_id)
17
+ @type = type
18
+ @options = options
19
+ end
20
+
21
+ def to_csf_params
22
+ super.merge(options: options)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module Knobs
6
+ class SimpleConfig < KnobConfig
7
+ TYPES = %i[text boolean color].freeze
8
+ BOOLEAN_VALUES = [true, false].freeze
9
+
10
+ attr_reader :type
11
+
12
+ validates :value, presence: true, unless: -> { type == :boolean }
13
+ validates :value, inclusion: { in: BOOLEAN_VALUES }, if: -> { type == :boolean }
14
+
15
+ validates :type, presence: true
16
+ validates :type, inclusion: { in: TYPES }, unless: -> { type.nil? }
17
+
18
+ def initialize(type, component, param, value, name: nil, group_id: nil)
19
+ super(component, param, value, name: name, group_id: group_id)
20
+ @type = type
21
+ end
22
+
23
+ def value_from_param(param)
24
+ if type == :boolean && param.is_a?(String) && param.present?
25
+ case param
26
+ when "true"
27
+ true
28
+ when "false"
29
+ false
30
+ end
31
+ else
32
+ super(param)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ class Stories
6
+ extend ActiveSupport::DescendantsTracker
7
+
8
+ class_attribute :story_configs, default: []
9
+ class_attribute :parameters, :title, :stories_layout
10
+
11
+ class << self
12
+ def story(name, component = default_component, &block)
13
+ story_config = StoryConfig.configure(story_id(name), name, component, layout, &block)
14
+ story_configs << story_config
15
+ story_config
16
+ end
17
+
18
+ def parameters(**params)
19
+ self.parameters = params
20
+ end
21
+
22
+ def layout(layout = nil)
23
+ # if no argument is passed act like a getter
24
+ self.stories_layout = layout unless layout.nil?
25
+ stories_layout
26
+ end
27
+
28
+ def to_csf_params
29
+ stories_csf = story_configs.map(&:to_csf_params)
30
+
31
+ csf_params = { title: title }
32
+ csf_params[:addons] = ["knobs"] if stories_csf.any? { |csf| csf.key?(:knobs) }
33
+ csf_params[:parameters] = parameters if parameters.present?
34
+ csf_params[:stories] = story_configs.map(&:to_csf_params)
35
+ csf_params
36
+ end
37
+
38
+ def write_csf_json
39
+ json_path = File.join(ViewComponent::Storybook.stories_path, "#{stories_name}.stories.json")
40
+ File.open(json_path, "w") do |f|
41
+ f.write(JSON.pretty_generate(to_csf_params))
42
+ end
43
+ json_path
44
+ end
45
+
46
+ def stories_name
47
+ name.chomp("Stories").underscore
48
+ end
49
+
50
+ # Returns all component stories classes.
51
+ def all
52
+ load_stories if descendants.empty?
53
+ descendants
54
+ end
55
+
56
+ # Returns +true+ if the stories exist.
57
+ def stories_exists?(stories_name)
58
+ all.any? { |stories| stories.stories_name == stories_name }
59
+ end
60
+
61
+ # Find a component stories by its underscored class name.
62
+ def find_stories(stories_name)
63
+ all.find { |stories| stories.stories_name == stories_name }
64
+ end
65
+
66
+ # Returns +true+ if the story of the component stories exists.
67
+ def story_exists?(name)
68
+ story_configs.map(&:name).include?(name.to_sym)
69
+ end
70
+
71
+ # find the story by name
72
+ def find_story(name)
73
+ story_configs.find { |config| config.name == name.to_sym }
74
+ end
75
+
76
+ private
77
+
78
+ def inherited(other)
79
+ super(other)
80
+ # setup class defaults
81
+ other.title = other.stories_name.humanize.titlecase
82
+ other.story_configs = []
83
+ end
84
+
85
+ def default_component
86
+ name.chomp("Stories").constantize
87
+ rescue StandardError
88
+ nil
89
+ end
90
+
91
+ def load_stories
92
+ Dir["#{stories_path}/**/*_stories.rb"].sort.each { |file| require_dependency file } if stories_path
93
+ end
94
+
95
+ def stories_path
96
+ Storybook.stories_path
97
+ end
98
+
99
+ def show_stories
100
+ Storybook.show_stories
101
+ end
102
+
103
+ def story_id(name)
104
+ "#{stories_name}/#{name.to_s.parameterize}".underscore
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ class StoryConfig
6
+ include ActiveModel::Validations
7
+
8
+ attr_reader :id, :name, :component
9
+ attr_accessor :knobs, :parameters, :layout, :content_block
10
+
11
+ def initialize(id, name, component, layout)
12
+ @id = id
13
+ @name = name
14
+ @component = component
15
+ @layout = layout
16
+ @knobs = []
17
+ end
18
+
19
+ def to_csf_params
20
+ csf_params = { name: name, parameters: { server: { id: id } } }
21
+ csf_params.deep_merge!(parameters: parameters) if parameters.present?
22
+ csf_params[:knobs] = knobs.map(&:to_csf_params) if knobs.present?
23
+ csf_params
24
+ end
25
+
26
+ def values_from_params(params)
27
+ knobs.map do |knob|
28
+ value = knob.value_from_param(params[knob.param])
29
+ value = knob.value if value.nil? # nil only not falsey
30
+ [knob.param, value]
31
+ end.to_h
32
+ end
33
+
34
+ def self.configure(id, name, component, layout, &configuration)
35
+ config = new(id, name, component, layout)
36
+ ViewComponent::Storybook::Dsl::StoryDsl.evaluate!(config, &configuration)
37
+ config
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :view_component_storybook do
4
+ desc "Write CSF JSON stories for all Stories"
5
+ task write_stories_json: :environment do
6
+ puts "Writing Stories JSON"
7
+ ViewComponent::Storybook::Stories.all.each do |stories|
8
+ json_path = stories.write_csf_json
9
+ puts "#{stories.name} => #{json_path}"
10
+ end
11
+ puts "Done"
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,207 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: view_component_storybook
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jon Palmer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-04-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: view_component
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '2.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '2.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.14'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.14'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '13.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '13.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: relaxed-rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.5'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.5'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.9'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.9'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec-rails
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.9'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.9'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.81'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.81'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop-rails
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 2.4.2
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 2.4.2
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop-rspec
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '1.38'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '1.38'
139
+ - !ruby/object:Gem::Dependency
140
+ name: simplecov
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: 0.18.5
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: 0.18.5
153
+ description: Generate Storybook CSF JSON for rendering Rails View Components in Storybook
154
+ email:
155
+ - 328224+jonspalmer@users.noreply.github.com
156
+ executables: []
157
+ extensions: []
158
+ extra_rdoc_files: []
159
+ files:
160
+ - LICENSE.txt
161
+ - README.md
162
+ - app/controllers/view_component/storybook/stories_controller.rb
163
+ - app/views/view_component/storybook/stories/show.html.erb
164
+ - lib/view_component/storybook.rb
165
+ - lib/view_component/storybook/dsl.rb
166
+ - lib/view_component/storybook/dsl/knobs_dsl.rb
167
+ - lib/view_component/storybook/dsl/story_dsl.rb
168
+ - lib/view_component/storybook/engine.rb
169
+ - lib/view_component/storybook/knobs.rb
170
+ - lib/view_component/storybook/knobs/array_config.rb
171
+ - lib/view_component/storybook/knobs/date_config.rb
172
+ - lib/view_component/storybook/knobs/knob_config.rb
173
+ - lib/view_component/storybook/knobs/number_config.rb
174
+ - lib/view_component/storybook/knobs/object_config.rb
175
+ - lib/view_component/storybook/knobs/options_config.rb
176
+ - lib/view_component/storybook/knobs/simple_config.rb
177
+ - lib/view_component/storybook/stories.rb
178
+ - lib/view_component/storybook/story_config.rb
179
+ - lib/view_component/storybook/tasks/write_stories_json.rake
180
+ - lib/view_component/storybook/version.rb
181
+ homepage: https://github.com/jonspalmer/view_component_storybook
182
+ licenses:
183
+ - MIT
184
+ metadata:
185
+ allowed_push_host: https://rubygems.org
186
+ homepage_uri: https://github.com/jonspalmer/view_component_storybook
187
+ source_code_uri: https://github.com/jonspalmer/view_component_storybook
188
+ post_install_message:
189
+ rdoc_options: []
190
+ require_paths:
191
+ - lib
192
+ required_ruby_version: !ruby/object:Gem::Requirement
193
+ requirements:
194
+ - - ">="
195
+ - !ruby/object:Gem::Version
196
+ version: 2.3.0
197
+ required_rubygems_version: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ requirements: []
203
+ rubygems_version: 3.0.6
204
+ signing_key:
205
+ specification_version: 4
206
+ summary: Storybook for Rails View Components
207
+ test_files: []