There are two goals for this project
- To try and build a starting point which I can use to put together a project in a very short amount of time.
- To try and answer some of the "what ifs" that I end up thinking about at my day job, but would be wholly inapropriate to implement in a work setting due to their "unrailsy" nature
While this is a project that I'm using in apps that I hope will serve real customers, and I'm putting a reasonable effort into mantaining this. Given the fact that there are experiemental code structure choises and dev builds importmaps, caution is advised!
Everyone should have pre-commit hooks setup!
-
Run
./install_git_hooks.sh
-
Voila!
- app
- features <- Idea behind this is that we focus more on feature based groupings, rather than categorising code. Part
of the idea is that it will make each feature its own isolated application, with some shared common utilities
- pages
- authentication
- dashbaord
- common
- components <- contains shared phlex components
- features <- Idea behind this is that we focus more on feature based groupings, rather than categorising code. Part
of the idea is that it will make each feature its own isolated application, with some shared common utilities
- View
- Phlex templating
- JS and CSS
- Served via Propshaft
- JS
- Imports "managed" with importmaps-rails
- Importantly, we're using v1 of importmaps as there are a handful of issues in v2 as it moves to a vendor only approach
- AlpineJS over Stimulus
- Routing with railsware's js-routes
- @rails/request.js fetch wrapper
- Imports "managed" with importmaps-rails
- CSS
- dartsass-rails for SCSS
- Nesting is becoming available in all the major browsers in pure CSS, so I could be convinced to drop dartsass at some point.
- Custom starter styles with a "prefer margin on bottom" approach and custom utility classes.
- dartsass-rails for SCSS
- Forms
- Forms are done with form_with
- Subjects of forms should be custom ActiveModel::Model's which handles all validations. The subject of a form should never be a Database model
- Hosting
- Custom development and production docker compose setup
- Testing
- Formatting
- Mail chatching
- Jobs
- Handled via Good Job
- May shift over to Solid Queue depending on how its feature set evolves
- Handled via Good Job
- Authentication
- Using a bispoque authentication system based on "has_secure_passsword" which should be simple to build upon with omniauth, or replace with another authentication system.
- Caching
- N+1 catching via Bullet
Prefer "x-ref": "foo"
over x_ref: "foo"
or "x-ref" => "foo"
Prefer get <path>, to: <controller>
style of routes as it allows for a more consistent hash style when paired with
standardrb
When we have a function whose value is not used, IE:
cats.push "tabby"
puts "foobar"
we call it without parans, unless passing a block using curly braces, IE:
submission.ensure(:password, message: "Password must be more than 8 character") { _1.size <= 8 }
When we use the return of the function, IE:
p make_number("123") + 123
asdf = make_number("321")
we call it with parans
class Authentication::Pages::LoginController < ApplicationController
class ViewContext < ActiveSupport::CurrentAttributes
attribute :form_object
end
class FormObject
include ActiveModel::Model
include HasErrors
attr_accessor :email, :password
validates_presence_of :email
validates_presence_of :password
validate :ensure_user_exists # Must run last for security
def user
@user ||= User.from_login_details(email:, password:)
end
def ensure_user_exists
return unless errors.blank?
if user.nil?
errors.add :email, :no_user, message: "and password provided did not match our records"
end
end
end
class View < ApplicationView
def template
h1 { "Login" }
render Form
end
end
class Form < ApplicationView
def template
form_with(model: ViewContext.form_object, url: authentication_login_path, id: "login") do |f|
f.label :email, "Email"
f.text_field :email
render ViewContext.form_object.errors_for(:email)
f.label :password, "Password"
f.password_field :password
render ViewContext.form_object.errors_for(:password)
f.submit "Login"
end
end
end
def view
redirect_to root_path if Current.user
ViewContext.form_object = FormObject.new
render View
end
def submit
ViewContext.form_object = FormObject.new(user_params)
if ViewContext.form_object.valid?
ViewContext.form_object.user.add_to_session(session)
redirect_to dashboard_home_path, status: :see_other
else
render_or_replace_id(
page: -> { View.new },
target_id: "login",
replacement: -> { Form.new }
)
end
end
def user_params
params.require(:authentication_pages_login_controller_form_object).permit(
:email,
:password
)
end
end