Writing better Stimulus controllers

栏目: IT技术 · 发布时间: 4年前

内容简介:In early 2018, Basecamp releasedIt’s hard to pin down a name for this stack, but the basic approach is a vanilla Rails app with server-rendered views, Turbolinks (“HTML-over-the-wire”,Many of the tenets of Basecamp and DHH’s approach to building software w
We write a lot of JavaScript at Basecamp, but we don’t use it to create “JavaScript applications” in the contemporary sense. All our applications have server-side rendered HTML at their core, then add sprinkles of JavaScript to make them sparkle. - DHH

In early 2018, Basecamp released StimulusJS into the world . Stimulus closed the loop on the “Basecamp-style” of building Rails applications.

It’s hard to pin down a name for this stack, but the basic approach is a vanilla Rails app with server-rendered views, Turbolinks (“HTML-over-the-wire”, pjax ) for snappy page loads, and finally, Stimulus to “sprinkle” interactive behavior on top of your boring old HTML pages.

Many of the tenets of Basecamp and DHH’s approach to building software weave in-and-out of this stack:

And frankly, the most compelling to me: the tradition of extracting code from real-world products (and not trying to lecture birds how to fly ).

I’m excited to see more refinement of this stack as Basecamp prepares to launch HEY .

In the coming months, we should see the release of Stimulus 2.0 to sharpen the APIs, a reboot of Server-generated JavaScript Responses ( SJR ), and a splash of web-sockets to snap everything together.

These techniques are extremely powerful, but require seeing the whole picture. Folks looking to dive into this stack (and style of development) will feel the “Rails as a Sharp Knife” metaphor more so than usual.

But I’ve been in the kitchen for a while and will help you make nice julienne cuts (and not slice off your thumb).

Server-rendered views in Rails are a known path. Turbolinks, with a few caveats, is pretty much a drop-in and go tool these days.

So today, I’ll be focusing on how to write better Stimulus controllers .

This article is explicitly not an introduction to Stimulus. The official documentation and Handbook are excellent resources that I will not be repeating here.

And if you’ve never written any Stimulus controllers, the lessons I want to share here may not sink in right away. I know because they didn’t sink in for me!

It took 18 months of living full-time in a codebase using this stack before things started clicking. Hopefully, I can help cut down that time for you. Let’s begin!

What may go wrong

The common failure paths I’ve seen when getting started with Stimulus:

Making controllers too specific (either via naming or functionality)

It’s tempting to start out writing one-to-one Stimulus controllers for each page or section where you want JavaScript. Especially if you’ve used React or Vue for your entire application view-layer. This is generally not the best way to go with Stimulus.

It will be hard to write beautifully composable controllers when you first start. That’s okay.

Trying to write React in Stimulus

Stimulus is not React. React is not Stimulus. Stimulus works best when we let the server do the rendering. There is no virtual DOM or reactive updating or passing “data down, actions up”.

Those patterns are not wrong, just different and trying to shoehorn them into a Turbolinks/Stimulus setup will not work.

Growing pains weaning off jQuery

Writing idiomatic ES6 can be a stumbling block for people coming from the old days of jQuery.

The native language has grown leaps and bounds, but you’ll still scratch your head from time to time wondering if people really think that:

new Array(...this.element.querySelectorAll(".item"));

is an improvement on $('.item') . (I’m right there with you, but I digress… )

How to write better Stimulus controllers

After taking Stimulus for a test drive and making a mess, I revisited the Handbook and suddenly I saw the examples in a whole new light.

For instance, the Handbook shows an example for lazy loading HTML:

<div data-controller="content-loader" data-content-loader-url="/messages.html">
  Loading...
</div>

Notice the use of data-content-loader-url to pass in the URL to lazily load.

The key idea here is that you aren’t making a MessageList component. You are making a generic async loading component that can render any provided URL.

Instead of the mental model of extracting page components, you go up a level and build “primitives” that you can glue together across multiple uses.

You could use this same controller to lazy load a section of a page, or each tab in a tab group, or in a server-fetched modal when hovering over a link.

You can see real-world examples of this technique on sites like GitHub.

(Note that GitHub does not use Stimulus directly, but the concept is identical)

Writing better Stimulus controllers

The GitHub activity feed first loads the shell of the page and then uses makes an AJAX call that fetches more HTML to inject into the page.

<!-- Snippet from github.com -->
<div data-src="/dashboard-feed" data-priority="0">
  ...
</div>

GitHub uses the same deferred loading technique for the “hover cards” across the site.

Writing better Stimulus controllers

<!-- Snippet from github.com -->
<a
  data-hovercard-type="user"
  data-hovercard-url="/users/swanson/hovercard"
  href="/swanson"
>
  swanson
</a>

By making general-purpose controllers, you start the see the true power of Stimulus.

Level one is an opinionated, more modern version of jQuery on("click") functions.

Level two is a set of “behaviors” that you can use to quickly build out interactive sprinkles throughout your app.

Example: toggling classes

One of the first Stimulus controllers you’ll write is a “toggle” or “show/hide” controller. You’re yearning for the simpler times of wiring up a click event to call $(el).hide() .

Your implementation will look something like this:

// toggle_controller.js
import { Controller } from "stimulus";

export default class extends Controller {
  static targets = ["content"];

  toggle() {
    this.contentTarget.classList.toggle("hidden");
  }
}

And you would use it like so:

<div data-controller="toggle">
  <button data-action="toggle#toggle">Toggle</button>
  <div data-target="toggle.content">
    Some special content
  </div>
</div>

To apply the lessons about building more configurable components that the Handbook recommends, rework the controller to not hard-code the CSS class to toggle.

This will become even more apparent in the upcoming Stimulus 2.0 release when “classes” have a dedicated API.

// toggle_controller.js
import { Controller } from "stimulus";

export default class extends Controller {
  static targets = ["content"];

  toggle() {
    this.contentTargets.forEach((t) => t.classList.toggle(data.get("class")));
  }
}

The controller now supports multiple targets and a configurable CSS class to toggle.

You’ll need to update the usage to:

<div data-controller="toggle" data-toggle-class="hidden">
  <button data-action="toggle#toggle">Toggle</button>
  <div data-target="toggle.content">
    Some special content
  </div>
</div>

This might seem unnecessary on first glance, but as you find more places to use this behavior, you may want a different class to be toggled.

Consider the case when you also needed some basic tabs to switch between content.

<div data-controller="toggle" data-toggle-class="active">
  <div
   
    data-action="click->toggle#toggle"
    data-target="toggle.content"
  >
    Tab One
  </div>
  <div
   
    data-action="click->toggle#toggle"
    data-target="toggle.content"
  >
    Tab Two
  </div>
</div>

You can use the same code. New feature, but no new JavaScript! The dream!

Example: filtering a list of results

Let’s work through another common example: filtering a list of results by specific fields.

In this case, users want to filter a list of shoes by brand, price, or color.

Writing better Stimulus controllers

We’ll write a controller to take the input values and append them to the current URL as query parameters.

Base URL: /app/shoes
Filtered URL: /app/shoes?brand=nike&price=100&color=6

This URL scheme makes it really easy to filter the results on the backend with Rails.

// filters_controller.js
import { Controller } from "stimulus";

export default class extends Controller {
  static targets = ["brand", "price", "color"];

  filter() {
    const url = `${window.location.pathname}?${this.params}`;

    Turbolinks.clearCache();
    Turbolinks.visit(url);
  }

  get params() {
    return [this.brand, this.price, this.color].join("&");
  }

  get brand() {
    return `brand=${this.brandTarget.value}`;
  }

  get price() {
    return `price=${this.priceTarget.value}`;
  }

  get color() {
    return `color=${this.colorTarget.value}`;
  }
}

This will work, but it’s not reusable outside of this page. If we want to apply the same type of filtering to a table of Orders or Users, we would have to make separate controllers.

Instead, change the controller to handle arbitrary inputs and it can be reused in both places – especially since the inputs tags already have the name attribute needed to construct the query params.

// filters_controller.js
import { Controller } from "stimulus";

export default class extends Controller {
  static targets = ["filter"];

  filter() {
    const url = `${window.location.pathname}?${this.params}`;

    Turbolinks.clearCache();
    Turbolinks.visit(url);
  }

  get params() {
    return this.filterTargets.map((t) => `${t.name}=${t.value}`).join("&");
  }
}

Example: lists of checkboxes

We’ve seen how to make controllers more reusable by passing in values and using generic targets. One other way is to use optional targets in your controllers.

Imagine you need to build a checkbox_list_controller to allow a user to check all (or none) of a list of checkboxes. Additionally, it needs an optional count target to display the number of selected items.

You can use the has[Name]Target attribute to check for if the target exists and then conditionally take some action.

// checkbox_list_controller.js
import { Controller } from "stimulus";

export default class extends Controller {
  static targets = ["count"];

  connect() {
    this.setCount();
  }

  checkAll() {
    this.setAllCheckboxes(true);
    this.setCount();
  }

  checkNone() {
    this.setAllCheckboxes(false);
    this.setCount();
  }

  onChecked() {
    this.setCount();
  }

  setAllCheckboxes(checked) {
    this.checkboxes.forEach((el) => {
      const checkbox = el;

      if (!checkbox.disabled) {
        checkbox.checked = checked;
      }
    });
  }

  setCount() {
    if (this.hasCountTarget) {
      const count = this.selectedCheckboxes.length;
      this.countTarget.innerHTML = `${count} selected`;
    }
  }

  get selectedCheckboxes() {
    return this.checkboxes.filter((c) => c.checked);
  }

  get checkboxes() {
    return new Array(...this.element.querySelectorAll("input[type=checkbox]"));
  }
}

Here we can use the controller to add “Check All” and “Check None” functionality to a basic form.

Writing better Stimulus controllers

We can use the same code to build a checkbox filter that displays the count of the number of selections and a “Clear filter” button (“check none”).

Writing better Stimulus controllers

As with the other examples, you can see the power of creating Stimulus controllers that can be used in multiple contexts.

Putting it all together: composing multiple controllers

We can combine all three controllers to build a highly interactive multi-select checkbox filter.

Here is a rundown of how it all works together:

  • Use the toggle_controller to show or hide the color filter options when clicking the input

Writing better Stimulus controllers

  • Use the checkbox_list_controller to keep the count of selected colors and add a “Clear filter” option

Writing better Stimulus controllers

  • Use the filters_controller to update the URL when filter inputs change, for both basic HTML inputs and our multi-select filter

Writing better Stimulus controllers

Each individual controller is simple and easy to implement but they can be combined to create more complicated behaviors.

Here is the full markup for this example.

<div>
  <div data-controller="filters">
    <div>
      <div>Brand</div>
      <%= select_tag :brand,
            options_from_collection_for_select(
              Shoe.brands, :to_s, :to_s, params[:brand]
            ),
            include_blank: "All Brands",
            class: "form-select",
            data: { action: "filters#filter", target: "filters.filter" } %>
    </div>
    <div>
      <div>Price Range</div>
      <%= select_tag :price,
            options_for_select(
              [ ["Under $100", 100], ["Under $200", 200] ], params[:price]
            ),
            include_blank: "Any Price",
            class: "form-select",
            data: { action: "filters#filter", target: "filters.filter" } %>
    </div>

    <div>
      <div>Colorway</div>
      <div
        data-controller="toggle checkbox-list"
      >
        <button
          data-action="toggle#toggle"
          data-target="checkbox-list.count"
        >
          All
        </button>

        <div data-target="toggle.content">
          <div>
            <div>
              <div>Select colorways...</div>

              <button
                data-action="checkbox-list#checkNone filters#filter"
              >
                Clear filter
              </button>
            </div>

            <div>
              <% Shoe.colors.each do |c| %>
                <%= label_tag nil, class: "leading-none flex items-center" do %>
                  <%= check_box_tag 'colors[]', c, params.fetch(:colors, []).include?(c),
                    class: "form-checkbox text-indigo-500 mr-2",
                    data: { target: "filters.filter"} %>
                  <%= c %>
                <% end %>
              <% end %>
            </div>

            <div>
              <button
                data-action="filters#filter"
              >
                Apply
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Wrap it up

Stimulus works best when it’s used to add sprinkles of behavior to your existing HTML. Since Rails and Turbolinks are super effective at handling server-rendered HTML, these tools are a natural fit.

Using Stimulus requires a change in mindset from both jQuery snippets and React/Vue. Think about adding behaviors, not about making full-fledged components.

You’ll avoid the common stumbling blocks with Stimulus if you can make your controllers small, concise, and re-usable.

You can compose multiple Stimulus controllers together to mix-and-match functionality and create more complex interactions.

These techniques can be difficult to wrap your head around, but you can end up building highly interactive apps without writing much app-specific JavaScript at all!

Writing better Stimulus controllers

It’s an exciting time as this stack evolves, more people find success with shipping software quickly, and it becomes a more known alternative to the “all-in on JavaScript SPA” approach.

Additional Resources


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

P3P Web隐私

P3P Web隐私

克劳娜著、技桥译 / 克劳娜 / 清华大学出版社 / 2004-5 / 45.0

自万维网络中出现商业站点以来,基于Web的商业需求和用户的隐私权利之间就存在着不断的斗争。Web开发者们需要收集有关用户的信息,但是他们也需要表示出对用户隐私的尊重。因此隐私偏好工程平台,或者称之为P3P,就作为满足双方利益的技术应运而生了。 P3P由万维网协会研制,它为Web用户提供了对自己公开信息的更多的控制。支持P3P的Web站点可以为浏览者声明他们的隐私策略。支持P3P的浏览......一起来看看 《P3P Web隐私》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器