view_component_storybook 0.1.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 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: []