Please wait...
Page is loading

Layouts

Bs4 theme includes prebuilt layouts for common structures

Prebuilt layouts

All prebuilt layouts are found here: bs4_iplan_theme/app/views/bs4_iplan_theme/layouts/
Corresponding sass and js files are found here:
bs4_iplan_theme/app/assets/stylesheets/vendor/bs4_iplan_theme_layouts/
bs4_iplan_theme/app/assets/javascripts/vendor/bs4_iplan_theme_layouts/

The base layout is:
bs4_iplan_theme/app/views/bs4_iplan_theme/layouts/application.html.haml
bs4_iplan_theme/app/assets/stylesheets/vendor/bs4_iplan_theme_layouts/application.css.sass
This layout defines all the shared stuff between layouts like: environment_header, system_header, top-header, page-spinner, viewport meta tags, body states etc..
This layout is also responsible for compiling the bs 4 theme (@import vendor/bootstrap-4.4.1-iplan-theme/theme.css.sass))
To find out more about application layout Click Here


Every prebuilt layout uses the application layout as follows:
* yield implementation can vary between different prebuilt layouts

-content_for :content do
  -# add an option for final layout to place content in #top-header
  %header#top-header=yield(:top_header)
  -# add an option for final layout to place main content
  =content_for?(:main) ? yield(:main) : yield

=render :template=>'bs4_iplan_theme/layouts/application', locals: {body_attr: {data: {'state': 'loaded'}}}

Getting started

To use a prebuilt layout in a custom layout:
* prebuilt_layout should be replaced with one of the prebuilt layouts
* custom_layout is the layout you are creating
* "content_for :main do" implementation can vary between different prebuilt layouts

-content_for :meta_tag_robots, 'noindex,nofollow' # Tell bots not to index or follow kitchen sink pages

-content_for :stylesheets do
  =stylesheet_link_tag 'app/.../custom_layout.bundle'

-content_for :javascripts do
  =javascript_include_tag 'app/controllers/.../custom_layout.bundle'

-content_for :main do
  =yield

=render :template=>'bs4_iplan_theme/layouts/prebuilt_layout'

Note that in the custom layout we are calling sass bundle and js bundle.
those bundles will need to include prebuilt layout sass and js files as follows:

Sass - custom_layout.bundle
//= require app/custom_layout/custom_layout-theme
//= require app/layouts/custom_layout/custom_layout
Sass - custom_layout-theme
in your theme file you can add custom layout specific variables which will override the default ones
@import app/custom_layout/custom-variables
@import vendor/bs4_iplan_theme_layouts/prebuilt_layout/prebuilt_layout.css.sass
JS - custom_layout.bundle
//= require vendor/bs4_iplan_theme_layouts/prebuilt_layout.bundle
//= require app/layouts/custom_layout/custom_layout

Structure & Features

Each built in layout inherits from the base application layout.
The base application layout has several key components and feature that can affect the page.

Components
  • system-header
  • environment-header
  • system-toasts
  • #page-spinner
    shown when entering or exiting the page using loading state
  • #modal-dialog-templates
    Templates for loading modal and error modal
States
  • loading
    When there is data-state="loading" on the body, it means we either entering the page or exiting it. In this state a spinner will be shown, and key elements that compose the page will be hidden (main, footer, .modal, .modal-backdrop, #system-toasts) Different built in layouts can extend this with other components if necessary
Features
  • body_attr
    Provides a way for a layouts inheriting directly from application layout (like app, login, minisite etc..) to set parameters on the body
    body_attr = {data: {'state': 'loaded', 'sidebar': 'static', 'inline-sidebar': string_if('open', content_for?(:sidebar))}}.merge(body_attr || {})
    render template: 'bs4_iplan_theme/layouts/application', locals: {body_attr: body_attr}

System toasts

System toasts are based on BS4 theme toasts

  • System toasts will pop from the top of the screen in the middle of the main area
  • System toast should start with .hide class
  • System toast should always have data{autohide: "false"} but, if added data{delay: xxx} auto hide will be ignored and delay will start once the toast is first in stack. For example, if we have 2 toasts, first toast has a 3 seconds delay and a toast after it will have no delay, the 3 seconds countdown for the first toast will start only when the second toast will be hidden
  • You can use content_for :system_toasts to start a page with a system toast
  • When displaying several system toasts at once toasts will be stacked
  • System toasts will be stack according the their order in the dom
  • When displaying several system toasts at once toasts with autohide delay
  • System toasts will be removed from the dom after they are hidden
Manually displaying system toasts
You should only display toasts manually when all other toasts are not in the doom
Flash messages
flash messages will be presented as system toasts and they will be placed on top of system toasts created by content_for :system_toasts
-load_page_path ||= built_in_layouts_layouts_path(show_system_toast: true)
-if params[:show_system_toast] === 'true'
  -content_for :system_toasts do
    .toast.hide{role: 'alert', aria: {live: "assertive", atomic: "true"}, class: "", data: {autohide: 'false', delay: 5000}}
      .toast-header
        %i.far.fa-info-circle.mr-2
        ='הודעה'
        =button_tag class: 'close ml-auto', 'data-dismiss': 'toast', type: 'button' do
          %span ×
      .toast-body
        %strong='זאת היא הודעת מערכת עם שחרור אחרי 5 שניות'

.row.mb-n2
  .col-auto.mb-2
    =link_to 'Load page with system toast', load_page_path, class: 'btn btn-light', data: {'main-spinner': true}
  .col-auto.mb-2
    =button_tag 'Show custom system toasts', type: :button, data: {action: 'show-toasts'}, class: 'btn btn-light'



:javascript
  $(()=>{
    $('[data-action="show-toasts"]').on('click', () => {
      if ($('#system-toasts .toast').length !== 0) return;
      $('#system-toasts').append($("#{preserve((render 'built_in_layouts/layouts/custom_toasts').gsub('"', '\\"'))}"));
      $('#system-toasts .toast').toast('show');
    });
  })

Redirect to format

Redirect to format triggers redirect event on the body (which will show spinner) and adds a flash message which will be presented on the following page as a system toast

.row.mb-n2
  .col-auto.mb-2=link_to 'sync request', built_in_layouts_layouts_path(redirect_to_format: true), class: 'btn btn-light', data: {'main-spinner': 'true'}
  .col-auto.mb-2=link_to 'ajax request', built_in_layouts_layouts_path(redirect_to_format: true), class: 'btn btn-light', remote: true
  .col-auto.mb-2
    =button_tag 'remote modal', class: 'btn btn-light', type: 'button',
                                data: {toggle: 'remote-modal', 'remote-modal-url': built_in_layouts_layouts_path(redirect_to_format: true, format: :fbox)}
  .col-auto.mb-2=button_tag 'redirect from open modal', class: 'btn btn-light', type: 'button', data: {toggle: 'modal', target: '#redirect-modal'}
-# =button_tag 'remote popover', class: 'btn btn-light', type: 'button'

.modal#redirect-modal{tabindex: '-1'}
  .modal-dialog
    .modal-content
      .modal-header
        %h5.modal-title='Redirect from modal'
        =button_tag class: 'close', 'data-dismiss': 'modal', type: 'button' do
          %span ×
      .modal-body
        =link_to 'sync request', built_in_layouts_layouts_path(redirect_to_format: true), class: 'btn btn-light', data: {'main-spinner': 'true'}
        =link_to 'async (ajax) request', built_in_layouts_layouts_path(redirect_to_format: true), class: 'btn btn-light', remote: true
        =button_tag 'remote modal', class: 'btn btn-light', type: 'button',
                                    data: {toggle: 'remote-modal', 'remote-modal-url': built_in_layouts_layouts_path(redirect_to_format: true, format: :fbox)}

        .mt-2=link_to_bs4_modal 'stacked modal', '#stacked-redirect-modal', data: {stack: :modal}, class: 'btn btn-link'

      .modal-footer
        =button_tag 'close', type: 'button', class: 'btn-block btn btn-secondary', 'data-dismiss': "modal"

.modal#stacked-redirect-modal{tabindex: '-1'}
  .modal-dialog
    .modal-content
      .modal-header
        %h5.modal-title='Redirect from stacked modal'
        =button_tag class: 'close', 'data-dismiss': 'modal', type: 'button' do
          %span ×
      .modal-body
        =link_to 'sync request', built_in_layouts_layouts_path(redirect_to_format: true), class: 'btn btn-light', data: {'main-spinner': 'true'}
        =link_to 'async (ajax) request', built_in_layouts_layouts_path(redirect_to_format: true), class: 'btn btn-light', remote: true
        =button_tag 'remote modal', class: 'btn btn-light', type: 'button',
                                    data: {toggle: 'remote-modal', 'remote-modal-url': built_in_layouts_layouts_path(redirect_to_format: true, format: :fbox)}

      .modal-footer
        =button_tag 'close', type: 'button', class: 'btn-block btn btn-secondary', 'data-dismiss': "modal"

Anchor marker

When using anchor tags in the built in layouts, the target elements will be covered by the layouts header which has position: absolute. To fix that we use an "Anchor marker" which is basically an element with the class .anchor-marker which will give it an offset from the target element equal to height of the header + 1rem for spacing. The marker should be placed as the first element in the target element and the anchor tag should point to the marker.

Anchor marker height & Scroll Spy
The anchor marker will receive height similar to its offset from the target element so that it will play nice with BS4 Scroll Spy

You can see anchor markers in action using the side menu and H2 labels in this page ("Prebuilt layouts", "Getting started", "System toasts" etc..)

=link_to 'Activate Scroll Spy on Side menu', 'javascript: $("body").scrollspy({ target: "#toc" })', class: 'btn btn-light'
On this page