Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
October 15, 2021 07:19 pm GMT

Hotwire: best practices for stimulus

From my experience building several production apps with Hotwire, Turbo frames and Turbo streams handle the bulk of things you need to build an interactive web application.

You will however, definitely need a little JavaScript sprinkles from Stimulus.

I want to run through all of the stimulus controllers included in Happi and talk about some Best Practices from what I have learnt so far.

The first controller youll write

In every Hotwire app Ive built so far, the first controller I end up needing is ToggleController. This is usually when I set up my Tailwind UI layout and need to start hiding and showing nav menus.

ToggleController

As youll see below, I am importing useClickOutside from stimulus-use, its a great library with small, composable helpers, I urge you to check it out!

The other thing I like to do here is leave some usage comments, it makes it a lot easier to peep into the controller and see how things work and what data attributes I need to add to my HTML.

import { Controller } from "@hotwired/stimulus";import { useClickOutside } from "stimulus-use";/* * Usage * ===== * * add data-controller="toggle" to common ancestor * * Action (add this to your button): * data-action="toggle#toggle" * * Targets (add this to the item to be shown/hidden): * data-toggle-target="toggleable" data-css-class="class-to-toggle" * */export default class extends Controller {  static targets = ["toggleable"];  connect() {    // Any clicks outside the controllers element can     // be setup to either add a 'hidden' class or     // remove a 'open' class etc.    useClickOutside(this);  }  toggle(e) {    e.preventDefault();    this.toggleableTargets.forEach((target) => {      target.classList.toggle(target.dataset.cssClass);    });  }  clickOutside(event) {    if (this.data.get("clickOutside") === "add") {      this.toggleableTargets.forEach((target) => {        target.classList.add(target.dataset.cssClass);      });    } else if (this.data.get("clickOutside") === "remove") {      this.toggleableTargets.forEach((target) => {        target.classList.remove(target.dataset.cssClass);      });    }  }}

The biggest thing I can stress is to make your controllers as generic as possible. I could have made this controller NavbarController and then it would only toggle a navbar. Because this is generic, I have reached for it so many times in my app and been able to reuse it.

AutoSubmitController

import { Controller } from "@hotwired/stimulus";import Rails from "@rails/ujs";/* * Usage * ===== * * add data-controller="auto-submit" to your <form> element * * Action (add this to a <select> field): * data-action="change->auto-submit#submit" * */export default class extends Controller {  submit() {    Rails.fire(this.element, "submit");  }}

This one is tiny, I needed it to auto submit a form when these dropdowns are changed, to go ahead and save changes. Again, Ive kept it generic, so it could be reused in other places that require similar behaviour.

Auto submit dropdowns

DisplayEmptyController

Happi empty state

This one is super handy, it allows the empty state to work properly with Turbo Streams. Without it, when Turbo streams push new messages onto the screen, the UI showing You dont have any messages would still be visible and everything would look broken.

It also relies on stimulus-uses useMutation hook, which means it just workstm with Turbo streams and we dont need any complex callbacks and still dont need to reach for custom ActionCable messages.

import { Controller } from "@hotwired/stimulus";import { useMutation } from "stimulus-use";/* * Usage * ===== * * add data-controller="display-empty" to common ancestor * * Classes: * data-display-empty-hide-class="hidden" * * Targets: * data-display-empty-target="emptyMessage" * data-display-empty-target="list" * */export default class extends Controller {  static targets = ["list", "emptyMessage"];  static classes = ["hide"];  connect() {    useMutation(this, {      element: this.listTarget,      childList: true,    });  }  mutate(entries) {    for (const mutation of entries) {      if (mutation.type === "childList") {        if (this.listTarget.children.length > 0) {          // hide empty state          this.emptyMessageTarget.classList.add(this.hideClass);        } else {          // show empty state          this.emptyMessageTarget.classList.remove(this.hideClass);        }      }    }  }}

FlashController

This ones not as generic as I would like, maybe I should call is AutoHideController? Its pretty straightforward, automatically hide after 3 seconds, but can also be dismissed by clicking the 'X'.

import { Controller } from "@hotwired/stimulus";/* * Usage * ===== * * add data-controller="flash" to flash container * p.s. you probably also want data-turbo-cache="false" * * Action (for close cross): * data-action="click->flash#dismiss" * */export default class extends Controller {  connect() {    setTimeout(() => {      this.hideAlert();    }, 3000);  }  dismiss(event) {    event.preventDefault();    event.stopPropagation();    this.hideAlert();  }  hideAlert() {    this.element.style.display = "none";  }}

HovercardController

This one loads in a hovercard, similar to hovering a users avatar on Twitter or GitHub. Note: If you are planning on using this, bonus points for making it more configurable and using Stimulus CSS classes for the hidden class.

It might also be smart to use the new Rails Request.js library rather than directly using fetch.

import { Controller } from "@hotwired/stimulus";/* * Usage * ===== * * add the following to the hoverable area * data-controller="hovercard" * data-hovercard-url-value="some-url" # Also make sure to `render layout: false` * data-action="mouseenter->hovercard#show mouseleave->hovercard#hide" * * Targets (add to your hovercard that gets loaded in): * data-hovercard-target="card" * */export default class extends Controller {  static targets = ["card"];  static values = { url: String };  show() {    if (this.hasCardTarget) {      this.cardTarget.classList.remove("hidden");    } else {      fetch(this.urlValue)        .then((r) => r.text())        .then((html) => {          const fragment = document            .createRange()            .createContextualFragment(html);          this.element.appendChild(fragment);        });    }  }  hide() {    if (this.hasCardTarget) {      this.cardTarget.classList.add("hidden");    }  }  disconnect() {    if (this.hasCardTarget) {      this.cardTarget.remove();    }  }}

MessageComposerController

This controller is really the only app-specific stimulus controller Ive written so far, which is pretty remarkable, considering Ive built a full production quality app, with just a handful of lines of JS, this really shows the power of Hotwire and Turbo.

Happi has canned responses, which help you automate writing common messages. When you click a canned response, this will take its HTML and push it into the action text trix editor.

import { Controller } from "@hotwired/stimulus";/* * Usage * ===== * * add this to the messages form: * data-controller="message-composer" * * Action (add this to your snippets): * data-action="click->message-composer#snippet" data-html="content..." * */export default class extends Controller {  connect() {    this.editor = this.element.querySelector("trix-editor").editor;  }  snippet(event) {    this.editor.setSelectedRange([0, 0]);    this.editor.insertHTML(event.target.dataset.html);  }}

NavigationSelectController

Another simple one here, used for responsive navigation on mobile via a select menu.

This is used within the settings page, on large screens, we have tabs down the side and on mobile collapse these into a dropdown that when changed, navigates to another sub-page within settings.

import { Controller } from "@hotwired/stimulus";import { Turbo } from "@hotwired/turbo-rails";/* * Usage * ===== * * add data-controller="navigation-select" to common ancestor * * Action: * data-action="change->navigation-select#change" * */export default class extends Controller {  change(event) {    const url = event.target.value;    Turbo.visit(url);  }}

SlugifyController

This ones used when creating a team on Happi. You have to pick a custom email address that ends in @prioritysupport.net, to make the UX a bit nicer we want to pre-fill this input with your company name.

Slugify in action

import ApplicationController from "./application_controller";/* * Usage * ===== * * add data-controller="slugify" to common ancestor or form tag * * Action (add to the title input): * data-action="slugify#change" * * Target (add to the slug input): * data-slugify-target="slugField" * */export default class extends ApplicationController {  static targets = ["slugField"];  change(event) {    const { value } = event.target;    this.slugFieldTarget.value = value.toLowerCase().replace(/[^a-z0-9]/, "");  }}

Thats it!

Yep, a full application with a rich user-interface, live updates with websockets and only 8 JavaScript files to maintain!

Whats even better here, is that 7 of the 8 stimulus controllers can be copied and pasted into other apps, I use a lot of these across different projects.

How to get the most out of Hotwire?

As you can probably tell from all my controllers shown above, my number 1 tip is to keep things generic, try to glean the reusable behaviour when you need functionality, rather than creating specific controllers for specific parts of your application.

Other than that, try to rely on Turbo frames or streams to do the heavy lifting, you should really be avoiding writing stimulus controllers unless absolutely necessary, you can do a lot more with Turbo than you might think.

Finally, check out Better stimulus and Boring Rails for a lot of actionable tips and tricks!


Original Link: https://dev.to/phawk/hotwire-best-practices-for-stimulus-40e

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To