superview 0.1.0 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2a8124e9df0a2fcaed7dd467327cedc7a676117a54c47896ba436c7f6cdc97c4
4
- data.tar.gz: 65bfc7d730a19ae0107519207dc4078a17772465305195186ddd5b3b152f6edb
3
+ metadata.gz: 185e73cab73c23d8de233721cde393cb453868e1cf3a6f5ef8b322b9b613ff32
4
+ data.tar.gz: 6bca9e94731ecd625cf20f904f0cac42fe969ac63ac72913de75f6a176f18a1e
5
5
  SHA512:
6
- metadata.gz: 8ca5b6d4459efebc79bc12dd3a4e99d39c14a200a4ad56d998bc784658d770526c78687121320b70a2a67beebe641d8ee247c149861205df7d60a3f59c640130
7
- data.tar.gz: 3e6c2af610a5b8a7f6952425bef2c9be9d5e39a49fd6f0128f73923737fea2b6fc70954883e79a17e48534047512d17aae26da1b3adc6a4511efa079bd7073d0
6
+ metadata.gz: a4dfdc42e1697576ac22f8af14c0d6f75be71b198b19a5c6e3097c895db0eb114401af25cf30c39e3615999f4bb41907bde50bc70e22aa6520c1c9805a8eb230
7
+ data.tar.gz: d00f990562345a341b69ac4c86ef72bf0deef57defbe4dcdfdfa0b9759079f8e43f57005d59c8058cec368fefa886090814517de823efc2dd7b94b84601dd896
data/Gemfile CHANGED
@@ -9,4 +9,4 @@ gem "rake", "~> 13.0"
9
9
 
10
10
  gem "rspec", "~> 3.0"
11
11
 
12
- gem "rails", "~> 7.0"
12
+ gem "rails", "~> 7.0"
data/Gemfile.lock CHANGED
@@ -1,8 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- superview (0.1.0)
4
+ superview (0.1.2)
5
5
  phlex-rails (~> 1.0)
6
+ zeitwerk (~> 2.0)
6
7
 
7
8
  GEM
8
9
  remote: https://rubygems.org/
@@ -96,6 +97,7 @@ GEM
96
97
  marcel (1.0.2)
97
98
  method_source (1.0.0)
98
99
  mini_mime (1.1.5)
100
+ mini_portile2 (2.8.5)
99
101
  minitest (5.19.0)
100
102
  net-imap (0.3.7)
101
103
  date
@@ -107,7 +109,8 @@ GEM
107
109
  net-smtp (0.3.3)
108
110
  net-protocol
109
111
  nio4r (2.5.9)
110
- nokogiri (1.15.4-arm64-darwin)
112
+ nokogiri (1.15.4)
113
+ mini_portile2 (~> 2.8.2)
111
114
  racc (~> 1.4)
112
115
  phlex (1.8.1)
113
116
  concurrent-ruby (~> 1.2)
@@ -174,6 +177,8 @@ GEM
174
177
 
175
178
  PLATFORMS
176
179
  arm64-darwin-22
180
+ arm64-darwin-23
181
+ x86_64-linux
177
182
 
178
183
  DEPENDENCIES
179
184
  rails (~> 7.0)
data/README.md CHANGED
@@ -1,10 +1,6 @@
1
1
  # Superview
2
2
 
3
- Include in controllers to map action names to class names. This makes it possible to embed Phlex components directly into Rails controllers without having to go through other templating systems like Erb.
4
-
5
- Instance methods will be assigned to views that have `attr_accessor` methods.
6
-
7
- Consider a blog post controller:
3
+ Build Rails applications, from the ground up, using [Phlex](https://www.phlex.fun/) components, like this.
8
4
 
9
5
  ```ruby
10
6
  class PostsController < ApplicationController
@@ -28,7 +24,10 @@ class PostsController < ApplicationController
28
24
  end
29
25
  ```
30
26
 
31
- The `@post` variable gets set in the `Show` view class via `Show#post=`.
27
+ Read more about it at:
28
+
29
+ * [Component driven development on Rails with Phlex](https://fly.io/ruby-dispatch/component-driven-development-on-rails-with-phlex/)
30
+ * [Hacking Rails Implicit Rendering for View Components & Fun](https://fly.io/ruby-dispatch/hacking-rails-implicit-rendering-for-view-components/)
32
31
 
33
32
  ## Installation
34
33
 
@@ -42,25 +41,109 @@ If bundler is not being used to manage dependencies, install the gem by executin
42
41
 
43
42
  ## Usage
44
43
 
45
- Install `phlex-rails` in your application.
44
+ Install `phlex-rails` in your Rails application.
46
45
 
47
46
  $ bin/rails generate phlex:install
48
47
 
49
- Then include the following any controller you'd like to render Phlex components.
48
+ Then add `include Superview::Actions` to any controllers you'd like to render Phlex components.
49
+
50
+ ```ruby
51
+ class PostsController < ApplicationController
52
+ include Superview::Actions
53
+
54
+ before_action :load_post
55
+
56
+ class Show < ApplicationComponent
57
+ attr_accessor :post
58
+
59
+ def template(&)
60
+ h1 { @post.title }
61
+ div(class: "prose") { @post.body }
62
+ end
63
+ end
64
+
65
+ private
66
+ def load_post
67
+ @post = Post.find(params[:id])
68
+ end
69
+ end
70
+ ```
71
+
72
+ The `Show` class will render when the `PostsController#show` action is called. To use along side other formats or render manually, you can define the `PostsController#show` as you'd expect:
50
73
 
51
74
  ```ruby
52
75
  class PostsController < ApplicationController
53
76
  include Superview::Actions
54
77
 
78
+ before_action :load_post
79
+
80
+ class Show < ApplicationComponent
81
+ attr_accessor :post
82
+
83
+ def template(&)
84
+ h1 { @post.title }
85
+ div(class: "prose") { @post.body }
86
+ end
87
+ end
88
+
89
+ def show
90
+ respond_to do |format|
91
+ format.html { render Show.new.tap { _1.post = @post } }
92
+ format.json { render json: @post }
93
+ end
94
+ end
95
+
96
+ private
97
+ def load_post
98
+ @post = Post.find(params[:id])
99
+ end
100
+ end
101
+ ```
102
+
103
+ ### Extracting inline views into the `./app/views` folder
104
+
105
+ Inline views are an amazingly productive way of prototyping apps, but as it matures you might be inclined to extract these views into the `./app/views` folders for organizational purposes or so you can share them between controllers.
106
+
107
+ First let's extract the `Show` class into `./app/views/posts/show.rb`
108
+
109
+ ```ruby
110
+ # ./app/views/posts/show.rb
111
+ module Posts
55
112
  class Show < ApplicationComponent
113
+ attr_accessor :post
114
+
56
115
  def template(&)
57
- h1 { "Hello World" }
116
+ h1 { @post.title }
117
+ div(class: "prose") { @post.body }
118
+ end
119
+ end
120
+ end
121
+ ```
122
+
123
+ Then include the `Posts` module in the controllers you'd like to use the views:
124
+
125
+ ```ruby
126
+ class PostsController < ApplicationController
127
+ include Superview::Actions
128
+ include Posts # Add this to your controller 🚨
129
+
130
+ before_action :load_post
131
+
132
+ def show
133
+ respond_to do |format|
134
+ format.html { render Show.new.tap { _1.post = @post } }
135
+ format.json { render json: @post }
58
136
  end
59
137
  end
138
+
139
+ private
140
+ def load_post
141
+ @post = Post.find(params[:id])
142
+ end
60
143
  end
61
144
  ```
62
145
 
63
- The `Show` class will render when the `PostsController#show` action is called.
146
+ That's it! Ruby includes all the classes in the `Posts` module, which Superview picks up and renders in the controller. If you have an `Index`, `Edit`, `New`, etc. class in the `Posts` namespace, those would be implicitly rendered for their respective action.
64
147
 
65
148
  ## Development
66
149
 
@@ -70,7 +153,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
70
153
 
71
154
  ## Contributing
72
155
 
73
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/superview. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/superview/blob/main/CODE_OF_CONDUCT.md).
156
+ Bug reports and pull requests are welcome on GitHub at https://github.com/rubymonolith/superview. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/rubymonolith/superview/blob/main/CODE_OF_CONDUCT.md).
74
157
 
75
158
  ## License
76
159
 
@@ -78,4 +161,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
78
161
 
79
162
  ## Code of Conduct
80
163
 
81
- Everyone interacting in the Superview project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/superview/blob/main/CODE_OF_CONDUCT.md).
164
+ Everyone interacting in the Superview project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/rubymonolith/superview/blob/main/CODE_OF_CONDUCT.md).
@@ -0,0 +1,86 @@
1
+ module Superview
2
+ # Include in controllers to map action names to class names. This makes it possible to
3
+ # embed Phlex components directly into Rails controllers without having to go through
4
+ # other templating systems like Erb.
5
+ #
6
+ # Instance methods will be assigned to views that have `attr_accessor` methods.
7
+ #
8
+ # Consider a blog post controller:
9
+ #
10
+ # ```ruby
11
+ # class PostsController < ApplicationController
12
+ # include Superview::Actions
13
+ #
14
+ # before_action :load_post
15
+ #
16
+ # class Show < ApplicationComponent
17
+ # attr_accessor :post
18
+ #
19
+ # def template(&)
20
+ # h1 { @post.title }
21
+ # div(class: "prose") { @post.body }
22
+ # end
23
+ # end
24
+ #
25
+ # private
26
+ # def load_post
27
+ # @post = Post.find(params[:id])
28
+ # end
29
+ # end
30
+ # ```
31
+ #
32
+ # The `@post` variable gets set in the `Show` view class via `Show#post=`.
33
+ module Actions
34
+ extend ActiveSupport::Concern
35
+
36
+ class_methods do
37
+ # Finds a class on the controller with the same name as the action. For example,
38
+ # `def index` would find the `Index` constant on the controller class to render
39
+ # for the action `index`.
40
+ def phlex_action_class(action:)
41
+ action_class = action.to_s.camelcase
42
+ const_get action_class if const_defined? action_class
43
+ end
44
+ end
45
+
46
+ protected
47
+
48
+ # Assigns the instance variables that are set in the controller to setter method
49
+ # on Phlex. For example, if a controller defines @users and a Phlex class has
50
+ # `attr_writer :users`, `attr_accessor :user`, or `def users=`, it will be automatically
51
+ # set by this method.
52
+ def assign_phlex_accessors(phlex_view)
53
+ phlex_view.tap do |view|
54
+ view_assigns.each do |variable, value|
55
+ attr_writer_name = "#{variable}="
56
+ view.send attr_writer_name, value if view.respond_to? attr_writer_name
57
+ end
58
+ end
59
+ end
60
+
61
+ # Initializers a Phlex view based on the action name, then assigns `view_assigns`
62
+ # to the view.
63
+ def phlex_action(action)
64
+ assign_phlex_accessors self.class.phlex_action_class(action: action).new
65
+ end
66
+
67
+ # Phlex action for the current action.
68
+ def phlex
69
+ phlex_action(action_name)
70
+ end
71
+
72
+ # Try rendering with the regular Rails rendering methods; if those don't work
73
+ # then try finding the Phlex class that corresponds with the action_name. If that's
74
+ # found then tell Rails to call `default_phlex_render`.
75
+ def method_for_action(action_name)
76
+ super || if self.class.phlex_action_class action: action_name
77
+ "default_phlex_render"
78
+ end
79
+ end
80
+
81
+ # Renders a Phlex view for the given action, if it's present.
82
+ def default_phlex_render
83
+ render phlex
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,159 @@
1
+ module Superview
2
+ module Assignable
3
+ # Include in RESTful Rails controllers to assign instance variables ActiveRecord scopes
4
+ # without all the boiler plate.
5
+ #
6
+ # Let's start with the most simple example
7
+ #
8
+ # ```ruby
9
+ # ./app/controllers/blog/posts_controller.rb
10
+ # class BlogsController < ApplicationController
11
+ # assign :blog
12
+ # end
13
+ # ```
14
+ #
15
+ # This would load the `Blog` scope for `collection` routes, like `index`, as `@blogs` and
16
+ # load `Blog.find(params[:id])` for `member` routes, like `show`, `edit`, `update`, etc.
17
+ # as `@blog`.
18
+ #
19
+ # Most applications need to load stuff from a user that's logged in, which is what the `from:`
20
+ # key makes possible.
21
+ #
22
+ # ```ruby
23
+ # ./app/controllers/blog/posts_controller.rb
24
+ # class BlogsController < ApplicationController
25
+ # assign :blog, from: :current_user
26
+ # end
27
+ # ```
28
+ #
29
+ # This assumes the controller has a `current_user` method defined in the controller that returns
30
+ # a `User` model with the relationship `has_many :blogs`. It loads the blog via
31
+ # `current_user.blogs.find(params[:id]).
32
+ #
33
+ # A blog has many posts, so how would we assign a post through a blog from the current user?
34
+ #
35
+ # ```ruby
36
+ # ./app/controllers/blog/posts_controller.rb
37
+ # class Blog::PostsController < ApplicationController
38
+ # assign :post, through: :blog, from: :current_user
39
+ # end
40
+ # ```
41
+ #
42
+ # This does not work like the `through:` ActiveRecord key, so pay attention ya know it all! This
43
+ # follows the idea of nested REST routes in Rails. In this case the `Blog` is the "parent resource"
44
+ # and the `Post` is the resource. How does is that queried? Glad you asked!
45
+ #
46
+ # First the specific blog is loaded via `@blog = current_user.blogs.find(params[:blog_id])` to set the
47
+ # parent model. Next the `Post` scope is set via `@posts = @blog.posts`. `@posts` for collection routes.
48
+ # Finally `@post = @posts.find(params[:id])` is set for member routes.
49
+
50
+ extend ActiveSupport::Concern
51
+
52
+ included do
53
+ class_attribute :model, :parent_model, :context_method_name
54
+
55
+ before_action :assign_parent_collection, if: :has_parent_model?
56
+ before_action :assign_parent_member, if: :has_parent_model?
57
+ before_action :assign_collection
58
+ before_action :assign_member
59
+ end
60
+
61
+ protected
62
+
63
+ def assign_collection
64
+ instance_variable_set "@#{model.model_name.plural}", model_scope
65
+ end
66
+
67
+ def assign_parent_collection
68
+ instance_variable_set "@#{parent_model.model_name.plural}", parent_model_scope
69
+ end
70
+
71
+ def model_scope
72
+ if has_parent_model?
73
+ parent_model_instance.association(model.model_name.collection)
74
+ elsif has_assignable_context?
75
+ assignable_context.association(model.model_name.collection).scope
76
+ else
77
+ model.scope_for_association
78
+ end
79
+ end
80
+
81
+ def parent_model_scope
82
+ if has_assignable_context?
83
+ assignable_context.association(parent_model.model_name.collection)
84
+ else
85
+ parent_model.scope_for_association
86
+ end
87
+ end
88
+
89
+ def parent_model_instance
90
+ parent_model_scope.find(params.fetch(parent_model_param_key))
91
+ end
92
+
93
+ def assign_parent_member
94
+ instance_variable_set "@#{parent_model.model_name.singular}", parent_model_instance
95
+ end
96
+
97
+ def has_parent_model?
98
+ parent_model.present?
99
+ end
100
+
101
+ def assign_member
102
+ instance_variable_set "@#{model.model_name.singular}", model_instance
103
+ end
104
+
105
+ def model_instance
106
+ if member?
107
+ model_scope.find params.fetch(model_param_key)
108
+ else
109
+ model_scope.build.tap do |post|
110
+ # # Blog is a reflection of User
111
+ # # Get the name of the `user` association.
112
+ # parent_from_association = parent_model_scope.reflection.inverse_of
113
+
114
+ # if model.reflect_on_association(parent_from_association.name)
115
+ # similar_association = model.association parent_from_association.name
116
+ # # Now let's see if that association exists on the current_model ..
117
+ # #
118
+ # # This isn't setting the foreign key ... errrggggg.
119
+ # raise 'hell'
120
+
121
+ # # post.association(association_name).target = parent_model_scope.owner
122
+ # end
123
+ end
124
+ end
125
+ end
126
+
127
+ def member?
128
+ params.key? model_param_key
129
+ end
130
+
131
+ def model_param_key
132
+ :id
133
+ end
134
+
135
+ def parent_model_param_key
136
+ "#{parent_model.model_name.singular}_id".to_sym
137
+ end
138
+
139
+ def assignable_context
140
+ self.send self.class.context_method_name
141
+ end
142
+
143
+ def has_assignable_context?
144
+ !!self.class.context_method_name
145
+ end
146
+
147
+ class_methods do
148
+ def assign(scope, through: nil, from: nil)
149
+ self.model = Assignable.find_scope scope
150
+ self.parent_model = Assignable.find_scope through
151
+ self.context_method_name = from
152
+ end
153
+ end
154
+
155
+ def self.find_scope(name)
156
+ name.to_s.singularize.camelize.constantize if name
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,78 @@
1
+ module Superview::Components
2
+ # Renders an HTML table for a collection. Each item is passed into the
3
+ # collection of the table.
4
+ #
5
+ # ```ruby
6
+ # render TableComponent.new(items: @posts) do |table|
7
+ # # This is how you'd usually render a table.
8
+ # table.column("Title") { show(_1, :title) }
9
+ #
10
+ # # If you need to render HTML in the title, add a `column` argument
11
+ # # to the block and call `title` or `item` on it.
12
+ # table.column do |column|
13
+ # # Titles might not always be text, so we need to handle rendering
14
+ # # Phlex markup within.
15
+ # column.title do
16
+ # link_to(user_blogs_path(@current_user)) { "Blogs" }
17
+ # end
18
+ # column.item { show(_1.blog, :title) }
19
+ # end
20
+ # end
21
+ # ```
22
+ class TableComponent < ApplicationComponent
23
+ include Phlex::DeferredRender
24
+
25
+ class Column
26
+ attr_accessor :title_template, :item_template
27
+
28
+ def title(&block)
29
+ @title_template = block
30
+ end
31
+
32
+ def item(&block)
33
+ @item_template = block
34
+ end
35
+
36
+ def self.build(title:, &block)
37
+ new.tap do |column|
38
+ column.title { title }
39
+ column.item(&block)
40
+ end
41
+ end
42
+ end
43
+
44
+ def initialize(items: [])
45
+ @items = items
46
+ @columns = []
47
+ end
48
+
49
+ def template(&)
50
+ table do
51
+ thead do
52
+ tr do
53
+ @columns.each do |column|
54
+ th(&column.title_template)
55
+ end
56
+ end
57
+ end
58
+ tbody do
59
+ @items.each do |item|
60
+ tr do
61
+ @columns.each do |column|
62
+ td { column.item_template.call(item) }
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ def column(title = nil, &block)
71
+ @columns << if title
72
+ Column.build(title: title, &block)
73
+ else
74
+ Column.new.tap(&block)
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,82 @@
1
+ module Superview
2
+ module Helpers
3
+ # RESTful links for creating Superviews in applications. For example, given a
4
+ # blog application, we might have links like:
5
+ #
6
+ # ```ruby
7
+ # create(Post) { "New Blog Post" }
8
+ # create(Post.new) { "New Blog Post" }
9
+ # ```
10
+ #
11
+ # Which generateds the html `<a href="/posts/new">New Blog Post</a>` and
12
+ #
13
+ # ```ruby
14
+ # show(@post) { @post.title }
15
+ # ```
16
+ #
17
+ # generates the html `<a href="/posts/1">My First Post</a>`. An attribute
18
+ # can be passed in as a second argument, which calls the method on the object
19
+ # passed into the link helper.
20
+ #
21
+ # ```ruby
22
+ # show(@post, :title)
23
+ # ```
24
+ #
25
+ # generates `<a href="/posts/new">New Blog Post</a>`.
26
+ #
27
+ # Link helpers are available per RESTful action.
28
+ #
29
+ # ```ruby
30
+ # delete(@post)
31
+ # edit(@post)
32
+ # ```
33
+ module Links
34
+ # Give us some sane link helpers to work with in Phlex. They kind
35
+ # of mimic Rails helpers, but are "Phlexable".
36
+ def link_to(target = nil, method: nil, **attributes, &)
37
+ url = case target
38
+ when URI
39
+ target.to_s
40
+ when NilClass
41
+ url_for(attributes)
42
+ else
43
+ url_for(target)
44
+ end
45
+ a(href: url, data_turbo_method: method, **attributes, &)
46
+ end
47
+
48
+ def show(model, attribute = nil, *args, **kwargs, &content)
49
+ content ||= Proc.new { model.send(attribute) }
50
+ link_to(model, *args, **kwargs, &content)
51
+ end
52
+
53
+ def edit(model, *args, **kwargs, &content)
54
+ content ||= Proc.new { "Edit #{model.class.model_name}" }
55
+ link_to([:edit, model], *args, **kwargs, &content)
56
+ end
57
+
58
+ def delete(model, *args, confirm: nil, **kwargs, &content)
59
+ content ||= Proc.new { "Delete #{model.class.model_name}" }
60
+ link_to(model, *args, method: :delete, data_turbo_confirm: confirm, **kwargs, &content)
61
+ end
62
+
63
+ def create(scope = nil, *args, **kwargs, &content)
64
+ target = if scope.respond_to? :proxy_association
65
+ owner = scope.proxy_association.owner
66
+ model = scope.proxy_association.reflection.klass.model_name
67
+ element = scope.proxy_association.reflection.klass.model_name.element.to_sym
68
+ [:new, owner, element]
69
+ elsif scope.respond_to? :model
70
+ model = scope.model
71
+ [:new, model.model_name.singular_route_key.to_sym]
72
+ elsif scope.respond_to? :model_name
73
+ [:new, scope.model_name.singular_route_key.to_sym]
74
+ end
75
+
76
+ content ||= Proc.new { "Create #{model}" }
77
+
78
+ link_to(target, *args, **kwargs, &content)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,48 @@
1
+ module Superview
2
+ module Helpers
3
+ module Turbo
4
+ # Renders the metatags for setting up Turbo Drive.
5
+ class MetaTags < ApplicationComponent
6
+ attr_accessor \
7
+ :method,
8
+ :scroll,
9
+ :exempts_page_from_cache,
10
+ :exempts_page_from_preview,
11
+ :page_requires_reload
12
+
13
+ METHOD = :replace
14
+ SCROLL = :reset
15
+
16
+ def initialize(method: METHOD, scroll: SCROLL, exempts_page_from_preview: nil, exempts_page_from_cache: nil, page_requires_reload: nil)
17
+ refreshes_with method: method, scroll: scroll
18
+ @exempts_page_from_cache = exempts_page_from_cache
19
+ @exempts_page_from_preview = exempts_page_from_preview
20
+ @page_requires_reload = page_requires_reload
21
+ end
22
+
23
+ def template
24
+ meta(name: "turbo-refresh-method", content: @method)
25
+ meta(name: "turbo-refresh-scroll", content: @scroll)
26
+ meta(name: "turbo-cache-control", content: "no-cache") if @exempts_page_from_cache
27
+ meta(name: "turbo-cache-control", content: "no-preview") if @exempts_page_from_preview
28
+ meta(name: "turbo-visit-control", content: "reload") if @page_requires_reload
29
+ end
30
+
31
+ def refreshes_with(method: METHOD, scroll: SCROLL)
32
+ self.method = method
33
+ self.scroll = scroll
34
+ end
35
+
36
+ def method=(value)
37
+ raise ArgumentError, "Invalid refresh option '#{value}'" unless value.in?(%i[ replace morph ])
38
+ @method = value
39
+ end
40
+
41
+ def scroll=(value)
42
+ raise ArgumentError, "Invalid scroll option '#{value}'" unless value.in?(%i[ reset preserve ])
43
+ @scroll = value
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,39 @@
1
+ module Superview
2
+ module Helpers
3
+ module Turbo
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ register_element :turbo_cable_stream_source
8
+ end
9
+
10
+ class_methods do
11
+ def turbo(*args, **kwargs, &block)
12
+ @turbo_meta_tags = MetaTags.new(*args, **kwargs)
13
+ define_method(:turbo, &block) if block
14
+ end
15
+
16
+ def turbo_meta_tags
17
+ @turbo_meta_tags ||= MetaTags.new
18
+ end
19
+ end
20
+
21
+ def turbo_stream_from(*streamables, **attributes)
22
+ attributes[:channel] = attributes[:channel]&.to_s || "Turbo::StreamsChannel"
23
+ attributes[:"signed-stream-name"] = ::Turbo::StreamsChannel.signed_stream_name(streamables)
24
+ turbo_cable_stream_source **attributes, class: "hidden", style: "display: none;"
25
+ end
26
+
27
+ def stream_from(*streamables)
28
+ streamables.each do |streamable|
29
+ case streamable
30
+ in association: ActiveRecord::Relation
31
+ association.each { turbo_stream_from streamable }
32
+ else
33
+ turbo_stream_from streamable
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Superview
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/superview.rb CHANGED
@@ -2,91 +2,13 @@
2
2
 
3
3
  require_relative "superview/version"
4
4
  require "active_support/concern"
5
+ require "zeitwerk"
5
6
 
6
7
  module Superview
7
- class Error < StandardError; end
8
- # Include in controllers to map action names to class names. This makes it possible to
9
- # embed Phlex components directly into Rails controllers without having to go through
10
- # other templating systems like Erb.
11
- #
12
- # Instance methods will be assigned to views that have `attr_accessor` methods.
13
- #
14
- # Consider a blog post controller:
15
- #
16
- # ```ruby
17
- # class PostsController < ApplicationController
18
- # include Superview::Actions
19
- #
20
- # before_action :load_post
21
- #
22
- # class Show < ApplicationComponent
23
- # attr_accessor :post
24
- #
25
- # def template(&)
26
- # h1 { @post.title }
27
- # div(class: "prose") { @post.body }
28
- # end
29
- # end
30
- #
31
- # private
32
- # def load_post
33
- # @post = Post.find(params[:id])
34
- # end
35
- # end
36
- # ```
37
- #
38
- # The `@post` variable gets set in the `Show` view class via `Show#post=`.
39
- module Actions
40
- extend ActiveSupport::Concern
41
-
42
- class_methods do
43
- # Finds a class on the controller with the same name as the action. For example,
44
- # `def index` would find the `Index` constant on the controller class to render
45
- # for the action `index`.
46
- def phlex_action_class(action:)
47
- action_class = action.to_s.camelcase
48
- const_get action_class if const_defined? action_class
49
- end
50
- end
51
-
52
- protected
53
-
54
- # Assigns the instance variables that are set in the controller to setter method
55
- # on Phlex. For example, if a controller defines @users and a Phlex class has
56
- # `attr_writer :users`, `attr_accessor :user`, or `def users=`, it will be automatically
57
- # set by this method.
58
- def assign_phlex_accessors(phlex_view)
59
- phlex_view.tap do |view|
60
- view_assigns.each do |variable, value|
61
- attr_writer_name = "#{variable}="
62
- view.send attr_writer_name, value if view.respond_to? attr_writer_name
63
- end
64
- end
65
- end
66
-
67
- # Initializers a Phlex view based on the action name, then assigns `view_assigns`
68
- # to the view.
69
- def phlex_action(action)
70
- assign_phlex_accessors self.class.phlex_action_class(action: action).new
71
- end
72
-
73
- # Phlex action for the current action.
74
- def phlex
75
- phlex_action(action_name)
76
- end
77
-
78
- # Try rendering with the regular Rails rendering methods; if those don't work
79
- # then try finding the Phlex class that corresponds with the action_name. If that's
80
- # found then tell Rails to call `default_phlex_render`.
81
- def method_for_action(action_name)
82
- super || if self.class.phlex_action_class action: action_name
83
- "default_phlex_render"
84
- end
85
- end
86
-
87
- # Renders a Phlex view for the given action, if it's present.
88
- def default_phlex_render
89
- render phlex
90
- end
8
+ Loader = Zeitwerk::Loader.for_gem.tap do |loader|
9
+ loader.ignore "#{__dir__}/generators"
10
+ loader.setup
91
11
  end
12
+
13
+ class Error < StandardError; end
92
14
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: superview
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brad Gessler
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-08-17 00:00:00.000000000 Z
11
+ date: 2024-02-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: phlex-rails
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: zeitwerk
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
27
41
  description: Build Rails applications entirely out of Phlex components.
28
42
  email:
29
43
  - bradgessler@gmail.com
@@ -40,6 +54,12 @@ files:
40
54
  - README.md
41
55
  - Rakefile
42
56
  - lib/superview.rb
57
+ - lib/superview/actions.rb
58
+ - lib/superview/assignable.rb
59
+ - lib/superview/components/table_component.rb
60
+ - lib/superview/helpers/links.rb
61
+ - lib/superview/helpers/turbo.rb
62
+ - lib/superview/helpers/turbo/meta_tags.rb
43
63
  - lib/superview/version.rb
44
64
  - sig/superview.rbs
45
65
  homepage: https://github.com/rubymonolith/superview
@@ -65,7 +85,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
65
85
  - !ruby/object:Gem::Version
66
86
  version: '0'
67
87
  requirements: []
68
- rubygems_version: 3.4.6
88
+ rubygems_version: 3.5.3
69
89
  signing_key:
70
90
  specification_version: 4
71
91
  summary: Build Rails applications entirely out of Phlex components.