在 StimulusJS 中处理 UIState 的最佳实践

Best practices for handling UIState in StimulusJS

刚开始使用 StimulusJS 并尝试遵循最佳实践。由于我来自 VueJs/Nuxt,组件之间的状态管理和通信非常不同,我的目标是遵循 StimulusJS 方式。

案例:

我有一个 Toggle 按钮可以切换电子商务目录中的 Sidebar。站点上实现的大多数切换从切换按钮到切换目标都有简单而密切的关系。但是,侧边栏在结构上与侧边栏 HTML 相去甚远。最好的方法是什么?切换按钮和切换目标都会有一个 class 切换 (.is-open)

  1. 将控制器向上移动,将所有元素作为子元素进行控制 这种方法的问题是 JS 数据挂钩分布在多个模板上,感觉不到组件

  2. 创建两个通过事件通信的控制器 一个 SidebarToggle(继承自我的基本切换控制器)和一个监听的 SidebarController。

  3. 将事件冒泡到 window 并使所有需要捕获此事件的组件甚至在 @window

  4. 还有别的吗?

下面的虚拟标记:

<section class="o-product-catalog mb-60px" data-controller="catalog">
   <div>
      <!-- Header -->
   </div>

   <section class="c-catalog-navigation container">
      <div class="c-catalog-filter-toggle">
         <p class="c-catalog-filter-toggle__label"><span ></span>Show/Hide Filters</p>
         <button class="c-catalog-filter-toggle__button"></button>
      </div>

      <div class="flex">
         <!-- Pagination.. -->
      </div>

   </section>

   <div class="container flex relative">
      <div class="Filters Sidebar">
         <div class="Catalog Facet"></div>
         <div class="Catalog Facet"></div>
         <div class="Catalog Facet"></div>
         <div class="Catalog Facet"></div>
      </div>

      <div class="Catalog Results">
         <div class="Catalog Item"></div>
      </div>
   </div>

</section>

当使用 Stimulus 构建东西时,我发现按照文档中的描述 start with the HTML 总是好的。

构建可访问性 HTML 时,您会发现答案变得更加清晰。您的 'toggle' 按钮将需要使用 aria-controls 来通知浏览器通过 id.

控制哪个侧边栏

从这里开始,您有一个从按钮到边栏的内置引用,您可以在 Stimulus 代码中利用它。

这也使查看 DOM 时发生的事情变得更加清晰。

至于与其他控制器的通信,您在 thinking with Browser events 中走在正确的轨道上,但最好将事件范围限定在可能需要它们的 DOM 元素中。

例子

HTML

  • 见下文,我们有相同的基本 DOM,但使用 aria-controls 来引用我们要控制的边栏的 ID 'catalog-sidebar'
  • 我在侧边栏中添加了一个单独的关闭按钮,以表明我们基本上只是使用相同的 data-action 方法来切换,无论是由点击还是调度事件触发。
<section class="o-product-catalog mb-60px" data-controller="catalog">
  <div>
    <!-- Header -->
  </div>

  <section class="c-catalog-navigation container">
    <div class="c-catalog-filter-toggle">
      <p class="c-catalog-filter-toggle__label">
        <span>Show/Hide Filters</span>
      </p>
      <button
        class="c-catalog-filter-toggle__button"
        aria-controls="catalog-sidebar"
        data-controller="sidebar-toggle"
        data-action="sidebar-toggle#toggle"
      >
        Show
      </button>
    </div>

    <div class="flex">
      <!-- Pagination.. -->
    </div>
  </section>

  <div class="container flex relative">
    <div
      class="Filters Sidebar"
      id="catalog-sidebar"
      aria-expanded="true"
      data-controller="sidebar"
      data-action="sidebar-toggle:toggle->sidebar#toggle"
    >
      <button type="button" data-action="sidebar#toggle">Close</button>
      <div class="Catalog Facet"></div>
      <div class="Catalog Facet"></div>
      <div class="Catalog Facet"></div>
      <div class="Catalog Facet"></div>
    </div>

    <div class="Catalog Results">
      <div class="Catalog Item"></div>
    </div>
  </div>
</section>

JavaScript

  • 两个控制器都在下面的同一代码段中。
  • Sidebar 有一个切换方法可以更改 aria-expanded 值(因此我们牢记可访问性)。
  • SidebarToggle 有一个切换方法(很难命名),它会在 connect 方法中将事件分派到找到的侧边栏。
import { Controller } from '@hotwired/stimulus';

/**
 * A sidebar (expanding menu).
 */
class Sidebar extends Controller {
  connect() {
    // ...
  }

  toggle() {
    const isExpanded = !!this.element.getAttribute('aria-expanded');
    this.element.setAttribute('aria-expanded', isExpanded ? 'false' : 'true');
  }
}

/**
 * A button which will toggle another sidebar elsewhere in the DOM.
 */
class SidebarToggle extends Controller {
  connect() {
    this.sidebar = document.getElementById(
      this.element.getAttribute('aria-controls')
    );

    if (!this.sidebar && this.application.debug) {
      console.error('should find a matching sidebar');
    }
  }

  toggle() {
    this.dispatch('toggle', { target: this.sidebar });
  }
}

export { Sidebar, SidebarToggle };

注意:我在本地没有 运行 此代码,但应该非常接近。