-
Notifications
You must be signed in to change notification settings - Fork 3.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add product puppeteer class and simplify component update behavior #3289
Conversation
This proposal really catches my attention. This PR could be the perfect opportunity to make a clear cut decision: use data attributes or IDs for JavaScript targeting and reserve classes solely for styling. I believe this would be a huge help for developers leveraging Dawn as their starter theme. Making it standard practice to target elements in JavaScript using only IDs or data attributes could seriously streamline things. Right now, it's a bit of a jumble—sometimes you're working with classes, other times with elements, data attributes, or IDs, and it can get pretty confusing. Especially if you're trying to use Dawn as a base theme and want to tidy it up without losing any JavaScript functionality, it's hard to know which class is essential for the JS to work. |
cea17d5
to
c2802fd
Compare
@@ -995,75 +995,35 @@ customElements.define('slideshow-component', SlideshowComponent); | |||
class VariantSelects extends HTMLElement { | |||
constructor() { | |||
super(); | |||
this.addEventListener('change', this.handleProductUpdate); | |||
this.initializeProductSwapUtility(); | |||
} | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the primary change of this PR: extracting product-update logic out of VariantSelects to a wrapper class.
Instead of this class mutating siblings, it now only updates children and publishes an event that indicates a variant change started. The new ProductInfo parent then mutates its children. This separation of logic hopefully makes it easier to understand what controls updates to a component (i.e. itself or a parent vs itself or one of its siblings).
This also enables us to eliminate some gnarly selectors from here and here.
this.innerHTML = ''; | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I migrated this functionality from the VariantSelects class, where it was called in the variant change event
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This class was used for quantity input event handlers, and was rendered further down the product template tree. I repurposed it to be a general Product wrapper class and migrated the onVariantChange
handler and (related functions) out of the VariantSelects
class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a stylistic note, I made functions in this class private if they don't seem like they'd be useful to access from outside of this class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update -- to maintain consistency with the rest of Dawn, I rolled back the private class function change and everything is now public.
this.currentVariant = this.querySelector('.product-variant-id'); | ||
this.submitButton = this.querySelector('[type="submit"]'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
currentVariant
can be grabbed from the product form class, submitButton
wasn't used anywhere.
@@ -452,8 +451,7 @@ | |||
{% render 'product-variant-picker', | |||
product: product, | |||
block: block, | |||
product_form_id: product_form_id, | |||
update_url: false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't update_url here anymore because this logic lives on the product
data-url="{{ product.url }}" | ||
data-product-id="{{ product.id }}" | ||
{% if update_url == false %} | ||
data-update-url="false" | ||
{% endif %} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of this is now handled by the product class
d66afd7
to
95c7d51
Compare
1ed8fda
to
a8e54bd
Compare
8303103
to
959f198
Compare
a8e54bd
to
7b50505
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this approach. I think the tophatting I did on the combined listing/2k variant PR covers this one.
959f198
to
f9a408d
Compare
9c11c55
to
984be42
Compare
…ing product-info class and migrating product-update specific logic out of the VariantSelects class. A product wrapper enables children to more trivially perform global updates by providing a heirarchical "namespace"--i.e. child publishes event, parent captures and declaratively updates other children VS child updates siblings. By extracting the VariantSelects onChange logic to use this pattern, VariantSelects is able to be a single-purpose class, it is easier to understand why siblings are updated, and we were able to eliminate some really gross logic that handled variant change updates differently depending on the wrapping context.
3a55c84
to
12005c7
Compare
…ing product-info class and migrating product-update specific logic out of the VariantSelects class. A product wrapper enables children to more trivially perform global updates by providing a heirarchical "namespace"--i.e. child publishes event, parent captures and declaratively updates other children VS child updates siblings. By extracting the VariantSelects onChange logic to use this pattern, VariantSelects is able to be a single-purpose class, it is easier to understand why siblings are updated, and we were able to eliminate some really gross logic that handled variant change updates differently depending on the wrapping context.
12005c7
to
db48d58
Compare
…ing product-info class and migrating product-update specific logic out of the VariantSelects class. A product wrapper enables children to more trivially perform global updates by providing a heirarchical "namespace"--i.e. child publishes event, parent captures and declaratively updates other children VS child updates siblings. By extracting the VariantSelects onChange logic to use this pattern, VariantSelects is able to be a single-purpose class, it is easier to understand why siblings are updated, and we were able to eliminate some really gross logic that handled variant change updates differently depending on the wrapping context. fix typo
db48d58
to
f23de51
Compare
…ing product-info class and migrating product-update specific logic out of the VariantSelects class. A product wrapper enables children to more trivially perform global updates by providing a heirarchical "namespace"--i.e. child publishes event, parent captures and declaratively updates other children VS child updates siblings. By extracting the VariantSelects onChange logic to use this pattern, VariantSelects is able to be a single-purpose class, it is easier to understand why siblings are updated, and we were able to eliminate some really gross logic that handled variant change updates differently depending on the wrapping context.
f23de51
to
831e093
Compare
…ing product-info class and migrating product-update specific logic out of the VariantSelects class. A product wrapper enables children to more trivially perform global updates by providing a heirarchical "namespace"--i.e. child publishes event, parent captures and declaratively updates other children VS child updates siblings. By extracting the VariantSelects onChange logic to use this pattern, VariantSelects is able to be a single-purpose class, it is easier to understand why siblings are updated, and we were able to eliminate some really gross logic that handled variant change updates differently depending on the wrapping context.
831e093
to
45f65d3
Compare
assets/product-info.js
Outdated
this.cartUpdateUnsubscriber?.(); | ||
} | ||
|
||
#initializeProductSwapUtility() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From here through #setQuantityBoundries
was migrated from VariantSelects with minimal changes.
474be3d
to
4f3b617
Compare
@@ -106,7 +106,6 @@ if (!customElements.get('quick-order-list')) { | |||
} | |||
|
|||
cartUpdateUnsubscriber = undefined; | |||
sectionRefreshUnsubscriber = undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I originally added this code so that the component would self-refresh when swapping between combined listing siblings.
This is no longer necessary, since we now replace the entire content of <main>
when swapping between siblings.
@@ -1386,39 +1101,24 @@ class ProductRecommendations extends HTMLElement { | |||
} | |||
|
|||
connectedCallback() { | |||
this.initializeRecommendations(); | |||
|
|||
this.unsubscribeFromSectionRefresh = subscribe(PUB_SUB_EVENTS.sectionRefreshed, (event) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I originally added this code so that the component would self-refresh when swapping between combined listing siblings.
This is no longer necessary, since we now replace the entire content of <main>
when swapping between siblings.
this.submitButton.removeAttribute('disabled'); | ||
this.submitButtonText.textContent = window.variantStrings.addToCart; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
toggleSubmitButton
logic was pulled out of the VariantSelects class
assets/quick-add.js
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changes in this file are because of changing how main-product section is wrapped in a product-info
component
id="MainProduct-{{ section.id }}" | ||
class="section-{{ section.id }}-padding gradient color-{{ section.settings.color_scheme }}" | ||
data-section="{{ section.id }}" | ||
data-product-id="{{ product.id }}" | ||
data-update-url="true" | ||
data-url="{{ product.url }}" | ||
> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Worth flagging, this node was changed from a section
to a product-info
assets/product-info.js
Outdated
|
||
connectedCallback() { | ||
if (!this.input) return; | ||
this.#initializeProductSwapUtility(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
while I like the idea of clearly mentioning which methods are private and won't be needed outside of the class logic, we're only introducing it here and it creates inconsistency with the rest of the JS.
Not a big deal but just having multiple take/approach in the same codebase adds to its confusion when reading through it I think.
…ing product-info class and migrating product-update specific logic out of the VariantSelects class. A product wrapper enables children to more trivially perform global updates by providing a heirarchical "namespace"--i.e. child publishes event, parent captures and declaratively updates other children VS child updates siblings. By extracting the VariantSelects onChange logic to use this pattern, VariantSelects is able to be a single-purpose class, it is easier to understand why siblings are updated, and we were able to eliminate some really gross logic that handled variant change updates differently depending on the wrapping context.
6b3187b
to
5427805
Compare
…ing product-info class and migrating product-update specific logic out of the VariantSelects class. A product wrapper enables children to more trivially perform global updates by providing a heirarchical "namespace"--i.e. child publishes event, parent captures and declaratively updates other children VS child updates siblings. By extracting the VariantSelects onChange logic to use this pattern, VariantSelects is able to be a single-purpose class, it is easier to understand why siblings are updated, and we were able to eliminate some really gross logic that handled variant change updates differently depending on the wrapping context. Refetch entire page on product swap Remove unused unsubscriber function Stability improvements for quick-add modal PR feedback - fixed padding + minor tweaks Un-privatize class methods for consistency with the rest of Dawn
59eb0aa
to
24304d1
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seem good 👌
Just one suggested nitpick, and I guess there is a conflict to fix.
…ing product-info class and migrating product-update specific logic out of the VariantSelects class. A product wrapper enables children to more trivially perform global updates by providing a heirarchical "namespace"--i.e. child publishes event, parent captures and declaratively updates other children VS child updates siblings. By extracting the VariantSelects onChange logic to use this pattern, VariantSelects is able to be a single-purpose class, it is easier to understand why siblings are updated, and we were able to eliminate some really gross logic that handled variant change updates differently depending on the wrapping context. Refetch entire page on product swap Remove unused unsubscriber function Stability improvements for quick-add modal PR feedback - fixed padding + minor tweaks Un-privatize class methods for consistency with the rest of Dawn Move css to be in section-main-product.css
24304d1
to
cfaa10c
Compare
tl;dr - this PR primarily copies code out of the variant picker class in
global.js
intoproduct-info.js
, which has been promoted to act as a wrapper for Product.Why are these changes introduced?
This PR came out of a conversation with @tyleralsbury earlier this year while working on adding support for combined listings / 2k variants. In that PR, the
VariantSelects
class needs to swap out the page content for its product when the selected variant is associated with a different Combined Listing child product.VariantSelects
can be embedded in a few different product contexts (main product, featured product, quick add modal), and I was running into an issue where the class needed some pretty janky logic to determine its wrapping context (here and here). For a more stable architecture, child components shouldn't care what context they are rendered in.An additional problem is that if components need to be updated when an event in a sibling happens, there wasn't a great "namespaced" context to manage the update. We had two primary patterns for handling this:
Directly update sibling content (link)
Publish an event with the section, sibling subscription triggers update (link)
What approach did you take?
We have 2 primary issues: child components need to be context aware, and handling product component updates was complicated and inconsistent.
To solve both of these problems, I introduced a product class that wraps the entire rendered product and serves as an event handling puppeteer.
We already had a product wrapper class called
ProductInfo
that wrapped part of the rendered product; I moved it higher up the tree and extracted component update logic into it.Other considerations
I left a comment, but we possibly could decompose further by moving the old ProductInfo content into a QuantityForm class.
Visual impact on existing themes
None
Testing steps/scenarios
Here is a zip file for these theme changes: https://github.com/Shopify/dawn/archive/refs/heads/add-product-wrapper-class.zip
Demo links
Checklist