Lesson 2 Connect a Form - BuddyLReno/rails-stimulus-intro GitHub Wiki

⬅️ to Course Overview || 📝 Lesson 2 Feedback || 🐞 Lesson 2 Bug Report

In this lesson, we're going to start working with the Short Stories form. This form is a Rails simple_form that contains inputs for a title and description. The form we're going to create a stimulus controller for, exists at /app/views/short_stories/_short_story_form.html.erb. In your browser, navigating to http://localhost:3000/short-stories should take you to a simple page with a "New Story" link on it. Adding new stories will add them to the local database and display them on that initial index page below the "New Story" link.

Let's start by creating the ShortStoriesController and Adding the targets we want to work with.

✅ Create the controller and add initial targets

Relevant Docs: Controllers | Targets | Target Descriptors

Tasks

  • Create the ShortStoryController
  • Add targets for:
    • title - title textbox
    • storyText - textarea input
    • submitButton - form submit button

Walkthrough

In app/frontend/controllers create a new file called short_story_controller.js with this content:

import { Controller } from 'stimulus';

export default class ShortStoryController extends Controller {
  static targets = [
    'submitButton',
    'title',
    'storyText'
  ];
}

In the _short_story_form.html.erb file, we need to hook up the controller and all the targets.

To the main section element, add the data-controller attribute:

<section class="Card"
  data-controller="short-story"

The html targets are a little different since their being built with ruby. Update each input's data hash with the appropriate targets:

Title input

<%= form.input :title, 
  input_html: { 
    class: "Card-input",
    data: { target: "short-story.title" }
  }
%>

Story Text textarea

<%= form.input :story_text, 
  as: :text, 
  input_html: { 
    class: "Card-textarea",
    data: { target: "short-story.storyText" }
  }
%>

Submit Button

<%= form.submit "Save", 
  disable_with: 'Saving...', 
  data: {
    target: "short-story.submitButton"
  }
%>

✅ Disable submit button on load

Relevant docs: Lifecycle Callbacks | Connection

Tasks

  • Add connect lifecycle method
  • Use connect to disable submit button.
  • Refactor with a setter

Walkthrough

Now that the controller is attached, we want the form to disable the submit button until content is added. Stimulus offers a few lifecycle methods, one of which, connect, would be super useful to do this. Using the connect lifecycle method, the button can be disabled everytime the controller attaches to the form. Here's a quick overview of the various lifecycle methods available:

Method Invoked by Stimulus
initialize() Once, when the controller is first instantiated
connect() Anytime the controller is connected to the DOM
disconnect() Anytime the controller is disconnected from the DOM

Start by adding the connect method to the controller with a console log.

connect() {
  console.log('Yo Adrian, I did it!');
}

Reloading your page should show the log in the console! This will occur every time the controller is connected to the DOM. If you dynamically add another bit of html to the page with the data-controller="short-story" attribute, stimulus will add a new instance of the ShortStoryController and run the lifecycle methods. You would then see a new console log happen for the new instance. This does not overwrite other instances of existing controllers of the same type. You can create small micro controllers (for things like controlling lottie animations) that won't interfere with one another when multiple exist on the page at the same time.

Time to disable the submit button. Replace the console log with the code to disable the submitButtonTarget.

connect() {
  this.submitButtonTarget.disabled = true;
}

If done correctly, the submit button should be disabled when you refresh your browser.

Let's do a quick refactor with a Setter.

💡 If you're not familiar with getters and setters in javascript, here's some relevant documentation

Below the targets declaration, add a setter called disableForm that takes in value. Value can be named whatever you want.

set disableForm(value) {
  
}

Add the button target and assign value to disabled.

set disableForm(value) {
  this.submitButtonTarget.disabled = value;
}

Now you can assign true/false to it like a regular property in the connect method.

connect() {
  this.disableForm = true;
}

✅ Use keyup event to re-enable the submit button

Relevant docs: Actions | keyup Event

Tasks

  • Connect form keyup event to controller to validateForm() method
  • Add getters for input target values.
  • Add properties for storing the previous value of each input state.
  • Store previous state on connect.
  • Check if previous input state is the same as the new input values and enable the button.

Walkthrough

Let's start by adding a keyup event to the controller. After the connect method, add a validateForm() method to the controller. Then attach the action to the form tag to listen for the event.

validateForm() {
}
<%= simple_form_for @short_story, data: { action: "keyup->short-story#validateForm" } do |form| %>

💡 _Notice that the action was added to the form tag and not to each input? This is because the keyup event bubbles up which enables you to take advantage of event delegation.

When each input element fires a keyup event, the action for keyup will catch it on the form tag and send it to the validateForm method!

Next, add some variables to store previous input state as well as some getters to make it easy to obtain the current value from the input targets.

// included for reference
export default class ShortStoryController extends Controller {
  static targets = [
    'submitButton',
    'title',
    'storyText'
  ];

  previousTitle = '';
  previousStory = '';

  get title() {
    return this.titleTarget.value;
  }

  get storyText() {
    return this.storyTextTarget.value;
  }

In the connect method, store the initial state of the inputs. This will help determine when the form has been edited.

connect() {
  this.previousTitle = this.title;
  this.previousStory = this.storyText;
  this.disableForm = true;
}

Easy! The initial state is stored (helpful when this form is re-used for editing the story!), and we can determine when an input has been changed. Add two more getters to create properties that tell us whether the inputs are changed or not.

get isTitleChanged() {
  return this.previousTitle !== this.title;
}

get isStoryChanged() {
  return this.previousStory !== this.storyText;
}

Cool! Now you can call this.isStoryChanged or this.isTitleChanged and get a boolean to tell you whether those properties have been changed or not. Let's add one more getter to the controller that states if the form is valid and then update validateForm to finally connect the dots on keyup.

get isFormValid() {
  return this.isTitleChanged || this.isStoryChanged;
}
validateForm() {
  this.disableForm = this.isFormValid ? false : true;
}

Done! Refresh your page and when you type into either textbox, the submit button will enable and disable itself based on the current values.

Final Controller

import { Controller } from 'stimulus';

export default class ShortStoryController extends Controller {
  static targets = [
    'submitButton',
    'title',
    'storyText'
  ];

  previousTitle = '';
  previousStory = '';

  get storyText() {
    return this.storyTextTarget.value;
  }

  get title() {
    return this.titleTarget.value;
  }

  get isTitleChanged() {
    return this.previousTitle !== this.title;
  }

  get isStoryChanged() {
    return this.previousStory !== this.storyText;
  }

  get isFormValid() {
    return this.isTitleChanged || this.isStoryChanged;
  }

  set disableForm(value) {
    this.submitButtonTarget.disabled = value;
  }

  connect() {
    this.previousTitle = this.title;
    this.previousStory = this.storyText;
    this.disableForm = true;
  }

  validateForm() {
    this.disableForm = this.isFormValid ? false : true;
  }
}

This concludes Lesson 2. On to 🔗 Lesson 3!