phlex 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of phlex might be problematic. Click here for more details.

Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +8 -0
  3. data/.rubocop.yml +3 -7
  4. data/Gemfile +2 -1
  5. data/Procfile.dev +3 -0
  6. data/Rakefile +3 -5
  7. data/docs/build.rb +15 -10
  8. data/docs/components/code_span.rb +9 -0
  9. data/docs/components/example.rb +1 -1
  10. data/docs/components/heading.rb +1 -1
  11. data/docs/components/layout.rb +38 -16
  12. data/docs/components/markdown.rb +17 -3
  13. data/docs/components/nav/item.rb +33 -0
  14. data/docs/components/nav.rb +6 -0
  15. data/docs/components/tabs/tab.rb +3 -1
  16. data/docs/components/title.rb +1 -1
  17. data/docs/page_builder.rb +3 -0
  18. data/docs/pages/helpers.rb +97 -0
  19. data/docs/pages/index.rb +6 -17
  20. data/docs/pages/library/collections.rb +101 -0
  21. data/docs/pages/rails/getting_started.rb +53 -0
  22. data/docs/pages/rails/helpers.rb +53 -0
  23. data/docs/pages/rails/layouts.rb +61 -0
  24. data/docs/pages/rails/migrating.rb +37 -0
  25. data/docs/pages/rails/rendering_views.rb +35 -0
  26. data/docs/pages/templates.rb +51 -149
  27. data/docs/pages/views.rb +55 -94
  28. data/fixtures/dummy/app/components/comment_component.html.erb +14 -0
  29. data/fixtures/dummy/app/components/comment_component.rb +8 -0
  30. data/fixtures/dummy/app/components/reaction_component.html.erb +3 -0
  31. data/fixtures/dummy/app/components/reaction_component.rb +7 -0
  32. data/fixtures/dummy/app/controllers/comments_controller.rb +4 -0
  33. data/fixtures/dummy/app/views/articles/form.rb +2 -0
  34. data/fixtures/dummy/app/views/card.rb +3 -1
  35. data/fixtures/dummy/app/views/comments/comment.rb +25 -0
  36. data/fixtures/dummy/app/views/comments/index.html.erb +3 -0
  37. data/fixtures/dummy/app/views/comments/reaction.rb +17 -0
  38. data/fixtures/dummy/app/views/comments/show.html.erb +3 -0
  39. data/fixtures/test_helper.rb +3 -0
  40. data/lib/generators/phlex/collection/USAGE +8 -0
  41. data/lib/generators/phlex/collection/collection_generator.rb +13 -0
  42. data/lib/generators/phlex/collection/templates/collection.rb.erb +15 -0
  43. data/lib/generators/phlex/layout/USAGE +8 -0
  44. data/lib/generators/phlex/layout/layout_generator.rb +13 -0
  45. data/lib/generators/phlex/layout/templates/layout.rb.erb +30 -0
  46. data/lib/generators/phlex/page/USAGE +8 -0
  47. data/lib/generators/phlex/page/page_generator.rb +13 -0
  48. data/lib/generators/phlex/page/templates/page.rb.erb +11 -0
  49. data/lib/generators/phlex/table/USAGE +8 -0
  50. data/lib/generators/phlex/table/table_generator.rb +14 -0
  51. data/lib/generators/phlex/table/templates/table.rb.erb +9 -0
  52. data/lib/phlex/collection.rb +58 -0
  53. data/lib/phlex/engine.rb +0 -3
  54. data/lib/phlex/html.rb +7 -13
  55. data/lib/phlex/rails/helpers.rb +81 -0
  56. data/lib/phlex/rails/layout.rb +15 -0
  57. data/lib/phlex/table.rb +104 -0
  58. data/lib/phlex/version.rb +1 -1
  59. data/lib/phlex/view.rb +10 -4
  60. data/lib/phlex.rb +1 -0
  61. metadata +52 -3
  62. data/lib/phlex/rails/tag_helpers.rb +0 -29
data/docs/pages/views.rb CHANGED
@@ -3,107 +3,69 @@
3
3
  module Pages
4
4
  class Views < ApplicationPage
5
5
  def template
6
- render Layout.new(title: "Components in Phlex") do
6
+ render Layout.new(title: "Phlex Views") do
7
7
  render Markdown.new(<<~MD)
8
8
  # Views
9
9
 
10
- ## Yielding content
10
+ Phlex Views are Ruby objects that represent your app's user interface — from pages and layouts and nav-bars, to headings and buttons and links.
11
11
 
12
- Your views can accept content as a block passed to the template method. You can capture the content block and pass it to the `content` method to yield it.
12
+ You can create a view class by subclassing `Phlex::View` and defining a `template` instance method.
13
13
  MD
14
14
 
15
15
  render Example.new do |e|
16
- e.tab "card.rb", <<~RUBY
17
- class Card < Phlex::View
18
- def template(&)
19
- article(class: "drop-shadow rounded p-5") {
20
- h1 "Amazing content!"
21
- yield_content(&)
22
- }
16
+ e.tab "hello.rb", <<~RUBY
17
+ class Hello < Phlex::View
18
+ def template
19
+ h1 { "👋 Hello World!" }
23
20
  end
24
21
  end
25
22
  RUBY
26
23
 
27
- e.execute "Card.new.call { 'Your content here.\n' }"
24
+ e.execute "Hello.new.call"
28
25
  end
29
26
 
30
27
  render Markdown.new(<<~MD)
31
- ## Delegating content
28
+ The `template` method determines what your view will output when its rendered. The above example will output an `<h1>` tag with the content `👋 Hello world!`. Click on the "Output" tab above to see for yourself.
32
29
 
33
- Alternatively, you can pass the content down as an argument to another view or tag.
34
- MD
30
+ ## Accepting arguments
35
31
 
36
- render Example.new do |e|
37
- e.tab "card.rb", <<~RUBY
38
- class Card < Phlex::View
39
- def template(&)
40
- article(class: "drop-shadow rounded p-5", &)
41
- end
42
- end
43
- RUBY
32
+ You can define an initializer for your views just like any other Ruby class. Let's make our `Hello` view take a `name` as a keyword argument, save it in an instance variable and render that variable in the template.
44
33
 
45
- e.execute "Card.new.call { 'Your content here.' }"
46
- end
47
-
48
- render Markdown.new(<<~MD)
49
- ## Nested views
50
-
51
- Components can render other views and optionally pass them content as a block.
34
+ We'll render this view with the arguments `name: "Joel"` and see what it produces.
52
35
  MD
53
36
 
54
37
  render Example.new do |e|
55
- e.tab "example.rb", <<~RUBY
56
- class Example < Phlex::View
57
- def template
58
- render Card.new do
59
- h1 "Hello"
60
- end
38
+ e.tab "hello.rb", <<~RUBY
39
+ class Hello < Phlex::View
40
+ def initialize(name:)
41
+ @name = name
61
42
  end
62
- end
63
- RUBY
64
43
 
65
- e.tab "card.rb", <<~RUBY
66
- class Card < Phlex::View
67
- def template(&)
68
- article(class: "drop-shadow rounded p-5", &)
44
+ def template
45
+ h1 { "👋 Hello \#{@name}!" }
69
46
  end
70
47
  end
71
48
  RUBY
72
49
 
73
- e.execute "Example.new.call"
50
+ e.execute "Hello.new(name: 'Joel').call"
74
51
  end
75
52
 
76
53
  render Markdown.new(<<~MD)
77
- If the block just wraps a string, the string is treated as _text content_.
54
+ ## Rendering views
55
+
56
+ Views can render other views in their templates using the `render` method. Let's try rendering a couple of instances of this `Hello` view from a new `Example` view and look at the output of the `Example` view.
78
57
  MD
79
58
 
80
59
  render Example.new do |e|
81
60
  e.tab "example.rb", <<~RUBY
82
61
  class Example < Phlex::View
83
62
  def template
84
- render(Card.new) { "Hi" }
85
- end
86
- end
87
- RUBY
88
-
89
- e.tab "card.rb", <<~RUBY
90
- class Card < Phlex::View
91
- def template(&)
92
- article(class: "drop-shadow rounded p-5", &)
63
+ render Hello.new(name: "Joel")
64
+ render Hello.new(name: "Alexandre")
93
65
  end
94
66
  end
95
67
  RUBY
96
68
 
97
- e.execute "Example.new.call"
98
- end
99
-
100
- render Markdown.new(<<~MD)
101
- ## Component attributes
102
-
103
- Besides content, views can define attributes in an initializer, which can then be rendered in the template.
104
- MD
105
-
106
- render Example.new do |e|
107
69
  e.tab "hello.rb", <<~RUBY
108
70
  class Hello < Phlex::View
109
71
  def initialize(name:)
@@ -111,15 +73,7 @@ module Pages
111
73
  end
112
74
 
113
75
  def template
114
- h1 "Hello \#{@name}!"
115
- end
116
- end
117
- RUBY
118
-
119
- e.tab "example.rb", <<~RUBY
120
- class Example < Phlex::View
121
- def template
122
- render Hello.new(name: "Joel")
76
+ h1 { "👋 Hello \#{@name}!" }
123
77
  end
124
78
  end
125
79
  RUBY
@@ -128,32 +82,17 @@ module Pages
128
82
  end
129
83
 
130
84
  render Markdown.new(<<~MD)
131
- It’s usually a good idea to use instance variables directly rather than creating accessor methods for them. Otherwise it’s easy to run into naming conflicts. For example, your layout view might have the attribute `title`, to render into a `<title>` element in the document head. If you define `attr_accessor :title`, that would overwrite the `title` method for creating `<title>` elements.
85
+ ## Passing content blocks
132
86
 
133
- ## Calculations with methods
134
-
135
- Views are just Ruby classes, so you can perform calculations on view attributes by defining your own methods.
87
+ Views can also yield content blocks, which can be passed in when rendering. Let's make a `Card` component that yields content in an `<article>` element with a `drop-shadow` class on it.
136
88
  MD
137
89
 
138
90
  render Example.new do |e|
139
- e.tab "status.rb", <<~RUBY
140
- class Status < Phlex::View
141
- def initialize(status:)
142
- @status = status
143
- end
144
-
145
- def template
146
- span status_emoji
147
- end
148
-
149
- private
150
-
151
- def status_emoji
152
- case @status
153
- when :success
154
- "✅"
155
- when :failure
156
- "❌"
91
+ e.tab "card.rb", <<~RUBY
92
+ class Card < Phlex::View
93
+ def template(&content)
94
+ article(class: "drop-shadow") do
95
+ yield_content(&content)
157
96
  end
158
97
  end
159
98
  end
@@ -162,13 +101,35 @@ module Pages
162
101
  e.tab "example.rb", <<~RUBY
163
102
  class Example < Phlex::View
164
103
  def template
165
- render Status.new(status: :success)
104
+ render Card.new do
105
+ h1 { "👋 Hello!" }
106
+ end
166
107
  end
167
108
  end
168
109
  RUBY
169
110
 
170
111
  e.execute "Example.new.call"
171
112
  end
113
+
114
+ render Markdown.new(<<~MD)
115
+ The template in the `Card` view accepts a block argument `&content` and uses the `yield_content` method to yield it in an `<article>` element.
116
+
117
+ The `Example` view renders a `Card` and passes it a block with an `<h1>` element.
118
+
119
+ Looking at the output of the `Example` view, we can see the `<h1>` element was rendered inside the `<article>` element from the `Card` view.
120
+
121
+ ## Delegating content blocks
122
+
123
+ Since the block of content was the only thing we need in the `<article>` element, we could have just passed the content block to the element instead.
124
+
125
+ ```ruby
126
+ class Card < Phlex::View
127
+ def template(&content)
128
+ article(class: "drop-shadow", &content)
129
+ end
130
+ end
131
+ ```
132
+ MD
172
133
  end
173
134
  end
174
135
  end
@@ -0,0 +1,14 @@
1
+ <div>
2
+ <span>
3
+ <%= @name %>
4
+ </span>
5
+ <span>
6
+ <%= @body %>
7
+ </span>
8
+
9
+ <%= content %>
10
+
11
+ <%= render Views::Comments::Reaction.new(emoji: 'hamburger') do |reaction| %>
12
+ <p>Emoji reaction for a comment from <%= @name %> with body <%= @body %></p>
13
+ <% end %>
14
+ </div>
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CommentComponent < ViewComponent::Base
4
+ def initialize(name:, body:)
5
+ @name = name
6
+ @body = body
7
+ end
8
+ end
@@ -0,0 +1,3 @@
1
+ <p><%= @emoji %></p>
2
+
3
+ <%= content %>
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ReactionComponent < ViewComponent::Base
4
+ def initialize(emoji:)
5
+ @emoji = emoji
6
+ end
7
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CommentsController < ActionController::Base
4
+ end
@@ -3,6 +3,8 @@
3
3
  module Views
4
4
  module Articles
5
5
  class Form < Phlex::View
6
+ include Phlex::Rails::Helpers::FormWith
7
+
6
8
  def template
7
9
  form_with url: "test" do |f|
8
10
  f.text_field :name
@@ -7,7 +7,9 @@ module Views
7
7
  end
8
8
 
9
9
  def title(text)
10
- h3 text, class: "font-bold"
10
+ h3 class: "font-bold" do
11
+ text
12
+ end
11
13
  end
12
14
  end
13
15
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Views
4
+ module Comments
5
+ class Comment < Phlex::View
6
+ def initialize(name:, body:)
7
+ @name = name
8
+ @body = body
9
+ end
10
+
11
+ def template(&block)
12
+ div {
13
+ span { @name }
14
+ span { @body }
15
+
16
+ yield_content(&block)
17
+
18
+ render(::ReactionComponent.new(emoji: "hamburger")) do
19
+ p { "Emoji reaction for a comment from #{@name} with body #{@body}" }
20
+ end
21
+ }
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,3 @@
1
+ <%= render(CommentComponent.new(name: "Matz", body: "hey, folks")) do %>
2
+ Hello, World from a ViewComponent!
3
+ <% end %>
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Views
4
+ module Comments
5
+ class Reaction < Phlex::View
6
+ def initialize(emoji:)
7
+ @emoji = emoji
8
+ end
9
+
10
+ def template(&block)
11
+ p { @emoji }
12
+
13
+ yield_content(&block)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ <%= render Views::Comments::Comment.new(name: "_why", body: "I'm back") do %>
2
+ Hello, World from a Phlex Component!
3
+ <% end %>
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "phlex"
4
4
  require "bundler"
5
+ require "view_component"
5
6
 
6
7
  Bundler.require :test
7
8
 
@@ -11,3 +12,5 @@ Combustion.initialize! :action_controller do
11
12
  end
12
13
 
13
14
  require "view_helper"
15
+
16
+ Zeitwerk::Loader.eager_load_all
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Generates a Phlex collection view with the given name
3
+
4
+ Example:
5
+ rails generate phlex:collection Articles::List
6
+
7
+ This will create:
8
+ app/views/articles/list.rb
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlex
4
+ module Generators
5
+ class CollectionGenerator < ::Rails::Generators::NamedBase
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ def create_view
9
+ template "collection.rb.erb", File.join("app/views", class_path, "#{file_name}.rb")
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ <% module_namespacing do -%>
2
+ module Views
3
+ class <%= class_name %> < ApplicationView
4
+ include Phlex::Collection
5
+
6
+ def collection_template(&)
7
+ ul(&)
8
+ end
9
+
10
+ def item_template
11
+ li { @item }
12
+ end
13
+ end
14
+ end
15
+ <% end %>
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Generates a Phlex layout view with the given name
3
+
4
+ Example:
5
+ rails generate phlex:layout Layout
6
+
7
+ This will create:
8
+ app/views/layout.rb
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlex
4
+ module Generators
5
+ class LayoutGenerator < ::Rails::Generators::NamedBase
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ def create_view
9
+ template "layout.rb.erb", File.join("app/views", class_path, "#{file_name}.rb")
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,30 @@
1
+ <% module_namespacing do -%>
2
+ module Views
3
+ class <%= class_name %> < ApplicationView
4
+ include Phlex::Rails::Layout
5
+
6
+ def initialize(title:)
7
+ @title = title
8
+ end
9
+
10
+ def template(&)
11
+ doctype
12
+
13
+ html do
14
+ head do
15
+ meta charset: "utf-8"
16
+ csp_meta_tag
17
+ csrf_meta_tags
18
+ meta name: "viewport", content: "width=device-width,initial-scale=1"
19
+ title { @title }
20
+ stylesheet_link_tag "application"
21
+ end
22
+
23
+ body do
24
+ main(&)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ <% end %>
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Generates a Phlex page view with the given name
3
+
4
+ Example:
5
+ rails generate phlex:page Articles::Index
6
+
7
+ This will create:
8
+ app/views/articles/index.rb
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlex
4
+ module Generators
5
+ class PageGenerator < ::Rails::Generators::NamedBase
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ def create_view
9
+ template "page.rb.erb", File.join("app/views", class_path, "#{file_name}.rb")
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ <% module_namespacing do -%>
2
+ module Views
3
+ class <%= class_name %> < ApplicationView
4
+ def template
5
+ render Layout.new(title: "<%= class_name.gsub("::", " ") %>") do
6
+ h1 { "👋 Hello World!" }
7
+ end
8
+ end
9
+ end
10
+ end
11
+ <% end %>
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Generates a Phlex table collection view with the given name
3
+
4
+ Example:
5
+ rails generate phlex:collection Articles::Table --properties title author created_at
6
+
7
+ This will create:
8
+ app/views/articles/table.rb
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlex
4
+ module Generators
5
+ class TableGenerator < ::Rails::Generators::NamedBase
6
+ source_root File.expand_path("templates", __dir__)
7
+ class_option :properties, type: :array, default: []
8
+
9
+ def create_view
10
+ template "table.rb.erb", File.join("app/views", class_path, "#{file_name}.rb")
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ <% module_namespacing do -%>
2
+ module Views
3
+ class <%= class_name %> < ApplicationView
4
+ include Phlex::Table
5
+ <% options["properties"].each do |property| %>
6
+ property "<%= property.humanize %>", &:<%= property.underscore %><% end %>
7
+ end
8
+ end
9
+ <% end %>
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlex
4
+ module Collection
5
+ def initialize(collection: nil, item: nil)
6
+ unless collection || item
7
+ raise ArgumentError, "You must pass a collection or an item as a keyword argument."
8
+ end
9
+
10
+ @collection = collection
11
+ @item = item
12
+ end
13
+
14
+ def template
15
+ if @item
16
+ item_template
17
+ else
18
+ collection_template { yield_items }
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def yield_items
25
+ if @item
26
+ raise ArgumentError, "You can only yield_items when rendering a collection. You are currently rendering an item."
27
+ end
28
+
29
+ @collection.each_with_index do |item, index|
30
+ @item = item
31
+ @index = index
32
+ @position = (index + 1)
33
+ @first = (index == 0)
34
+ @last = (@position == @collection.size)
35
+
36
+ item_template
37
+ end
38
+
39
+ @item = nil
40
+ @index = nil
41
+ @first = nil
42
+ @last = nil
43
+ @position = nil
44
+ end
45
+
46
+ def first?
47
+ raise ArgumentError unless @item
48
+
49
+ @first
50
+ end
51
+
52
+ def last?
53
+ raise ArgumentError unless @item
54
+
55
+ @last
56
+ end
57
+ end
58
+ end
data/lib/phlex/engine.rb CHANGED
@@ -4,8 +4,5 @@ require "rails/engine"
4
4
 
5
5
  module Phlex
6
6
  class Engine < ::Rails::Engine
7
- initializer "phlex.tag_helpers" do
8
- Phlex::View.include(Phlex::Rails::TagHelpers)
9
- end
10
7
  end
11
8
  end
data/lib/phlex/html.rb CHANGED
@@ -113,6 +113,7 @@ module Phlex
113
113
  area: "area",
114
114
  br: "br",
115
115
  embed: "embed",
116
+ hr: "hr",
116
117
  img: "img",
117
118
  input: "input",
118
119
  link: "link",
@@ -130,10 +131,12 @@ module Phlex
130
131
  # frozen_string_literal: true
131
132
 
132
133
  def #{element}(content = nil, **attributes, &block)
134
+ if content
135
+ raise ArgumentError, %(👋 You can no longer pass content to #{element} as a positional argument.\n Instead, you can pass it as a block, e.g. #{element} { "Hello" })
136
+ end
137
+
133
138
  if attributes.length > 0
134
- if content
135
- @_target << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[attributes.hash] || _attributes(attributes)) << ">" << CGI.escape_html(content) << "</#{tag}>"
136
- elsif block_given?
139
+ if block_given?
137
140
  @_target << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[attributes.hash] || _attributes(attributes)) << ">"
138
141
  yield_content(&block)
139
142
  @_target << "</#{tag}>"
@@ -141,16 +144,7 @@ module Phlex
141
144
  @_target << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[attributes.hash] || _attributes(attributes)) << "></#{tag}>"
142
145
  end
143
146
  else
144
- if content
145
- case content
146
- when String
147
- @_target << "<#{tag}>" << CGI.escape_html(content) << "</#{tag}>"
148
- when Symbol
149
- @_target << "<#{tag}>" << CGI.escape_html(content.name) << "</#{tag}>"
150
- else
151
- @_target << "<#{tag}>" << CGI.escape_html(content.to_s) << "</#{tag}>"
152
- end
153
- elsif block_given?
147
+ if block_given?
154
148
  @_target << "<#{tag}>"
155
149
  yield_content(&block)
156
150
  @_target << "</#{tag}>"