Skip to content

Commit

Permalink
Use the elements finder in render_elements helper
Browse files Browse the repository at this point in the history
If you have custom element finding logic, you can pass your own elements finder instance to the helper as options[:finder]. This custom class needs to implement the interface of Alchemy::ElementsFinder.
  • Loading branch information
tvdeyen committed Feb 21, 2019
1 parent 342d6b3 commit efc1ef1
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 192 deletions.
129 changes: 46 additions & 83 deletions app/helpers/alchemy/elements_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ module ElementsHelper
include Alchemy::UrlHelper
include Alchemy::ElementsBlockHelper

# Renders all elements from current page
# Renders elements from given page
#
# == Examples:
#
Expand Down Expand Up @@ -45,53 +45,69 @@ module ElementsHelper
# with: 'contact_teaser'
# }) %>
#
# @param [Hash] options
# Additional options.
# === Custom elements finder:
#
# Having a custom element finder class:
#
# class MyCustomNewsArchive
# def elements(page:)
# news_page.elements.available.named('news').order(created_at: :desc)
# end
#
# private
#
# def news_page
# Alchemy::Page.where(page_layout: 'news-archive')
# end
# end
#
# In your view:
#
# <div class="news-archive">
# <%= render_elements finder: MyCustomNewsArchive.new %>
# </div>
#
# @option options [Alchemy::Page|String] :from_page (@page)
# The page the elements are rendered from. You can pass a page_layout String or a {Alchemy::Page} object.
# @option options [Array<String>|String] :only
# A list of element names only to be rendered.
# @option options [Array<String>|String] :except
# A list of element names not to be rendered.
# @option options [Number] :count
# The amount of elements to be rendered (begins with first element found)
# @option options [Array or String] :except ([])
# A list of element names not to be rendered.
# @option options [Number] :offset
# The offset to begin loading elements from
# @option options [Hash] :fallback
# Define elements that are rendered from another page.
# @option options [Alchemy::Page or String] :from_page (@page)
# The page the elements are rendered from. You can pass a page_layout String or a {Alchemy::Page} object.
# @option options [Array or String] :only ([])
# A list of element names only to be rendered.
# @option options [Boolean] :random
# @option options [Boolean] :random (false)
# Randomize the output of elements
# @option options [Boolean] :reverse
# @option options [Boolean] :reverse (false)
# Reverse the rendering order
# @option options [String] :sort_by
# The name of a {Alchemy::Content} to sort the elements by
# @option options [String] :separator
# A string that will be used to join the element partials. Default nil
# A string that will be used to join the element partials.
# @option options [Class] :finder (Alchemy::ElementsFinder)
# A class instance that will return elements that get rendered.
# Use this for your custom element loading logic in views.
#
def render_elements(options = {})
options = {
from_page: @page,
render_format: 'html',
reverse: false
render_format: 'html'
}.update(options)

pages = pages_holding_elements(options.delete(:from_page))

if pages.blank?
warning('No page to get elements from was found')
return
if options[:sort_by]
Alchemy::Deprecation.warn "options[:sort_by] has been removed without replacement. " /
"Please implement your own element sorting by passing a custom finder instance to options[:finder]."
end

elements = collect_elements_from_pages(pages, options)
finder = options[:finder] || Alchemy::ElementsFinder.new(options)
elements = finder.elements(page: options[:from_page])

if options[:sort_by].present?
elements = sort_elements_by_content(
elements,
options.delete(:sort_by),
options[:reverse]
)
buff = []
elements.each_with_index do |element, i|
buff << render_element(element, :view, options, i + 1)
end

render_element_view_partials(elements, options)
buff.join(options[:separator]).html_safe
end

# This helper renders a {Alchemy::Element} partial.
Expand Down Expand Up @@ -246,58 +262,5 @@ def sort_elements_by_content(elements, content_name, reverse = false)

reverse ? sorted_elements.reverse : sorted_elements
end

private

def pages_holding_elements(page)
case page
when String
Language.current.pages.where(
page_layout: page,
restricted: false
).to_a
when Page
page
end
end

def collect_elements_from_pages(page, options)
if page.is_a? Array
elements = page.collect { |p| p.find_elements(options) }.flatten
else
elements = page.find_elements(options)
end
if fallback_required?(elements, options)
elements += fallback_elements(options)
end
elements
end

def fallback_required?(elements, options)
options[:fallback] && elements.detect { |e| e.name == options[:fallback][:for] }.nil?
end

def fallback_elements(options)
fallback_options = options.delete(:fallback)
case fallback_options[:from]
when String
page = Language.current.pages.find_by(
page_layout: fallback_options[:from],
restricted: false
)
when Page
page = fallback_options[:from]
end
return [] if page.blank?
page.elements.not_trashed.named(fallback_options[:with].presence || fallback_options[:for])
end

def render_element_view_partials(elements, options = {})
buff = []
elements.each_with_index do |element, i|
buff << render_element(element, :view, options, i + 1)
end
buff.join(options[:separator]).html_safe
end
end
end
132 changes: 23 additions & 109 deletions spec/helpers/alchemy/elements_helper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,146 +68,60 @@ module Alchemy
describe "#render_elements" do
subject { helper.render_elements(options) }

let(:another_element) { build_stubbed(:alchemy_element, page: page) }
let(:elements) { [element, another_element] }
let(:page) { create(:alchemy_page, :public) }
let!(:element) { create(:alchemy_element, name: 'headline', page: page) }
let!(:another_element) { create(:alchemy_element, page: page) }

context 'without any options' do
let(:options) { {} }

before do
expect(page).to receive(:find_elements).and_return(elements)
end

it "should render all elements from page." do
it "should render all elements from current page." do
is_expected.to have_selector("##{element.name}_#{element.id}")
is_expected.to have_selector("##{another_element.name}_#{another_element.id}")
end
end

context "with from_page option" do
context 'is a page object' do
let(:another_page) { build_stubbed(:alchemy_page, :public) }
let(:options) { {from_page: another_page} }
let(:another_page) { create(:alchemy_page, :public) }

before do
expect(another_page).to receive(:find_elements).and_return(elements)
let(:options) do
{ from_page: another_page }
end

let!(:element) { create(:alchemy_element, name: 'headline', page: another_page) }
let!(:another_element) { create(:alchemy_element, page: another_page) }

it "should render all elements from that page." do
is_expected.to have_selector("##{element.name}_#{element.id}")
is_expected.to have_selector("##{another_element.name}_#{another_element.id}")
end
end

context 'is a string' do
let(:another_page) { build_stubbed(:alchemy_page, :public) }
let(:another_element) { build_stubbed(:alchemy_element, page: another_page) }
let(:other_elements) { [another_element] }
let(:options) { {from_page: 'news'} }

before do
allow(Language).to receive(:current).and_return double(pages: double(where: pages))
expect(another_page).to receive(:find_elements).and_return(other_elements)
end

context 'and one page can be found by page layout' do
let(:pages) { [another_page] }

it "it renders all elements from that page." do
is_expected.to have_selector("##{another_element.name}_#{another_element.id}")
end
end

context 'and an array of pages has been found' do
let(:pages) { [page, another_page] }

before do
expect(page).to receive(:find_elements).and_return(elements)
end

it 'renders elements from these pages' do
is_expected.to have_selector("##{element.name}_#{element.id}")
is_expected.to have_selector("##{another_element.name}_#{another_element.id}")
end
context 'if from_page is nil' do
let(:options) do
{ from_page: nil }
end
end
end

context 'if page is nil' do
let(:options) { {from_page: nil} }
it { is_expected.to be_blank }
end

context 'with sort_by and reverse option given' do
let(:options) { {sort_by: true, reverse: true} }
let(:sorted_elements) { [another_element, element] }

before do
expect(elements).to receive(:sort_by).and_return(sorted_elements)
expect(sorted_elements).to receive(:reverse).and_return(elements)
expect(page).to receive(:find_elements).and_return(elements)
end

it "renders the sorted elements in reverse order" do
is_expected.not_to be_blank
end
end

context 'with sort_by option given' do
let(:options) { {sort_by: 'title'} }
let(:sorted_elements) { [another_element, element] }

before do
expect(elements).to receive(:sort_by).and_return(sorted_elements)
expect(elements).not_to receive(:reverse)
expect(page).to receive(:find_elements).and_return(elements)
end

it "renders the elements in the order of given content name" do
is_expected.not_to be_blank
it { is_expected.to be_empty }
end
end

context "with option fallback" do
let(:another_page) { build_stubbed(:alchemy_page, :public, name: 'Another Page', page_layout: 'news') }
let(:another_element) { build_stubbed(:alchemy_element, page: another_page, name: 'news') }
let(:elements) { [another_element] }

context 'with string given as :fallback_from' do
let(:options) { {fallback: {for: 'higgs', with: 'news', from: 'news'}} }

before do
allow(Language).to receive(:current).and_return double(pages: double(find_by: another_page))
allow(another_page).to receive(:elements).and_return double(not_trashed: double(named: elements))
end

it "renders the fallback element" do
is_expected.to have_selector("#news_#{another_element.id}")
end
end

context 'with page given as :fallback_from' do
let(:options) { {fallback: {for: 'higgs', with: 'news', from: another_page}} }

before do
allow(another_page).to receive(:elements).and_return double(not_trashed: double(named: elements))
end
context 'with option separator given' do
let(:options) { {separator: '<hr>'} }

it "renders the fallback element" do
is_expected.to have_selector("#news_#{another_element.id}")
end
it "joins element partials with given string" do
is_expected.to have_selector('hr')
end
end

context 'with option separator given' do
let(:options) { {separator: '<hr>'} }

before do
expect(page).to receive(:find_elements).and_return(elements)
context 'with custom elements finder' do
let(:options) do
{ finder: CustomNewsElementsFinder.new }
end

it "joins element partials with given string" do
is_expected.to have_selector('hr')
it 'uses that to load elements to render' do
is_expected.to have_selector("#news_1001")
end
end
end
Expand Down

0 comments on commit efc1ef1

Please sign in to comment.