订阅设置
简介
此示例演示如何实现订阅设置图标下拉列表。这使用户无需离开电子邮件即可更改其订阅设置。它也适用于网站。
设置
我们使用 amp-list
组件从服务器查询电子邮件或页面加载时用户的当前订阅设置。
<script async custom-element="amp-list" src="https://cdn.ampproject.org/v0/amp-list-0.1.js"></script>
我们将使用 amp-mustache
来呈现链接到服务器响应的订阅状态的下拉图标。
<script async custom-template="amp-mustache" src="https://cdn.ampproject.org/v0/amp-mustache-0.2.js"></script>
我们还将使用 amp-form
在用户从图标下拉列表中选择新设置时更新服务器上的用户订阅设置。
<script async custom-element="amp-form" src="https://cdn.ampproject.org/v0/amp-form-0.1.js"></script>
最后,我们使用 amp-bind
来响应事件更新下拉列表的状态。
<script async custom-element="amp-bind" src="https://cdn.ampproject.org/v0/amp-bind-0.1.js"></script>
实现
服务器
服务器响应 https://amp.js.cn/documentation/examples/interactivity-dynamic-content/subscription_settings/subscription
上的 GET 和 POST 请求。
它使用如下 JSON 对象响应 GET 请求
{ "currentSubscription": "only-mentions", "options": [ { "value": "watching", "isSelected": false, "text": "Watching", "imgUrl": "/images/watching.jpg" }, { "value": "only-mentions", "isSelected": true, "text": "Only mentions", "imgUrl": "/images/only-mentions.jpg" }, { "value": "ignoring", "isSelected": false, "text": "Ignoring", "imgUrl": "/images/ignoring.jpg" } ] }
currentSubscription
和 isSelected
因用户当前的订阅设置而异。isSelected
一次仅对 options
中的一个对象为 true
。
服务器期望 POST 请求在其表单数据中为 nextSubscription
字段指定上述订阅设置之一(例如,ignoring
)。然后,它会将用户的当前订阅设置更新为指定的值。
AMP-HTML
步骤 1:基本下拉列表
我们从一个用于下拉列表的原生 select
元素开始,并使用我们支持的订阅设置填充它。select
元素是一个很好的选择,因为它在所有平台上都具有高度可访问性。
<select>
<option value="watching">Watching</option>
<option value="only-mentions">Only mentions</option>
<option value="ignoring">Ignoring</option>
</select>
步骤 2:获取当前订阅设置
我们将 select
元素包装在 amp-list
中,并将其 src
设置为我们的服务器正在侦听 GET 请求的 URL。这将查询用户当前的订阅设置。
我们使用 amp-mustache
模板来呈现 select
元素,并确保与用户订阅设置对应的 option
元素具有 selected
属性。要理解为什么代码是重复的,请考虑 selected
属性是一个 布尔属性 并查看 amp-mustache
限制。
最后,我们添加一个 占位符和回退,以使用户了解用户界面的状态并优雅地处理任何问题。
<amp-list src="https://amp.js.cn/documentation/examples/interactivity-dynamic-content/subscription_settings/subscription" binding="refresh" items="." single-item layout="fill">
<template type="amp-mustache">
<select>
{{#options}}
{{#isSelected}}
<option value="{{value}}" selected>
{{text}}
</option>
{{/isSelected}}
{{^isSelected}}
<option value="{{value}}">
{{text}}
</option>
{{/isSelected}}
{{/options}}
</select>
</template>
<div placeholder>Loading...</div>
<div fallback>Something went wrong. Please refresh</div>
</amp-list>
请注意,amp-list
使用 single-item
进行注释,因为我们的服务器使用单个逻辑值(订阅设置)响应 GET 请求,我们只希望为响应调用一次模板。设置 items="."
可确保模板可以访问响应对象的根字段。
步骤 3:在选择时更新订阅设置
要在用户选择新设置时更新服务器上的用户订阅设置,我们首先将 select
元素包装在 AMP form
元素中,将其 method
设置为 post
,并将其 action-xhr
属性设置为我们的服务器正在侦听 POST 请求的 URL。
select
元素的 name
属性设置为 nextSubscription
。nextSubscription
是服务器期望在 POST 请求中找到用户的新订阅设置的表单数据字段的名称(请参阅服务器部分),并且 select
元素包含用户选择的订阅设置。
最后,我们在 select
元素的值更改时触发表单提交。我们通过使用 on
属性 将事件处理程序附加到 select
元素的 change
事件 来执行此操作,调用 form
元素的 submit
操作。请注意,我们为 form
元素赋予了 id
,以便我们可以在事件处理程序中引用它。
您可以通过选择新的订阅设置并刷新页面来测试代码。页面加载时 select
元素的订阅设置现在是您选择的那个!
<amp-list src="https://amp.js.cn/documentation/examples/interactivity-dynamic-content/subscription_settings/subscription" binding="refresh" items="." single-item layout="fill">
<template type="amp-mustache">
<form action-xhr="https://amp.js.cn/documentation/examples/interactivity-dynamic-content/subscription_settings/subscription" method="post" id="form1">
<select name="nextSubscription" on="change: form1.submit;">
{{#options}}
{{#isSelected}}
<option value="{{value}}" selected>
{{text}}
</option>
{{/isSelected}}
{{^isSelected}}
<option value="{{value}}">
{{text}}
</option>
{{/isSelected}}
{{/options}}
</select>
</form>
</template>
<div placeholder>Loading...</div>
<div fallback>Something went wrong. Please refresh</div>
</amp-list>
步骤 4:提交时禁用
从下拉列表中选择新的订阅设置后,不清楚表单何时正在提交或已提交。此外,没有任何措施可以阻止用户在表单仍在提交时尝试选择新的订阅设置。
理想情况下,在表单正在提交时,select
元素将被禁用。但是,这种方法存在一个问题:禁用控件的值不会随表单一起提交。在不同的情况下,我们可能会考虑使用 readonly
属性而不是 disabled
属性来避免此问题,但是 select
元素不支持 readonly
属性。幸运的是,我们可以通过提交 隐藏的 input
元素的值来解决此问题,该元素不需要被禁用,因为用户无法与其交互或看到它,并且我们确保该元素始终包含与 select
元素相同的值。
我们首先添加一个隐藏的 input
元素,并将 select
元素的 name
移动到隐藏的 input
元素,以便提交其值。为了使隐藏的 input
元素的值与 select
元素的值保持同步,我们使用 AMP.setState
在 select
元素的值更改时更新新的 nextSubscription
状态变量,并将隐藏的 input
元素的 value
属性 绑定 到该状态变量。
要禁用 select
元素,我们需要一个状态变量来表示表单当前是否正在提交,我们可以在 select
元素的 disabled
属性绑定中使用该变量。我们重用 nextSubscription
状态变量,方法是在表单未提交时通过在 form
元素的 submit-success
和 submit-error
事件上将 nextSubscription
更新为 null
来确保该变量为 null
。最后,我们将 select
元素的 disabled
属性绑定到 !!nextSubscription
,因为当表单正在提交时,该状态变量为真值。我们对其每个 option
元素执行相同的操作,以防止用户在表单正在提交时使用键盘在值之间切换。
<amp-list src="https://amp.js.cn/documentation/examples/interactivity-dynamic-content/subscription_settings/subscription" binding="refresh" items="." single-item layout="fill">
<template type="amp-mustache">
<form action-xhr="https://amp.js.cn/documentation/examples/interactivity-dynamic-content/subscription_settings/subscription" method="post" id="form2"
on="submit-success: AMP.setState({ nextSubscription: null });
submit-error: AMP.setState({ nextSubscription: null });">
<input type="hidden" name="nextSubscription" [value]="nextSubscription">
<select
on="change: AMP.setState({ nextSubscription: event.value }), form2.submit;"
[disabled]="!!nextSubscription">
{{#options}}
{{#isSelected}}
<option value="{{value}}" [disabled]="!!nextSubscription" selected>
{{text}}
</option>
{{/isSelected}}
{{^isSelected}}
<option value="{{value}}" [disabled]="!!nextSubscription">
{{text}}
</option>
{{/isSelected}}
{{/options}}
</select>
</form>
</template>
<div placeholder>Loading...</div>
<div fallback>Something went wrong. Please refresh</div>
</amp-list>
请注意,尽管 disabled
属性是一个布尔属性,我们仍然可以有效地绑定它,因为当绑定表达式为 false
时,amp-bind
会移除该属性。
步骤 5:处理表单提交失败
如果表单提交失败,我们需要显示错误消息,并将 select
元素恢复到其先前的值。
为了在表单提交失败时显示错误消息,我们将包含错误消息的元素添加到 form
元素的子元素中。我们使用 submit-error
属性 来注解该子元素。此属性确保该元素仅在表单提交失败后才可见。
要将 select
元素的值恢复到其先前的值,需要绑定每个 option
元素的 selected
属性。这需要维护 select
元素的当前值在一个 currentSubscription
状态变量中,并将该元素的先前值维护在 previousSubscription
状态变量中。
当 select
元素的值发生更改时,我们将 currentSubscription
设置为新值,并将 previousSubscription
设置为该元素的先前值。如果这不是该元素的值第一次更改,我们会将其存储在 currentSubscription
状态变量中。否则,它将存储在 amp-list
响应的 currentSubscription
字段中。
我们通过绑定每个 option
的 selected
属性,确保 select
元素选中的 option
与 currentSubscription
保持同步。最后,我们在 form
元素的 submit-error
事件中将 currentSubscription
设置为 previousSubscription
,从而在表单提交失败时将 select
元素恢复到其先前的值。
以下代码片段配置为使表单提交失败。测试一下新的行为!
<amp-list src="https://amp.js.cn/documentation/examples/interactivity-dynamic-content/subscription_settings/subscription" binding="refresh" items="." single-item layout="fill">
<template type="amp-mustache">
<form action-xhr="https://amp.js.cn/documentation/examples/interactivity-dynamic-content/subscription_settings/subscription?fail=true" method="post" id="form3"
on="submit-success: AMP.setState({ nextSubscription: null });
submit-error:
AMP.setState({
currentSubscription: previousSubscription,
nextSubscription: null
});">
<input type="hidden" name="nextSubscription" [value]="nextSubscription">
<select
on="change:
AMP.setState({
previousSubscription: currentSubscription || '{{currentSubscription}}',
currentSubscription: event.value,
nextSubscription: event.value
}),
form3.submit;"
[disabled]="!!nextSubscription">
{{#options}}
{{#isSelected}}
<option value="{{value}}" [disabled]="!!nextSubscription"
selected [selected]="(currentSubscription || '{{currentSubscription}}') == '{{value}}'">
{{text}}
</option>
{{/isSelected}}
{{^isSelected}}
<option value="{{value}}" [disabled]="!!nextSubscription"
[selected]="(currentSubscription || '{{currentSubscription}}') == '{{value}}'">
{{text}}
</option>
{{/isSelected}}
{{/options}}
</select>
<div submit-error>Something went wrong. Please try again</div>
</form>
</template>
<div placeholder>Loading...</div>
<div fallback>Something went wrong. Please refresh</div>
</amp-list>
步骤 6:图标下拉菜单
仅使用电子邮件客户端支持的 CSS 来将 select
元素本身样式化为看起来像图标是不可行的。相反,我们将隐藏 select
元素的持久用户界面(显示当前值的框),并为每个下拉菜单状态显示不同的图标。但是,我们将保持点击时显示下拉菜单。
幸运的是,样式化 select
会影响持久用户界面,而样式化 option
会影响下拉菜单。我们可以利用这一事实来隐藏前者,但保留后者。在使用 CSS 隐藏元素的三个常见选择(display: none;
、visibility: hidden;
和 opacity: 0;
)中,opacity: 0;
最适合,因为我们希望保持元素在制表符顺序中,并继续接收其点击事件。
.subscription-select { opacity: 0; cursor: pointer; }
我们还为 select
元素设置了 cursor: pointer;
,因为它现在看起来像一个按钮。
下一步是创建一个新的基于图标的用户界面。我们从一个包含 div
元素的 amp-img
组件开始,每个订阅设置对应一个。div
元素使用以下 icon
CSS 类进行注解,默认情况下会隐藏所有图像。
.icon > * { visibility: hidden; }
对应于 select
元素当前状态的 amp-img
组件通过以下 visible
CSS 类变为可见。
.visible { visibility: visible; }
如果 select
元素的值未更改,那么我们将从 amp-list
响应中的 isSelected
字段确定 select
元素的当前状态。否则,我们将从 currentSubscription
状态变量确定当前状态。
尽管最多只有一个 amp-img
组件在任何时候都是可见的,但每个组件仍然会占用页面上的空间。这意味着默认情况下,这些组件将一个接一个地位于下方。使用 display
属性而不是 visibility
属性可以解决此问题,但会导致图标在订阅设置更改之间闪烁,因为 AMP 不会预加载那些一开始就设置为 display: none;
的 amp-img
组件。相反,我们使用以下 relative
和 absolute
CSS 类,将 amp-img
组件在 Z 轴上相互堆叠。
.relative { position: relative; } .absolute { position: absolute; top: 0; right: 0; bottom: 0; left: 0; }
请注意,amp-img
组件相对于彼此的堆叠顺序并不重要,因为最多只有一个在任何时候都是可见的。
我们还使用这些 CSS 类将图标定位在不可见的 select
用户界面之后,以便当用户尝试点击我们的图标之一时,他们实际上是在点击不可见的 select
元素并触发其事件!我们将新图标保持在 select
元素之后,方法是将图标的代码放置在 select
元素的代码之前(请参阅不使用 z-index
属性的堆叠)。
我们使用以下 icon-sized
CSS 类来确保 select
元素的点击目标边界框与图标的大小相同。
.icon-sized { width: 30px; height: 30px; }
屏幕阅读器可以从 select
元素获取他们所需的所有信息,因此我们使用 aria-hidden
属性 使图标对屏幕阅读器不可见。
最后,当 select
元素获得焦点时,我们使用 :focus-within
伪选择器在图标周围显示一个轮廓。
.icon-container:focus-within { outline-offset: 2px; outline: 1px solid black; outline: -webkit-focus-ring-color auto 1px; }
我们完成了!
<amp-list src="https://amp.js.cn/documentation/examples/interactivity-dynamic-content/subscription_settings/subscription" binding="refresh" items="." single-item layout="fill">
<template type="amp-mustache">
<form action-xhr="https://amp.js.cn/documentation/examples/interactivity-dynamic-content/subscription_settings/subscription" method="post" id="form4"
on="submit-success: AMP.setState({ nextSubscription: null });
submit-error:
AMP.setState({
currentSubscription: previousSubscription,
nextSubscription: null
});">
<div class="icon-container relative icon-sized"
[class]="(nextSubscription ? 'disabled ' : ' ') + 'icon-container relative icon-sized'">
<div class="icon icon-sized" aria-hidden>
{{#options}}
<amp-img width="30" height="30" src="{{imgUrl}}"
class="{{#isSelected}}visible {{/isSelected}}absolute"
[class]="((currentSubscription || '{{currentSubscription}}') == '{{value}}' ? 'visible ' : ' ') + 'absolute'">
</amp-img>
{{/options}}
</div>
<input type="hidden" name="nextSubscription" [value]="nextSubscription">
<select
class="subscription-select absolute icon-sized"
on="change:
AMP.setState({
previousSubscription: currentSubscription || '{{currentSubscription}}',
currentSubscription: event.value,
nextSubscription: event.value
}),
form4.submit;"
[disabled]="!!nextSubscription">
{{#options}}
{{#isSelected}}
<option value="{{value}}" [disabled]="!!nextSubscription"
selected [selected]="(currentSubscription || '{{currentSubscription}}') == '{{value}}'">
{{text}}
</option>
{{/isSelected}}
{{^isSelected}}
<option value="{{value}}" [disabled]="!!nextSubscription"
[selected]="(currentSubscription || '{{currentSubscription}}') == '{{value}}'">
{{text}}
</option>
{{/isSelected}}
{{/options}}
</select>
</div>
<div submit-error>Something went wrong. Please try again</div>
</form>
</template>
<div placeholder>Loading...</div>
<div fallback>Something went wrong. Please refresh</div>
</amp-list>
如果此页面上的解释没有涵盖您的所有问题,请随时与其他 AMP 用户联系,讨论您的具体用例。
前往 Stack Overflow 一个无法解释的功能?AMP 项目强烈鼓励您的参与和贡献!我们希望您能成为我们开源社区的持续参与者,但我们也欢迎您为自己特别感兴趣的问题做出一次性贡献。
在 GitHub 上编辑示例