Skip to content

Instantly share code, notes, and snippets.

@pgruener
Created January 7, 2022 22:49
Show Gist options
  • Save pgruener/dbbc2513ca84ca8af3a3426575ce1238 to your computer and use it in GitHub Desktop.
Save pgruener/dbbc2513ca84ca8af3a3426575ce1238 to your computer and use it in GitHub Desktop.
SpinaCMS Extension for modular master pages as lightweight pagebuilder
# 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 %>
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)
#
# 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