Giter Site home page Giter Site logo

dry-rb / dry-view Goto Github PK

View Code? Open in Web Editor NEW
149.0 16.0 19.0 789 KB

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

Ruby 98.55% HTML 1.45%
view html-renderer uiview mvc ruby ruby-gem ruby-library tilt slim haml

dry-view's Introduction

dry-view Gem Version CI Status

Links

Supported Ruby versions

This library officially supports the following Ruby versions:

  • MRI >= 3.0.0
  • jruby >= 9.4 (not tested on CI)

License

See LICENSE file.

dry-view's People

Contributors

actions-user avatar adam12 avatar dry-bot avatar flash-gordon avatar gotar avatar gustavocaso avatar katafrakt avatar kwando avatar liseki avatar olegykz avatar olleolleolle avatar parndt avatar skryukov avatar solnic avatar timriley avatar waiting-for-dev avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

dry-view's Issues

Create dry-view-rails integration gem

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.

Entity attributes not being called properly with manual initialisation

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

Provide example app using Rails

  • Generate basic Rails app
  • Build a context class that includes all of Rails' default view helpers
  • Something to make dry-view fetching/rendering from a controller nicer?
  • Examples of some actual views

Allow direct passthrough of input value via expose

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.

Support required input keys via exposure block arguments

Right now exposures only get access to 2 things:

  1. the input as an entire hash,
  2. and any other other exposure dependencies

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!

Rename Layout to ViewController?

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?

Decorate view part children as parts

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:

  1. The decorator would need to query the part class for the child values it has declared that it wants to be decorated
  2. The decorator would then decorate those child values first (recursively going through these steps 1 & 2 to allow for children of children etc. to be decorated)
  3. Once the decorator has collected up all the children and turned them into view parts, it'd then decorate the top-level value and pass all the decorated children in at the same time

Template isn't reloaded until rack app is restarted.

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.

Use a part object as the layout template's scope

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.

Make scope available everywhere

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).

Reconsider terminology

Here's what we have now.

Core, user-facing concepts:

  • Controller — the main entry point to a view
  • Template — holds the view's contents, configured per-controller
  • Partial — a template fragment intended for re-use from loops or multiple templates
  • Format — a specific representation of a view, via a named template. Allows a a single view to be rendered in different ways, via differing template extensions (e.g. index.html.slim and index.json.yajl). Multi-format support is provided in whole by Tilt.
  • Exposure — defined within each controller, determines one of the values that are made available to the template. Exposures can have exposure dependencies too, where one exposure has access to the value from another.
  • Part — wraps the value returned from an exposure, has access to rendering-related facilities. Parts can come as custom subclasses providing view-specific behaviour around different values.
  • Scope — the rendering context for a template (i.e. the template's 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.
  • Context — an object whose methods are made available to all templates via the scope. This makes it "global", the baseline environment for an entire rendering (for both templates and parts).

Supporting concepts:

  • Part builder — configurable object that determines the part class to use when wrapping exposure values. A framework like Hanami might provide its own scope builder when integrating dry-view.
  • Part namespace — a configurable module used by part builders to determine how part classes are looked up
  • Scope builder — configurable object that determines the scope class to use when preparing scopes. A framework like Hanami might provide its own scope builder when integrating dry-view.
  • Scope namespace — a configurable module used by scope builders to determine how scope classes are looked up
  • Tilt adapter — low-level shim that ensures a compatible template engine is used when rendering templates in particular languages (right now, erb and haml).

Collections should not be wrapped in view part classes intended for their elements

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.

Add `Part#new`

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?

Improve performance

Our benchmark shows that dry-view is ~2.5x slower (in this specific benchmark at least) than ActionView. After quick profiling, you can see that template? is extremely expensive:

dry-view-profile

Make renderer optional for initialising view parts

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".

stack level too deep (SystemStackError) when using layout

Steps to reproduce:

  1. Clone locally hanami (enhancement/container-actions-views branch, at 95a194a)
  2. From the Hanami codebase find: Hanami::Loaders::View#configure, uncomment the layout line
  3. Clone locally Soundeck (https://github.com/jodosha/soundeck.git, feature/views branch, at f5d70fd)
  4. Make Soundeck Gemfile to point to local hanami gem
  5. Start the server for Soundeck app (bundle exec hanami server)
  6. Visit http://localhost:2300/
  7. The app crashes

Exposures are not inherited in sub-classes

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

Make it easier to check for falsy view parts inside templates

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

Improve standard part class resolution

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:

  • Resolving part classes from a specified namespace (e.g. MyApp::View::Parts)
  • Allowing as: to be a symbol to indicate an alternative name to use for the part class resolution noted above
  • Running user-supplied arbitrary code to make additional assessments of a value before determining its part class
  • Allowing the above steps to be re-run for constituent members of a collection or aggregate value

These would help serve the uses we've seen in apps so far:

  • Automatically wrap values in part classes that match their name, so there's no need to explicitly specify classes with as: options everywhere
  • If a value responds to #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 rules

Crash when a template is missing

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.

Make definition of locals nicer

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 😏

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.