 Hot Glue is a Rails scaffold builder for the Turbo era. It is an evolution of the admin-interface style scaffolding systems of the 2010s ([activeadmin](https://github.com/activeadmin/activeadmin), [rails_admin](https://github.com/sferik/rails_admin), and [active_scaffold](https://github.com/activescaffold/active_scaffold)). Using Turbo-Rails and Hotwire (default in Rails 7) you get a lightning-fast out-of-the-box CRUD-building experience. Every page displays only a list view: new and edit operations happen as 'edit-in-place,' so the user never leaves the page. Because all page navigation is Turbo's responsibility, everything plugs & plays nicely into a Turbo-backed Rails app. Alternatively, you can use this tool to create a Turbo-backed *section* of your Rails app -- such as an admin interface -- while still treating the rest of the Rails app as an API or building out other features by hand. It will read your relationships and field types to generate your code for you, leaving you with a 'sourdough starter' to work from. If you modify the generated code, you're on your own if you want to preserve your changes and also re-generate scaffolding after adding fields. By default, it generates code that gives users full control over objects they 'own' and by default it spits out functionality giving access to all fields. (Handily, Hot Glue leaves the command you used in a comment at the top of your generated controller so you can regenerate it again in the future.) Alternatively, refinements allow you to scope records using custom access control or Pundit. Hot Glue scaffold comes with pagination by default and now has an option to add searching too. Hot Glue generates quick and dirty functionality. It lets you be crafty. However, like with a real glue gun, please be sure to use it with caution. * Build plug-and-play scaffolding mixing generated ERB with the power of Hotwire and Turbo-Rails * Everything edits-in-place (unless you use `--big-edit`) * Automatically reads your models (make them, add relationships, **and** migrate your database before building your scaffolding!) * Excellent for CREATE-READ-UPDATE-DELETE (CRUD), lists with pagination * Great for prototyping, but you should learn Rails fundamentals first. * Defaults to Devise & Rspec, but these are optional * Use Pagy for pagination * Create system specs automatically along with the generated code. * Nest your routes model-by-model for built-in poor man's authentication. * Throw the scaffolding away when your app is ready to graduate to its next phase. How is it different than Rails scaffolding? Although inspired by the Rails scaffold generators (built-in to Rails), Hot Glue does something similiar but has made opinionated decisions that deviate from the normal Rails scaffold: 1. The Hot Glue scaffolds are complete packages and are pre-optimized for 'edit-in-place' so that new and edit operations happen in-page smoothly. 2. Hot Glue does not create your models along with your scaffolding. Instead, create them first using `rails generate model X` 3. Hot Glue *reads* the fields on your database *and* the relationships defined on your models. Unlike the Rails scaffolding you must add relationships and migrate your DB before building your scaffolding. 4. Hot Glue has many more features for building layouts quickly, like choosing which fields to include or exclude and how to lay them out on the page, searching and scoping, related portals and nested sets, and applying modifiers (like currency or date format) and more. Other than the opinionated differences and additional features, Hot Glue produces code that is fundamentally very similiar and works consistent with the Rails 7 Hotwire & Turbo paradigms. # Get Hot Glue ## [GET THE COURSE TODAY](https://school.jfbcodes.com/8188/?utm_source=github.com&utm_campaign=github_hot_glue_readme_page) **only $60 USD!** --- --- ## HOW EASY? ``` rails generate hot_glue:scaffold Thing ``` Generate a quick scaffold to manage a table called `pronouns`  Instantly get a simple CRUD interface  # Getting Started Video [Hot Glue Getting Started (JSBundling)](https://www.youtube.com/watch?v=bzpXOhBkiDk) _If you are on Rails 6, see [LEGACY SETUP FOR RAILS 6](https://github.com/hot-glue-for-rails/hot-glue/blob/main/README2.md) and complete those steps FIRST._ ## The Super-Quick Setup https://jasonfleetwoodboldt.com/courses/stepping-up-rails/jason-fleetwood-boldts-rails-cookbook/hot-glue-quick-install-mega-script/ Copy & paste the whole code block into your terminal. Remember, there is a small "Copy" button at the top-right of the code block. Be sure to use your Node + Ruby version managers to switch into the Node & Ruby versions **before running the quick script**. For more of a step-by-step, see the full cookbook at: https://jasonfleetwoodboldt.com/courses/stepping-up-rails/jason-fleetwood-boldts-rails-cookbook/ These are the sections you need, you can ignore any others: * Section 1A for a new JS Bundling app, then skip down to * Section 2B: Rspec + Friends * Section 2B-Capy: Capybara for Rspec, then skip down to * Section 3 for a welcome controller ( you can skip everything in Section 4 ) * Section 5 for debugging tools * _Section 6 is the Hot Glue installer itself_ (this gem) - for Bootstrap, choose section 6A * Section 7A to install Bootstrap along with CSSBundling * Section 8 to set up Devise if you want authentication. (See how Hot Glue interacts with Devise below.) If you do this through the quick setup above, you can then skip down past the next section to the "HOT GLUE DOCS" below. ## Step-By-Step Setup ### 1. RAILS NEW To understand the options for `rails new`, see [this post](https://jasonfleetwoodboldt.com/courses/rails-7-crash-course/rails-7-bootstrap/) It is important that you know what kind of app you are creating (Importmap, JSBundling, or Shakapacker) because there are specific differences in how you will work with them. (Hot Glue is compatible with all 3 paradigms, but if you don't take the time to understand the setup, you will be confused as to why things aren't working.) To run Turbo (which Hot Glue requires), you must either (1) be running an ImportMap-Rails app, or (2) be running a Node-compiled app using any of JSBundling, Shakapacker, or its alternatives. (1) To use ImportMap Rails, start with `rails new MyGreatApp` (For full instructions to install Bootstrap with Importmap, check out [these instructions](https://jasonfleetwoodboldt.com/courses/stepping-up-rails/importmap-rails-with-bootstrap-sprockets-stimulus-and-turbo-long-tutorial/)) (2) To use JSBundling, start with `rails new MyGreatApp --javascript=esbuild` **If using JSBundling, make sure to use the new `./bin/dev` to start your server instead of the old `rails server` or else your Turbo interactions will not work correctly.** (If you want Bootstrap for a JSBundling app, install it following [these instructions](https://jasonfleetwoodboldt.com/courses/stepping-up-rails/rails-7-up-running-with-jsbundling-esbuild-stimulus-turbo-bootstrap-circleci/)) (3) To use Shakapacker, start with `rails new MyGreatApp --skip-javascript` and [see this post](https://jasonfleetwoodboldt.com/courses/react-heart-rails/rails-7-shakapacker-and-reactonrails-quick-setup-part-1/) (For the old method of installing Bootstrap [see this post](https://jasonfleetwoodboldt.com/courses/stepping-up-rails/rails-7-bootstrap/)) (Remember, for Rails 6 you must go through the [LEGACY SETUP FOR RAILS 6](https://github.com/hot-glue-for-rails/hot-glue/blob/main/README2.md) before continuing.) ### 2. ADD RSPEC, FACTORY-BOT, AND FFAKER add these 3 gems to your gemfile **inside a group for both :development and :test*. Do not add these gems to *only* the :test group or else your Rspec installer and generators will not work correctly. ``` group :development, :test do gem 'rspec-rails' gem 'factory_bot_rails' gem 'ffaker' end ``` #### Rspec Installer - run `rails generate rspec:install` - Because you are not using Minitest, you can delete the `test/` folder at the root of your repository. ### 3. CSS Bundling Rails (optional) ``` bundle add cssbundling-rails ./bin/rails css:install:bootstrap ``` ### 4. HOTGLUE INSTALLER Add `gem 'hot-glue'` to your Gemfile & `bundle install` During in installation, you MUST supply a `--layout` flag. #### `--layout` flag (only two options: `hotglue` or `bootstrap`; default is `bootstrap`) Here you will set up and install Hot Glue for the first time. It will install a config file that will save two preferences: layout (`hotglue` or `bootstrap`) The installer will create `config/hot_glue.yml`. #### `--theme` flag During the installation, **if** your `--layout` flag is set to `hotglue` you must also pass `--theme` flag. the themes are: • like_mountain_view (Google) • like_los_gatos (Netflix) • like_bootstrap (bootstrap 4 copy) • dark_knight (_The Dark Night_ (2008) inspired) • like_cupertino (modern Apple-UX inspired) #### `--markup` flag (NOTE: haml and slim are no longer supported at this time) default is `erb`. IMPORTANT: As of right now, HAML and SLIM are not currently supported so the only option is also the default `erb`. #### example installing ERB using Bootstrap layout: `./bin/rails generate hot_glue:install --markup=erb --layout=bootstrap` #### Example installing using Hot Glue layout and the 'like_mountain_view' (Gmail-inspired) theme: `rails generate hot_glue:install --markup=erb --layout=hotglue --theme=like_mountain_view` The Hot Glue installer did several things for you in this step. Examine the git diffs or see 'Hot Glue Installer Notes' below. ### 4. Devise (If you are on Rails 6, you must do ALL of the steps in the Legacy Setup steps. Be sure not to skip **Legacy Step #5** below) https://github.com/jasonfb/hot-glue/blob/main/README2.md You MUST run the installer FIRST or else you will put your app into a non-workable state: ``` ./bin/rails generate devise:install ``` IMPORTANT: Follow the instructions the Devise installer gives you, *Except Step 3*, you can skip this step: ``` 3. Ensure you have flash messages in app/views/layouts/application.html.erb. For example:
<%= notice %>
<%= alert %>
``` Be sure to create primary auth model with: `./bin/rails generate devise User name:string` Remember, you don't need to tell Devise that your User has an email, an encrypted password, a reset token, and a 'remember me' flag to let the user stay logged in. Those features come by default with Devise, and you'll find the fields for them in the newly generated migration file. In this example above, you are creating all of those fields along with a simple 'name' (string) field for your User table. Devise 4.9.0 is now fully compatible with Rails 7. #### Hot Glue Installer Notes These things were **done for you** in Step #3 (above). You don't need to think about them but if you are familiar with Capybara and/or adding Hot Glue to an existing app, you may want to: ##### Hot Glue modified `application.html.erb` Note: if you have some kind of non-standard application layout, like one at a different file or if you have modified your opening tag, this may not have been automatically applied by the installer. - This was added to your `application.html.erb` ``` <%= render partial: 'layouts/flash_notices' %> ``` ##### Hot Glue modified `rails_helper.rb` Note: if you have some kind of non-standard rails_helper.rb, like one that does not use the standard ` do |config|` syntax after your `RSpec.configure` this may not have been automatically applied by the installer. - configure Rspec to work with Factory Bot inside of `rails_helper.rb` ``` RSpec.configure do |config| // ... more rspec configuration (not shown) config.include FactoryBot::Syntax::Methods end ``` ##### Hot Glue switched Capybara from RACK-TEST to HEADLESS CHROME - By default Capybara is installed with :rack_test as its driver. - This does not support Javascript. Hot Glue is not targeted for fallback browsers. - From the [Capybara docs](https://github.com/teamcapybara/capybara#drivers): ``` By default, Capybara uses the :rack_test driver, which is fast but limited: it does not support JavaScript ``` - To fix this, you must switch to a Javascript-supporting Capybara driver. You can choose one of: `Capybara.default_driver = :selenium` `Capybara.default_driver = :selenium_chrome` `Capybara.default_driver = :selenium_chrome_headless` By default, the installer should have added this option to your `rails_helper.rb` file: ``` Capybara.default_driver = :selenium_chrome_headless ``` Alternatively, you can define your own driver like so: ``` Capybara.register_driver :my_headless_chrome_desktop do |app| options = Selenium::WebDriver::Chrome::Options.new options.add_argument('--headless') options.add_argument('--disable-gpu') options.add_argument('--window-size=1280,1200') driver = Capybara::Selenium::Driver.new(app, browser: :chrome, options: options) driver end Capybara.default_driver = :my_headless_chrome_desktop ``` ##### Hot Glue Added a Quick (Old-School) Capybara Login For Devise - for a quick Capybara login, create a support helper in `spec/support/` and log-in as your user - in the default code, the devise login would be for an object called account and lives at the route `/accounts/sign_in` - modify the generated code (it was installed by the installed) for your devise login ``` def login_as(account) visit '/accounts/sign_in' within("#new_account") do fill_in 'Email', with: account.email fill_in 'Password', with: 'password' end click_button 'Log in' end ``` --- --- --- # HOT GLUE DOCS Remember: Use `bin/rails generate model Thing` to generate models. Then add `has_many`, `belongs_to`, and _migrate your database_ before building the scaffold with Hot Glue. You will also need every Rails model to contain _either_ a database column _or_ an object-level method named one of these five things: `name` `to_label` `full_name` `display_name` `email` If your database doesn't contain one of these five, add a method to your model using `def to_label`. This will be used as the default label for the object throughout the Hot Glue build system. ## First Argument (no double slash) TitleCase class name of the thing you want to build a scaffolding for. ``` ./bin/rails generate hot_glue:scaffold Thing ``` (note: Your `Thing` object must `belong_to` an authenticated `User` or alternatively you must create a Gd controller, see below.) About these docs: The options (take an argument) flags (do not take an argument) have been merged together. Flags will be listed with no trailing `=` ### `--god` or `--gd` Use this flag to create controllers with no root authentication. You can still use an auth_identifier, which can be useful for a meta-leval authentication to the controller. For example, FOR ADMIN CONTROLLERS ONLY, supply a auth_identifier and use `--god` flag. In Gd mode, the objects are loaded directly from the base class (these controllers have full access) ``` def load_thing @thing = Thing.find(params[:id]) end ``` ### `--namespace=` pass `--namespace=` as an option to denote a namespace to apply to the Rails path helpers `./bin/rails generate hot_glue:scaffold Thing --namespace=dashboard` This produces several views at `app/views/dashboard/things/` and a controller at`app/controllers/dashboard/things_controller.rb` The controller looks like so: ``` class Dashboard::ThingsController < ApplicationController before_action :authenticate_user! before_action :load_thing, only: [:show, :edit, :update, :destroy] def load_thing @thing = current_user.things.find(params[:id]) end ... end ``` ### `--auth=` By default, it will be assumed you have a `current_user` for your user authentication. This will be treated as the "authentication root" for the "poor man's auth" explained above. The poor man's auth presumes that object graphs have only one natural way to traverse them (that is, one primary way to traverse them), and that all relationships infer that a set of things or their descendants are granted access to "me" for reading, writing, updating, and deleting. Of course this is a sloppy way to do access control, and can easily leave open endpoints your real users shouldn't have access to. When you display anything built with the scaffolding, Hot Glue assumes the `current_user` will have `has_many` association that matches the pluralized name of the scaffold. In the case of nesting, we will automatically find the nested objects first, then continue down the nest chain to find the target object. This is how Hot Glue assumes all object are 'anchored' to the logged-in user. (As explained in the `--nested` section.) If you use Devise, you probably already have a `current_user` method available in your controllers. If you don't use Devise, you can implement it in your ApplicationController. If you use a different object other than "User" for authentication, override using the `auth` option. `./bin/rails generate hot_glue:scaffold Thing --auth=current_account` You will note that in this example it is presumed that the Account object will have an association for `things` It is also presumed that when viewing their own dashboard of things, the user will want to see ALL of their associated things. If you supply nesting (see below), your nest chain will automatically begin with your auth root object (see nesting) ### `--auth_identifier=` Your controller will call a method authenticate_ (AUTH IDENTIFIER) bang, like: `authenticate_user!` Before all of the controller actions. If you leave this blank, it will default to using the variable name supplied by auth with "current_" stripped away. (This is setup for devise.) Be sure to implement the following method in your ApplicationController or some other method. Here's a quick example using Devise. You will note in the code below, user_signed_in? is implemented when you add Devise methods to your User table. As well, the `after_sign_in_path_for(user)` here is a hook for Devise also that provides you with after login redirect to the page where the user first intended to go. ``` def authenticate_user! if ! user_signed_in? session['user_return_to'] = request.path redirect_to new_user_registration_path end end def after_sign_in_path_for(user) session['user_return_to'] || account_url(user) end ``` The default (do not pass `auth_identifier=`) will match the `auth` (So if you use 'account' as the auth, `authenticate_account!` will get invoked from your generated controller; the default is always 'user', so you can leave both auth and auth_identifier off if you want 'user') `./bin/rails generate hot_glue:scaffold Thing --auth=current_account --auth_identifier=login` In this example, the controller produced with: ``` before_action :authenticate_login! ``` However, the object graph anchors would continue to start from current_account. That is, ``` @thing = current_account.things.find(params[:id]) ``` Use empty string to **turn this method off**: `./bin/rails generate hot_glue:scaffold Thing --auth=current_account --auth_identifier=''` In this case a controller would be generated that would have NO before_action to authenticate the account, but it would still treat the current_account as the auth root for the purpose of loading the objects. Please note that this example would produce non-functional code, so you would need to manually fix your controllers to make sure `current_account` is available to the controller. ### `--nested=` This object is nested within another tree of objects, and there is a nested route in your `routes.rb` file with the specified parent controllers above this controller. When specifying the parent(s), be sure to use **singular case**. #### Example #1: One-level Nesting Invoice `has_many :lines` and a Line `belongs_to :invoice` ``` resources :invoices do resource :lines do end ``` `./bin/rails generate hot_glue:scaffold Invoice` `./bin/rails generate hot_glue:scaffold Line --nested=invoice` Remember, nested should match how the hierarchy of nesting is in your `routes.rb` file. (Which Hot Glue does not create or edit for you.) #### Example #2: Two-level Nesting Invoice `has_many :lines` and a Line `belongs_to :invoice` Line `has_many :charges` and Charge `belongs_to :line` **config/routes.rb** ``` resources :invoices do resources :lines do resources :charge end end ``` _For multi-level nesting use slashes to separate your levels of nesting._ `./bin/rails generate hot_glue:scaffold Invoice` `./bin/rails generate hot_glue:scaffold Line --nested=invoice` `./bin/rails generate hot_glue:scaffold Charge --nested=invoice/line` For non-Gd controllers, your auth root will be used as the starting point when loading the objects from the URL if this object is nested. (For Gd controllers the root object will be loaded directly from the ActiveRecord object.) In the example above, @invoice will be loaded from `@invoice = current_user.invoices.find(params[:invoice_id])` Then, @line will be loaded `@line = @invoice.lines.find(params[:line_id])` Then, finally the @charge will be loaded `@charge = @line.charges.find(params[:id])` This is "starfish access control" or "poor man's access control." It works when the current user has several things they can manage, and by extension can manage children of those things. #### Example #3: Polymorphic Nesting Use `(` and `)` to specify a non-standard upwards relationship from the child, as in the case of a polymorphic belongs_to ``` class Blast has_many :rules, as: :ruleable end class Rule # ruleable_id # ruleable_type belongs_to :ruleable, polymorphic: true end ``` routes.rb ``` resources :blasts do resources :rules end ``` `rails generate hot_glue:scaffold Blast --downnest=rules` `rails generate hot_glue:scaffold Rules --nested='blast(ruleable)'` Notices the relationship from the parent to child is `rules` but from the child to parent, it is `ruleable` instead of `blast` ### `--downnest=` Automatically create subviews down your object tree. This should be the name of a has_many relationship based from the current object. You will need to build scaffolding with the same name for the related object as well. On the list view, the object you are currently building will be built with a sub-view list of the objects related from the given line. The downnested child table (not to be confused with this object's `--nested` setting, where you are specifying this object's _parents_) is called a **child portal**. When you create a record in the child portal, the related record is automatically set to be owned by its parent (as specified by `--nested`). For an example, see the [v0.4.7 release notes](https://github.com/jasonfb/hot-glue/releases/tag/v0.4.7). Can now be created with more space (wider) by adding a `+` to the end of the downnest name - e.g. `--downnest=abc+,xyz` The 'Abcs' portal will display as 5 bootstrap columns instead of the typical 4. (You may use multiple ++ to keep making it wider but the inverse with minus is not supported If you are nesting from a controller that uses big edit, your child portals do not display on the list page. Instead, they display on the edit page, and they display below the record using a Bootstrap tab nav that is automatically built for you. #### Polymorphic Downnesting Here, a `Blast` `has_many :rules, as: :ruleable` The child object is named `Rule` but it can belong to a Blast or an Agent. (Agent also has a similar has_many for Rules) `belongs_to :ruleable, polymorphic: true` We build the blast & agent controllers like so: `bin/rails generate hot_glue:scaffold Blast --downnest='blast_rules(rules)'` `bin/rails generate hot_glue:scaffold Agent --downnest='agent_rules(rules)'` Notice that the relationship name is `rules` (not blast_rules), so what goes before the parenthesis is the controller name (with prefix) What goes inside the controller name is the real relationship name. For the children, we can't build one controller for the Rule, instead we build one for the `AgentRules` and another for the `BlastRules` `bin/rails generate hot_glue:scaffold Rule --nested='blast(ruleable)' --controller-prefix='Blast'` `bin/rails generate hot_glue:scaffold Rule --nested='agent(ruleable)' --controller-prefix='Agent'` (I realize building one child controller for each type of polymorph is tedius, but this is the best solution I could come up with.) As these are children, what goes into the `--netsed` setting inside the parentheses is the polymorphic name specified by `as:` when declaring the `belongs_to` config/routes.rb ``` resources :agents do resources :agent_rules end resources :blasts do resources :blast_rules end ``` Outside a polymorphic relationship, you sometimes have children with `belongs_to` that uses a custom name instead of the name of the class (using class_name on the belongs to) Imagine a `followings` table with two foreign keys: follower_id and follows_id (both pointing to a BskyUser) `belongs_to :follower, class_name: "BskyUser", foreign_key: :follower_id` `belongs_to :follows, class_name: "BskyUser", foreign_key: :follows_id` Here, specify nested using square braces for the non-standard parent name `--nested='bsky_users[follower]'` and `--nested='bsky_users[follows]'` ### `--stacked-downnesting` This puts the downnested portals on top of one another (stacked top to bottom) instead of side-by-side (left to right). This is useful if you have a lot of downnested portals and you want to keep the page from getting too wide. ### `--downnest-shows-headings` (default: false) Show headings (the label of the list) above downnested portals. ### `--record-scope=` Record scope allows you to apply a model based scope for the controller being generated. This is applied on top of all other scopes, searches, and modifiers applied to the built controller. `bin/rails :generate hot_glue:scaffold Order --record-scope='.is_open'` Be sure to use single quotes (`'`) and don't forget the dot (`.`) before your scope(s). Make sure your Order model has a scope `is_open`, like so: ``` scope :is_open, -> {where(state == 'open')} ``` Now all records displayed through the generated controller_ ### `--big-edit` If you do not want inline editing of your list items but instead want to fallback to full-page style behavior for your edit views, use `--big-edit`. The user will be taken to a full-screen edit page instead of an edit-in-place interaction. When using `--big-edit`, any downnested portals will be displayed on the edit page instead of on the list page. Big edit makes all edit and magic button operations happen using `'data-turbo': false`, fully reloading the page and submitting HTML requests instead of TURBO_STREAM requests. Likewise, the controller's `update` action always redirects instead of using Turbo. ### `--include=` Separate field names by COMMA If you specify an include list, it will be treated as a whitelist: no fields will be included unless specified on the include list. `./bin/rails generate hot_glue:scaffold Account --include=first_name,last_name,company_name,created_at,kyc_verified_at` You may not specify both include and exclude. Many options allow you to specify fields in multiple modes: • Standard • Smart layout • Specified grouping see "Layout & Manipulation features" for details on how to build your `--include` statement, including how to use `:` and `,` Also review Omitted Fields (`-` and `=`), Dynamic Blocks (`**`), Omitted Dynamic Blocks (`**-` and `**=`) Also see Set Column Widths ( `(` ... `)` ) to set the column widths explicitly in your `--include` ### `--exclude=` (separate field names by COMMA) By default, all fields are included unless they are on the default exclude list. (The default exclude list is `id`, `created_at`, `updated_at`, `encrypted_password`, `reset_password_token`, `reset_password_sent_at`, `remember_created_at`, `confirmation_token`, `confirmed_at`, `confirmation_sent_at`, `unconfirmed_email`.) If you specify any exclude list, those excluded **and** the default exclude list will be excluded. (If you need any of the fields on the default exclude list, you must use `--include` instead.) `./bin/rails generate hot_glue:scaffold Account --exclude=password` ### `--display-list-after-update` After an update-in-place normally only the edit view is swapped out for the show view of the record you just edited. Sometimes you might want to redisplay the entire list after you make an update (for example, if your action removes that record from the result set). To do this, use flag `--display-list-after-update`. The update will behave like delete and re-fetch all the records in the result and tell Turbo to swap out the entire list. ### `--hawk=` Hawk a foreign key that is not the object's owner to within a specified scope. Assuming a Pet belong_to a :human, when building an Appointments scaffold, you can hawk the `pet_id` to the current human's pets. (Whoever is the authentication object.) The hawk has two forms: a short-form (`--hawk=key`) and long form (`--hawk=key{scope}) The short form looks like this. It presumes there is a 'pets' association from `current_user` `--hawk=pet_id` (The long form equivalent of this would be `--hawk=pet_id{current_user.pets}`) This is covered in [Example #3 in the Hot Glue Tutorial](https://school.jfbcodes.com/8188) To hawk to a scope that is not the currently authenticated user, use the long form with `{...}` to specify the scope. Be sure to note to add the association name itself, like `users`: `--hawk=user_id{current_user.family.users}` This would hawk the Appointment's `user_id` key to any users who are within the scope of the current_user's has_many association (so, for any other "my" family, would be `current_user.family.users`). This is covered in [Example #4 in the Hot Glue Tutorial](https://school.jfbcodes.com/8188) ##### Polymoprhism with the Hawk If the field (foreign key) being hawked is a polymorphic foreign key, you need to list multiple objects which define the allowed scopes (one for each kind of parent type). In this case, you will use **spaces** to separate scopes (NOT commas) for example, if we have a `thing` that can belong (via parent_id and parent_type) to either people or places, we could restrict this thing to only people and places associated from the `account` object (which would be in-scope based on, for example, the nesting arrangement or a logged in-user, or the account currently being managed) `--hawk=parent_id{account.people account.places}` Hot glue wil convert the spaces to commas when writing the controller code. ##### Using the object inside of the hawk In the example above, we aren't using the name of the scaffold within the hawk. However, if you are using the object's name in the hawk (for example `thing` for a `ThingsController`), the view will need this as a local variable `thing` and the controller will need this as an instsance variable `@thing` In this special case, Hot Glue converts the local variable `thing` used within your hawk code into instance variable `@thing` For example, if we were building a Job scaffold, here we want restrict follow_up_target_id to a list of eligible targets (in our case, with an email address that matches the company's website), we could use a view helper. Notice that `targets_by_company` is defined in the helper below, and we pass it a relation from the current object which is `job` `hot_glue:scaffold Job --hawk='follow_up_target_id{targets_by_company(job.company)}' --code-in-controller='include JobHelper;` (notice that it is `job` not `@job`) // app/helpers/job_helper.rb ``` module JobHelper def targets_by_company(company) domain = company.website Target.where("email LIKE ?", "%#{domain}%" ) end end ``` The generated controller code looks like: ``` modified_params = hawk_params({follow_up_target_id: [targets_by_company(@job.company)]}, modified_params) ``` (`hawk_param` is defined in Hot Glue itself. Notice that the `@` was appended to the front of `job`) The edit form will look like this: ``` <%= f.collection_select(:follow_up_target_id, targets_by_company(job.company), :id, :name, { prompt: true, selected: job.follow_up_target_id }) %> ``` (In the _form view, the `job` is a local variable as we do not rely on the instance variables in subviews.) ### `--with-turbo-streams` If and only if you specify `--with-turbo-streams`, your views will contain `turbo_stream_from` directives. Whereas your views will always contain `turbo_frame_tags` (whether or not this flag is specified) and will use the Turbo stream replacement mechanism for non-idempotent actions (create & update). This flag just brings the magic of live-reload to the scaffold interfaces themselves. **_To test_**: Open the same interface in two separate browser windows. Make an edit in one window and watch your edit appear in the other window instantly. This happens using two interconnected mechanisms: 1) by default, all Hot Glue scaffold is wrapped in `turbo_frame_tag`s. The id of these tags is your namespace + the Rails dom_id(...). That means all Hot Glue scaffold is namespaced to the namespaces you use and won't collide with other turbo_frame_tag you might be using elsewhere 2) by appending **model callbacks**, we can automatically broadcast updates to the users who are using the Hot Glue scaffold. The model callbacks (after_update_commit and after_destroy_commit) get appended automatically to the top of your model file. Each model callback targets the scaffold being built (so just this scaffold), using its namespace, and renders the line partial (or destroys the content in the case of delete) from the scaffolding. please note that *creating* and *deleting* do not yet have a full & complete implementation: Your pages won't re-render the pages being viewed cross-peer (that is, between two users using the app at the same time) if the insertion or deletion causes the pagination to be off for another user. ### `--plural=` You don't need this if the pluralized version is just + "s" of the singular version. Only use for non-standard plurlizations, and be sure to pass it as TitleCase (as if you pluralized the model name which is non-standard for Rails) An better alternative is to define the non-standard plurlizations globally in your app, which Hot Glue will respect. Make a file at `config/initializers/inflections.rb` Add new inflection rules using the following format: ``` ActiveSupport::Inflector.inflections do |inflect| inflect.irregular 'clothing', 'clothes' inflect.irregular 'human', 'humans' end ``` ### `--ujs_syntax=true` (Default is set automatically based on whether you have turbo-rails installed) If you are pre-Turbo (UJS), your delete buttons will come out like this: `data: {'confirm': 'Are you sure you want to delete....?'}` If you are Turbo (Rails 7 or Rails 6 with proactive Turbo-Rails install), your delete button will be: `data: {'turbo-confirm': 'Are you sure you want to delete....?'}` If you specify the flag, you preference will be used. If you leave the flag off, Hot Glue will detect the presence of Turbo-Rails in your app. **WARNING**: If you created a new Rails app since October 2021 and you have the yanked turbo-rails Gems on your local machine, you will have some bugs with the delete buttons and also not be on the latest version of turbo-rails. Make sure to uninstall the yanked 7.1.0 and 7.1.1 from your machine with `gem uninstall turbo-rails` and also fix any Rails apps created since October 2021 by fixing the Gemfile. Details here: https://stackoverflow.com/questions/70671324/new-rails-7-turbo-app-doesnt-show-the-data-turbo-confirm-alert-messages-dont-f ### `--magic-buttons=` If you pass a list of magic buttons (separated by commas), they will appear in the button area on your list. It will be assumed there will be corresponding bang methods on your models. The bang (`!`) methods can respond in one of four ways: • With true, in which case a generic success message will be shown in the flash notice (“Approved” or “Rejected” in this case) • With false, in which case a generic error message will be shown in the flash alert (“Could not approve…”) • With a string, which will be assumed to be a “success” case, and will be passed to the front-end in the alert notice. • Raise an ActiveRecord exception This means you can be a somewhat lazy about your bang methods, but keep in mind the truth operator compares boolean true NOT any object is truth. So your return object must either be actually true (boolean), or an object that is string or string-like (responds to .to_s). Want to just say it didn’t work? Return false. Want to just say it was OK? Return true. Want to say it was successful but provide a more detailed response? Return a string. Finally, you can raise an ActiveRecord error which will also get passed to the user in the flash alert area. For more information see [Example 6 in the Tutorial](https://school.jfbcodes.com/8188) You can also define methods on your model that have the same name as the button with `_able?` at the end. (Prior to v0.6.20, these methods expected names with `able?` but no underscore.) The button will be display as disabled if the method returns false. ### `--related-sets=` Used to show a checkbox set of related records. The relationship should be a `has_and_belongs_to_many` or a `has_many through:` from the object being built. Consider the classic example of three tables: users, user_roles, and roles User `has_many :user_roles` and `has_many :roles, through: :user_roles` UserRole `belongs_to :user` and `belongs_to :role` and Role `has_many :user_roles` and `has_many :user, through: :user_roles` We'll generate a scaffold to edit the users table. A checkbox set of related roles will also appear to allow editing of roles. (In this example, the only field to be edited is the email field.) ``` rails generate hot_glue:scaffold User --related-sets=roles --include=email,roles --gd ``` Note this leaves open a privileged escalation attack (a security vulnerability). To fix this, you'll need to use Pundit with special syntax designed for this purpose. Please see [Example #17 in the Hot Glue Tutorial](https://school.jfbcodes.com/8188) • Remember you model should have `accepts_nested_attributes_for :roles, allow_destroy: true` • If you are using an --include list (not auto-detect or smart layout), be sure to treat the tags as-if it was one field on your layout and insert it according to where you want it. • Each related set can take two additional parameters: specify the label to use as the related label using curly braces `{`...`}`, and specify any hawk scope to be applied to the displayed list of associated objects (like the --hawk, with the need to explicitly call `--hawk`) using `[`...`]` Both parameters are optional. If the label field is unspecified, it will default to `label`. If the 2nd parameter is unspecified, it will display all records in the related table, with the base class + `.all` If the 1st parameter is left off, still use square braces `[...]` for the hawk. Example: `rails generate hot_glue:scaffold User --nested=company --related-sets=roles{name}[company.roles] --include=email,roles --gd` This shows the related set of `roles` using the field named `name` on the role object to display its name. Only the roles associated with the current company via the `company.roles` association. Notice that here `company` must be in scope, which can either be supplied by you in the base class, or in this example we have nested User within Company (so its nest path would be different than the example above), which would put `company` in the scope of the build. ### `--factory-creation={ ... }` The code you specify inside of `{` and `}` will be used to generate a new object. The factory should instantiate with any arguments (I suggest Ruby keyword arguments) and must provide a method that is the name of the thing. You may use semi-colons to separate multiple lines of code. For example, a user Factory might be called like so: `./bin/rails generate hot_glue:scaffold User --factory-creation='factory = UserFactory.new(params: user_params)' --gd` (Note we are relying on the `user_params` method provided by the controller.) You must do one of two things: 1) In the code you specify, set an instance variable `@user` to be the newly created thing. (Your code should contain something like `@thing = ` to trigger this option.) 2) Make a local variable called `factory` **and** have a method of the name of the object (`user`) on a local variable called `factory` that your code created (The code example above is the option for #2 because it does not contain `@user =`) If using number #2, Hot Glue will append this to the code specified: ``` @user = factory.user ``` Here's a sample UserFactory that will create a new user only if one with a matching email address doesn't exist. (Otherwise, it will update the existing record.) Your initialize method can take any params you need it to, and using this pattern your business logic is applied consistently throughout your app. (You must, of course, use your Factory everywhere else in your app too.) ``` class UserFactory attr_reader :user attr_accessor :email def initialize(params: {}) user = User.find_or_create_by(email: params[:email]) user.update(params) if user.new_record? # do special new user logic here, like sending an email end end end ``` be sure your factory code creates a local variable that follows this name **