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/subscriptionGETPOST 要求。

它會以類似以下的 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 要求的網址。這會查詢使用者目前的訂閱設定。

我們使用 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 要求的網址。

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 元素的值,從而解決此問題,隱藏元素不需要停用,因為使用者無法與其互動或看到它,我們可以確保它始終包含與 select 元素相同的值。

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

若要停用 select 元素,我們需要一個狀態變數來表示表單目前是否正在提交,我們可以在 select 元素的 disabled 屬性繫結中使用該變數。我們重複使用 nextSubscription 狀態變數,方法是確保在表單提交時將其設為 null,方法是在 form 元素的 submit-successsubmit-error 事件中,將 nextSubscription 更新為 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 屬性。這需要在 currentSubscription 狀態變數中維護 select 元素的目前值,並在 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 類別,將 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 屬性,讓圖示對螢幕閱讀器隱藏。

最後,我們在使用 :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.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 上編輯範例