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.dev.org.tw/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.dev.org.tw/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.dev.org.tw/documentation/examples/interactivity-dynamic-content/subscription_settings/subscription" binding="refresh" items="." single-item layout="fill">
  <template type="amp-mustache">
    <form action-xhr="https://amp.dev.org.tw/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 屬性。幸好,我們可以透過新的 隱藏 input 元素 提交 select 元素的值來解決此問題,隱藏的 input 元素不需要停用,因為使用者無法與其互動或看到它,我們確保它始終包含與 select 元素相同的值。

我們先新增一個隱藏的 input 元素,並將 select 元素的 name 移至隱藏的 input 元素,以便提交其值。為了讓隱藏的 input 元素的值與 select 元素的值保持同步,每當 select 元素的值變更時,我們會使用 AMP.setState 更新新的 nextSubscription 狀態變數,並將隱藏的 input 元素的 value 屬性 繫結 至狀態變數。

若要停用 select 元素,我們需要一個狀態變數,代表表單目前是否正在提交,我們可以在 select 元素的 disabled 屬性繫結中使用該變數。我們重複使用 nextSubscription 狀態變數,方法是確保在表單提交時,透過在 form 元素的 submit-successsubmit-error 事件中將 nextSubscription 更新為 null,使其為 null。最後,我們將 select 元素的 disabled 屬性繫結至 !!nextSubscription,因為狀態變數在表單提交時為真值。我們對其每個 option 元素執行相同的操作,以防止使用者在表單提交時使用鍵盤在值之間切換。

載入中...
發生錯誤。請重新整理
<amp-list src="https://amp.dev.org.tw/documentation/examples/interactivity-dynamic-content/subscription_settings/subscription" binding="refresh" items="." single-item layout="fill">
  <template type="amp-mustache">
    <form action-xhr="https://amp.dev.org.tw/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 屬性。這需要將 select 元素的目前值維護在 currentSubscription 狀態變數中,並將元素的先前值維護在 previousSubscription 狀態變數中。

select 元素的值變更時,我們會將 currentSubscription 設定為新值,並將 previousSubscription 設定為元素的先前值。如果這不是元素的值第一次變更,我們會將這些值儲存在 currentSubscription 狀態變數中。否則,它會儲存在 amp-list 回應的 currentSubscription 欄位中。

我們透過繫結每個 option 元素的 selected 屬性,確保 select 元素的選取 optioncurrentSubscription 保持同步。最後,我們透過在 form 元素的 submit-error 事件中將 currentSubscription 設定為 previousSubscription,在表單提交失敗時將 select 元素還原為先前的值。

以下程式碼片段已設定為讓表單提交失敗。測試新的行為!

載入中...
發生錯誤。請重新整理
<amp-list src="https://amp.dev.org.tw/documentation/examples/interactivity-dynamic-content/subscription_settings/subscription" binding="refresh" items="." single-item layout="fill">
  <template type="amp-mustache">
    <form action-xhr="https://amp.dev.org.tw/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;,因為它現在看起來像按鈕。

下一步是建立新的圖示式使用者介面。我們先從包含每個訂閱設定的 amp-img 元件的 div 元素開始。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 元素的程式碼之前 (請參閱 不使用 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.dev.org.tw/documentation/examples/interactivity-dynamic-content/subscription_settings/subscription" binding="refresh" items="." single-item layout="fill">
  <template type="amp-mustache">
    <form action-xhr="https://amp.dev.org.tw/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 上編輯範例