Permalink
Cannot retrieve contributors at this time
Fetching contributors…
| <!-- | |
| @license | |
| Copyright (c) 2016 The Polymer Project Authors. All rights reserved. | |
| This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt | |
| The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt | |
| The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt | |
| Code distributed by Google as part of the polymer project is also | |
| subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt | |
| --> | |
| <link rel="import" href="../bower_components/polymer/polymer.html"> | |
| <link rel="import" href="../bower_components/app-route/app-route.html"> | |
| <link rel="import" href="../bower_components/iron-flex-layout/iron-flex-layout.html"> | |
| <link rel="import" href="shop-button.html"> | |
| <link rel="import" href="shop-common-styles.html"> | |
| <link rel="import" href="shop-form-styles.html"> | |
| <link rel="import" href="shop-input.html"> | |
| <link rel="import" href="shop-select.html"> | |
| <link rel="import" href="shop-checkbox.html"> | |
| <dom-module id="shop-checkout"> | |
| <template> | |
| <style include="shop-common-styles shop-button shop-form-styles shop-input shop-select shop-checkbox"> | |
| .main-frame { | |
| transition: opacity 0.5s; | |
| } | |
| :host([waiting]) .main-frame { | |
| opacity: 0.1; | |
| } | |
| shop-input, shop-select { | |
| font-size: 16px; | |
| } | |
| shop-select { | |
| margin-bottom: 20px; | |
| } | |
| paper-spinner-lite { | |
| position: fixed; | |
| top: calc(50% - 14px); | |
| left: calc(50% - 14px); | |
| } | |
| .billing-address-picker { | |
| margin: 28px 0; | |
| height: 20px; | |
| @apply(--layout-horizontal); | |
| } | |
| .billing-address-picker > label { | |
| margin-left: 12px; | |
| } | |
| .grid { | |
| margin-top: 40px; | |
| @apply(--layout-horizontal); | |
| } | |
| .grid > section { | |
| @apply(--layout-flex); | |
| } | |
| .grid > section:not(:first-child) { | |
| margin-left: 80px; | |
| } | |
| .row { | |
| @apply(--layout-horizontal); | |
| @apply(--layout-end); | |
| } | |
| .column { | |
| @apply(--layout-vertical); | |
| } | |
| .row > .flex, | |
| .input-row > * { | |
| @apply(--layout-flex); | |
| } | |
| .input-row > *:not(:first-child) { | |
| margin-left: 8px; | |
| } | |
| .shop-select-label { | |
| line-height: 20px; | |
| } | |
| .order-summary-row { | |
| line-height: 24px; | |
| } | |
| .total-row { | |
| font-weight: 500; | |
| margin: 30px 0; | |
| } | |
| @media (max-width: 767px) { | |
| .grid { | |
| display: block; | |
| margin-top: 0; | |
| } | |
| .grid > section:not(:first-child) { | |
| margin-left: 0; | |
| } | |
| } | |
| </style> | |
| <div class="main-frame"> | |
| <iron-pages selected="[[state]]" attr-for-selected="state"> | |
| <div state="init"> | |
| <form | |
| id="checkoutForm" | |
| is="iron-form" | |
| method="post" | |
| action="/data/sample_success_response.json" | |
| on-iron-form-response="_didReceiveResponse" | |
| on-iron-form-presubmit="_willSendRequest"> | |
| <div class="subsection" visible$="[[!_hasItems]]"> | |
| <p class="empty-cart">Your <iron-icon icon="shopping-cart"></iron-icon> is empty.</p> | |
| </div> | |
| <header class="subsection" visible$="[[_hasItems]]"> | |
| <h1>Checkout</h1> | |
| <span>Shop is a demo app - form data will not be sent</span> | |
| </header> | |
| <div class="subsection grid" visible$="[[_hasItems]]"> | |
| <section> | |
| <h2 id="accountInfoHeading">Account Information</h2> | |
| <div class="row input-row"> | |
| <shop-input> | |
| <input type="email" id="accountEmail" name="accountEmail" | |
| placeholder="Email" autofocus required | |
| aria-labelledby="accountEmailLabel accountInfoHeading"> | |
| <shop-md-decorator error-message="Invalid Email" aria-hidden="true"> | |
| <label id="accountEmailLabel">Email</label> | |
| <shop-underline></shop-underline> | |
| </shop-md-decorator> | |
| </shop-input> | |
| </div> | |
| <div class="row input-row"> | |
| <shop-input> | |
| <input type="tel" id="accountPhone" name="accountPhone" pattern="\d{10,}" | |
| placeholder="Phone Number" required | |
| aria-labelledby="accountPhoneLabel accountInfoHeading"> | |
| <shop-md-decorator error-message="Invalid Phone Number" aria-hidden="true"> | |
| <label id="accountPhoneLabel">Phone Number</label> | |
| <shop-underline></shop-underline> | |
| </shop-md-decorator> | |
| </shop-input> | |
| </div> | |
| <h2 id="shipAddressHeading">Shipping Address</h2> | |
| <div class="row input-row"> | |
| <shop-input> | |
| <input type="text" id="shipAddress" name="shipAddress" pattern=".{5,}" | |
| placeholder="Address" required | |
| aria-labelledby="shipAddressLabel shipAddressHeading"> | |
| <shop-md-decorator error-message="Invalid Address" aria-hidden="true"> | |
| <label id="shipAddressLabel">Address</label> | |
| <shop-underline></shop-underline> | |
| </shop-md-decorator> | |
| </shop-input> | |
| </div> | |
| <div class="row input-row"> | |
| <shop-input> | |
| <input type="text" id="shipCity" name="shipCity" pattern=".{2,}" | |
| placeholder="City" required | |
| aria-labelledby="shipCityLabel shipAddressHeading"> | |
| <shop-md-decorator error-message="Invalid City" aria-hidden="true"> | |
| <label id="shipCityLabel">City</label> | |
| <shop-underline></shop-underline> | |
| </shop-md-decorator> | |
| </shop-input> | |
| </div> | |
| <div class="row input-row"> | |
| <shop-input> | |
| <input type="text" id="shipState" name="shipState" pattern=".{2,}" | |
| placeholder="State/Province" required | |
| aria-labelledby="shipStateLabel shipAddressHeading"> | |
| <shop-md-decorator error-message="Invalid State/Province" aria-hidden="true"> | |
| <label id="shipStateLabel">State/Province</label> | |
| <shop-underline></shop-underline> | |
| </shop-md-decorator> | |
| </shop-input> | |
| <shop-input> | |
| <input type="text" id="shipZip" name="shipZip" pattern=".{4,}" | |
| placeholder="Zip/Postal Code" required | |
| aria-labelledby="shipZipLabel shipAddressHeading"> | |
| <shop-md-decorator error-message="Invalid Zip/Postal Code" aria-hidden="true"> | |
| <label id="shipZipLabel">Zip/Postal Code</label> | |
| <shop-underline></shop-underline> | |
| </shop-md-decorator> | |
| </shop-input> | |
| </div> | |
| <div class="column"> | |
| <label id="shipCountryLabel" class="shop-select-label">Country</label> | |
| <shop-select> | |
| <select id="shipCountry" name="shipCountry" required | |
| aria-labelledby="shipCountryLabel shipAddressHeading"> | |
| <option value="US" selected>United States</option> | |
| <option value="CA">Canada</option> | |
| </select> | |
| <shop-md-decorator aria-hidden="true"> | |
| <shop-underline></shop-underline> | |
| </shop-md-decorator> | |
| </shop-select> | |
| </div> | |
| <h2 id="billAddressHeading">Billing Address</h2> | |
| <div class="billing-address-picker"> | |
| <shop-checkbox> | |
| <input type="checkbox" id="setBilling" name="setBilling" | |
| checked$="[[hasBillingAddress]]" on-change="_toggleBillingAddress"> | |
| <shop-md-decorator></shop-md-decorator aria-hidden="true"> | |
| </shop-checkbox> | |
| <label for="setBilling">Use different billing address</label> | |
| </div> | |
| <div hidden$="[[!hasBillingAddress]]"> | |
| <div class="row input-row"> | |
| <shop-input> | |
| <input type="text" id="billAddress" name="billAddress" pattern=".{5,}" | |
| placeholder="Address" required$="[[hasBillingAddress]]" | |
| autocomplete="billing street-address" | |
| aria-labelledby="billAddressLabel billAddressHeading"> | |
| <shop-md-decorator error-message="Invalid Address" aria-hidden="true"> | |
| <label id="billAddressLabel">Address</label> | |
| <shop-underline></shop-underline> | |
| </shop-md-decorator> | |
| </shop-input> | |
| </div> | |
| <div class="row input-row"> | |
| <shop-input> | |
| <input type="text" id="billCity" name="billCity" pattern=".{2,}" | |
| placeholder="City" required$="[[hasBillingAddress]]" | |
| autocomplete="billing address-level2" | |
| aria-labelledby="billCityLabel billAddressHeading"> | |
| <shop-md-decorator error-message="Invalid City" aria-hidden="true"> | |
| <label id="billCityLabel">City</label> | |
| <shop-underline></shop-underline> | |
| </shop-md-decorator> | |
| </shop-input> | |
| </div> | |
| <div class="row input-row"> | |
| <shop-input> | |
| <input type="text" id="billState" name="billState" pattern=".{2,}" | |
| placeholder="State/Province" required$="[[hasBillingAddress]]" | |
| autocomplete="billing address-level1" | |
| aria-labelledby="billStateLabel billAddressHeading"> | |
| <shop-md-decorator error-message="Invalid State/Province" aria-hidden="true"> | |
| <label id="billStateLabel">State/Province</label> | |
| <shop-underline></shop-underline> | |
| </shop-md-decorator> | |
| </shop-input> | |
| <shop-input> | |
| <input type="text" id="billZip" name="billZip" pattern=".{4,}" | |
| placeholder="Zip/Postal Code" required$="[[hasBillingAddress]]" | |
| autocomplete="billing postal-code" | |
| aria-labelledby="billZipLabel billAddressHeading"> | |
| <shop-md-decorator error-message="Invalid Zip/Postal Code" aria-hidden="true"> | |
| <label id="billZipLabel">Zip/Postal Code</label> | |
| <shop-underline></shop-underline> | |
| </shop-md-decorator> | |
| </shop-input> | |
| </div> | |
| <div class="column"> | |
| <label id="billCountryLabel" class="shop-select-label">Country</label> | |
| <shop-select> | |
| <select id="billCountry" name="billCountry" required$="[[hasBillingAddress]]" | |
| autocomplete="billing country" | |
| aria-labelledby="billCountryLabel billAddressHeading"> | |
| <option value="US" selected>United States</option> | |
| <option value="CA">Canada</option> | |
| </select> | |
| <shop-md-decorator aria-hidden="true"> | |
| <shop-underline></shop-underline> | |
| </shop-md-decorator> | |
| </shop-select> | |
| </div> | |
| </div> | |
| </section> | |
| <section> | |
| <h2>Payment Method</h2> | |
| <div class="row input-row"> | |
| <shop-input> | |
| <input type="text" id="ccName" name="ccName" pattern=".{3,}" | |
| placeholder="Cardholder Name" required | |
| autocomplete="cc-name"> | |
| <shop-md-decorator error-message="Invalid Cardholder Name" aria-hidden="true"> | |
| <label for="ccName">Cardholder Name</label> | |
| <shop-underline></shop-underline> | |
| </shop-md-decorator> | |
| </shop-input> | |
| </div> | |
| <div class="row input-row"> | |
| <shop-input> | |
| <input type="tel" id="ccNumber" name="ccNumber" pattern="[\d\s]{15,}" | |
| placeholder="Card Number" required | |
| autocomplete="cc-number"> | |
| <shop-md-decorator error-message="Invalid Card Number" aria-hidden="true"> | |
| <label for="ccNumber">Card Number</label> | |
| <shop-underline></shop-underline> | |
| </shop-md-decorator> | |
| </shop-input> | |
| </div> | |
| <div class="row input-row"> | |
| <div class="column"> | |
| <label for="ccExpMonth">Expiry</label> | |
| <shop-select> | |
| <select id="ccExpMonth" name="ccExpMonth" required | |
| autocomplete="cc-exp-month" aria-label="Expiry month"> | |
| <option value="01" selected>Jan</option> | |
| <option value="02">Feb</option> | |
| <option value="03">Mar</option> | |
| <option value="04">Apr</option> | |
| <option value="05">May</option> | |
| <option value="06">Jun</option> | |
| <option value="07">Jul</option> | |
| <option value="08">Aug</option> | |
| <option value="09">Sep</option> | |
| <option value="10">Oct</option> | |
| <option value="11">Nov</option> | |
| <option value="12">Dec</option> | |
| </select> | |
| <shop-md-decorator aria-hidden="true"> | |
| <shop-underline></shop-underline> | |
| </shop-md-decorator> | |
| </shop-select> | |
| </div> | |
| <shop-select> | |
| <select id="ccExpYear" name="ccExpYear" required | |
| autocomplete="cc-exp-year" aria-label="Expiry year"> | |
| <option value="2016" selected>2016</option> | |
| <option value="2017">2017</option> | |
| <option value="2018">2018</option> | |
| <option value="2019">2019</option> | |
| <option value="2020">2020</option> | |
| <option value="2021">2021</option> | |
| <option value="2022">2022</option> | |
| <option value="2023">2023</option> | |
| <option value="2024">2024</option> | |
| <option value="2025">2025</option> | |
| <option value="2026">2026</option> | |
| </select> | |
| <shop-md-decorator aria-hidden="true"> | |
| <shop-underline></shop-underline> | |
| </shop-md-decorator> | |
| </shop-select> | |
| <shop-input> | |
| <input type="tel" id="ccCVV" name="ccCVV" pattern="\d{3,4}" | |
| placeholder="CVV" required | |
| autocomplete="cc-csc"> | |
| <shop-md-decorator error-message="Invalid CVV" aria-hidden="true"> | |
| <label for="ccCVV">CVV</label> | |
| <shop-underline></shop-underline> | |
| </shop-md-decorator> | |
| </shop-input> | |
| </div> | |
| <h2>Order Summary</h2> | |
| <template is="dom-repeat" items="[[cart]]" as="entry"> | |
| <div class="row order-summary-row"> | |
| <div class="flex">[[entry.item.title]]</div> | |
| <div>[[_getEntryTotal(entry)]]</div> | |
| </div> | |
| </template> | |
| <div class="row total-row"> | |
| <div class="flex">Total</div> | |
| <div>[[_formatPrice(total)]]</div> | |
| </div> | |
| <shop-button responsive id="submitBox"> | |
| <input type="button" on-click="_submit" value="Place Order"> | |
| </shop-button> | |
| </section> | |
| </div> | |
| </form> | |
| </div> | |
| <!-- Success message UI --> | |
| <header state="success"> | |
| <h1>Thank you</h1> | |
| <p>[[response.successMessage]]</p> | |
| <shop-button responsive> | |
| <a href="/">Finish</a> | |
| </shop-button> | |
| </header> | |
| <!-- Error message UI --> | |
| <header state="error"> | |
| <h1>We couldn´t process your order</h1> | |
| <p id="errorMessage">[[response.errorMessage]]</p> | |
| <shop-button responsive> | |
| <a href="/checkout">Try again</a> | |
| </shop-button> | |
| </header> | |
| </iron-pages> | |
| </div> | |
| <!-- Handles the routing for the success and error subroutes --> | |
| <app-route | |
| route="[[route]]" | |
| pattern="/:state" | |
| on-active-changed="_activeRouteHandler"> | |
| </app-route> | |
| <!-- Show spinner when waiting for the server to repond --> | |
| <paper-spinner-lite active="[[waiting]]"></paper-spinner-lite> | |
| </template> | |
| <script> | |
| Polymer({ | |
| is: 'shop-checkout', | |
| properties: { | |
| /** | |
| * The route for the state. e.g. `success` and `error` are mounted in the | |
| * `checkout/` route. | |
| */ | |
| route: { | |
| type: Object, | |
| notify: true | |
| }, | |
| /** | |
| * The total price of the contents in the user's cart. | |
| */ | |
| total: Number, | |
| /** | |
| * The state of the form. Valid values are: | |
| * `init`, `success` and `error`. | |
| */ | |
| state: { | |
| type: String, | |
| value: 'init' | |
| }, | |
| /** | |
| * An array containing the items in the cart. | |
| */ | |
| cart: Array, | |
| /** | |
| * The server's response. | |
| */ | |
| response: Object, | |
| /** | |
| * If true, the user must enter a billing address. | |
| */ | |
| hasBillingAddress: { | |
| type: Boolean, | |
| value: false | |
| }, | |
| /** | |
| * If true, shop-checkout is currently visible on the screen. | |
| */ | |
| visible: { | |
| type: Boolean, | |
| observer: '_visibleChanged' | |
| }, | |
| /** | |
| * True when waiting for the server to repond. | |
| */ | |
| waiting: { | |
| type: Boolean, | |
| readOnly: true, | |
| reflectToAttribute: true | |
| }, | |
| /** | |
| * True when waiting for the server to repond. | |
| */ | |
| _hasItems: { | |
| type: Boolean, | |
| computed: '_computeHasItem(cart.length)' | |
| } | |
| }, | |
| _submit: function(e) { | |
| if (this._validateForm()) { | |
| // To send the form data to the server: | |
| // 2) Remove the code below. | |
| // 3) Uncomment `this.$.checkoutForm.submit()`. | |
| this.$.checkoutForm.fire('iron-form-presubmit', null, { bubbles: false }); | |
| this.debounce('_submitForm', function() { | |
| this.$.checkoutForm.fire('iron-form-response', { | |
| response: { | |
| success: 1, | |
| successMessage: 'Demo checkout process complete.' | |
| } | |
| }, { bubbles: false }); | |
| }, 1000); | |
| // this.$.checkoutForm.submit(); | |
| } | |
| }, | |
| /** | |
| * Sets the valid state and updates the location. | |
| */ | |
| _pushState: function(state) { | |
| this._validState = state; | |
| this.set('route.path', state); | |
| }, | |
| /** | |
| * Checks that the `:state` subroute is correct. That is, the state has been pushed | |
| * after receiving response from the server. e.g. Users can only go to `/checkout/success` | |
| * if the server responsed with a success message. | |
| */ | |
| _activeRouteHandler: function(e) { | |
| var route = e.target; | |
| if (e.detail.value) { | |
| var state = route.data.state; | |
| if (this._validState === state) { | |
| this.state = state; | |
| this._validState = ''; | |
| return; | |
| } | |
| } | |
| this.state = 'init'; | |
| }, | |
| /** | |
| * Sets the initial state. | |
| */ | |
| _reset: function() { | |
| var form = this.$.checkoutForm; | |
| this._setWaiting(false); | |
| form.reset(); | |
| // Remove the `aria-invalid` attribute from the form inputs. | |
| for (var el, i = 0; el = form.elements[i], i < form.elements.length; i++) { | |
| el.removeAttribute('aria-invalid'); | |
| } | |
| }, | |
| /** | |
| * Validates the form's inputs and adds the `aria-invalid` attribute to the inputs | |
| * that don't match the pattern specified in the markup. | |
| */ | |
| _validateForm: function() { | |
| var form = this.$.checkoutForm; | |
| var firstInvalid = false; | |
| for (var el, i = 0; el = form.elements[i], i < form.elements.length; i++) { | |
| if (el.checkValidity()) { | |
| el.removeAttribute('aria-invalid'); | |
| } else { | |
| if (!firstInvalid) { | |
| // announce error message | |
| if (el.nextElementSibling) { | |
| this.fire('announce', el.nextElementSibling.getAttribute('error-message')); | |
| } | |
| if (el.scrollIntoViewIfNeeded) { | |
| // safari, chrome | |
| el.scrollIntoViewIfNeeded(); | |
| } else { | |
| // firefox, edge, ie | |
| el.scrollIntoView(false); | |
| } | |
| el.focus(); | |
| firstInvalid = true; | |
| } | |
| el.setAttribute('aria-invalid', 'true'); | |
| } | |
| } | |
| return !firstInvalid; | |
| }, | |
| /** | |
| * Adds the cart data to the payload that will be sent to the server | |
| * and updates the UI to reflect the waiting state. | |
| */ | |
| _willSendRequest: function(e) { | |
| var form = e.target; | |
| var body = form.request.body; | |
| this._setWaiting(true); | |
| if (!body) { | |
| return; | |
| } | |
| // Populate the request body where `cartItemsId[i]` is the ID and `cartItemsQuantity[i]` | |
| // is the quantity for some item `i`. | |
| body.cartItemsId = []; | |
| body.cartItemsQuantity = []; | |
| this.cart.forEach(function(cartItem) { | |
| body.cartItemsId.push(cartItem.item.name); | |
| body.cartItemsQuantity.push(cartItem.quantity); | |
| }); | |
| }, | |
| /** | |
| * Handles the response from the server by checking the response status | |
| * and transitioning to the success or error UI. | |
| */ | |
| _didReceiveResponse: function(e) { | |
| var response = e.detail.response; | |
| this.response = response; | |
| this._setWaiting(true); | |
| if (response.success) { | |
| this._pushState('success'); | |
| this._reset(); | |
| this.fire('clear-cart'); | |
| } else { | |
| this._pushState('error'); | |
| } | |
| }, | |
| _toggleBillingAddress: function(e) { | |
| this.hasBillingAddress = e.target.checked; | |
| if (this.hasBillingAddress) { | |
| this.$.billAddress.focus(); | |
| } | |
| }, | |
| _computeHasItem: function(cartLength) { | |
| return cartLength > 0; | |
| }, | |
| _formatPrice: function(total) { | |
| return isNaN(total) ? '' : '$' + total.toFixed(2); | |
| }, | |
| _getEntryTotal: function(entry) { | |
| return this._formatPrice(entry.quantity * entry.item.price); | |
| }, | |
| _visibleChanged: function(visible) { | |
| if (!visible) { | |
| return; | |
| } | |
| // Reset the UI states | |
| this._reset(); | |
| // Notify the page's title | |
| this.fire('change-section', { title: 'Checkout' }); | |
| } | |
| }); | |
| </script> | |
| </dom-module> |