AMP
  • 电子邮件

订阅设置

简介

此示例演示如何实现订阅设置图标下拉列表。 这允许用户在不离开电子邮件的情况下更改其订阅设置。 它也适用于网站。

设置

我们使用 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 处响应 GETPOST 请求。

它使用如下 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"
    }
  ]
}

currentSubscriptionisSelected 因用户的当前订阅设置而异。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>
在 playground 中打开此代码段

步骤 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>
在 playground 中打开此代码段

请注意,amp-list 使用 single-item 进行注释,因为我们的服务器使用单个逻辑值(订阅设置)响应 GET 请求,我们只希望模板为响应调用一次。设置 items="." 确保模板可以访问响应对象的根字段。

步骤 3:选择时更新订阅设置

要在用户选择新设置时更新服务器上的用户订阅设置,我们首先将 select 元素包装在 AMP form 元素中,将其 method 设置为 post,并将其 action-xhr 属性设置为我们的服务器正在侦听 POST 请求的 URL。

select 元素的 name 属性设置为 nextSubscriptionnextSubscription 是表单数据字段的名称,服务器期望用户的新订阅设置在 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>
在 playground 中打开此代码段

步骤 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-successsubmit-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>
在 playground 中打开此代码段

请注意,尽管 disabled 属性是一个 布尔属性,我们仍然可以有效地绑定它,因为 当绑定表达式为 false 时,amp-bind 将删除该属性

步骤 5:处理表单提交失败

如果表单提交失败,我们需要显示错误消息,并将 select 元素恢复到其先前的值。

要在表单提交失败时显示错误消息,我们将包含错误消息的元素添加到 form 元素的子元素中。我们使用 submit-error 属性 注释该子元素。此属性确保该元素仅在表单提交失败后可见。

select 元素的值恢复到其先前的值需要绑定每个 option 元素的 selected 属性。这需要在 currentSubscription 状态变量中维护 select 元素的当前值,并在 previousSubscription 状态变量中维护元素的先前值。

select 元素的值更改时,我们将 currentSubscription 设置为新值,并将 previousSubscription 设置为元素的先前值。如果这不是元素的值第一次更改,我们将这些值存储在 currentSubscription 状态变量中。否则,它将存储在 amp-list 响应的 currentSubscription 字段中。

我们通过绑定每个 optionselected 属性来确保 select 元素选中的 optioncurrentSubscription 保持同步。最后,我们通过在 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>
在 playground 中打开此代码段

步骤 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 组件。相反,我们使用以下 relativeabsolute 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>
在 playground 中打开此代码段
需要进一步解释?

如果此页面上的解释没有涵盖您的所有问题,请随时与其他 AMP 用户联系,讨论您的具体用例。

转到 Stack Overflow
未解释的功能?

AMP 项目强烈鼓励您的参与和贡献!我们希望您成为我们开源社区的持续参与者,但也欢迎您对特别感兴趣的问题做出一次性贡献。

在 GitHub 上编辑示例