Thoughts on software development and occasionally other things.

Auto-resizing text fields with Stimulus

Having a text field grow with its contents is a much nicer experience than having to scroll all the way through it. Writing and reviewing articles for my website wasn’t the easiest because of that. I mostly wrote in a text editor and copied/pasted to avoid having to scroll as much. That work-flow put some barriers up in terms of getting an article written. I’ve been experimenting with Stimulus lately, and figured it would be a good learning experience to solve the problem using it.

It really isn’t all that complex. There are different, and likely better, ways to solve this problem.

// expandable_field_controller.js
import { Controller } from 'stimulus'

export default class extends Controller {

  static targets = ['input']

  initialize() {
    this.setup()
    this.resize()
  }

  setup() {
    this.inputTarget.style.height = `${this.inputTarget.scrollHeight}px`
  }

  resize() {
    this.inputTarget.style.height = 'auto'
    this.inputTarget.style.height = `${this.inputTarget.scrollHeight}px`
  }

}

It’s worth noting that I have a min-height set on <textarea> controls that this is used with. Setting the height to auto on the resize() call prevented a weird bug when reducing the height (?). I chose to use the input event to trigger the behaviour.

<div data-controller="expandable-field">
  <%=
    form.text_area :content,
      data: {
        target: "expandable-field.input",
        action: "input->expandable-field#resize"
      }
  %>
</div>

Dynamic routes with Ruby on Rails

Often the need to build dynamic pages with static routes into Ruby on Rails applications comes up. One such example is an, ‘About Us’, page. The content has to be modifiable through a web interface, but the route is typically static, such as, /about. There are many approaches to handling this without getting into building something complex like a full content management system.

Here is a minimal model to represent a page.

Page(id: uuid, title: string, permalink: string, content: string)

A typical model would have a resourceful route defined for it (resources :pages) letting a page be accessible at /pages/:id. Overriding Page#to_param and setting it to the permalink attribute is a step in the right direction by producing a URL like /pages/about. It’s not ideal to define a named route that points to an action which loads a particular page:

##
# This approach works, but has drawbacks...
#

# config/routes.rb
get 'about', to: 'pages#about'

# app/controllers/pages_controller.rb
...
def about
  @page = Page.find_by(permalink: 'about')
end
...

This works, but it’s too rigid. If the permalink changes, or another page is added, the routes and controller would need to be updated. Routes cannot be dynamically generated and updated (i.e. defining named routes for all pages at present and in the future). However, Rails provides routing constraints, which can be used to achieve something similar.

get ':permalink',
  to: 'pages#show',
  constraints: lambda { |request| Page.exists?(permalink: request[:permalink]) }

This effectively defines a top-level route for all pages. In most cases it’s best to place this route near the bottom, so that is has the lowest priority. Here’s the controller action to back this route up:

# app/controllers/pages_controller.rb
...
def show
  @page = Page.find_by(permalink: params[:permalink])
end
...

An advantage of this approach over routing all unmatched requests to a controller action is that the Rails router can still take care of serving the 404 page!

Permalinks with Ruby on Rails

There are many ways to get nice looking permalinks using Ruby on Rails. By default, Rails uses the resource name and its primary key (/articles/1001 for example). I wanted something a little different for my personal website. There are more sophisticated ways to achieve this, but I went with a straight-forward approach using normal routes.

constraints year: /\d{4}/, month: /\w{3}/, day: /\d{1,2}/, permalink: /[\w\-]+/ do
  get 'articles/:year/:month/:day/:permalink', to: 'articles#show', as: :article
end

The other piece that makes this work is an instance method on the Article model.

def url
  [
    "",
    "articles",
    article.created_at.year,
    article.created_at.strftime("%b").downcase,
    article.created_at.day,
    article.permalink
  ] * "/"
end

Now calling @article.url will return a string that can be used to link directly to an article! The controller can use any logic it wants to find the article in the database. If the permalink column is unique in the database, then it could query it directly.

It is safe to expect more

It has been a long time since this domain was home to anything more than an empty page. Things are changing. I am making a commitment to my self to keep this, my personal space on the web, much more than an empty page.