superview 0.1.0 → 0.1.1
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 +4 -4
- data/Gemfile +1 -1
- data/Gemfile.lock +7 -2
- data/README.md +95 -12
- data/lib/superview/actions.rb +86 -0
- data/lib/superview/assignable.rb +159 -0
- data/lib/superview/helpers/links.rb +82 -0
- data/lib/superview/helpers/turbo/meta_tags.rb +48 -0
- data/lib/superview/helpers/turbo.rb +39 -0
- data/lib/superview/version.rb +1 -1
- data/lib/superview.rb +6 -84
- metadata +22 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1922614da732feb087d1d3b616b217c6c42f34b7cfc4d93cf77388f49b938d86
|
4
|
+
data.tar.gz: 9730279b3566ebaf8505048a477eeecf71b815d007ace7ca6b4ef5d1dd1a4683
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 46eb0606d45d420ff8ed76f63b4c4cb0e92b7a00305afba551feac44ff925f7a0202c7cb1e47a35a9e336ea76af0d8c2ce5fc5b7c2dc136fc43ffd715c23db17
|
7
|
+
data.tar.gz: a346e31ca903c626d99e2c5f2211569d1e886641cf16101e986c7a5cde5aff6127d345c0447f3ed0ca74dd7e51187814b0487773a3664f5d8bdbeb6caaf84454
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
superview (0.1.
|
4
|
+
superview (0.1.1)
|
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
|
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
|
-
|
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
|
-
|
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
|
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 {
|
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
|
-
|
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/
|
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/
|
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,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
|
data/lib/superview/version.rb
CHANGED
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
|
-
|
8
|
-
|
9
|
-
|
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.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brad Gessler
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-02-27 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,11 @@ 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/helpers/links.rb
|
60
|
+
- lib/superview/helpers/turbo.rb
|
61
|
+
- lib/superview/helpers/turbo/meta_tags.rb
|
43
62
|
- lib/superview/version.rb
|
44
63
|
- sig/superview.rbs
|
45
64
|
homepage: https://github.com/rubymonolith/superview
|
@@ -65,7 +84,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
65
84
|
- !ruby/object:Gem::Version
|
66
85
|
version: '0'
|
67
86
|
requirements: []
|
68
|
-
rubygems_version: 3.
|
87
|
+
rubygems_version: 3.5.3
|
69
88
|
signing_key:
|
70
89
|
specification_version: 4
|
71
90
|
summary: Build Rails applications entirely out of Phlex components.
|