响应式控制器

响应式控制器是一个可以挂钩到组件的响应式更新周期的对象。控制器可以将与某个功能相关的状态和行为打包在一起,使其可以在多个组件定义中重用。

你可以使用控制器来实现需要自己的状态和访问组件生命周期的功能,例如:

  • 处理全局事件,如鼠标事件
  • 管理异步任务,如通过网络获取数据
  • 运行动画

响应式控制器允许你通过组合更小的、本身不是组件的部分来构建组件。它们可以被视为可重用的、部分的组件定义,具有自己的标识和状态。

响应式控制器在许多方面与类混入(mixin)相似。主要区别在于它们有自己的标识,不会添加到组件的原型中,这有助于包含它们的 API,并允许你在每个宿主组件中使用多个控制器实例。更多详情请参见控制器和混入

每个控制器都有自己的创建 API,但通常你会创建一个实例并将其存储在组件中:

与控制器实例关联的组件被称为宿主组件。

控制器实例将自己注册以接收来自宿主组件的生命周期回调,并在控制器有新数据要渲染时触发宿主更新。这就是 ClockController 示例如何定期渲染当前时间的方式。

控制器通常会暴露一些在宿主的 render() 方法中使用的功能。例如,许多控制器会有一些状态,比如当前值:

由于每个控制器都有自己的 API,请参考具体控制器的文档了解如何使用它们。

响应式控制器是与宿主组件关联的对象,它实现了一个或多个宿主生命周期回调或与其宿主交互。它可以通过多种方式实现,但我们将重点关注使用 JavaScript 类,使用构造函数进行初始化,使用方法处理生命周期。

控制器通过调用 host.addController(this) 将自己注册到其宿主组件。通常控制器会存储对其宿主组件的引用,以便以后与之交互。

你可以添加其他构造函数参数用于一次性配置。

一旦你的控制器注册到宿主组件,你就可以添加生命周期回调和其他类字段和方法来实现所需的状态和行为。

响应式控制器生命周期,在 ReactiveController 接口中定义,是响应式更新周期的一个子集。LitElement 在其生命周期回调期间调用任何已安装的控制器。这些回调是可选的。

  • hostConnected():
    • Called when the host is connected.
    • Called after creating the renderRoot, so a shadow root will exist at this point.
    • Useful for setting up event listeners, observers, etc.
  • hostUpdate():
    • Called before the host's update() and render() methods.
    • Useful for reading DOM before it's updated (for example, for animations).
  • hostUpdated():
    • Called after updates, before the host's updated() method.
    • Useful for reading DOM after it's modified (for example, for animations).
  • hostDisconnected():
    • Called when the host is disconnected.
    • Useful for cleaning up things added in hostConnected(), such as event listeners and observers.

For more information, see Reactive update cycle.

A reactive controller host implements a small API for adding controllers and requesting updates, and is responsible for calling its controller's lifecycle methods.

This is the minimum API exposed on a controller host:

  • addController(controller: ReactiveController)
  • removeController(controller: ReactiveController)
  • requestUpdate()
  • updateComplete: Promise<boolean>

You can also create controllers that are specific to HTMLElement, ReactiveElement, LitElement and require more of those APIs; or even controllers that are tied to a specific element class or other interface.

LitElement and ReactiveElement are controller hosts, but hosts can also be other objects like base classes from other web components libraries, components from frameworks, or other controllers.

Building controllers from other controllers

Permalink to "Building controllers from other controllers"

Controllers can be composed of other controllers as well. To do this create a child controller and forward the host to it.

Combining controllers with directives can be a very powerful technique, especially for directives that need to do work before or after rendering, like animation directives; or controllers that need references to specific elements in a template.

There are two main patterns of using controllers with directives:

  • Controller directives. These are directives that themselves are controllers in order to hook into the host lifecycle.
  • Controllers that own directives. These are controllers that create one or more directives for use in the host's template.

For more information about writing directives, see Custom directives.

Reactive controllers do not need to be stored as instance fields on the host. Anything added to a host using addController() is a controller. In particular, a directive can also be a controller. This enables a directive to hook into the host lifecycle.

Directives do not need to be standalone functions, they can be methods on other objects as well, such as controllers. This can be useful in cases where a controller needs a specific reference to an element in a template.

For example, imagine a ResizeController that lets you observe an element's size with a ResizeObserver. To work we need both a ResizeController instance, and a directive that is placed on the element we want to observe:

To implement this, you create a directive and call it from a method:

TO DO

  • Review and cleanup this example

Reactive controllers are very general and have a very broad set of possible use cases. They are particularly good for connecting a component to an external resource, like user input, state management, or remote APIs. Here are a few common use cases.

Reactive controllers can be used to connect to external inputs. For example, keyboard and mouse events, resize observers, or mutation observers. The controller can provide the current value of the input to use in rendering, and request a host update when the value changes.

This example shows how a controller can perform setup and cleanup work when its host is connected and disconnected, and request an update when an input changes:

Asynchronous tasks, such as long running computations or network I/O, typically have state that changes over time, and will need to notify the host when the task state changes (completes, errors, etc.).

Controllers are a great way to bundle task execution and state to make it easy to use inside a component. A task written as a controller usually has inputs that a host can set, and outputs that a host can render.

@lit/task contains a generic Task controller that can pull inputs from the host, execute a task function, and render different templates depending on the task state.

You can use Task to create a custom controller with an API tailored for your specific task. Here we wrap Task in a NamesController that can fetch one of a specified list of names from a demo REST API. NameController exposes a kind property as an input, and a render() method that can render one of four templates depending on the task state. The task logic, and how it updates the host, are abstracted from the host component.

TO DO

  • Animations