订阅设置
简介
此示例演示如何实现订阅设置图标下拉列表。 这允许用户在不离开电子邮件的情况下更改其订阅设置。 它也适用于网站。
设置
我们使用 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
属性。幸运的是,我们可以通过提交 select
元素的值来解决此问题,该值通过新的 隐藏的 input
元素,该元素不需要禁用,因为用户无法与其交互或查看它,我们确保始终包含与 select
元素相同的值。
我们首先添加一个隐藏的 input
元素,并将 select
元素的 name
移动到隐藏的 input
元素,以便提交其值。 为了使隐藏的 input
元素的值与 select
元素的值同步,我们使用 AMP.setState
每当 select
元素的值发生变化时更新新的 nextSubscription
状态变量,并 绑定 隐藏的 input
元素的 value
属性到状态变量。
要禁用 select
元素,我们需要一个状态变量来表示表单当前是否正在提交,以便在 select
元素的 disabled
属性绑定中使用。我们重用 nextSubscription
状态变量,方法是确保当表单未提交时,该变量为 null
,具体做法是在 form
元素的 submit-success
和 submit-error
事件中将 nextSubscription
更新为 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
属性。这需要在 currentSubscription
状态变量中维护 select
元素的当前值,并在 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
元素的代码之前来使新图标保持在 select
元素后面(请参阅 不使用 z-index
属性的堆叠)。
我们使用以下 icon-sized
CSS 类来确保 select
元素的单击目标边界框与图标的大小相同
.icon-sized { width: 30px; height: 30px; }
屏幕阅读器可以从 select
元素获取所有需要的信息,因此我们使用 aria-hidden
属性 使图标对屏幕阅读器不可见。
最后,当使用 :focus-within
伪选择器 将 select
元素聚焦时,我们在图标周围显示轮廓
.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 撰写