diff --git a/CHANGELOG.md b/CHANGELOG.md index a4449c35..0934f409 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Draper Changelog +## Unreleased + +### New Features +* Delegate missing constants to the object class via `const_missing` when using `delegate_all` [#955](https://github.com/drapergem/draper/pull/955) + ## 4.0.6 - 2025-11-15 ### Fixes diff --git a/README.md b/README.md index 588434b3..271afc47 100644 --- a/README.md +++ b/README.md @@ -547,6 +547,21 @@ the decorator will first try to call the method on the parent decorator class. I the method does not exist on the parent decorator class, it will then try to call the method on the decorated `object`. This is a very permissive interface. +Constants defined on the source model class are also accessible via the decorator +class when using `delegate_all`: + +```ruby +class Car + WHEELS_COUNT = 4 +end + +class CarDecorator < ApplicationDecorator + delegate_all +end + +CarDecorator::WHEELS_COUNT # => 4 +``` + If you want to strictly control which methods are called within views, you can choose to only delegate certain methods from the decorator to the source model: diff --git a/lib/draper/automatic_delegation.rb b/lib/draper/automatic_delegation.rb index 0546f4c4..97f2310f 100644 --- a/lib/draper/automatic_delegation.rb +++ b/lib/draper/automatic_delegation.rb @@ -49,6 +49,13 @@ def respond_to_missing?(method, include_private = false) super || delegatable?(method) end + # Proxies missing constants to the source class. + def const_missing(name) + return object_class.const_get(name) if object_class? && object_class.const_defined?(name) + + super + end + # @private def delegatable?(method) object_class? && object_class.respond_to?(method) diff --git a/spec/draper/decorator_spec.rb b/spec/draper/decorator_spec.rb index 5536c696..4bf181cd 100644 --- a/spec/draper/decorator_spec.rb +++ b/spec/draper/decorator_spec.rb @@ -722,6 +722,30 @@ def hello_world end end + context ".const_missing" do + context "without an object class" do + it "raises a NameError on missing constants" do + expect{Decorator::HELLO_WORLD}.to raise_error NameError, /HELLO_WORLD/ + end + end + + context "with an object class" do + it "delegates constants that exist on the object class" do + object_class = Class.new + object_class.const_set(:HELLO_WORLD, :delegated) + allow(Decorator).to receive_messages object_class: object_class + + expect(Decorator::HELLO_WORLD).to be :delegated + end + + it "raises a NameError for constants that do not exist on the object class" do + allow(Decorator).to receive_messages object_class: Class.new + + expect{Decorator::HELLO_WORLD}.to raise_error NameError, /HELLO_WORLD/ + end + end + end + describe "#respond_to?" do it "returns true for its own methods" do Decorator.class_eval{def hello_world; end}