dry-rb / dry-view Goto Github PK
View Code? Open in Web Editor NEWComplete, standalone view rendering system that gives you everything you need to write well-factored view code.
Home Page: https://dry-rb.org/gems/dry-view
License: MIT License
Complete, standalone view rendering system that gives you everything you need to write well-factored view code.
Home Page: https://dry-rb.org/gems/dry-view
License: MIT License
A common scenario: context object has a "current user", exposure needs to return some set of resources scoped to that user.
This will makes it easier to test view parts that don't have any internal calls to #render
.
As a bonus, it would be nice to provide a default renderer that returns a helpful error message in case of any calls to #render
, e.g. "no renderer available".
One of the objections people have raised to typical dry-view usage is that they don't like the "View objects" (i.e. subclasses of Dry::View::Layout
) injecting dependencies and getting objects from repos, etc.
My feeling is that this discomfort people have is because of our name, because we're calling these "view objects". I wonder if it would be better to call them "View Controllers" instead. This feels like a better name for what they actually do: receive input (often straight from the HTTP request), collaborate with other objects, prepare the scope for the template, and then render the template with that scope. In this case, the template is the view and the ruby code is actually a view controller.
WDYT?
Right now is missing from the dry-rb.org website
If a view controller is called like this:
basket = current_basket # grab a full object of some sort
vc.(basket: basket)
Then it would be nice if that view controller could pass that object straight through to the view via expose
:
class MyView < Dry::View::Controller
# configure etc.
expose :basket
end
If there's no matching method or block passed to expose, then we should generate one that simply passes through a matching value from the input.
Pulling this over from dry-rb/dry-web#23:
Once #9 (or equivalent) is merged, we should ship a gem that exemplifies how to bundle 3rd party dry-view templates.
This would make it clearer this object is actually coming from the layout class' scope
setting.
Right now, most of a view's logic sits inside a single #locals
method where it returns the hash of locals it wants passed to the template. This is not very nice, and leads to a lot of awkward methods like this.
It'd be better if we could have each of our view's locals provided via a dedicated method that is marked as a local or an "exposed" method, so instead of this:
def locals(options)
# setup stuff here
super.merge(
foo: something,
bar: something_else,
)
end
we have something like this:
local def foo
something
end
local def bar
something_else
end
Where local
or maybe expose
is a class-level macro that accepts a method name (e.g. local :foo
, like in that example above) and then makes it part of the set of locals passed to the template.
The tricky thing here is that our view instances here are long-lived, with all the per-request data coming into #call
only, so that data can't really sit on the instance and be available for instance methods to play with. We might be able to do some trickery to make it look like this though π
I just stumbled upon a use case where I wanted to recursively render a template via a part, based on "changing" value
, I ended up implementing Part#new
to simplify this:
def new(pages)
self.class.new(name: _name, value: pages, renderer: _renderer)
end
So how about we just add it to dry-view, because I'm 100% sure it'll be useful in many cases?
Originally reported in dry-rb/dry-web#19, I still think this is a reasonable change to make.
Instead of using a struct with a page accessor as the layout's scope, use a Rodakase::View::Part
object like we already do for the non-layout templates.
This allows rendering of partials via == partial_name
instead of having to chain them onto the page object (i.e. == page.partial_name
).
The general upshot is that it makes authoring layouts much more consistent with authoring templates by making the same partial rendering behaviour apply everywhere.
Having an issue where the underlying attributes of an entity passed to a view part seem to not be available as methods like Iβd expect.
Iβm initialisating this directly on a View::Context
object as I want them available across the app:
# auto_register: false
require "foo/view/context"
require "main/view/parts/thingie"
require "main/import"
module Main
module View
class Context < Foo::View::Context
include Import[
"thing_repo"
]
def things
thing_repo.listing.map { |thing|
Main::View::Parts::Thing.new(
name: :thing,
value: thing,
context: self
)
}
end
Hereβs the corresponding view part:
# auto_register: false
require "main/view/part"
module Main
module View
module Parts
class Thingie < Main::View::Part
end
end
end
end
Calling thingie.name
in the template results in:
undefined method `name' for #<Main::View::Parts::Thingie:0x00007f7f9941b048> Did you mean? _name
Accessing the value directly in the view part is fine:
def name
_value[:name]
end
When using render
provided by the scope object, and the template is not found, you get an exception that doesn't tell you much about what happened. It's inconsistent with how view objects work, where #call
would raise a nice error message when a template wasn't found.
Most Dry::View::Layout
subclasses only configure their template name, and nothing else. It would be handy to add a shorthand for this, e.g.
class MyView < MyApp::View["foo/bar"]
end
where "foo/bar"
is the template name.
I had to do the following workaround to make this work:
def self.inherited(klass)
super
exposures.exposures.each do |name, exposure|
klass.exposures.exposures[name] = exposure.dup
end
end
Sometimes, particularly for private exposures, you just don't want a part involved at all. We should support something like this:
expose :foo, decorate: false do
# ...
end
When you're exposing a collection object and specifying a view part class, e.g.
expose :articles, as: MyArticlePart
Then the article collection as well as the articles themselves is wrapped in MyArticlePart
.
This is a bug. In this case the collection should just be wrapped in a plain Dry::View::Part
with MyArticlePart
being reserved for wrapping the collection elements only.
It seems the to_s
isn't being called on path
in the error message. Calling to_s
ourselves does give us the right output though.
Puma caught this error: Template +things/index+ could not be found in paths:
- #<Dry::View::Path:0x00007fde84aafab8> (Dry::View::TemplateNotFoundError)
https://github.com/dry-rb/dry-view/blob/master/lib/dry/view/errors.rb#L20
Reason for this is that it should be possible to decorate an attribute exposed as an instance method directly on a part class (versus an attribute only defined as a method on the value).
Steps to reproduce:
hanami
(enhancement/container-actions-views
branch, at 95a194a
)Hanami::Loaders::View#configure
, uncomment the layout
linefeature/views
branch, at f5d70fd
)Gemfile
to point to local hanami
gembundle exec hanami server
)In some cases, you want to check to see something is truthy vs nil or falsey directly in a template, e.g.
- if some_thing
/ something here
- else
/ something else
This is difficult with dry-view since all the objects in a template are actually Part
objects, which are always truthy.
It would be good if there was some well-established API on these part objects that we could use to check for truthiness/falseyness instead of having to work around them and reach into their _value
accessor to check, e.g.
/ Current hack/workaround
- if some_thing._value
/ something here
Create a Rendered
class which olds the compiled exposures and the view's string output.
Here's what we have now.
Core, user-facing concepts:
index.html.slim
and index.json.yajl
). Multi-format support is provided in whole by Tilt.self
). Holds the locals (the parts returned from the exposures), the context, and other rendering-related facilities. Scopes can come as subclasses providing behaviour intended for specific templates or partials.Supporting concepts:
erb
and haml
).This better expresses its purpose and nicely matches "default_format"
Falsey part attributes are still decorated via the decorate
api. Only truthy values should be decorated.
We have an examples/rails/
example app in this repo, which demonstrates a very barebones integration. For better usage within Rails, we need a dry-view-rails integration gem providing a railtie, support for conventional paths, template/code reloading, etc.
To get started, you could copy the structure of the dry-system-rails gem.
Right now exposures only get access to 2 things:
input
as an entire hash,For example:
expose :greeting do |input, prefix|
# This is what we have to do if we want to require an input key
greeting = input.fetch(:greeting)
"#{prefix} #{greeting}"
end
expose :prefix
">> "
end
If we made a change to the block arguments structure, we could make it much easier to require input keys than by having to call input.fetch
every time.
We could change the arguments so the exposure dependencies come first, as regular positional arguments, followed by keyword arguments for getting specific keys from the input (i.e. what is passed to the view controller's #call
:
expose :greeting do |prefix, greeting:|
"#{prefix} #{greeting}"
end
expose :prefix do
">> "
end
With this change in arguments structure, we can now use required keyword arguments that will automatically bubble up exceptions when those arguments are missing from the input args. This is much nicer and easier to do than having to run input.fetch
everywhere.
We also get the added bonus of supporting default values for the keyword arguments!
Hello!
I'm trying to use dry-view in my latest project. It works find, however there is one inconvenience.
While I'm developing after I modify template I need to restart rack app in order to have view output updated (First render is fine, subsequent renders would use cached template instead of reparse updated template).
It would be cool if there was either setting to parse template on each request, or a method that could be called or something.
Is this is expected behavior?
EDIT:
At first I was thinking that it might be some weird hamlit issue, but I could reproduce same behavior with slim
EDIT 2:
For now I swapped dry-view to hanami-view since I'm just starting out (plus to see if the problem is not related, how I set up my app). with hanami-view I don't have this issue. Would still prefer to find a way to use dry-view.
I think this might be helpful:
Just make it so the "scope" on a Dry::View::Layout
is available on every template rendered from it. This would make e.g. providing a CSRF token (which has to come from the "outside world") to all the templates that get rendered in a view (both the top-level template and any constituent partials) much simpler.
This is a bit more coupling of the view object to the templates, but I think the extra convenience would make up for it. And we could make it clear that this is only intended for a few truly "global" or "environmental" things. It might fit also nicely if we're renaming Layout
to ViewController
too (#12).
Right now only a single "value" object is wrapped in a view part before it is passed to a template.
This is fine for many simple objects but when you're working with aggregates, the value's "children" should ideally be wrapped in view parts too.
It's possible to do this manually right now, e.g.
require_relative "product"
module Parts
class ProductCollection < Dry::View::Part
def products
@products ||= value.products.map { |product|
Parts::Product.new(
name: :product,
value: product,
renderer: _renderer,
context: _context,
)
}
end
end
end
But this is awkward and not the kind of experience we'd want to promote.
Instead, we should provide a way for a view part to declare which of its children it would want to be wrapped in parts, e.g.
require_relative "product"
module Parts
class ProductCollection < Dry::View::Part
decorate :products, as: Parts::Product
end
end
I'm thinking the decorate
API could accept the same kinds of arguments we'd use for expose
within the view controller class.
With this in place, we'd then need to do some work on the actual process of decorating the values in view parts:
Just like we do with the Dry::ViewPart.decorate
. Perhaps an opportunity to unify the context/part classes somehow?
Right now, applying any sort of custom behavior when decorating values with view parts requires the view controller's whole decorator to be replaced. This means a lot of extra setup/infrastructural code has to go into apps. It'd be good if we could build in some extra degree of customisation.
Things that would be nice to make easy, out of the box:
MyApp::View::Parts
)as:
to be a symbol to indicate an alternative name to use for the part class resolution noted aboveThese would help serve the uses we've seen in apps so far:
as:
options everywhere#pager
and #to_a
, then it's a paginated collection, so the value itself should be wrapped in a PaginatedResults
view part, and its children wrapped in parts according to the usual rulesIn some cases it is useful to know the format
of the current rendering from within e.g. view parts. We should make it possible to get at this.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
π Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. πππ
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google β€οΈ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.