Js-buy-sdk: [1.0alpha] Line items for products without variants don't have images

Created on 18 Jul 2017  路  18Comments  路  Source: Shopify/js-buy-sdk

After retrieving a checkout via fetchCheckout, I'd like to display the images associated with the line items.

If an item does not have variants though, the variant field of the lineItem is a nonspecified default variant with Default Title as title and null as image.
This means that for such a line item, one must fetch all products, match all variants of all products with the lineItem variant id to find the original product, and use the image for that product. This is clearly not optimal.

Steps to reproduce:

  • Create a product with no variants
  • Add a product image
  • Add the product to a checkout
  • The lineItem in the checkout won't have any reference to the image

Some possible solutions:

  • Make the default variant inherit the product images (and possibly other attributes such as title)
  • Add a product reference to the variant field of lineItems (which should already be, according to the productVariant documentation and the type info for the variant field returned with the query)

Most helpful comment

Same problem here.

I came up with a pretty hacky solution and thought I'd share and get some opinions on the solution.

Use case

Lots of products without variants, a cart that should display the product's image since the default variant doesn't carry over the product image.

Idea

Reading the API docs, a lineItem consists of variantId, quantity and optional customAttributes.
I modified my addToCart function to add the product by variantId and additionally add the current product image url as a custom attribute. The cart then needs to check if a variant image is present, to comply with normal variant image behavior. If no variant image is present but the lineItem has a certain custom attribute use this as image url.

Code

addToCart = (variantId, productImg) => {

    const lineItemsToAdd = [{variantId, quantity: 1, customAttributes: {key: 'thumbnail', 'value': productImg}}];
    const checkoutId = this.checkout.id

    Client.addLineItems(checkoutId, lineItemsToAdd).then(checkout => {
      this.checkout = checkout
    });
  }

As I said it's hacky since custom attributes are not meant to be used in this way but when I just ignore the attributes while processing the order I can't see any direct drawbacks.

Feedback is very welcome.

All 18 comments

This looks like a poor modelling choice on our part. The rightish way to do this would be to fetch the line item, with the variant, and the variants product, and it's images. This seems kinda weird.

I've filed an issue internally, and I'll report back once we figure out a good way to fix this. Thanks for pointing this out!

Also running in to this. Do you know when a release will address this?

The solution to this isn't simple. The whole default variant representing no variant thing was a very early modelling choice made at shopify. Like from a decade ago. With our API, it'd be easy to propagate the image down from product, to variant, to line item, but then, going the other way, requesting the line item directly, it starts to get more difficult to fetch variant from line item, then product from variant. We're trying to figure out a way to get this working that doesn't completely drag performance-wise, and doesn't break existing behaviours from a decade of apps built against shopify. Some times things that look simple from the outside are kinda tough on the inside. We are looking at this issue, but it may take some time to address. Thanks for your patience.

Thanks a bunch for the detail. I'm sure you've considered this, but I'm curious to ask - couldn't you just put a pointer to the product on the linetem variant? The lineitem variant looks to be very nearly an implementation of the ProductVariant, and the ProductVariant has a pointer to the product. I'd even settle for needing to do an additional query to the product to grab the images.

Of course there's a lot I'm not privy to about how things are structured on your end, but it seems like a way to avoid massive / risky overhauls.

the multiple query approach is available today. The problem is more that when you have no variants, you actually have the one default variant, and presentation logic wraps most of the weirdness of sussing out an image. You can do the same thing in this API. The storefront API supports requesting from line item to variant to product, to product images, but that's a pretty hefty query that isn't needed in the majority of cases. The weirdness is that the logic has to change the second you introduce a real product variant, and all of a sudden, instead of going by inference, you get real variants with primary images.

Here's the issue I'm running in to:

I have a store front powered by the Storefront API. I visit one page, I add it to my cart. I visit another page, I add that to my cart.

I click my cart tab, slide out the cart drawer, and to create the visual line items in the drawer, I only have information available about the first product that comes in via the lineItem.variant.

Unless I'm missing some getProductByVariantId method, I'm not sure how to then grab the product image, since, as you have probably guessed, we have largely default variants only for this store.

So I guess:

  1. Am I missing a way to get at the product related to the checkout's lineItem.variant?
  2. If so, for my purposes anyway, no problem waiting for the eventual update. Otherwise I'm 99.9% done with a migration from stable to alpha, and unable to provide a complete layout.

You can make a raw query to the storefront API.

node(id: "checkout-id") {
  ... on Checkout {
    lineItems(first: 100) {
      edges {
        node {
          variant {
            product {
              images(first: 1) {
                edges {
                  node {
                    src
                    altText
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

I was trying to suss out the graphql client earlier, to see if I could find how to use a raw query similar to what you shared, but haven't been able to find what I need. The examples for using the client.query method are all about pulling lists, vs taking an ID, and I'm not familiar enough with GraphQL, or the client, at this point to improvise.

Is there a raw query builder method available on graphQLClient, or a way to put the above query together with something similar to the examples? For example:

const query = client.query((root) => {
  root.add('shop', (shop) => {
    shop.add('name');
    shop.addConnection('products', {args: {first: 10}}, (product) => {
      product.add('title');
    });
  });
});

There's actually two approaches. I'm working on a way to make this easier right now, and to make it so that you could just used the js buy SDK as a starting point and customize your own queries out of it, but for the time being we have:

  • a babel plugin
    This takes tagged template strings and transforms them in place into query builder syntax.
  • a compiler
    This takes .graphql files and transforms them into es modules that export a function that you can hand to client.send. client.send supports functions that receive the client as its only argument, so like
import query from './query';
...
client.send(query, { variables });

The compiler also supports shared fragments. If you don't define the fragment in your graphql file, the compiler will look at the documents passed in to see if any are in the same directory, and named the same as the fragment so ProductFragment would look for a file called ProductFragment.graphql. The compiler will also transform a json schema, or IDL schema into the type bundle that the graphql client needs to operate.

Lastly, there will soon be a rollup plugin, so instead of compiling graphql to js, the above example could be rewritten as:

import query from './query.graphql';
...
client.send(query, { variables });

and it'd be transformed at build time.

Right now these tools take some wiring, but I'm hoping once I get a build-time plugin done, it'll be way easier to get things up and running.

A step in the right direction, but with the Babel option, here is what I experience. My sample code:

import Client, {Config} from 'shopify-buy';
import {gql} from 'babel-plugin-graphql-js-client-transform';
document.addEventListener("DOMContentLoaded", function(event) { 

  var config = new Config({
    domain: 'domain.myshopify.com',
    storefrontAccessToken: 'apikey'
  });

  client = new Client(config);
  const query = gql(client)`
    query {
      shop {
        name
      }
    }
  `;
  console.log(query);
  client.send(query);
})

I get: This function should not be invoked. It should be used to tag template literals that will be
transcompiled into graphql-js-client's query builder syntax.

Which I believe is how the documentation says to invoke it.

I'm guessing this plugin is something you would want to use to compile your queries / query functions in development, and then pass the compiled function, paste that in to your client code? At 29,000 additional lines, that's a bear.

Will give the command line option a try.

Compiler CLI output from running graphql-js-client-compiler --help:

/Users/user/.npm-packages/bin/graphql-js-client-compiler line 1: /Applications: is a directory
/Users/user/.npm-packages/bin/graphql-js-client-compiler: line 2: syntax error near unexpected token (' /Users/user/.npm-packages/bin/graphql-js-client-compiler: line 2:The MIT License (MIT)'

FWIW, it's pretty straightforward to just add axios to the client and send a post request to the API with a raw query, and has me sorted out on this issue, using your query above (thanks again).

Perhaps adding an ajax method to the js buy sdk would be a quick way to solve this?

Raw queries do not return a response that we can reload or paginate. If you just want raw queries you don't need a client.

Well I ended up with a bit of a frankenclient - JS Buy alpha for checkout manipulation, raw queries for cart presentation. Seems to be the best option until the image issue is addressed.

For the babel plugin, the plugin must be in your babelrc. It's parsed at build time. Relay's babel plugin works the same way. If you're getting the error, babel isn't running this plugin.

For the compiler , the build artifact may be broken. It sounds like the license is being injected incorrectly.

Same problem here.

I came up with a pretty hacky solution and thought I'd share and get some opinions on the solution.

Use case

Lots of products without variants, a cart that should display the product's image since the default variant doesn't carry over the product image.

Idea

Reading the API docs, a lineItem consists of variantId, quantity and optional customAttributes.
I modified my addToCart function to add the product by variantId and additionally add the current product image url as a custom attribute. The cart then needs to check if a variant image is present, to comply with normal variant image behavior. If no variant image is present but the lineItem has a certain custom attribute use this as image url.

Code

addToCart = (variantId, productImg) => {

    const lineItemsToAdd = [{variantId, quantity: 1, customAttributes: {key: 'thumbnail', 'value': productImg}}];
    const checkoutId = this.checkout.id

    Client.addLineItems(checkoutId, lineItemsToAdd).then(checkout => {
      this.checkout = checkout
    });
  }

As I said it's hacky since custom attributes are not meant to be used in this way but when I just ignore the attributes while processing the order I can't see any direct drawbacks.

Feedback is very welcome.

I've been forced to upgrade a client's site which uses the js-buy-sdk because the v0 API has mysteriously broken and nobody on Shopify support even seems to realise it exists let alone is able to help.

I'm now faced with this 6 month old issue that forces me to make a regression or use a hacky workaround.

I hate to moan, but this has seriously eroded my trust in Shopify.

This is a paid product - please fix.

This PR addresses the issue.

Was this page helpful?
0 / 5 - 0 ratings