README.md in keynote-1.1.1 vs README.md in keynote-2.0.0
- old
+ new
@@ -1,37 +1,52 @@
+[![Gem Version](https://badge.fury.io/rb/keynote.svg)](https://rubygems.org/gems/keynote)
+[![Build](https://github.com/evilmartians/keynote/workflows/Build/badge.svg)](https://github.com/evilmartians/keynote/actions)
+
# Keynote
-*Flexible presenters for Rails.*
+> [!TIP]
+> Flexible presenters for Rails.
-[![Travis CI](https://api.travis-ci.org/rf-/keynote.png)](https://travis-ci.org/rf-/keynote/builds)
-[![Code Climate](https://codeclimate.com/github/rf-/keynote.png)](https://codeclimate.com/github/rf-/keynote)
-
A presenter is an object that encapsulates view logic. Like Rails helpers,
presenters help you keep complex logic out of your templates.
Keynote provides a consistent interface for defining and instantiating
presenters.
+## Requirements
+
+- Ruby >= 3.0.0
+- Rails >= 7.0
+
+For older Ruby and Rails versions, you can use Keynote <2.0.
+
## Usage
+Add Keynote to your Gemfile:
+
+```ruby
+gem "keynote", "~> 2.0"
+```
+
+Don't forget to run `bundle install`.
+
### The basic idea
A simple case is making a presenter that's named after a model class and holds
helper methods related to that model.
-``` ruby
+```ruby
# app/presenters/user_presenter.rb
-
class UserPresenter < Keynote::Presenter
presents :user
def display_name
"#{user.first_name} #{user.last_name}"
end
def profile_link
- link_to user, display_name, data: { user_id: user.id }
+ link_to user, display_name, data: {user_id: user.id}
end
end
```
You can then instantiate it by calling the `present` method (aliased to `k`) in
@@ -53,36 +68,65 @@
after the class of that object -- in this case, the model is a `User`, so
Keynote looks for a class called `UserPresenter`.
### Generating HTML
-To make it easier to generate slightly more complex chunks of HTML, Keynote
-includes a modified version of Magnus Holm's [Rumble](https://github.com/judofyr/rumble)
+To make it easier to generate slightly more complex chunks of HTML, Keynote provides several ways to generate HTML fragments.
+
+#### Using `build_html`
+
+Keynote includes a modified version of Magnus Holm's [Rumble](https://github.com/judofyr/rumble)
library. Rumble gives us a simple block-based syntax for generating HTML
fragments. Here's a small example:
-``` ruby
+```ruby
build_html do
div id: :content do
- h1 'Hello World', class: :main
+ h1 "Hello World", class: :main
end
end
```
Becomes:
-``` html
+```html
<div id="content">
<h1 class="main">Hello World</h1>
</div>
```
You can use tag helpers like `div`, `span`, and `a` only within a block passed
to the `build_html` method. The `build_html` method returns a safe string. See
[the documentation for `Keynote::Rumble`](http://rubydoc.info/gems/keynote/Keynote/Rumble)
for more information.
+#### Using inlined partials
+
+You can extend your presenter class with the `Keynote::Inline` module to enable inline templating in any template language supported by Rails. This is useful for small, self-contained templates that don't need to be extracted into separate files.
+
+```ruby
+# app/presenters/user_presenter.rb
+class UserPresenter < Keynote::Presenter
+ presents :user
+
+ include Keynote::Inline
+ # To user Haml or Slim, enabled them explicitly
+ # inline :haml, :slim
+
+ def profile_link
+ erb do
+ <<~ERB
+ <div class="profile_link">
+ <%= link_to user, display_name, data: { user_id: user.id } %>
+ <i class="fa-user"></i>
+ </div>
+ ERB
+ end
+ end
+end
+```
+
### A more complex example
Let's add to our original example by introducing a named presenter. In addition
to `UserPresenter`, which has general-purpose methods for displaying the User
model, we'll create `HeaderPresenter`, which has methods that are specific to
@@ -156,101 +200,129 @@
```
You can also generate prefixed methods like `user_first_name` by passing
`prefix: true` to the `delegate` method.
+## Testing
+
+Testing a Keynote presenter is similar to using it in views or controllers. You
+can test presenters with RSpec, Test::Unit, MiniTest, or MiniTest::Unit.
+
+### RSpec
+
+Your test files should be in `spec/presenters` or labeled with
+[`type: :presenter` metadata].
+
+Here's an example:
+
+```ruby
+# spec/presenters/user_presenter_spec.rb
+
+RSpec.describe UserPresenter do
+ describe "#display_name" do
+ it "returns the name of the user" do
+ user = User.new(first_name: "Alice", last_name: "Smith")
+
+ expect(present(user).display_name).to eq("Alice Smith")
+ end
+ end
+end
+```
+
+### MiniTest
+
+Your test classes should inherit from Keynote::TestCase.
+
+```ruby
+class UserPresenterTest < Keynote::TestCase
+ setup do
+ user = User.new(first_name: "Alice", last_name: "Smith")
+ @presenter = present(user)
+ end
+
+ test "display name" do
+ assert_equal @presenter.display_name, "Alice Smith"
+ end
+end
+```
+
## Rationale
-### Why use presenters or decorators at all?
+### Why use presenters or decorators at all
The main alternative is to use helpers. Helpers are fine for many use cases --
Rails' built-in tag and form helpers are great. They have some drawbacks,
though:
-* Every helper method you write gets mixed into the same view object as the
+- Every helper method you write gets mixed into the same view object as the
built-in Rails helpers, URL generators, and all the other junk that comes
along with `ActionView::Base`. In a freshly-generated Rails project:
```ruby
- >> ApplicationController.new.view_context.public_methods.count
- => 318
- >> ApplicationController.new.view_context.private_methods.count
- => 119
+ ApplicationController.new.view_context.public_methods.count
+ # => 318
+ ApplicationController.new.view_context.private_methods.count
+ # => 119
```
-* Helpers can't have state that isn't "global" relative to the view, which
+- Helpers can't have state that isn't "global" relative to the view, which
can make it hard to write helpers that work together.
-* By default, every helper is available in every view. This makes it difficult
+- By default, every helper is available in every view. This makes it difficult
to set boundaries between different parts of your app and organize your view
code cleanly.
-### Why not use decorators?
+### Why not use decorators
The biggest distinction between Keynote and similar libraries like [Draper] and
[DisplayCase] is that Keynote presenters aren't decorators – undefined method
calls don't fall through to an underlying model.
Applying the Decorator pattern to generating views is a reasonable thing to do.
However, this practice also has some downsides.
-* Decorators make the most sense when there's exactly one object that's
+- Decorators make the most sense when there's exactly one object that's
relevant to the methods you want to encapsulate. They're less helpful when
you want to do things like define a class whose responsibility is to help
render a specific part of your user interface, which may involve bringing in
data from multiple models or collections.
-* When reading code that uses decorators, it often isn't obvious if a given
+- When reading code that uses decorators, it often isn't obvious if a given
method is defined on the decorator or the underlying model, especially when
the decorator is applied in the controller instead of the view.
-* Passing decorated models between controllers and views can make it unclear
+- Passing decorated models between controllers and views can make it unclear
whether a view (especially a nested partial) depends on a model having some
specific decorator applied to it. This makes refactoring view and decorator
code harder than it needs to be.
## Generators
Keynote doesn't automatically generate presenters when you generate models or
resources. To generate a presenter, you can use the `presenter` generator,
like so:
-``` bash
+```sh
$ rails g presenter FooBar foo bar
create app/presenters/foo_bar_presenter.rb
create spec/presenters/foo_bar_presenter_spec.rb
```
That project uses RSpec, but the generator can also create test files for
Test::Unit or MiniTest::Rails if applicable.
-## Compatibility
+## Contributing
-Keynote is supported on Rails 3.1 through 5.1. Keynote presenters are testable
-with Test::Unit, RSpec, and MiniTest::Rails (>= 2.0).
+Bug reports and pull requests are welcome on GitHub at [https://github.com/evilmartians/keynote](https://github.com/evilmartians/keynote).
-If you find problems with any of the above integrations, please open an issue.
+For gem development, clone the repo and run `bundle install` to install the dependencies. Then, run `bundle exec rake` to run the tests.
-## Development
+(Optionally) Run `bundle exec lefthook install` to configure git hooks (so you never miss linter complaints before opening a PR).
-This repo uses [Roadshow] to generate a [Docker Compose] file for each
-supported version of Rails (with a compatible version of Ruby for each one).
+## License
-To run specs across all versions, you can either [get the Roadshow tool] and
-run `roadshow run`, or use Docker Compose directly:
+The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
-```
-$ for fn in scenarios/*.docker-compose-yml; do docker-compose -f $fn run --rm scenario; done
-```
-
-To update the set of scenarios, edit `scenarios.yml` and run `roadshow
-generate`, although the Gemfiles in the `scenarios` directory need to be
-maintained manually.
-
-Feel free to submit pull requests according to the usual conventions for Ruby
-projects.
-
[DisplayCase]: https://github.com/objects-on-rails/display-case
[Draper]: https://github.com/drapergem/draper
[Roadshow]: https://github.com/rf-/roadshow
-[Docker Compose]: https://docs.docker.com/compose/
-[get the Roadshow tool]: https://github.com/rf-/roadshow/releases
+[`type: :presenter` metadata]: https://relishapp.com/rspec/rspec-rails/docs/directory-structure