Slide 1

Slide 1 text

Chris Oliver Powerful TypeScript Rails Features You Might Not know

Slide 2

Slide 2 text

Learn Your Tools

Slide 3

Slide 3 text

ActiveRecord Excluding User.where.not(id: users.map(&:id)) # SELECT "users".* FROM "users" WHERE "users"."id" NOT IN (1,2) User.all.excluding(users) # SELECT "users".* FROM "users" WHERE "users"."id" NOT IN (1,2)

Slide 4

Slide 4 text

ActiveRecord Strict Loading class Project < ApplicationRecord has_many :comments, strict_loading: true end project = Project.first project.comments ActiveRecord::StrictLoadingViolationError `Project` is marked as strict_loading. The Comment association named `:comments` cannot be lazily loaded.

Slide 5

Slide 5 text

ActiveRecord Strict Loading project = Project.includes(:comments).first Project Load (0.3ms) SELECT "projects".* FROM "projects" ORDER BY "projects"."id" ASC LIMIT ? [["LIMIT", 1]] Comment Load (0.1ms) SELECT "comments".* FROM "comments" WHERE "comments"."project_id" = ? [["project_id", 1]] => # project.comments => [#]

Slide 6

Slide 6 text

Generated Columns class AddNameVirtualColumnToUsers < ActiveRecord::Migration[7.0] def change add_column :users, :full_name, :virtual, type: :string, as: "first_name || ' ' || last_name", stored: true end end

Slide 7

Slide 7 text

attr_readonly class User < ApplicationRecord attr_readonly :super_admin end

Slide 8

Slide 8 text

with_options class Account < ActiveRecord::Base with_options dependent: :destroy do has_many :customers has_many :products has_many :invoices has_many :expenses end end

Slide 9

Slide 9 text

with_options I18n.with_options locale: user.locale, scope: 'newsletter' do |i18n| subject i18n.t :subject body i18n.t :body, user_name: user.name end

Slide 10

Slide 10 text

Try method_name if respond_to?(:method_name) try(:method_name) try(:method_name) || default (method_name if respond_to?(:method_name)) || default

Slide 11

Slide 11 text

ActionText Embeds

Slide 12

Slide 12 text

Searching Users json.array! @users do |user| json.sgid user.attachable_sgid json.content render( partial: "users/user", locals: {user: user}, formats: [:html] ) end

Slide 13

Slide 13 text

Signed GlobalIDs User.first.attachable_sgid.to_s "eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaEpJaDluYVdRNkx5OX FkVzF3YzNSaGNuUXRZWEJ3TDFWelpYSXZNUVk2QmtWVSIsImV4c CI6IjIwMjMtMTAtMjhUMTY6MDY6MTEuMjEyWiIsInB1ciI6ImRl ZmF1bHQifX0=--217284b31bc4e28f6e2cf2890ccee87ca7d3e a2d"

Slide 14

Slide 14 text

ActionText Embeds

Slide 15

Slide 15 text

ActionText Embeds import Trix from "trix" let attachment = new Trix.Attachment({ content: "
Chris Oliver
", sgid: "BAh7CEkiCG…" }) element.editor.insertAttachment(attachment)

Slide 16

Slide 16 text

ActionText Embeds

Hey !

Slide 17

Slide 17 text

Signed GlobalIDs Base64.decode64("eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaEpJaDluYVdRNkx5 OXFkVzF3YzNSaGNuUXRZWEJ3TDFWelpYSXZNUVk2QmtWVSIsImV4cCI6IjIwMjMtM TAtMjhUMTY6MD Y6MTEuMjEyWiIsInB1ciI6ImRlZmF1bHQifX0=“) => “{\"_rails\":{\"message\": \"BAhJIh9naWQ6Ly9qdW1wc3RhcnQtYXBwL1VzZXIvMQY6BkVU\",\"exp\": \"2023-10-28T16:06:11.212Z\",\"pur\":\"attachable\"}}"

Slide 18

Slide 18 text

Signed GlobalIDs Base64.decode64(“BAhJIh9naWQ6Ly9qdW1wc3RhcnQtYXBwL1VzZXI vMQY6BkVU") => “\x04\bI\"\x1Fgid://jumpstart-pro/User/1\x06:\x06ET"

Slide 19

Slide 19 text

ActionText Embeds

Slide 20

Slide 20 text

Serialize Coders module ActionText class RichText < Record serialize :body, coder: ActionText::Content end end

Slide 21

Slide 21 text

Serialize Coders class ActionText::Content def self.load(content) new(content) if content end def self.dump(content) case content when nil nil when self content.to_html end end end

Slide 22

Slide 22 text

ActionMailbox Inbound Emails

Slide 23

Slide 23 text

ActionMailbox Inbound Emails # app/mailboxes/application_mailbox.rb class ApplicationMailbox < ActionMailbox::Base routing /@replies\./i => :replies end

Slide 24

Slide 24 text

class RepliesMailbox < ApplicationMailbox MATCHER = /^reply-(\d+)@replies\./ def process conversation.posts.create!( author: author, body: body, message_id: mail.message_id ) end private def conversation Conversation.find(conversation_id) end def conversation_id mail.recipients.find { |recipient| MATCHER.match?(recipient) }[MATCHER, 1] end end ActionMailbox Inbound Emails

Slide 25

Slide 25 text

Routing constraints constraints subdomain: :app do root "dashboard#show" end root "homepage#show"

Slide 26

Slide 26 text

Routing constraints authenticated :user, -> { _1.admin? } do resource :admin end authenticated :user do root "dashboard#show" end root "homepage#show"

Slide 27

Slide 27 text

Routing constraints

Slide 28

Slide 28 text

Draw Routes Rails.application.routes.draw do draw :api # ... end # config/routes/api.rb namespace :api, defaults: {format: :json} do namespace :v1 do resources :accounts end end

Slide 29

Slide 29 text

Custom Generators $ bin/rails g api_client OpenAI --base-url https://api.openai.com

Slide 30

Slide 30 text

Custom Generators $ bin/rails generate generator ApiClient creates an ApiClient generator: lib/generators/api_client/ lib/generators/api_client/api_client_generator.rb lib/generators/api_client/USAGE lib/generators/api_client/templates/ test/lib/generators/api_client_generator_test.rb

Slide 31

Slide 31 text

Custom Turbo Stream Actions

Slide 32

Slide 32 text

Custom Turbo Stream Actions

Slide 33

Slide 33 text

Custom Turbo Stream Actions # create.turbo_stream.erb <%= turbo_stream_action_tag “notification”, title: “Hello world” %>

Slide 34

Slide 34 text

Custom Turbo Stream Actions import "@hotwired/turbo-rails" Turbo.StreamActions.notification = function() { Notification.requestPermission(function(status) { if (status == "granted") { new Notification(this.getAttribute("title")) } }) }

Slide 35

Slide 35 text

Custom Turbo Stream Actions

Slide 36

Slide 36 text

truncate_words content = 'And they found that many people were sleeping better.' content.truncate_words(5, omission: '... (continued)') # => "And they found that many... (continued)"

Slide 37

Slide 37 text

starts_at > Time.current starts_at.after?(Time.current) starts_at.future? Time Helpers starts_at < Time.current starts_at.before?(Time.current) starts_at.past?

Slide 38

Slide 38 text

Time.current.all_day #=> Fri, 06 Oct 2023 00:00:00 UTC +00:00.. Fri, 06 Oct 2023 23:59:59 UTC +00:00 Time.current.all_week #=> Mon, 02 Oct 2023 00:00:00 UTC +00:00.. Sun, 08 Oct 2023 23:59:59 UTC +00:00 Time.current.all_month #=> Sun, 01 Oct 2023 00:00:00 UTC +00:00.. Tue, 31 Oct 2023 23:59:59 UTC +00:00 Time Helpers

Slide 39

Slide 39 text

123 -> 123 1234 -> 1,234 10512 -> 10.5K 2300123 -> 2.3M Abbreviated numbers

Slide 40

Slide 40 text

Abbreviated numbers number_to_human(123) # => "123" number_to_human(12345) # => "12.3 Thousand" number_to_human(1234567) # => "1.23 Million" number_to_human(489939, precision: 2) # => "490 Thousand" number_to_human(489939, precision: 4) # => "489.9 Thousand" number_to_human(1234567, precision: 4, significant: false)# => "1.2346 Million"

Slide 41

Slide 41 text

def number_to_social(number) return number_with_delimiter(number) if number < 10_000 number_to_human(number, precision: 1, round_mode: :down, significant: false, format: "%n%u", units: {thousand: "K", million: "M", billion: "B"} ) end Abbreviated numbers

Slide 42

Slide 42 text

Abbreviated numbers number_to_social(123) #=> 123 number_to_social(1_234) #=> 1,234 number_to_social(10_512) #=> 10.5K number_to_social(2_300_123) #=> 2.3M

Slide 43

Slide 43 text

Rails 7.1

Slide 44

Slide 44 text

Rails.env.local? Rails.env.development? || Rails.env.test? Rails.env.local?

Slide 45

Slide 45 text

ActiveSupport Inquiry "production".inquiry.production? # => true "active".inquiry.inactive? # => false

Slide 46

Slide 46 text

Unused routes $ rails routes --unused Found 4 unused routes: Prefix Verb URI Pattern Controller#Action edit_comment GET /comments/:id/edit(.:format) comments#edit PATCH /comments/:id(.:format) comments#update PUT /comments/:id(.:format) comments#update DELETE /comments/:id(.:format) comments#destroy

Slide 47

Slide 47 text

Template strict locals <%# locals: (message:) -%> <%= tag.div id: dom_id(message) do %> <%= message %> <% end %> <%= render partial: “message” %> ArgumentError: missing local: :message <%= render partial: “message”, locals: {message: @message} %>

Slide 48

Slide 48 text

Template strict locals <%# locals: (message: “Hello”) -%>
<%= message %>
<%= render partial: “message” %> <%= render partial: “message”, locals: {message: “Hey”} %>

Slide 49

Slide 49 text

Template strict locals <%# locals: () -%>
Hello RailsWorld!
<%= render partial: “message” %>

Slide 50

Slide 50 text

normalizes class User < ApplicationRecord end def email=(value) super value&.strip&.downcase end

Slide 51

Slide 51 text

normalizes class User < ApplicationRecord end normalizes :email, with: ->(email) { email.strip.downcase } normalizes :email, with: ->{ _1.strip.downcase }

Slide 52

Slide 52 text

has_secure_password authenticate_by & password_challenge

Slide 53

Slide 53 text

authenticate_by class User < ApplicationRecord has_secure_password end User.find_by(email: "[email protected]") &.authenticate(“railsworld2023") #=> false or user User.authenticate_by( email: "[email protected]", password: "railsworld2023" ) #=> user or nil

Slide 54

Slide 54 text

password_challenge Current.user.update(password_params) def password_params params.require(:user).permit( :password, :password_confirmation, :password_challenge ).with_defaults(password_challenge: "") end

Slide 55

Slide 55 text

generates_token_for

Slide 56

Slide 56 text

generates_token_for class User < ApplicationRecord generates_token_for :password_reset, expires_in: 15.minutes do # BCrypt salt changes when password is updated BCrypt::Password.new(password_digest).salt[-10..] end end

Slide 57

Slide 57 text

generates_token_for User.find_by_token_for(:password_reset, params[:token]) #=> User or nil user.generate_token_for(:password_reset) “eyJfcmFpbHMiOnsiZGF0YSI6WzkwMjU0MTYzNSwiaC9oTkRJck9uL…”

Slide 58

Slide 58 text

ActiveStorage Variants

Slide 59

Slide 59 text

Named variants class User < ApplicationRecord has_one_attached :avatar do |attachable| attachable.variant :thumbnail, resize_to_limit: [200, 200] end end

Slide 60

Slide 60 text

Preprocessed variants class User < ApplicationRecord has_one_attached :avatar do |attachable| attachable.variant :thumbnail, resize_to_limit: [200, 200], preprocessed: true end end

Slide 61

Slide 61 text

if @project.file.previewable? @project.file.preview(:thumbnail) elsif @project.file.variable? @project.file.variant(:thumbnail) end # Render a preview or variant @project.file.represenation(:thumbnail) Representations

Slide 62

Slide 62 text

Fact L Learn