Created
January 7, 2022 22:49
-
-
Save pgruener/dbbc2513ca84ca8af3a3426575ce1238 to your computer and use it in GitHub Desktop.
SpinaCMS Extension for modular master pages as lightweight pagebuilder
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# corresponding to the sample theme with the master view_template "master_page" | |
# this template iterates the sub_pages / descendents of the master_page and renders | |
# them concatenated: | |
<% current_page.children.sorted.active.live.each do |page| %> | |
<%= render_spina_part(page) %> | |
<% end %> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module SpinaExt | |
def self.view_template_config_for(view_template) | |
# dont interrupt unknown access types, where Current singleton doesnt reference current view_template | |
return unless Spina::Current.theme | |
Spina::Current.theme.view_templates.detect { |tpl| tpl[:name] == view_template } | |
end | |
def self.unaccessible_view_templates | |
return @unaccessible_view_templates if @unaccessible_view_templates | |
return [] unless Spina::Current.theme | |
templates_with_dependencies = Spina::Current.theme.view_templates.select { |view_template_config| view_template_config.key?(:dependent_of) } | |
@unaccessible_view_templates = templates_with_dependencies.map { |view_template_config| view_template_config[:name] } | |
end | |
module PagesHelper | |
CURRENT_INSTANCE_PROPS = %i[page account user theme].freeze | |
# | |
# Helper to render another part/view_template within a rendered part or view_template. | |
# The path generation is copied from spina gem in | |
# app/controllers/concerns/spina/frontend.rb -> *render_with_template* | |
# as there is no encapsulation available yet. | |
# | |
# @param [Spina::Page] page The page object to be rendered | |
# @param [Symbol] specific_part (opt) If not supplied the page's default template is | |
# rendered. But if another view should be rendered instead, this parameter defines | |
# it. | |
# | |
# @return [String] The rendered markup. | |
# | |
def render_spina_part(page, specific_part = nil) | |
# memoize singleton values of current | |
previous_current = OpenStruct.new(Hash[CURRENT_INSTANCE_PROPS.map { |prop| [prop, Spina::Current.public_send(prop)] }]) | |
Spina::Current.page = page | |
Spina::Current.page.view_context = self | |
render template: "#{current_theme.name.parameterize.underscore}/pages/#{specific_part || page.view_template || 'show'}" | |
ensure # recover the previous calling context | |
CURRENT_INSTANCE_PROPS.each do |prop| | |
Spina::Current.public_send("#{prop}=", previous_current.public_send(prop)) | |
end | |
end | |
end | |
module Page | |
def accessible? | |
!sub_page? | |
end | |
# | |
# Checks if current page is a sub_page, what means it's just not an own, | |
# accessible page. Its just a (dynamic) part or partial of a master page. | |
# | |
# @return [Boolean] | |
# | |
def sub_page? | |
view_template_config.try(:key?, :dependent_of) | |
end | |
# | |
# Checks if the current page has sub_pages, so it is a page, which consists | |
# of several other pages, which are combined for rendering as one (this) page. | |
# | |
# @return [Boolean] | |
# | |
def master_page? | |
return false if sub_page? || !Spina::Current.theme | |
Spina::Current.theme.view_templates.detect { |tpl| tpl[:dependent_of] == view_template_config[:name] }.present? | |
end | |
# | |
# Detects the configuration of the assigned view_template. | |
# | |
# @return [Hash] View template configuration (from theme config). | |
# | |
def view_template_config | |
@view_template_config ||= SpinaExt.view_template_config_for(view_template) | |
end | |
end | |
module PagesController | |
def authorize_page | |
super # first run original plauzibilization | |
# afterwards verify that page is logically accessible | |
raise ActiveRecord::RecordNotFound unless page.accessible? | |
end | |
end | |
module AdminPagesController | |
# | |
# Modified new action to respect *parent_id* as predefined parent for | |
# pages with subpages defined by *dependent_of* in corresponding view_template. | |
# | |
def new | |
super.tap { |page| | |
# be sure only to inject minorily into existing functionality if all known criteria are fulfilled | |
next unless params[:parent_id] | |
next unless (view_template_config = SpinaExt.view_template_config_for(page.view_template)) | |
next unless view_template_config.key?(:dependent_of) | |
parent = Spina::Page.find_by_id(params[:parent_id].to_i) or next # ignore if invalid id was permitted | |
next if parent.view_template != view_template_config[:dependent_of] | |
# pre-assign the permitted parent if it matches to the dependent_of view_template definition | |
page.parent = parent | |
# also init the internal title with the view_template title | |
page.title ||= view_template_config[:title] | |
} | |
end | |
end | |
module SitemapsController | |
def show | |
# orig selection | |
super | |
# filter unaccessibles | |
@pages = @pages.where.not(view_template: SpinaExt.unaccessible_view_templates) | |
end | |
end | |
module MenuPresenter | |
# TODO: That was necessary for a situation, which I dont find anymore. | |
# TODO: now it crashes the menu rendering .. so it needs to be more conditional | |
# TODO: if the original reason for it "comes back" | |
# TODO: (crashes at: presenter = Spina::MenuPresenter.new(Spina::Navigation.find_by_name!('main')).to_html in public/shared/_navigation) | |
# def scoped_collection(_collection) | |
# super.where.not(view_template: SpinaExt.unaccessible_view_templates) | |
# end | |
# | |
# Adds possibility to influence markup with ignoring a menu wrapping element. | |
# | |
# @param [Array] collection Navigation items | |
# | |
# @return [String] | |
# | |
def render_menu(collection) | |
if menu_tag == :none | |
render_items(scoped_collection(collection)) | |
else | |
super | |
end | |
end | |
end | |
module UserInterface | |
module HeaderComponent | |
# | |
# Skips the "preview" button in page edit view, if the page is part of a "master view". | |
# In this case this view isnt accessible, so prevent this link from rendering. | |
# | |
def after_breadcrumbs(*args, **opts, &block) | |
super(*args, **opts, &block).tap { | |
# check if breadcrumbs should be skipped due to dependent resources (view_template definition of *dependent_of*) | |
return if !block_given? && !page_accessible? # this hides the external link button to open the page if it's not accessible | |
} | |
end | |
# | |
# If the current page in edit mode is a "master page", which consists of | |
# several separate organized sub-pages, add a "new" button to create thoose | |
# sub pages with the configured sub view_templates. | |
# | |
def actions(*args, **opts, &block) | |
res = super | |
return res if block_given? # keep default when setting the slot | |
if (dependent_templates = dependent_view_templates_for_current_page) | |
dependent_new_page_menu(dependent_templates) + res.to_s.html_safe | |
else | |
res | |
end | |
end | |
def current_page | |
@current_page ||= controller.instance_variable_get(:@page) | |
end | |
# | |
# Checks if the current page is accessible externally or if it is just part of a | |
# "master page", which is considered to act like a partial. | |
# | |
# @return [Boolean] | |
# | |
def page_accessible? | |
current_page&.accessible? | |
end | |
# | |
# Generates a new-page menu with possible sub_pages of a "master page". | |
# It also injects another get parameter (parent_id), to the generated | |
# menu-items, to provide efficient defaults for creating new partials. | |
# | |
# @param [Array] dependent_templates The dependent templates, which | |
# should be rendered as possible view_templates for sub_pages. | |
# | |
# @return [String] HTML save markup. | |
# | |
def dependent_new_page_menu(dependent_templates) | |
render(Spina::Pages::NewPageButtonComponent.new(resource: current_page&.resource, filtered_view_templates: dependent_templates)).tap { |html| | |
dependent_templates.each do |dependent_template| | |
html.sub!(/(<a[^>]+href="[^"]+\?view_template=#{dependent_template.name}[^"]*)"/, "\\1&parent_id=#{current_page.id}\"") | |
end | |
}.html_safe | |
end | |
# | |
# Detects the view_templates, which are possible as sub_pages/partials for the | |
# current "master page". | |
# | |
# @return [Array or nil] | |
# | |
def dependent_view_templates_for_current_page | |
return unless current_page | |
return unless Spina::Current.theme | |
tpl_configs = Spina::Current.theme.view_templates&.select { |view_template_config| | |
view_template_config[:dependent_of] == current_page.view_template | |
} | |
return unless tpl_configs&.any? | |
# gather template instances for configurations | |
Spina::Current.theme.new_page_templates(resource: current_page.resource).map { |tpl_instance| | |
tpl_configs.detect { |tpl_config| tpl_config[:name] == tpl_instance.name } ? tpl_instance : nil | |
}.compact | |
end | |
end | |
module NewPageButtonComponent | |
def initialize(view_templates = [], resource: nil, filtered_view_templates: false) | |
super(view_templates, resource: resource) | |
if filtered_view_templates | |
@view_templates = filtered_view_templates | |
else | |
# remove view_templates from selection, which are dependent to another view_template | |
@view_templates.reject! { |tpl| SpinaExt.view_template_config_for(tpl.name).try(:key?, :dependent_of) } | |
end | |
end | |
end | |
end | |
module Theme | |
# | |
# Interpretes view_templates' *sub_pages* to dependency view_templates on | |
# same layer. | |
# | |
def register | |
super.tap { |themes| | |
new_theme = themes.last | |
next unless new_theme.view_templates | |
dependent_view_templates = [] | |
new_theme.view_templates.each do |view_template| | |
extract_sub_pages_dependencies(view_template)&.each do |dependency_templates| | |
dependent_view_templates << dependency_templates if dependency_templates | |
end | |
end | |
new_theme.view_templates.concat(dependent_view_templates) | |
} | |
end | |
# | |
# A view_template may have sub view_templates as a dependency definition. | |
# This is not really supported by spina, so this method extracts those | |
# dependencies and adds an own *dependent_of* marker to them. | |
# | |
# @param [Hash] view_template The object to be trimmed. | |
# | |
# @return [Array] All the sub_templates with additional dependency marker: *dependent_of*. | |
# | |
def extract_sub_pages_dependencies(view_template) | |
return unless view_template.key?(:sub_pages) | |
view_template.delete(:sub_pages).each do |sub_template| | |
sub_template[:dependent_of] = view_template[:name] | |
end | |
end | |
end | |
end | |
require 'spina/pages_helper' | |
Spina::PagesHelper.prepend(SpinaExt::PagesHelper) | |
require 'spina/page' | |
Spina::Page.prepend(SpinaExt::Page) | |
require 'spina/menu_presenter' | |
Spina::MenuPresenter.prepend(SpinaExt::MenuPresenter) | |
require 'spina/admin/pages_controller' | |
Spina::Admin::PagesController.prepend(SpinaExt::AdminPagesController) | |
require 'spina/pages_controller' | |
Spina::PagesController.prepend(SpinaExt::PagesController) | |
require 'spina/sitemaps_controller' | |
Spina::SitemapsController.prepend(SpinaExt::SitemapsController) | |
require 'spina/user_interface/header_component' | |
Spina::UserInterface::HeaderComponent.prepend(SpinaExt::UserInterface::HeaderComponent) | |
require 'spina/pages/new_page_button_component' | |
Spina::Pages::NewPageButtonComponent.prepend(SpinaExt::UserInterface::NewPageButtonComponent) | |
require 'spina/theme' | |
Spina::Theme.singleton_class.prepend(SpinaExt::Theme) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# in the corresponding theme definition the master view_template | |
# gets the property *sub_pages*, which defines further view_templates, | |
# which are dynamically added beyond the "master page". | |
{ | |
# | |
# ... | |
# | |
# | |
# View templates | |
# Every page has a view template stored in app/views/my_theme/pages/* | |
# You define which parts you want to enable for every view template | |
# by referencing them from the theme.parts configuration above. | |
theme.view_templates = [ | |
{ name: 'show', title: 'Page', parts: %w[text] }, | |
{ | |
name: 'landing', | |
title: 'Landing Page', | |
parts: %w[cta_section_image] | |
}, | |
{ | |
name: 'master_page', | |
title: 'Landing Page Pages', | |
parts: %w[logo_light logo_dark cta_label cta_href], | |
sub_pages: [ | |
{ | |
name: 'lp_hero', | |
title: 'LP Section: Hero', | |
parts: %w[section_id image thriller h1 text cta_label cta_href brand_logos] | |
}, | |
{ | |
name: 'lp_section_cta_2r_image', | |
title: 'LP Section: CTA with image', | |
parts: %w[section_id image_alignment image h1 h2 text cta_label cta_href brand_logos] | |
} | |
#, ... | |
] | |
} | |
} | |
#, | |
# ... | |
# | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment