Enable user interaction(启用用户交互)¶
Almost every application needs user interaction and Slate provides three primary functions for enabling interactivity:
- Input Widgets: Simple widgets that provide common UX elements for capturing input such as dropdown lists, text fields, radio buttons, and more.
- Selection state within other widgets: Most other widget types support some type of selection. Selection works a bit differently in each widget, so you'll need to play around to understand what selection type is possible.
- Event triggers: All widgets have a set of event triggers that broadcast when something in the widget state changes or when a user takes an action. Review the Events documentation for more details on how to incorporate events into your application.
The simplest pattern for capturing user input would be a static form: add input widgets and provide static options, then the user selections can be referenced in queries and functions to provide dynamic view. Normally, however, there is some complexity of “chained” inputs, where the selection in one or more inputs affects the set of available options in the next set of inputs. It's important to keep these chains short and intelligible - having too many parameters to set can lead to unintuitive and un-performant applications. See the 'Open Ended Exploration' anti-pattern below.
In this more complex configuration of dependent inputs, it's best practice to separate the workflow of configuring filters and the workflow of analyzing the resulting data. Put simply, these means that you should set any queries that depend on these user inputs to manual and provide the user with a button widget to Update Data. This pattern ensures that your application doesn't waste resources (and user time) by re-running all the queries with every filter change. This is especially key if you have any kind of free-text input - a text field or text area type widget - because otherwise downstream dependencies like queries will re-evaluate with every user keystroke.
:::callout{title="Writeback workflows"} Any time your application is capturing user input to write back data, you must configure the query to run manually and trigger the query on an explicit user action. Otherwise your query to persist the data will run with every input change, including every keystroke in text input widgets, leading to highly unexpected behavior. :::
Maintain user selections between sessions¶
If your application requires configuring a number of different inputs to get a useful view, users may want a way to “save” their configuration to share or return to later.
It is possible to build custom versions of this functionality, but it is easier to use the built-in shareable views feature.
Reset selections and manage default values¶
Another common pattern is to give a single action for the user to reset all fields to a set of default values. The simplest pattern here is to define a v_defaults variable:
{
"w_multiselectWidget_raw": ["a", "b"],
"w_multiselectWidget_display": ["Alpha", "Beta"],
"w_textInput": "default",
...
}
Then, in the configuration for each widget, in the json definition (under the </> icon) you can template the particular version of the selected value property.
For any widget that has a display value in addition to the raw value, make sure you template both the selectedValues and selectedDisplayValues:
{
...
selectedValues: "{{v_defaults.w_multiselectWidget_raw}}",
selectedDisplayValues: "{{v_defaults.w_multiselectWidget_display}}",
...
}
The final step is to configure an event to trigger an update to the v_defaults variable, which will cause the dependency graph to update all the downstream nodes, which will include all the input widgets with templated selection values, and the selections will return to default.
It would seem enough to simply set up an event like this: w_resetWidgetButton.click → v_defaults.set
// Set v_defaults to itself
return {{v_defaults}}
As discussed in the Why Things Happen in Slate section above, there is a nuance here around “forcing” the dependency graph to re-evaluate if the resolved value of the node hasn't changed. Therefore we need to add some “entropy” so that Slate understands this is a new value. It's as simple as:
// Get the default values
const defaults = {{v_defaults}}
// Generate some randomness
defaults.entropy = Math.random()
// Set the value
return defaults
With this pattern, you can easily give the user a button to click to reset the defaults or reset them after a query is submitted.
You can go further with defaults by setting variable values through URL parameters. For instance, you may want to give users following a link from one application a different default that users coming to the app directly, and another set again for users who view it within an iframe inside a different tool all together.
To integrate this into the pattern above, rather than use a single, complex variable with multiple properties, you'd “explode” that variable to have one per default, so you could use the variable name in the URL- see Variables for more details on how this works). One additional variable would serve as “entropy” and you could combine them all in a function that returns an object just like the one we originally had in a single variable:
const defaults = {
"w_multiselectWidget_raw": {{v_multiSelect_raw}},
"w_multiselectWidget_display": {{v_multiSelect_raw}},
"w_textInput": {{v_textInput}},
"entropy": {{v_entropy}}
...
}
Whenever you wanted to move back to the defaults, you could simply have an event reset v_entropy to a random value and the defaults would reset.
Validating User Input¶
Frequently you will need to validate user input to disable actions and provide user feedback. There are many ways to do this in Slate, but here's a common pattern that might be helpful in general cases. This function gathers all the user inputs and then implements checks. Each check can disable the form and/or provide feedback to the user.
In this example, we will validate a form for collecting information about projects, including the title, URL, contact name, project description, and project status. We want to both check the user input is valid (such that the email address has correct formatting) and also that the project doesn't already exist (such that the primary key is not already in use).
In the end we produce a JSON output that represents our validation and that we can reference throughout our application to provide user feedback and disable certain actions.
f_validateInputs
// -----------------------------
// Collect Inputs for Validation
// -----------------------------
// Values coming from user input
var inputs = {
Project_Title: {{i_projectTitle.text}},
Primary_URL: {{i_primaryUrl.text}},
Contact: {{i_contact.text}},
Project_Quote: {{i_projectQuote.text}},
Status: {{i_status.selectedValue}},
}
// Other values useful for validation
var uniqueTitle = {{q_searchDuplicateProject.result.[0].hits.length}} ? false : true
// --------------------
// Initialize Variables
// --------------------
// Global flag to determine if the action should be disabled
var disable = false;
// Messages to display to the user
var messages = [];
// Text to display in the 'Save' button
var button_text = "Save " + (inputs.Project_Title ? inputs.Project_Title : "")
// --------------------------------
// Implement Form Validation Checks
// --------------------------------
// For all new projects, check if the `title` is already in use
if ({{i_newProjectToggle.selectedValue}} && !uniqueTitle){
disable = true;
messages.push(`Title must be unique. "${{{i_projectTitle.text}}}" already exists.`)
}
// For new projects, check if the title is already a primary key
if ({{i_newProjectToggle.selectedValue}} && {{q_checkUniquePrimaryKey.result.[0].rows.length}}){
disable = true;
messages.push(`This title conflicts with an existing project. The project was created as "${{{q_checkUniquePrimaryKey.result.[0].rows.[0].primaryKey.project_id}}}" and is now titled as
"${{{q_checkUniquePrimaryKey.result.[0].rows.[0].columns.title}}}"`)
}
// Check if all the required fields have a value
if (!(inputs.Project_Title &&
inputs.Contact &&
inputs.Primary_URL &&
inputs.Project_Quote &&
inputs.Status
)){
disable = true;
messages.push("Complete all required fields.");
}
const email_regex = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
if (inputs.Contact && !email_regex.test(inputs.Contact)){
messages.push('Enter a valid email for "Contact Email"')
disable = true;
}
// Check character count for project description
if (inputs.Project_Quote && inputs.Project_Quote.length > 140){
messages.push ("'Project Quote' must be less that 140 characters.");
disable = true;
}
// Dynamic Save vs. Update text for the button
if (inputs.Project_Title && uniqueTitle) {
button_text = "Update " + inputs.Project_Title
}
return {
inputs,
disable,
messages,
button_text
}
We could use the output of this function to template the disabled property of our w_submit button to {{f_validate.disable}}. We can also have a simply text widget to display the error messages in a red warning:
{{#each f_validateForm.messages}}
<div class='pt-tag pt-intent-danger pt-minimal'>{{this}}</div>
{{/each}}
This is a very simple example and could easily be extended to have messages that are specific to each check and displayed next to a particular widget, our provide classes to apply to specific widgets to control their display - for instance you could apply an invalid class that has CSS to turn the input header red.
“Dynamic Inputs”¶
Sometimes you need to capture input that doesn't seem to fit into static input widgets. In many cases you can tilt the problem on it's head and find a simple solution, but let's say you need to allow users to build a more complex filter or your use case seemingly can't be done with any creative use of static inputs.
You might be tempted to use a Repeating Container widget for this workflow, however these widgets are limited to display only. You can use an input widget inside a repeating container, but it is scoped only to that container and you won't be able to reference any instance of that input widget except from another widget inside the container.
Instead, you can have a single instance of your inputs, but allow the user to make selections multiple times by allowing them to ”save” their selections. You can display the accumulated selections in an HTML widget and even build in functionality to remove previous selections.
To implement this pattern, use a variable v_userSelections set to an empty array ([]). Then each time the user clicks the button to save their selection, you can update this variable:
w_saveSelection.click → v_userSelections.set
// Get the existing selections
const userSelections = {{v_userSelections}}
// Get the new selection from the current state of the input widgets
const newSelection = {
primaryCategory = {{w_primaryCategory.selectedValue}},
secondaryCategories = {{w_secondaryCategory.selectedValues}}
...
}
// Combine the new selections with the prior selections
const userSelections.push(newSelection);
// Update the value of the variable
return userSelections;
You can then implement further events to allow users to manipulate their existing selections, most commonly to have a "delete" action for each so they can be removed.
With this pattern, you can allow users to build up arbitrarily complex sets of input criteria. Be cautious as complexity blossoms and read further into the considerations for event-heavy patterns in the Events section above.
In general this pattern is a specific case of the general Stateful Application pattern discussed in more detail below.
\:::callout{title=”Mutually dependent filters”} The nature of the dependency graph means you can't configure widgets depend on each other in a circular relationship. Dependencies between input widgets need to “trickle down” and you won't be able to build a set of filters where the selection in any filter selection affect all widgets. \:::
Shareable views¶
Shareable views allow users to capture the current state of a Slate application and generate a unique URL that can be shared with others. When another user opens the URL, the application loads with the same selections and configurations that were present when the view was created.
A shareable view stores the following:
- The current value of all widget interaction properties; these are properties that are changeable by user interaction, such as
selectedValue. - The current value of all local variables.
When a shareable view URL is loaded, the saved values override widget and local variable defaults, and downstream dependencies initialize accordingly.
Create a shareable view¶
To manually create a shareable view, open the application in View mode and select Get shareable view from the Actions dropdown menu. This generates a unique URL with a view ID that you can share.
Slate application builders can programmatically create and load shareable views using the slate.saveView and slate.loadView actions. The slate.viewSaved event fires after a view is saved, providing the view ID that can be passed to slate.loadView.
Limitations¶
Shareable views have the following limitations:
- Home page only: Shareable views currently only save and restore the home page state of a Slate application. Only local variables are included; shared variables are not saved or restored.
- Widget and variable names: Shareable views are tied to widget and variable names. If a widget or variable is renamed after a view is created, the saved state for that widget or variable will not be restored when the view is loaded.
- Widget type changes: If a widget's type is changed between when a view is saved and when it is opened, the saved state for that widget will not be applied.
- Version independent: Shareable views work across any version of the application. A view created on one version of the application will load on any other version, as long as the referenced widget and variable names still exist.
中文翻译¶
启用用户交互¶
几乎每个应用程序都需要用户交互,Slate 提供了三种主要功能来实现交互性:
- 输入组件(Input Widgets): 提供常见用户体验元素的简单组件,用于捕获输入,如下拉列表、文本字段、单选按钮等。
- 其他组件中的选择状态(Selection state within other widgets): 大多数其他组件类型支持某种类型的选择。每个组件中的选择方式略有不同,因此您需要尝试以了解可能的选择类型。
- 事件触发器(Event triggers): 所有组件都有一组事件触发器,当组件状态发生变化或用户执行操作时会广播这些事件。请查阅事件文档了解如何将事件集成到应用程序中的更多详细信息。
捕获用户输入的最简单模式是静态表单:添加输入组件并提供静态选项,然后可以在查询和函数中引用用户选择以提供动态视图。然而,通常存在"链式"输入的复杂性,即一个或多个输入中的选择会影响下一组输入中的可用选项集。保持这些链简短且易于理解非常重要——设置过多参数可能导致应用程序不直观且性能不佳。请参见下面的"开放式探索"反模式。
在这种更复杂的依赖输入配置中,最佳实践是将配置过滤器的工作流与分析结果数据的工作流分开。简单来说,这意味着您应将依赖于这些用户输入的任何查询设置为manual,并为用户提供一个按钮组件来更新数据(Update Data)。这种模式确保您的应用程序不会在每次过滤器更改时重新运行所有查询,从而浪费资源(和用户时间)。如果您有任何类型的自由文本输入——文本字段(text field)或文本区域(text area)类型组件——这一点尤其关键,否则下游依赖项(如查询)将在每次用户按键时重新评估。
:::callout{title="回写工作流(Writeback workflows)"} 每当您的应用程序捕获用户输入以回写数据时,您必须将查询配置为手动运行,并在明确的用户操作上触发查询。否则,您的持久化数据查询将在每次输入更改时运行,包括文本输入组件中的每次按键,从而导致高度意外的行为。 :::
在会话之间维护用户选择¶
如果您的应用程序需要配置多个不同的输入才能获得有用的视图,用户可能希望有一种方式来"保存"他们的配置,以便分享或稍后返回。
可以构建此功能的自定义版本,但使用内置的可共享视图功能更简单。
重置选择和管理默认值¶
另一种常见模式是为用户提供单个操作,将所有字段重置为一组默认值。这里最简单的模式是定义一个v_defaults变量:
{
"w_multiselectWidget_raw": ["a", "b"],
"w_multiselectWidget_display": ["Alpha", "Beta"],
"w_textInput": "default",
...
}
然后,在每个组件的配置中,在 JSON 定义(在</>图标下)中,您可以使用模板化所选值属性的特定版本。
对于任何除了原始值之外还有显示值的组件,请确保对selectedValues和selectedDisplayValues都进行模板化:
{
...
selectedValues: "{{v_defaults.w_multiselectWidget_raw}}",
selectedDisplayValues: "{{v_defaults.w_multiselectWidget_display}}",
...
}
最后一步是配置一个事件来触发对v_defaults变量的更新,这将导致依赖关系图更新所有下游节点,包括所有具有模板化选择值的输入组件,并且选择将恢复为默认值。
简单地设置一个这样的事件似乎就足够了:w_resetWidgetButton.click → v_defaults.set
// 将 v_defaults 设置为其自身
return {{v_defaults}}
正如上面理解 Slate 中事件发生的原因部分所讨论的,如果节点的解析值没有改变,这里存在一个关于"强制"依赖关系图重新评估的细微差别。因此,我们需要添加一些"熵",以便 Slate 理解这是一个新值。这很简单:
// 获取默认值
const defaults = {{v_defaults}}
// 生成一些随机性
defaults.entropy = Math.random()
// 设置值
return defaults
使用这种模式,您可以轻松地为用户提供一个按钮来重置默认值,或者在提交查询后重置它们。
您可以通过 URL 参数设置变量值来进一步处理默认值。例如,您可能希望为从一个应用程序点击链接过来的用户提供与直接访问应用程序的用户不同的默认值,并为在另一个工具中的 iframe 内查看应用程序的用户提供另一组默认值。
要将此集成到上述模式中,您可以将该变量"分解"为每个默认值一个变量,而不是使用一个具有多个属性的复杂变量,这样您就可以在 URL 中使用变量名称——请参阅变量(Variables)了解其工作原理的更多详细信息。一个额外的变量将用作"熵",您可以将它们全部组合在一个函数中,该函数返回一个对象,就像我们最初在单个变量中拥有的对象一样:
const defaults = {
"w_multiselectWidget_raw": {{v_multiSelect_raw}},
"w_multiselectWidget_display": {{v_multiSelect_raw}},
"w_textInput": {{v_textInput}},
"entropy": {{v_entropy}}
...
}
每当您想要恢复到默认值时,只需让一个事件将v_entropy重置为随机值,默认值就会重置。
验证用户输入¶
您经常需要验证用户输入以禁用操作并提供用户反馈。在 Slate 中有多种方法可以做到这一点,但这里有一个在一般情况下可能有帮助的常见模式。此函数收集所有用户输入,然后执行检查。每个检查可以禁用表单和/或向用户提供反馈。
在此示例中,我们将验证一个用于收集项目信息的表单,包括标题、URL、联系人姓名、项目描述和项目状态。我们既要检查用户输入是否有效(例如电子邮件地址格式是否正确),也要检查项目是否已存在(例如主键是否已被使用)。
最后,我们生成一个 JSON 输出,代表我们的验证结果,我们可以在整个应用程序中引用它来提供用户反馈和禁用某些操作。
f_validateInputs
// -----------------------------
// 收集输入以进行验证
// -----------------------------
// 来自用户输入的值
var inputs = {
Project_Title: {{i_projectTitle.text}},
Primary_URL: {{i_primaryUrl.text}},
Contact: {{i_contact.text}},
Project_Quote: {{i_projectQuote.text}},
Status: {{i_status.selectedValue}},
}
// 用于验证的其他值
var uniqueTitle = {{q_searchDuplicateProject.result.[0].hits.length}} ? false : true
// --------------------
// 初始化变量
// --------------------
// 确定是否应禁用操作的全局标志
var disable = false;
// 向用户显示的消息
var messages = [];
// 在"保存"按钮中显示的文本
var button_text = "Save " + (inputs.Project_Title ? inputs.Project_Title : "")
// --------------------------------
// 实现表单验证检查
// --------------------------------
// 对于所有新项目,检查`title`是否已被使用
if ({{i_newProjectToggle.selectedValue}} && !uniqueTitle){
disable = true;
messages.push(`标题必须唯一。"${{{i_projectTitle.text}}}"已存在。`)
}
// 对于新项目,检查标题是否已经是主键
if ({{i_newProjectToggle.selectedValue}} && {{q_checkUniquePrimaryKey.result.[0].rows.length}}){
disable = true;
messages.push(`此标题与现有项目冲突。该项目创建为"${{{q_checkUniquePrimaryKey.result.[0].rows.[0].primaryKey.project_id}}}",现在标题为
"${{{q_checkUniquePrimaryKey.result.[0].rows.[0].columns.title}}}"`)
}
// 检查所有必填字段是否有值
if (!(inputs.Project_Title &&
inputs.Contact &&
inputs.Primary_URL &&
inputs.Project_Quote &&
inputs.Status
)){
disable = true;
messages.push("请填写所有必填字段。");
}
const email_regex = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
if (inputs.Contact && !email_regex.test(inputs.Contact)){
messages.push('请输入有效的"联系邮箱"')
disable = true;
}
// 检查项目描述的字符数
if (inputs.Project_Quote && inputs.Project_Quote.length > 140){
messages.push ("'项目引用'必须少于140个字符。");
disable = true;
}
// 按钮的动态保存与更新文本
if (inputs.Project_Title && uniqueTitle) {
button_text = "更新 " + inputs.Project_Title
}
return {
inputs,
disable,
messages,
button_text
}
我们可以使用此函数的输出来模板化w_submit按钮的disabled属性,设置为{{f_validate.disable}}。我们还可以使用一个简单的文本组件以红色警告显示错误消息:
{{#each f_validateForm.messages}}
<div class='pt-tag pt-intent-danger pt-minimal'>{{this}}</div>
{{/each}}
这是一个非常简单的示例,可以轻松扩展为每个检查都有特定的消息,并显示在特定组件旁边,或者提供应用于特定组件以控制其显示的类——例如,您可以应用一个invalid类,该类具有 CSS 将输入标题变为红色。
"动态输入"¶
有时您需要捕获似乎不适合静态输入组件的输入。在许多情况下,您可以换个角度思考问题并找到简单的解决方案,但假设您需要允许用户构建更复杂的过滤器,或者您的用例似乎无法通过任何创造性的静态输入使用方式来实现。
您可能会倾向于为此工作流使用重复容器(Repeating Container)组件,但这些组件仅限于显示。您可以在重复容器内使用输入组件,但其作用域仅限于该容器,并且您将无法从容器外部的任何其他组件引用该输入组件的任何实例。
相反,您可以拥有输入组件的单个实例,但允许用户通过"保存"他们的选择来多次进行选择。您可以在 HTML 组件中显示累积的选择,甚至可以构建删除先前选择的功能。
要实现此模式,请使用设置为空数组([])的变量v_userSelections。然后,每次用户单击按钮保存他们的选择时,您可以更新此变量:
w_saveSelection.click → v_userSelections.set
// 获取现有选择
const userSelections = {{v_userSelections}}
// 从输入组件的当前状态获取新选择
const newSelection = {
primaryCategory = {{w_primaryCategory.selectedValue}},
secondaryCategories = {{w_secondaryCategory.selectedValues}}
...
}
// 将新选择与先前选择合并
const userSelections.push(newSelection);
// 更新变量的值
return userSelections;
然后,您可以实现进一步的事件,允许用户操作他们现有的选择,最常见的是为每个选择提供"删除"操作,以便可以将其移除。
使用这种模式,您可以允许用户构建任意复杂的输入条件集。请注意复杂性会增长,并进一步阅读上面事件部分中关于事件密集型模式的注意事项。
通常,这种模式是下面更详细讨论的通用有状态应用程序(Stateful Application)模式的一个特定案例。
:::callout{title="相互依赖的过滤器(Mutually dependent filters)"} 依赖关系图的特性意味着您不能以循环关系配置相互依赖的组件。输入组件之间的依赖关系需要"向下传递",并且您将无法构建一组过滤器,其中任何过滤器选择都会影响所有组件。 :::
可共享视图¶
可共享视图允许用户捕获 Slate 应用程序的当前状态,并生成一个可以与他人共享的唯一 URL。当其他用户打开该 URL 时,应用程序将加载创建视图时存在的相同选择和配置。
可共享视图存储以下内容:
当加载可共享视图 URL 时,保存的值会覆盖组件和本地变量的默认值,下游依赖项会相应初始化。
创建可共享视图¶
要手动创建可共享视图,请在查看模式下打开应用程序,然后从操作下拉菜单中选择获取可共享视图。这将生成一个带有视图 ID 的唯一 URL,您可以分享。
Slate 应用程序构建者可以使用slate.saveView和slate.loadView操作以编程方式创建和加载可共享视图。保存视图后会触发slate.viewSaved事件,提供可以传递给slate.loadView的视图 ID。
限制¶
可共享视图具有以下限制:
- 仅限主页: 可共享视图目前仅保存和恢复 Slate 应用程序的主页状态。仅包含本地变量;共享变量不会被保存或恢复。
- 组件和变量名称: 可共享视图与组件和变量名称绑定。如果在创建视图后重命名了组件或变量,则在加载视图时不会恢复该组件或变量的保存状态。
- 组件类型更改: 如果在保存视图和打开视图之间更改了组件的类型,则不会应用该组件的保存状态。
- 版本无关: 可共享视图适用于应用程序的任何版本。在一个应用程序版本上创建的视图可以在任何其他版本上加载,只要引用的组件和变量名称仍然存在。