[Shopify][React]カスタムアプリでCart.attributesを表示する

import {
  View,
  Text,
  Grid,
  GridItem,
  Link,
  Divider,
  useApi,
  useAttributeValues,
  useSettings,
  reactExtension,
} from '@shopify/ui-extensions-react/checkout';

export default reactExtension("purchase.checkout.block.render", () => (
  <Extension />
));

function Extension() {
  const { attributes_lines,hide_empty,hide_edit } = useSettings();
  const attributeKeys = attributes_lines ? attributes_lines.split("\n") : [];
  if(!attributeKeys.length){
    return '';
  }
  let i,j,tmp;
  let key,value;
  /* 指定キーのattributeを表示 */
  const attributeValues = attributeKeys.length ? useAttributeValues(attributeKeys) : [];
  const { shop } = useApi();
  const cartUrl = shop.storefrontUrl.replace(/\/$/,'') + '/cart';
  const rowSpan = attributeKeys.length + 1;
  let rows = [];
  let attributeKeysIndexList = [];
  for (i=0;i<attributeKeys.length;i++) {
    value = i < attributeValues.length ? attributeValues[i] : ' ';
    if(value){
      if(!hide_empty || (hide_empty && value.replace(/[  ]+/g,'') != '')){
        attributeKeysIndexList.push(i);
      }
    }
  }
  for (j=0;j<attributeKeysIndexList.length;j++) {
    i = attributeKeysIndexList[j];
    /* ラベル */
    key = attributeKeys[i];
    tmp = attributesAltLineKeyValues.find((elm) => elm[0] == key);
    if(tmp && 1 < tmp.length && tmp[1]){
      key = tmp[1];
    }
    /* 値 */
    value = i < attributeValues.length ? attributeValues[i] : ' ';
    rows.push(<View border="none" padding="base" key={`${j}_key`}><Text size="base">{key}</Text></View>);
    rows.push(<View border="none" padding="base" key={`${j}_value`}><Text size="base">{value}</Text></View>);
    if(j < 1 && !hide_edit){
      rows.push(
      <GridItem rowSpan={rowSpan} spacing="base" key={`${j}_griditem`}>
        <View border="none" padding="base" key={`${j}_grid`}>
          <Link to={cartUrl} padding="none">変更</Link>
        </View>
      </GridItem>
      );
    }
    if(1 < attributeKeysIndexList.length && j < attributeKeysIndexList.length-1){
      rows.push(
        <GridItem columnSpan={2} key={`${j}_divider`}>
          <Divider></Divider>
        </GridItem>
      );
    }
  }
  let gridColumns = hide_edit ? ['auto', 'fill'] : ['auto', 'fill', 'auto'];
  let gridRows = [];
  for(i=0;i<attributeKeysIndexList.length+(attributeKeysIndexList.length-1);i++){
    gridRows.push('auto');
  }
  return (
    <Grid
      border="base"
      cornerRadius='base'
      padding='none'
      columns={gridColumns}
      rows={gridRows}
    >
      {rows}
    </Grid>
  );
}

shopify.extension.toml

[extensions.settings]
[[extensions.settings.fields]]
key = "attributes_lines"
type = "multi_line_text_field"
name = "表示するattribute.key"
description = ""
[[extensions.settings.fields]]
key = "hide_attributes"
type = "multi_line_text_field"
name = "非表示条件のcart.attributesのキー:値"
description = ""
[[extensions.settings.fields]]
key = "hide_empty"
type = "boolean"
name = "値が空欄の項目を表示しない"
[[extensions.settings.fields]]
key = "hide_edit"
type = "boolean"
name = "変更リンクを表示しない"

[Liquid]在庫判定

{% if variant.inventory_management == blank or variant.inventory_policy == "continue" or 0 < variant.inventory_quantity %}
//在庫を追跡しないor在庫切れでも販売を続けるor在庫あり
{% else %}
//在庫なし
{% endif %}

[WordPress]ACFカスタムフィールドプレビュー

公開済みの記事を編集してプレビューするとカスタムフィールドの値が古いままなのを解決する
※公式対応までのつなぎ

 
functions.phpに下記追記

/**
 * $dataをjsonで出力
 */
function echo_json($data)
{
    header("Content-Type: application/json; charset=utf-8");
    echo json_encode($data);
}
/**
 * テンプレートのadmin.jsを読み込む
 */
function add_admin_style()
{
    $path_js = get_template_directory_uri().'/scripts/admin.js';
    wp_enqueue_script('admin_script', $path_js);
}
add_action('admin_enqueue_scripts', 'add_admin_style');
/**
 * ajaxで送信されたカスタムフィールドの値をdbへ保存
 */
function save_acf_by_ajax()
{
    if(empty($_POST['post_id'])){
        echo_json(['success' => false,'preview_id' => 0]);
        wp_die();
        exit;
    }
    $post_id = $_POST['post_id'];
    $post_status = get_post_status($post_id);
    if($post_status !== "publish"){
        echo_json(['success' => false,'preview_id' => 0]);
        wp_die();
        exit;
    }
    $preview_id = 0;
    global $wpdb;
    $row = $wpdb->get_row("SELECT p.ID FROM $wpdb->posts AS p WHERE p.post_type = '_cf_preview_id' AND p.post_parent = ${post_id}");
    if(empty($row)) {
        $post = array(
            'post_name'      => '_cf_preview_id',
            'post_title'     => '_cf_preview_id',
            'post_status'    => 'private',
            'post_type'      => '_cf_preview_id',
            'post_author'    => 1,
            'ping_status'    => 'closed',
            'post_parent'    => $post_id,
            'comment_status' => 'closed',
        );
        $preview_id = wp_insert_post($post);
    }else{
        $preview_id = $row->ID;
    }
    unset($_POST['action']);
    unset($_POST['post_id']);
    unset($_POST['preview_id']);
    if(!empty($preview_id)) {
        foreach ($_POST as $key => $value) {
            update_field($key, $value, $preview_id);
        }
    }
    $_POST['post_id'] = $post_id;
    $_POST['preview_id'] = $preview_id;
    $_POST['success'] = !empty($preview_id);
    echo_json($_POST);
    wp_die();
}
add_action( 'wp_ajax_save_acf_by_ajax_action', 'save_acf_by_ajax' );
/**
 * プレビュー時カスタムフィールドの値用のpostidを返す
 */
function get_cf_post_id($post_id = '')
{
    if(empty($post_id)){
        $post_id = get_the_ID();
    }
    $cf_post_id = $post_id;
    if(is_preview()){
        $post_status = get_post_status($post_id);
        if($post_status === 'publish'){
            global $wpdb;
            $row = $wpdb->get_row("SELECT p.ID FROM $wpdb->posts AS p WHERE p.post_type = '_cf_preview_id' AND p.post_parent = ${post_id}");
            if (!empty($row)) {
                $cf_post_id = $row->ID;
            }
        }
    }
    return $cf_post_id;
}

wp-content/themes/テーマ/assets/js/admin.jsに下記追記

class CFSaveManager {
    qsall(selector,elm){
        if(!elm)elm = document;
        return elm.querySelectorAll(selector);
    }
    qs(selector,elm){
        if(!elm)elm = document;
        return elm.querySelector(selector);
    }
    constructor(){
    }
    /* 初期化 */
    init(){
        this.initAutoSave();
    }
    /* プレビューボタンにイベント設定 */
    initAutoSave(){
        const button = this.qs('.block-editor-post-preview__button-toggle');
        if(!button){
            setTimeout(() => {
                this.initAutoSave();
            },200);
            return;
        }
        button.addEventListener('click',() => {
            this.autoSave();
        });
    }
    /* ajaxでカスタムフィールドの値をapiへ送信・保存 */
    autoSave(){
        const elm = this.qs('[name="post_ID"]');
        const form = this.qs('form.metabox-location-normal');
        const formData = new FormData(form);
        let sendFormData = new FormData();
        for (let item of formData) {
            if(/^acf\[.+\]$/.test(item[0])){
                var matches = item[0].match(/acf\[(.+)\]/);
                if(matches && 1 < matches.length){
                    sendFormData.append(matches[1],item[1]);
                }
            }else{
                sendFormData.append(item[0],item[1]);
            }
        }
        sendFormData.append('action','save_acf_by_ajax_action');
        sendFormData.append('post_id',elm.value);
        const options = {
            method  : "POST",
            headers : {
                'Accept': 'application/json'
            },
            body    : sendFormData
        };
        fetch(window.ajaxurl,options)
            .then((response)=>{
                if (!response.ok) {
                    throw new Error();
                }
                return response.json();
            })
            .then((json)=>{
                console.log(json);
            })
            .catch(console.error);
    }
}
window.addEventListener('DOMContentLoaded',(e) => {
    const cfsm = new CFSaveManager();
    cfsm.init();
});

wpテンプレでカスタムフィールドの値を出力する部分

$post_id = get_the_ID();
$cf_preview_id = $post_id;
if(is_preview()){
    $post_status = get_post_status($post_id);
    if($post_status === 'publish'){
        global $wpdb;
        $row = $wpdb->get_row("SELECT p.ID FROM $wpdb->posts AS p WHERE p.post_type = '_cf_preview_id' AND p.post_parent = ${post_id}");
        if (!empty($row)) {
            $cf_preview_id = $row->ID;
        }
    }
}
$value = get_field( "test_text1" ,$cf_preview_id );
echo "cf_preview_id:$cf_preview_id<br>";
echo "<h2>test_text1:".esc_html($value)."</h2>";

[JavaScript]ShopifyCartAPIを叩く(update.js)

let sendData = {
    updates : {}
};
this.deleteItems.forEach((item,index)=>{
    sendData.updates[item.key] = 0;
});
let options = {
    method  : 'POST',
    credentials: 'same-origin',
    headers : {
        'Content-Type': 'application/json'
    },
    body    : JSON.stringify(sendData)
};
fetch(window.Shopify.routes.root+'cart/update.js',options)
.then((response)=>{
    if (!response.ok) {
        throw new Error();
    }
    return response.json;
})
.then((json)=>{
    this.finishFetch(2);
})
.catch((error)=>{
    console.log(error);
});

[JavaScript]ShopifyCartAPIを叩く(change.js)

let sendData = {
    id         : item.key,
    quantity   : item.quantity,
    properties : {}
};
item.properties.forEach((property,index)=>{
    sendData.properties['プロパティ'+(index+1)] = property.value;
});
let options = {
    method       : 'POST',
    credentials  : 'same-origin',
    headers : {
        'Content-Type' : 'application/json'
    },
    body         : JSON.stringify(sendData)
};
fetch(window.Shopify.routes.root+'cart/change.js',options)
.then((response)=>{
    if (!response.ok) {
        throw new Error();
    }
    return response.json;
})
.then((json)=>{
    this.finishFetch();
})
.catch((error)=>{
    console.log(error);
});

[Shopify][JavaScript]住所編集の国で都道府県更新

Liquid(HTML)

国<select name="address[country]" {% if form.country != blank %}data-default-label="{{ form.country }}{% else %}data-default="Japan{% endif %}">{{ country_option_tags }}</select><br>
都道府県<select name="address[province]" data-default-label="{{ form.province }}"></select>

JavaScript

document.querySelectorAll('[name="address[country]"]').forEach((elm,index)=>{
    elm.addEventListener('change',(e)=>{
        onChangeCountry(e);
    });
});
onChangeCountry(e){
    const option = e.target.querySelector('[value="'+e.target.value+'"]');
    const provinceSelect = e.target.form.querySelector('[name="address[province]"]');
    if(option && provinceSelect){
        let options = [];
        const provinces = JSON.parse(option.dataset['provinces']);
        provinces.forEach((province,index)=>{
            options.push('<option value="'+province[0]+'">'+province[1]+'</option>');
        });
        provinceSelect.innerHTML = options.join('');
    }
}

[JavaScript]ファイルサイズ取得

const options = {
   method  : "HEAD"
};
fetch(elm.href,options)
    .then((response)=>{
        if (!response.ok) {
            throw new Error();
        }
        return response;
    })
    .then((response)=>{
        const fileSize = response.headers.get("Content-Length");
        if(fileSize) {
            console.log(fileSize);
        }else{
            throw new Error();
        }
    })
    .catch((error)=>{
    });