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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +143 -0
- data/app/controllers/view_component/storybook/stories_controller.rb +47 -0
- data/app/views/view_component/storybook/stories/show.html.erb +1 -0
- data/lib/view_component/storybook.rb +33 -0
- data/lib/view_component/storybook/dsl.rb +14 -0
- data/lib/view_component/storybook/dsl/knobs_dsl.rb +81 -0
- data/lib/view_component/storybook/dsl/story_dsl.rb +39 -0
- data/lib/view_component/storybook/engine.rb +46 -0
- data/lib/view_component/storybook/knobs.rb +19 -0
- data/lib/view_component/storybook/knobs/array_config.rb +34 -0
- data/lib/view_component/storybook/knobs/date_config.rb +36 -0
- data/lib/view_component/storybook/knobs/knob_config.rb +39 -0
- data/lib/view_component/storybook/knobs/number_config.rb +36 -0
- data/lib/view_component/storybook/knobs/object_config.rb +23 -0
- data/lib/view_component/storybook/knobs/options_config.rb +27 -0
- data/lib/view_component/storybook/knobs/simple_config.rb +38 -0
- data/lib/view_component/storybook/stories.rb +109 -0
- data/lib/view_component/storybook/story_config.rb +41 -0
- data/lib/view_component/storybook/tasks/write_stories_json.rake +13 -0
- data/lib/view_component/storybook/version.rb +7 -0
- metadata +207 -0
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,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
|
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: []
|