-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
Iván edited this page Apr 5, 2023
·
4 revisions
The code is structured as follows.
Several custom ViewComponents designed to minimise repetitive HTML coding in Views and Helpers:
- AccordionComponent: Used to create flowbite accordions for html views. Creation requires passing an :accordion Hash including an Array of :objects to display in accordion.
- ButtonComponent: manage regular buttons used in views receiving a :button hash. Different :kinds are implemented (add/remove, delete, back/forward, import/export, link, jump, etc.)
- CalendarComponent: calendar view as ViewComponent using TailwindCSS, it takes a start_date and, optionally a collection of events to render.
- DropdownComponent: Two-level Dropdown menus relying on custom javascript controller.
- FieldsComponent: The main workhorse to generate views in Mudclub. It renders fields: passed as array of table cell rows managing different kinds of content for each field (icon, label, text-box, button, etc.).
- FlashComponent: Display controller notifications.
- GridComponent: Sortable & filterable content grids.
- ModalComponent: Used to manage standardised modal views.
- ModalPieComponent: Display chartkick pie charts in modal views.
- NestedComponent: Unified way to manage nested-forms. relies on stimulus-rail-nested-form
- SearchComboComponent: Search interface box capable of taking multiple search items. relies on custom search javascript controller.
- SubmitComponent: Cancel/Return and (optiona) submit/edit/save buttons to attach in any view.
- TopbarComponent: Unified application topbar - managed by ApplicationController
Each component has a ruby file, initializing and updating the component:
# TopbarComponent - dynamic display of application top bar as ViewComponent
class TopbarComponent < ApplicationComponent
def initialize(user:, login:, logout:)
clubperson = Person.find(0)
@clublogo = clubperson.logo
@clubname = clubperson.nick
@user = user
@profile = set_profile(user:, login:, logout:)
@tabcls = 'hover:bg-blue-700 hover:text-white focus:bg-blue-700 focus:text-white focus:ring-2 focus:ring-gray-200 whitespace-nowrap shadow rounded ml-2 px-2 py-2 rounded-md font-semibold'
@lnkcls = 'no-underline block pl-2 pr-2 py-2 hover:bg-blue-700 hover:text-white whitespace-nowrap'
@profcls = 'align-middle rounded-full min-h-8 min-w-8 align-middle hover:bg-blue-700 hover:ring-4 hover:ring-blue-200 focus:ring-4 focus:ring-blue-200'
@logincls = 'login_button rounded hover:bg-blue-700 max-h-8 min-h-6'
if user
@menu_tabs = menu_tabs(user)
@admin_tab = admin_tab(user) if user.admin? or user.is_coach?
end
@prof_tab = prof_tab(user)
@ham_menu = set_hamburger_menu if user
end
# <... a bit more code ...>
end
... and an html.erb template file, used to render the component:
<!-- Navbar goes here -->
<nav class="sticky top-0 z-50 w-full h-15 bg-blue-900 text-gray-300">
<div class="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8">
<div class="relative flex items-center justify-between h-16">
<!-- Mobile menu, show/hide based on menu state. -->
<div class="absolute inset-y-0 left-0 flex items-center sm:hidden">
<%= render DropdownComponent.new(button: @ham_menu) if @ham_menu %>
</div>
<!-- Regular menu for large screens, show/hide based on menu state. -->
<div class="flex-1 flex items-center justify-center sm:items-stretch sm:justify-start">
<!-- REST OF REGULAR MENU CONTENT -->
</div>
</div>
<!-- REST OF HTML TOPBAR CONTENT -->
</div>
</nav>
Perform all data preparation and actions - leverage Helper & Components to maximize "ruby" coding vs HTML or CSS.
Object management - including associations and custom information access.
class Drill < ApplicationRecord
has_paper_trail on: [:create, :update]
belongs_to :coach
belongs_to :kind
has_and_belongs_to_many :skills
accepts_nested_attributes_for :skills, reject_if: :all_blank, allow_destroy: true
has_many :drill_targets
has_many :targets, through: :drill_targets
accepts_nested_attributes_for :targets, reject_if: :all_blank, allow_destroy: true
accepts_nested_attributes_for :drill_targets, reject_if: :all_blank, allow_destroy: true
has_one_attached :playbook
has_rich_text :explanation
scope :real, -> { where("id>0") }
scope :by_name, -> (name) { where("unaccent(name) ILIKE unaccent(?) OR unaccent(description) ILIKE unaccent(?)","%#{name}%","%#{name}%").distinct }
scope :by_kind, -> (kind_id) { (kind_id and kind_id.to_i>0) ? where(kind_id: kind_id.to_i) : where("kind_id>0") }
scope :by_skill, -> (skill_id) { (skill_id and skill_id.to_i>0) ? joins(:skills).where(skills: {id: skill_id.to_i}) : all }
self.inheritance_column = "not_sti"
FILTER_PARAMS = %i[name kind_id skill_id column direction].freeze
def self.filter(filters)
res = Drill.by_name(filters['name'])
.by_kind(filters['kind_id'])
.by_skill(filters['skill_id'])
filters['column'] ? res.order("#{filters['column']} #{filters['direction']}") : res.order(:name)
end
# search all drills for specific subsets
def self.search(search=nil)
#<... more code...>
res.order(:kind_id)
end
#<... more code...>
# build new @drill from raw input hash given by form submital submittal
# return nil if unsuccessful
def rebuild(d_data)
self.name = d_data[:name]
self.description = d_data[:description]
#<... more code...>
self
end
#<... more code...>
end
Auxiliary code to define required Component definitions for each view.
module DrillsHelper
# specific search bar to search through drills
def drill_search_bar(search_in:, task_id: nil, scratch: nil)
session.delete('drill_filters') if scratch
fields = [
{kind: "search-text", key: :name, label: I18n.t("person.name_a"), value: session.dig('drill_filters', 'name'), size: 10},
{kind: "search-select", key: :kind_id, label: "#{I18n.t("kind.single")}:", value: session.dig('drill_filters', 'kind_id'), options: Kind.real.pluck(:name, :id)},
{kind: "search-select", key: :skill_id, label: I18n.t("skill.single"), value: session.dig('drill_filters', 'skill_id'), options: Skill.real.pluck(:concept, :id)}
]
fields << {kind: "hidden", key: :task_id, value: task_id} if task_id
res = [[{kind: "search-combo", url: search_in, fields: fields}]]
end
# return icon and top of FieldsComponent
def drill_title_fields(title:, rows: nil, cols: nil)
title_start(icon: "drill.svg", title: title, rows: rows, cols: cols)
end
#<... more code to create Component inputs ...>
end
Actual HTML views to be served - minimalistic files mostly just rendering Components already created by Controllers.
<div id="drills-index">
<%= render @title %>
<%= render @search %>
<%= turbo_frame_tag "search-results" do %>
<%= render @grid %>
<% end %>
</div>
(c) Iván González Angullo (iangullo@gmail.com)