Skip to content

Commit

Permalink
Merge branch 'main' into remove-p-shorthand-post-deprecation
Browse files Browse the repository at this point in the history
* main:
  Ignore /tmp if you're running BT in tmp/starter like CI does
  Fix some CHANGELOG issues and refit some examples + words
  Skip needless CI steps (#60)
  We don't need to depend on sqlite3 that was for an earlier version of the ViewComponent integration
  Fix #54 to use new section yield syntax
  Mixing `yield` call styles (#54)
  Expose Sections as an alternative to `content_for` (#57)
  Introduce Capybara to the test suite (#55)
  Add `content_from` to let partials relay contents (#53)
  v0.1.9
  v0.1.8
  Document release 🎉 (#52)
  Rename `Partial#output_buffer` to `Partial#yield` (#41)
  Allow outer partial access when capturing (#49)
  Fix accessing `partial` before rendering leaks state (#47)
  Use load hooks for monkey_patch loading (#48)
  • Loading branch information
kaspth committed Sep 6, 2022
2 parents 00bf2c1 + 0aed3aa commit e3e3880
Show file tree
Hide file tree
Showing 27 changed files with 654 additions and 107 deletions.
16 changes: 8 additions & 8 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,23 +42,23 @@ aliases:
run: dockerize -wait tcp://localhost:5432 -timeout 1m
jobs:
'Local Minitest':
docker:
- <<: *ruby_node_browsers_docker_image
- <<: *postgres_docker_image
- image: circleci/redis
# docker:
# - <<: *ruby_node_browsers_docker_image
# - <<: *postgres_docker_image
# - image: circleci/redis
executor: ruby/default
steps:
- browser-tools/install-browser-tools
# - browser-tools/install-browser-tools
- checkout

# Install dependencies
- run: "bundle install"
- run: "bundle clean --force"
- run: "yarn install"
- *wait_for_docker
# - run: "yarn install"
# - *wait_for_docker
- run:
name: Run unit tests
command: bundle exec rails test
command: bin/test

'Local Standard Ruby':
docker:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
Gemfile.lock
/tmp
/pkg
*~
156 changes: 155 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,163 @@
## CHANGELOG

* Let partials respond to named content sections

```erb
<% partial.content_for :title, "Title content" %> # Before
<% partial.title "Title content" %> # After
# Which can then be output
<% partial.title %> # => "Title content"
<% partial.title? %> # => true
```

Note, `title` responds to `present?` so rendering could also be made conditional with:

```erb
<% partial.title if partial.title? %> # Instead of this…
<% partial.title.presence %> # …you can do this
```

#### Passing procs or components

Procs and objects that implement `render_in`, like ViewComponents, can also be appended as content:

```erb
<% partial.title { "some content" } %>
<% partial.title TitleComponent.new(Current.user) %>
```

#### Capturing `options`

Options can also be captured and output:

```erb
<% partial.title class: "text-m4" %> # partial.title.options # => { class: "text-m4" }
# When output `to_s` is called and options automatically pipe through `tag.attributes`:
<h1 <% partial.title.options %>> # => <h1 class="text-m4">
```

#### Proxying to the view context and appending content

A content section appends to its content when calling any view context method on it, e.g.:

```erb
<% partial.title.t ".title" %>
<% partial.title.link_to @document.name, @document %>
<% partial.title.render "title", user: Current.user %>
<% partial.title.render TitleComponent.new(Current.user) do |component| %>
<% … %>
<% end %>
```

#### Building elements with `tag` proxy

These `tag` calls let you generate elements based on the stored content and options:

```erb
<% partial.title "content", class: "post-title" %> # Adding some content and options…
<% partial.title.h2 %> # => <h2 class="post-title">content</h2>
<% partial.title.h2 "more" %> # => <h2 class="post-title">contentmore</h2>
```

* Add `NicePartials#t` to aid I18n.

When using NicePartials with I18n you end up with lots of calls that look like:

```erb
<% partial.title t(".title") %>
<% partial.description t(".header") %>
<% partial.byline t("custom.key") %>
```

With NicePartials' `t` method, you can write the above as:

```erb
<% partial.t :title, description: :header, byline: "custom.key" %>
```

Clarifying what keys get converted to what content sections on the partial rather than the syntax heavy `partial.… t(".…")`.

Like the Rails built-in `t` method, it's just a shorthand alias for `translate` so that's available too.

* Add `Partial#content_from`

`content_from` lets a partial extract contents from another partial.
Additionally, contents can be renamed by passing a hash:

```erb
<% partial.title "Hello there" %>
<% partial.byline "Somebody" %>
<%= render "shared/title" do |cp| %>
# Here the inner partial `cp` accesses the outer partial through `partial`
# extracting the `title` and `byline` contents.
# `byline` is renamed to `name` in `cp`.
<% cp.content_from partial, :title, byline: :name %>
<% end %>
```

### 0.1.9

* Remove need to insert `<% yield p = np %>` in partials.

Nice Partials now automatically captures blocks passed to `render`.
Instead of `p`, a `partial` method has been added to access the
current `NicePartials::Partial` object.

Here's a script to help update your view code:

```ruby
files_to_inspect = []

Dir["app/views/**/*.html.erb"].each do |path|
if contents = File.read(path).match(/(<%=? yield\(?.*? = np\)? %>\n+)/m)&.post_match
files_to_inspect << path if contents.match?(/render.*?do \|/)

contents.gsub! /\bp\.(?=yield|helpers|content_for|content_for\?)/, "partial."
File.write path, contents
end
end

if files_to_inspect.any?
puts "These files had render calls with a block parameter and likely require some manual edits:"
puts files_to_inspect
else
puts "No files with render calls with a block parameter found, you're likely all set"
end
```

* Support manual `yield`s in partials.

Due to the automatic yield support above, support has also been added for manual `yield some_object` calls.

Nice Partials automatically appends the `partial` to the yielded arguments, so you can
change `render … do |some_object|` to `render … do |some_object, partial|`.

* Deprecate `p` as the partial object access. Use `partial` instead.

* Expose `partial.yield` to access the captured output buffer.

Lets you access what a `<%= yield %>` call returned, like this:

```erb
<%= render "card" do %>
This is the content of the internal output buffer
<% end %>
```

```erb
# app/views/cards/_card.html.erb
# This can be replaced with `partial.yield`.
<%= yield %> # Will output "This is the content of the internal output buffer"
```

### 0.1.7

* Rely on `ActiveSupport.on_load :action_view`
* Add support for Ruby 3.0

### 0.1.0

* Initial release

4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ gemspec
gem "minitest"
gem "rake"
gem "irb"

gem "view_component"
gem "rails"
gem "capybara"
30 changes: 24 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# nice_partials [![[version]](https://badge.fury.io/rb/nice_partials.svg)](https://badge.fury.io/rb/nice_partials) [![[travis]](https://travis-ci.org/andrewculver/nice_partials.svg)](https://travis-ci.org/andrewculver/nice_partials)

Nice Partials extends the concept of [`content_for` blocks and `yield`](https://guides.rubyonrails.org/layouts_and_rendering.html#using-the-content-for-method) for those times when a partial needs to provide one or more named "content areas" or "slots". This thin, optional layer of magic helps make traditional Rails view partials an even better fit for extracting components from your views, like so:
Nice Partials lets [`content_for` and `yield`](https://guides.rubyonrails.org/layouts_and_rendering.html#using-the-content-for-method) calls be partial specific, to provide named "content areas" or "slots". This optional layer of magic helps make traditional Rails view partials a better fit for extracting components from your views, like so:

`app/views/components/_card.html.erb`:
```html+erb
Expand All @@ -17,7 +17,7 @@ Nice Partials extends the concept of [`content_for` blocks and `yield`](https://
</div>
```

These partials can still be utilized with a standard `render` call, but you can specify how to populate the content areas like so:
Then you can call `render`, but specify how to populate the content areas:

```html+erb
<%= render 'components/card', title: 'Some Title' do |partial| %>
Expand Down Expand Up @@ -106,20 +106,38 @@ You only need to use Nice Partials when:

### Using Nice Partials

Nice Partials is invoked automatically when you render your partial with a block that takes a single parameter like so:
Nice Partials is invoked automatically when you render your partial with a block like so:

```html+erb
<%= render 'components/card' do |partial| %>
<%= partial.content_for :some_section %>
<%= partial.content_for :some_section do %>
Some content!
<% end %>
<% end %>
```

It's always been natural to pass blocks to a partial in Rails, but not to pass blocks that take parameters, so when you do this, we know it's a partial that uses Nice Partials.

Now within the partial file itself, you can use `<%= partial.yield :some_section %>` to render whatever content areas you want to be passed into your partial.

### Accessing the content returned from `yield`

In a regular Rails partial:

```html+erb
<%= render 'components/card' do %>
Some content!
Yet more content!
<% end %>
```

You can access the inner content lines through what's returned from `yield`:

```html+erb
<%# app/views/components/_card.html.erb %>
<%= yield %> # => "Some content!\n\nYet more content!"
```

With Nice Partials, you can call `partial.yield` without arguments and return the same `"Some content!\n\nYet more content!"`.

### Defining and using well isolated helper methods

To minimize the amount of pollution in the global helper namespace, you can use the shared context object to define helper methods specifically for your partials _within your partial_ like so:
Expand Down
9 changes: 0 additions & 9 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,3 @@ desc "#{gemspec.name} | IRB"
task :irb do
sh "irb -I ./lib -r #{gemspec.name.gsub '-','/'}"
end

# # #
# Run specs

desc "#{gemspec.name} | Test"
task :test do
sh "for file in test/{**/,}*_test.rb; do ruby -Ilib:test $file; done"
end
task default: :test
5 changes: 5 additions & 0 deletions bin/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env ruby
$: << File.expand_path("../test", __dir__)

require "bundler/setup"
require "rails/plugin/test"
5 changes: 3 additions & 2 deletions lib/nice_partials.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# frozen_string_literal: true

require_relative "nice_partials/version"
require_relative "nice_partials/helper"
require_relative "nice_partials/monkey_patch"

module NicePartials
def self.locale_prefix_from(lookup_context, block)
Expand All @@ -14,5 +12,8 @@ def self.locale_prefix_from(lookup_context, block)
end

ActiveSupport.on_load :action_view do
require_relative "nice_partials/monkey_patch"

require_relative "nice_partials/helper"
include NicePartials::Helper
end
47 changes: 39 additions & 8 deletions lib/nice_partials/monkey_patch.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,31 +29,62 @@ def with_nice_partials_t_prefix(lookup_context, block)
module NicePartials::RenderingWithAutoContext
ActionView::Base.prepend self

def partial
@partial ||= nice_partial
def __partials
@__partials ||= NicePartials::Partial::Stack.new
end
delegate :partial, to: :__partials

def render(options = {}, locals = {}, &block)
_partial = @partial
# Overrides `ActionView::Helpers::RenderingHelper#render` to push a new `nice_partial`
# on the stack, so rendering has a fresh `partial` to store content in.
def render(*)
__partials.prepend nice_partial
super
ensure
@partial = _partial
__partials.shift
end

# Since Action View passes any `yield`s in partials through `_layout_for`, we
# override `_layout_for` to detects if it's a capturing yield and append the
# current partial to the arguments.
#
# So `render … do |some_object|` can become `render … do |some_object, partial|`
# without needing to find and update the inner `yield some_object` call.
def _layout_for(*arguments, &block)
if block && !arguments.first.is_a?(Symbol)
partial.capture(*arguments, &block)
capture_with_outer_partial_access(*arguments, &block)
else
super
end
end

# Reverts `partial` to return the outer partial before the `render` call started.
#
# So we don't clobber the `partial` shown here:
#
# <%= render "card" do |inner_partial| %>
# <% inner_partial.content_for :title, partial.content_for(:title) %>
# <% end %>
#
# Note: this happens because the `@partial` instance variable is shared between all
# `render` calls since rendering happens in one `ActionView::Base` instance.
def capture_with_outer_partial_access(*arguments, &block)
__partials.locate_previous
__partials.first.capture(*arguments, &block)
ensure
__partials.reset_locator
end
end

module NicePartials::PartialRendering
ActionView::PartialRenderer.prepend self

# Automatically captures the `block` in case the partial has no manual capturing `yield` call.
#
# This manual equivalent would be inserting this:
#
# <% yield partial %>
def render_partial_template(view, locals, template, layout, block)
view.partial.capture(&block) if block && !template.has_capturing_yield?
view.capture_with_outer_partial_access(&block) if block && !template.has_capturing_yield?
super
end
end
Expand All @@ -72,6 +103,6 @@ module NicePartials::CapturingYieldDetection
# Note: `<%= yield %>` becomes `yield :layout` with no `render` `block`, though this method assumes a block is passed.
def has_capturing_yield?
defined?(@has_capturing_yield) ? @has_capturing_yield :
@has_capturing_yield = source.match?(/\byield[\(? ]+(%>|[^:])/)
@has_capturing_yield = source.match?(/[^\.\b]yield[\(? ]+(%>|[^:])/)
end
end
Loading

0 comments on commit e3e3880

Please sign in to comment.