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 textboxstoryText
- textarea inputsubmitButton
- 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!