Created
August 25, 2023 08:10
-
-
Save tresorama/bf512a84b6a32faafadd1600664118d1 to your computer and use it in GitHub Desktop.
wordpress-gutenberg--POC--dynamic-data
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'), | |
); |
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
Iteration #1
WIP - Test of an implementation that can have third party dynami cdata source providers