From c0750d55670c5d5fae63edad80b3fc187ff0eba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomek=20Wytr=C4=99bowicz?= Date: Tue, 27 Feb 2024 12:31:37 +0100 Subject: [PATCH] Move rendering of Order Attribution inputs fully to JS (#44335) - Define custom element for order-attribution to move DOM manipulations to JS only. To support multiple checkout & register forms on the same page. Co-authored-by: Justin Palmer <228780+layoutd@users.noreply.github.com> Co-authored-by: github-actions --- .../44335-fix-44159-oa-missing-fields-element | 4 + .../legacy/js/frontend/order-attribution.js | 75 +++++++++++++++---- .../Orders/OrderAttributionController.php | 13 ++-- 3 files changed, 70 insertions(+), 22 deletions(-) create mode 100644 plugins/woocommerce/changelog/44335-fix-44159-oa-missing-fields-element diff --git a/plugins/woocommerce/changelog/44335-fix-44159-oa-missing-fields-element b/plugins/woocommerce/changelog/44335-fix-44159-oa-missing-fields-element new file mode 100644 index 00000000000..087cf98ede2 --- /dev/null +++ b/plugins/woocommerce/changelog/44335-fix-44159-oa-missing-fields-element @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Move the rendering of Order Attribution inputs fully to JS. Support multiple instances on the same page. \ No newline at end of file diff --git a/plugins/woocommerce/client/legacy/js/frontend/order-attribution.js b/plugins/woocommerce/client/legacy/js/frontend/order-attribution.js index 358d01af258..28c234e0734 100644 --- a/plugins/woocommerce/client/legacy/js/frontend/order-attribution.js +++ b/plugins/woocommerce/client/legacy/js/frontend/order-attribution.js @@ -7,6 +7,7 @@ const $ = document.querySelector.bind( document ); const propertyAccessor = ( obj, path ) => path.split( '.' ).reduce( ( acc, part ) => acc && acc[ part ], obj ); const returnNull = () => null; + const stringifyFalsyInputValue = ( value ) => value === null || value === undefined ? '' : value; // Hardcode Checkout store key (`wc.wcBlocksData.CHECKOUT_STORE_KEY`), as we no longer have `wc-blocks-checkout` as a dependency. const CHECKOUT_STORE_KEY = 'wc/store/checkout'; @@ -31,11 +32,9 @@ * @param {Object} values Object containing field values. */ function updateFormValues( values ) { - // Update inputs if any exist. - if( $( `input[name^="${params.prefix}"]` ) ) { - for( const key of Object.keys( wc_order_attribution.fields ) ) { - $( `input[name="${params.prefix}${key}"]` ).value = values && values[ key ] || ''; - } + // Update `` elements if any exist. + for( const element of document.querySelectorAll( 'wc-order-attribution-inputs' ) ) { + element.values = values; } }; @@ -105,15 +104,6 @@ // Run init. wc_order_attribution.setOrderTracking( params.allowTracking ); - // Wait for (async) classic checkout initialization and set source values once loaded. - if ( $( 'form.woocommerce-checkout' ) !== null ) { - const previousInitCheckout = document.body.oninit_checkout; - document.body.oninit_checkout = () => { - updateFormValues( getData() ); - previousInitCheckout && previousInitCheckout(); - }; - } - // Work around the lack of explicit script dependency for the checkout block. // Conditionally, wait for and use 'wp-data' & 'wc-blocks-checkout. @@ -136,4 +126,61 @@ eventuallyInitializeCheckoutBlock(); } + /** + * Define an element to contribute order attribute values to the enclosing form. + * To be used with the classic checkout. + */ + window.customElements.define( 'wc-order-attribution-inputs', class extends HTMLElement { + // Our bundler version does not support private class members, so we use a convention of `_` prefix. + // #values + // #fieldNames + constructor(){ + super(); + // Cache fieldNames available at the construction time, to avoid malformed behavior if they change in runtime. + this._fieldNames = Object.keys( wc_order_attribution.fields ); + // Allow values to be lazily set before CE upgrade. + if ( this.hasOwnProperty( '_values' ) ) { + let values = this.values; + // Restore the setter. + delete this.values; + this.values = values || {}; + } + } + /** + * Stamp input elements to the element's light DOM. + * + * We could use `.elementInternals.setFromValue` and avoid sprouting `` elements, + * but it's not yet supported in Safari. + */ + connectedCallback() { + let inputs = ''; + for( const fieldName of this._fieldNames ) { + const value = stringifyFalsyInputValue( this.values[ fieldName ] ); + inputs += ``; + } + this.innerHTML = inputs; + } + + /** + * Update form values. + */ + set values( values ) { + this._values = values; + if( this.isConnected ) { + for( const fieldName of this._fieldNames ) { + const input = this.querySelector( `input[name="${params.prefix}${fieldName}"]` ); + if( input ) { + input.value = stringifyFalsyInputValue( this.values[ fieldName ] ); + } else { + console.warn( `Field "${fieldName}" not found. Most likely, the '' element was manipulated.`); + } + } + } + } + get values() { + return this._values; + } + } ); + + }( window.wc_order_attribution ) ); diff --git a/plugins/woocommerce/src/Internal/Orders/OrderAttributionController.php b/plugins/woocommerce/src/Internal/Orders/OrderAttributionController.php index 3f8e50f15bd..b92b4dbb7b0 100644 --- a/plugins/woocommerce/src/Internal/Orders/OrderAttributionController.php +++ b/plugins/woocommerce/src/Internal/Orders/OrderAttributionController.php @@ -111,8 +111,8 @@ class OrderAttributionController implements RegisterHooksInterface { } ); - add_action( 'woocommerce_checkout_after_customer_details', array( $this, 'source_form_elements' ) ); - add_action( 'woocommerce_register_form', array( $this, 'source_form_elements' ) ); + add_action( 'woocommerce_checkout_after_customer_details', array( $this, 'stamp_html_element' ) ); + add_action( 'woocommerce_register_form', array( $this, 'stamp_html_element' ) ); // Update order based on submitted fields. add_action( @@ -342,14 +342,11 @@ class OrderAttributionController implements RegisterHooksInterface { } /** - * Print `` elements for source fields. - * To be picked up and populated with data by the JS. + * Add `` element that contributes the order attribution values to the enclosing form. * Used for checkout & customer register forms. */ - public function source_form_elements() { - foreach ( $this->field_names as $field_name ) { - printf( '', esc_attr( $this->get_prefixed_field_name( $field_name ) ) ); - } + public function stamp_html_element() { + printf( '' ); } /**