Skip to content

Instantly share code, notes, and snippets.

@tresorama
Created August 25, 2023 08:10
Show Gist options
  • Save tresorama/bf512a84b6a32faafadd1600664118d1 to your computer and use it in GitHub Desktop.
Save tresorama/bf512a84b6a32faafadd1600664118d1 to your computer and use it in GitHub Desktop.
wordpress-gutenberg--POC--dynamic-data
import { createHigherOrderComponent } from '@wordpress/compose';
import * as wpHooks from '@wordpress/hooks';
import { BlockControls, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, PanelRow } from '@wordpress/components';
import { Icon, backup as backupIcon } from '@wordpress/icons';
import { Post, useEntityRecord } from '@wordpress/core-data';
import { store as coreStore } from '@wordpress/core-data';
import { useSelect, useDispatch } from '@wordpress/data';
import { BlockEditProps } from '@wordpress/blocks';
const TEXT_ALLOWED_BLOCKS = ["core/heading", "core/paragraph"];
// add attributes to registration of block
// that is when block.json is parsed and registered
//
wpHooks.addFilter(
'blocks.registerBlockType',
'tccb/add-dynamic-data--text',
(props, name: string) => {
if (!TEXT_ALLOWED_BLOCKS.includes(name)) return props;
return {
...props,
"usesContext": ["postType", "postId", "queryId"],
};
},
);
const useDynamicDataSources = (props: BlockEditProps<any>) => {
const { attributes, context } = props;
// extract dynamic data fields "names" and return as list
// fetch data
const post = useEntityRecord<Post>('postType', context.postType, context.postId);
const acfFields = post.record.acf || {};
// const meta = post.record.meta || {};
return [
{ section_name: 'Post', fields: ['post_title'].map(_ => "{" + _ + "}") },
{ section_name: 'ACF', fields: Object.keys(acfFields).map(_ => "{acf:" + _ + "}") },
] satisfies { section_name: string, fields: Array<string>; }[];
};
const useContentWithDynamicData = (props: BlockEditProps<any>) => {
const { attributes, context } = props;
// given a "string" that rapresetn text content of a block
// replace predefined placeholders with dynamic data
// that come from wordpress/data
// fetch data
const post = useEntityRecord('postType', context.postType, context.postId);
const acfFields = post.record.acf || {};
// const meta = post.record.meta || {};
// do templating
let subsitutedContent: string = attributes.content;
// replace core wp data
subsitutedContent = subsitutedContent.replaceAll("{post_title}", post.record.title.raw);
// replace acf fields
// ("stream_name" field slug must be written as {acf:stream_name})
{
//const text = "Hello {acf:text1}, this is a sample text. {acf:text2} can be used multiple times. Anche {acf:ciao_mamma}";
//const dynamicPlaceholder = "{acf:any_string_with_underscore}";
const fixedPart = "acf:";
const escapedFixedPart = fixedPart.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
const regex = new RegExp(escapedFixedPart + "([^}]+?)}", "g");
let extractedValues: string[] = [];
subsitutedContent.replace(regex, (match, value) => {
extractedValues.push(value);
return match;
});
//console.log(extractedValues); // expected ["text1", "text2", "ciao_mamma"]
extractedValues.forEach(suffix => {
const fieldValue = acfFields[suffix];
if (!fieldValue) return;
const placeholder = "{acf:" + suffix + "}";
subsitutedContent = subsitutedContent.replaceAll(placeholder, fieldValue);
});
}
return { post, subsitutedContent };
};
// handle the "edit" component
wpHooks.addFilter(
'editor.BlockEdit',
'tccb/add-dynamic-data-selector--text',
createHigherOrderComponent(BlockEdit => (props: BlockEditProps<any>) => {
// abort if not desired block
if (!TEXT_ALLOWED_BLOCKS.includes(props.name)) return <BlockEdit {...props} />;
//
const { attributes, isSelected, setAttributes } = props;
const { content = "" } = attributes;
// get all dynamic data sources available in this block.
// dyanmic data source depends on block context
// here we have only "field" names of dynamic data
const dynamicDataSources = useDynamicDataSources(props);
// when user select a field from the tollbar select
// we paste the field name refernce insied the content
const handleSelectDynamicData = (fieldName: string) => {
setAttributes({ content: content + fieldName });
};
// replace placeholders of fields with real value from the Data layer
const typedContent = content;
const { subsitutedContent } = useContentWithDynamicData(props);
return (
<>
{/* Toolbar */}
<BlockControls controls={{}}>
<select onChange={e => handleSelectDynamicData(e.currentTarget.value)} value="this-select-value-never-change--is-only-a-list-from-whichto-copy-string-into-content">
<option value="this-select-value-never-change--is-only-a-list-from-whichto-copy-string-into-content">Insert Dynamic</option>
{dynamicDataSources.map(({ section_name, fields }) => fields.map(fieldName => <option value={fieldName}>{fieldName} ({section_name})</option>))}
</select>
</BlockControls>
{/* Canvas */}
<BlockEdit
{...props}
attributes={{
...props.attributes,
content: isSelected ? typedContent : subsitutedContent
}}
/>
</>
);
}, 'tccb/add-dynamic-data-selector--text'),
);
@tresorama
Copy link
Author

tresorama commented Nov 17, 2023

Iteration #1

WIP - Test of an implementation that can have third party dynami cdata source providers

type Source = {
  type: string,
  fields_keys: string[]
}

// use this function in a block's "Edit" React function, to obtain the available dynamic data
// then the React JSX must render this list of "dynamic data sources" as a a UI that let user
// select one item of the list.
const useDynamicDataSources = (props: BlockEditProps<any>) => {

	// extract dynamic data fields "names" and return as list
  const sources:Source[] = wp.hook.applyFilters('dynamic_data_sources_list', [] );

  return sources;

};

// use this function in a block's "Edit" React function, to resolve dynamic data bindings, 
// and place "real data" in the block attributes
// then the React JSX must render these "attributes" instead of raw attributes
const useResolvedDynamicDataValue = (blockProps) => {
  const {attributes} = blockProps;

  let resolvedAttributes = attributes;
  // insert a way of iterating over all dynamic data bindings selected on this block
  // and for each of them extract the real value from the DB and place in "newAttributes"
  //
  // PSEUDO CODE HERE:
  for (const binding in "dynamic_data_bindings") {
     const attributes_key = ...
     const resolvedValue =  wp.hook.applyFilters(
         'dynamic_data_source_replacer',
         attributes[attribute_key], 
         attributes[attribute_key], 
         binding,
         blockProps
       );
     resolvedAttributes[binding.attribute_name] = resolvedValue
   }

 


  return resolvedAttributes;
}



const wp_init = () => {
  
  // post data...
  wp.hook.addFilter('dynamic_data_sources_list', (sources: Source[], blockProps) => {
    const source = {
      type: "post_data",
      fields_keys: [
        "post_title",
        "post_author_name",
        "post_date",
        ...
      ]
    };

    return [...sources, source];

  });
  wp.hook.addFilter('dynamic_data_source_replacer', (value, rawValue, field) => {
    if (field.type !== 'post_data') return rawValue;

    if (field.key === 'post_title') {
      // ...
      // PROBLEM: we cannot use React hooks here if this function runs conditionally
      // SOLUTION: provide "post" object from outside
      // ...
      // PROBLEM: if we provide "post" we should also provide other "already extracted data from db" 
      //          or the consumer could lack some data. If we do this we limit freedom of extender
      // SOLUTION: get data outside of react, so without React hooks.
      return "..."
    }

    if (field.key === 'post_author_name') {
      // ...
      // PROBLEM: same as above (sould not use React Hooks)
      return "..."
    }
  })

  // post metas (native custom fields)...
  wp.hook.addFilter('dynamic_data_sources_list', (sources: Source[], blockProps) => {
      const {context} = blockProps;
      
      const post = useEntityRecord<Post>('postType', context.postType, context.postId);
      // PROBLEM: same as above (sould not use React Hooks)
      
      const post_metas = post.record.meta || {};
      const source = {
        type: "post_meta",
        fields_keys: Object.keys(post_metas),
      };

      return [...sources, source];
  });
  wp.hook.addFilter('dynamic_data_source_replacer', (value, rawValue, field) => {
    if (field.type !== 'post_meta') return rawValue;

      const field_value = ....

      return field_value;
  })

}

const acf_init = () => {

  wp.hook.addFilter('dynamic_data_sources_list', (sources: Source[], blockProps) => {
      const {context} = blockProps;

      const post = useEntityRecord<Post>('postType', context.postType, context.postId);
      // PROBLEM: same as above (sould not use React Hooks)
      
      const acf_fields = post.record.acf || {};
      const source = {
        type: "acf",
        fields_keys: Object.keys(acf_fields),
      };

      return [...sources, source];
  });
  wp.hook.addFilter('dynamic_data_source_replacer', (value, rawValue, field) => {
    if (field.type !== 'acf') return rawValue;

      const field_value = ....

      return field_value;
  })
}

@tresorama
Copy link
Author

tresorama commented Dec 6, 2023

Iteration #2

WIP - Test of an implementation that can have third party dynami cdata source providers

type Source = {
  type: string,
  fields_keys: string[]
}

// use this function in a block's "Edit" React function, to obtain the available dynamic data
// then the React JSX must render this list of "dynamic data sources" as a a UI that let user
// select one item of the list.
const useDynamicDataSources = (props: BlockEditProps<any>) => {
  // extract dynamic data fields "names" and return as list
  const sources:Source[] = wp.hook.applyFilters('dynamic_data_sources_list', [] );
  return sources;
};



type UnresolvedBinding = {
  type: string, // "acf" | "wp"
  key: string,// "post_title" | "post_data" |"acf_custom_field_name"
}
type ResolvedBinding = {
  type: string, // "acf" | "wp"
  key: string,// "post_title" | "post_data" |"acf_custom_field_name"
  data: unknown, // the data extracted from DB 
}

// use this function in a block's "Edit" React function, to resolve dynamic data bindings, 
// and place "real data" in the block attributes
// then the React JSX must render these "attributes" instead of raw attributes
const useResolvedDynamicDataValue = (blockProps) => {
  
  const { attributes } = blockProps;

  // "attributes" is the object of Block attrutes.
  // when the user select a dynamic data binding the "selection" is inserted inside the attributes value


  // we iterate over all attributes of this block and for each
  //   - check if this attribute has a dynamic data binding
  //     - if "yes" => get the data from the db for that binding and replace the value in the attributes
  //     - if "no"  => do nothing
  let resolvedAttributes = {};
  for (const attributeKey in attributes) {
    const attributeValue = attributes[attributeKey];

    // extract bindings tag of this attributses...

    type BindingTag = string;
    const bindingsTags:BindingTag[] = getBindingsTagFromAttribute(attributeValue);
    // [ 
    //   "{acf--my_custom_field}", 
    //   "{wp--post_title}"
    // ]

    
    // if no bindings found return raw attribute value...
    
    if (bindingsTags.length === 0) {
      resolvedAttributes[attributeKey] = attributeValue;
      continue;
    }
    
    
    // for each bindings resolve the value and replace it in attrivute...

    type ResolvedBinding = {
      tag: BindingTag,
    }  & (
      | { type: "TEXT", value: string }
      | { type: "IMAGE", value: {url: string, alt: string } }
      | { type: "LINK", value: {url: string, label:string } }
    )
    const resolvedBindings: ResolvedBinding[] = bindingsTags.map(binding => {
      const db_data = wp.hook.applyFilter("dynamic_data--get_binding_real_data", binding, blockProps);
      return {
        tag: binding,
        type: db_data.type,
        value: db_data.value,
      }
    });
    // [ 
    //   { tag: "{acf--my_custom_field}", type:"TEXT", value: "Green Bay" },
    //   { tag: "{wp--post_title}", type:"TEXT", value: "Top Webiste of 2023" },
    // ]

    let newValue = attributeValue;
    for (const b of resolvedBindings) {
      if (b.type === 'TEXT') {
        newValue = String(attributeValue).replaceAll(b.tag, b.value);
        continue;
      }
      
      if (b.type === 'IMAGE') {
        newValue = b.value;
        continue;
      }
      
      if (b.type === 'LINK') {
        newValue = b.value;
        continue;
      }
    }


    resolvedAttributes[attributeKey] = newValue;
  }


  // return the "resolved"  block attributes
  return resolvedAttributes;
}



const wp_init = () => {
  
  // post data...
  wp.hook.addFilter('dynamic_data_sources_list', (sources: Source[], blockProps) => {
    const source = {
      type: "post_data",
      fields_keys: [
        "post_title",
        "post_author_name",
        "post_date",
        ...
      ]
    };

    return [...sources, source];

  });
  wp.hook.addFilter('dynamic_data_source_replacer', (value, rawValue, field) => {
    if (field.type !== 'post_data') return rawValue;

    if (field.key === 'post_title') {
      // ...
      // PROBLEM: we cannot use React hooks here if this function runs conditionally
      // SOLUTION: provide "post" object from outside
      // ...
      // PROBLEM: if we provide "post" we should also provide other "already extracted data from db" 
      //          or the consumer could lack some data. If we do this we limit freedom of extender
      // SOLUTION: get data outside of react, so without React hooks.
      return "..."
    }

    if (field.key === 'post_author_name') {
      // ...
      // PROBLEM: same as above (sould not use React Hooks)
      return "..."
    }
  })

  // post metas (native custom fields)...
  wp.hook.addFilter('dynamic_data_sources_list', (sources: Source[], blockProps) => {
      const {context} = blockProps;
      
      const post = useEntityRecord<Post>('postType', context.postType, context.postId);
      // PROBLEM: same as above (sould not use React Hooks)
      
      const post_metas = post.record.meta || {};
      const source = {
        type: "post_meta",
        fields_keys: Object.keys(post_metas),
      };

      return [...sources, source];
  });
  wp.hook.addFilter('dynamic_data_source_replacer', (value, rawValue, field) => {
    if (field.type !== 'post_meta') return rawValue;

      const field_value = ....

      return field_value;
  })

}

const acf_init = () => {

  wp.hook.addFilter('dynamic_data_sources_list', (sources: Source[], blockProps) => {
      const {context} = blockProps;

      const post = useEntityRecord<Post>('postType', context.postType, context.postId);
      // PROBLEM: same as above (sould not use React Hooks)
      
      const acf_fields = post.record.acf || {};
      const source = {
        type: "acf",
        fields_keys: Object.keys(acf_fields),
      };

      return [...sources, source];
  });
  wp.hook.addFilter('dynamic_data_source_replacer', (value, rawValue, field) => {
    if (field.type !== 'acf') return rawValue;

      const field_value = ....

      return field_value;
  })
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment