订阅设置
简介
此示例演示如何实现订阅设置图标下拉列表。这允许用户更改其订阅设置,而无需离开电子邮件。它也适用于网站。
设置
我们使用 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
元素的值来解决此问题。由于用户无法与其交互或查看它,因此不需要禁用它,我们确保该元素始终包含与 select
元素相同的值。
我们首先添加一个隐藏的 input
元素,然后将 select
元素的 name
移动到隐藏的 input
元素,以便提交其值。为了使隐藏的 input
元素的值与 select
元素的值保持同步,我们使用 AMP.setState
在 select
元素的值更改时更新新的 nextSubscription
状态变量,并 绑定 隐藏的 input
元素的 value
属性到该状态变量。
要禁用 select
元素,我们需要一个表示表单是否正在提交的状态变量,该变量可用于 select
元素的 disabled
属性绑定。我们通过确保在 form
元素的 submit-success
和 submit-error
事件上将 nextSubscription
更新为 null
来确保表单未提交时 nextSubscription
为 null
,从而重复使用 nextSubscription
状态变量。最后,我们将 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;
最适合,因为我们希望保持元素在 tab 顺序中,并继续接收其点击事件。
.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 类在 z 轴上将 amp-img
组件堆叠在彼此的顶部。
.relative { position: relative; } .absolute { position: absolute; top: 0; right: 0; bottom: 0; left: 0; }
请注意,amp-img
组件彼此之间的堆叠顺序无关紧要,因为一次最多只有一个可见。
我们还使用这些 CSS 类将图标定位在不可见的 select
用户界面的后面,这样当用户尝试单击我们的图标之一时,他们实际上是在单击不可见的 select
元素并触发其事件! 我们通过将图标的代码放在 select
元素的代码之前(请参阅 不使用 z-index
属性的堆叠)来将新图标保持在 select
元素的后面。
我们使用以下 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 上编辑示例-
由 @TomerAberbach 撰写