Introduction
This is not a Rails show, but from time to time we're going to be showcasing techniques or gems on a Rails project.
Creating a Rails project that looks relatively well and works out of the box is something that can take some time, specially if we're going to use it for just one or two shots in a video.
A great solution for this kinds of issues is to create a Rails Application Template.
A Rails Application template is just a ruby script file that follows a simple DSL allowing us to invoke different Rails related actions (as well as external shell commands) in order to perform specific actions to our newly created application without our involvement.
Creating our own template
The first step is to create a template file.
touch template.rb
The gem
method
As I mentioned on a previous episode, I use AwesomePrint
in most of my projects. So we can call the gem
method to add a gem to our Gemfile
. If you had other gems that you regularly include and use on your projects, they could be added here as well
"awesome_print" gem
We can even create gem groups.
:development do
gem_group "rubocop"
gem end
:development, :test do
gem_group "rspec-rails"
gem end
Using a template
Lets try out to see how we're going so far.
To create a new Rails application using our template, we can pass to the rails new
command the -m
flag with the path to our template.
rails new blog -m ./template.rb
Note that if we publish this template somewhere over the internet, we can use the file url instead of the local path.
rails new blog -m http://example.com/template.rb
But lets go back to trying it out locally.
rails new blog -m ./template.rb
$ Using --skip-bundle --skip-keeps --skip-jbuilder --skip-test --skip-system-test --minimal --css=tailwind from /home/fedex/.railsrc
create
create README.md
create Rakefile
...
apply /sandbox/template/template.rb
gemfile awesome_print
gemfile group :development
gsub Gemfile
gemfile rubocop
gsub Gemfile
gemfile group :development, :test
gsub Gemfile
gemfile rspec-rails
gsub Gemfile
create .rubocop.yml
$
Now that the application creation has finished, we can check out our Gemfile
and see that awesome-print
and both groups we added are present.
"https://rubygems.org"
source :github) { |repo| "https://github.com/#{repo}.git" }
git_source(
"3.2.2"
ruby
# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
"rails", "~> 7.0.6"
gem
# ...
"awesome_print"
gem
:development do
group "rubocop"
gem end
:development, :test do
group "rspec-rails"
gem end
That's how a Rails application template works. You define what you want on your application from its inception and the template takes care of producing it.
The file
method
Since we included rubocop
on our Gemfile
, lets ask our template to write a .rubocop.yml
file to configure it. To create a plain file, we use the file
method passing the file relative path from our root directory as the first argument and the desired file contents as the second.
".rubocop.yml", <<~CODE
file ---
AllCops:
NewCops: enable
Layout/HashAlignment:
EnforcedColonStyle: table
EnforcedHashRocketStyle: table
Lint/MissingSuper:
Enabled: false
Style/BlockDelimiters:
EnforcedStyle: semantic
BracesRequiredMethods: ['let', 'let!']
CODE
Updating an application template on a created app
We now have an application, so we can't use rails new
to implement our template changes.
But what we can do is run a generator to execute it
To apply a template to an existing application, we would run the app:template
generator with the LOCATION
environment variable passed in as an argument.
bin/rails app:template LOCATION=./template.rb
Again, the path can be a remote url.
bin/rails app:template LOCATION=http://example.com/template.rb
But of course, we will stick with the local one.
cd blog
$ bin/rails app:template LOCATION=../template.rb
$ gemfile awesome_print
gemfile group :development
gsub Gemfile
gemfile rubocop
gsub Gemfile
gemfile group :development, :test
gsub Gemfile
gemfile rspec-rails
gsub Gemfile
create .rubocop.yml
Unfortunately, most template methods are not idempotent, so we can see that our gem
calls have been duplicated. So keep this in mind.
"https://rubygems.org"
source :github) { |repo| "https://github.com/#{repo}.git" }
git_source(
# ...
"awesome_print"
gem
:development do
group "rubocop"
gem end
:development, :test do
group "rspec-rails"
gem end
"awesome_print"
gem
:development do
group "rubocop"
gem end
:development, :test do
group "rspec-rails"
gem end
With that caveat in mind, lets take a look at our newly created .rubocop.yml
file
---
AllCops:
NewCops: enable
Layout/HashAlignment:
EnforcedColonStyle: table
EnforcedHashRocketStyle: table
Lint/MissingSuper:
Enabled: false
Style/BlockDelimiters:
EnforcedStyle: semantic
BracesRequiredMethods: ['let', 'let!']
# ...
Adding Tailwind
So far, we've been creating files one way or another. Lets do something more involved.
Over the last few years, I've gotten used to using TailwindCSS in my projects. Lets install it using our template.
We first need to add the gem to the Gemfile
, which will make sure we'll have the tailwind-rails
gem available after bundle install
has been run.
"tailwindcss-rails" gem
The next step to installing TailwindCSS the Rails way is to run the tailwindcss:install
command.
"tailwindcss:install" rails_command
To run a command, we can use the rails_command
method. This method is equivalent to calling the rails
command in the terminal from the project's root directory. Like the terminal command, we need to pass the name of the generator to run; in our case, it's tailwindcss:install
.
Running generators
We can always use generators to, for example, create a scaffold
for a user.
We just need to use the generate
method, passing the generator name as the first argument, and the generator's argument as the second.
And of course, since we're creating a table on the database, we need to run our migrations.
:scaffold, "user name:string")
generate("db:migrate") rails_command(
Also, we don't see the rails welcome screen when we go to the root path, so lets add a route to our routes file.
'root to: "users#index"' route
Lets run the generator one more time and run the server.
rails new blog -m ./template.rb
$ cd blog
$ bin/dev $
If we navigate to localhost:3000
, we can now see the users#index
page, stylized with TailwindCSS and completely functional, which means that the migrations have already been run.
Committing our code
Finally, since this'll be the default application definition to use on every project, we can create a commit so that we don't have a bunch of files to stage right out of the box.
To do so, we can call the git
method with the :init
symbol as an argument to initialize the repository.
Then we can stage all changes. and finally commit with a message.
:init
git add: "--all"
git commit: "-m 'Initial commit'" git
Lets run the template and check the status of our repository.
Something went wrong here, we can see that even though we added all the files and commited, there are still several files to commit.
rails new blog -m ./template.rb
$ cd blog
$ git status
$ On branch main
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: app/assets/config/manifest.js
modified: app/views/layouts/application.html.erb
Untracked files:
(use "git add <file>..." to include in what will be committed)
app/javascript/
bin/bundle
bin/importmap
config/importmap.rb
vendor/javascript/
The problem was that some of the commands we run actually get run after the bundle install
step.
An example of this is the Tailwind installer. We need the tailwindcss-rails
gem available in order to run rails tailwindcss:install
, so it gets run after bundling.
"tailwindcss:install" rails_command
We also saw that the scaffold generator was actually styled using Tailwind, which means it was run even after that.
For this situations, Rails Application Templates have an after_bundle
callback.
The after_bundle
method will run all the steps inside it's block after the bundle install step has been run, giving us some extra flexibility.
do
after_bundle :init
git add: "--all"
git commit: "-m 'Initial commit'"
git end
If we run the template now, we can see that all the changes have been commited as we expected.
rails new blog -m ./template.rb
$ cd blog
$ git status
$ On branch main
nothing to commit, working tree clean
Asking questions
Rails Application Templates ship with a series of methods that allows us to ask questions to our users in order to customize the template at execution time. We have the ask
, =yes?= and =no?= methods.
For example, we can ask our user if they actually want to commit the changes.
do
after_bundle if yes?("Do you want to add an initial commit?")
:init
git add: "."
git commit: %Q{ -m 'Initial commit' }
git end
end
And when we run our template, we'll be able to decide if we want to execute this last step.
rails new blog -m ./template.rb
$ # ...
Do you want to add an initial commit? y
run git init from "."
Reinitialized existing Git repository in /sandbox/template/blog/.git/
run git add . from "."
run git commit -m 'Initial commit' from "."
main (root-commit) a1f4548] Initial commit
[106 files changed, 1945 insertions(+)
create mode 100644 .gitattributes
# ...
create mode 100644 .gitignore
create mode 100644 tmp/storage/.keep
create mode 100644 vendor/.keep
create mode 100644 vendor/javascript/.keep
Conclusion
That's a quick look at how Rails Application templates work.
You can learn more about them on the Rails Guides.
You can also find a great collection of templates on the Rails Bytes site.
Finally, you can view our templates for NB Casts episodes on GitHub. We'll be updating and modifying it according to our needs and it's free for you to use.
I hope you enjoyed this episode, and I'll see you on the next one.