Skip to content
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

dynamic select control... #34

Open
SatishKumarPrajapati opened this issue Aug 7, 2018 · 21 comments
Open

dynamic select control... #34

SatishKumarPrajapati opened this issue Aug 7, 2018 · 21 comments

Comments

@SatishKumarPrajapati
Copy link

is there any method to create dynamic select control ?

@ajitbohra ajitbohra added the [Status] Needs More Info Typically requires follow-up from the original reporter in order to be actionable and relevant. label Aug 13, 2018
@ajitbohra
Copy link
Member

@SatishKumarPrajapati you mean adding dynamic items to select control ?

@SatishKumarPrajapati
Copy link
Author

@ajitbohra yes i want to add dynamic items to select control...
is there any method to achieve this... ?

@ajitbohra
Copy link
Member

You can add dynamically generated array of objects containing label, value property to select control.

<SelectControl
		multiple
		label={ __( 'Select some users:' ) }
		value={ this.state.users } // e.g: value = [ 'a', 'c' ]
		onChange={ ( users ) => { this.setState( { users } ) } }
		options={ [
			{ value: 'a', label: 'User A' },
			{ value: 'b', label: 'User B' },
			{ value: 'c', label: 'User c' },
		] }
	/>

For more info
https://github.com/WordPress/gutenberg/tree/master/packages/components/src/select-control

@ajitbohra ajitbohra removed the [Status] Needs More Info Typically requires follow-up from the original reporter in order to be actionable and relevant. label Aug 14, 2018
@SatishKumarPrajapati
Copy link
Author

I want to get data from PHP function.... and I tried to that but not able to get succeed...
any example where it is happening

@SatishKumarPrajapati
Copy link
Author

@ajitbohra is there any example for dynamic select control ?

@ajitbohra
Copy link
Member

@SatishKumarPrajapati

check wp_localize_script to pass dynamic data to JavaScript
https://developer.wordpress.org/reference/functions/wp_localize_script/

@FadiZahhar
Copy link

I'm not able to create a dynamic option also, any help on this, I tired to add the options as an array but no luck .

@rogerlos
Copy link

I came up with a workaround:

https://gist.github.com/rogerlos/7502541070942e16a1188dd0bb9ac2b9

@pbiron
Copy link

pbiron commented Nov 11, 2018

I've been struggling with this for a long time as well. My use case is to mimic the various wp_dropdown_xxx() functions in core, for use as InspectorControls. Just yesterday I finally managed to come up with something that does some of what wp_dropdown_categories() (it does all of what I need it to do for the block I'm building).

/**
 * External dependencies
 */
const { groupBy }           = lodash;
const { createElement }  = wp.element;
const { select }                = wp.data;
const { TreeSelect }         = wp.components;

/**
 * MyTaxonomySelect
 *
 * A poor man's wp_dropdown_categories() as a component, for use in
 * <InspectorControls />
 *
 */
export default function MyTaxonomySelect( { taxonomy, hierarchical, hide_empty, label, noOptionLabel, selectedId, onChange } ) {
	taxonomy = taxonomy || 'category';
	hierarchical = hierarchical || false;
	const args = {
		hide_empty: hide_empty || false,
	};
	const { getEntityRecords } = select( 'core' );
	const termsList = getEntityRecords( 'taxonomy', taxonomy, args );

	const termsTree = hierarchical ? buildTermsTree( termsList ) : termsList;

	/**
	 * Returns terms in a tree form.
	 *
	 * Note: borrowed from QueryControls.
	 *
	 * @param {Array} flatTerms  Array of terms in flat format.
	 *
	 * @return {Array} Array of terms in tree format.
	 */
	function buildTermsTree( flatTerms ) {
		const termsByParent = groupBy( flatTerms, 'parent' );
		const fillWithChildren = ( terms ) => {
			return terms.map( ( term ) => {
				const children = termsByParent[ term.id ];

				return {
					...term,
					children: children && children.length ? fillWithChildren( children ) : [],
				};
			} );
		};

		return fillWithChildren( termsByParent[ '0' ] || [] );
	}

	return (
			<TreeSelect
				{ ...{ label, noOptionLabel, onChange } }
				tree={ termsTree }
				selectedId={ selectedId }
			/>
	);
}

The only problem with it (for my immediate need) is that:

  • when you select the block this is an InspectorControl for the dynamic options aren't yet populated.
  • you have to de-select the block and then re-select it
    • then the terms from the taxonomy appear

Any help in solving that problem would be greatly appreciated!

@pbiron
Copy link

pbiron commented Nov 11, 2018

here's how I'm using it in my block:

...
edit: function ( props ) {
	return [
		<ServerSideRender
			key='block'
			block='my-plugin/my-block'
			attributes={ props.attributes }
		/>,

		<InspectorControls key='inspector'>
			<PanelBody
					title={ 'Settings' }
					initialOpen={ true }>
				<MyTaxonomySelect
					taxonomy={ 'my-tax' }
					hierarchical={ true }
					hide_empty={ true }
					label={ 'My Tax' }
					noOptionLabel={ 'Select a term' }
					selectedId={ props.attributes.term }
					onChange={ ( value ) => props.setAttributes( { term: value } ) }
				/>
			</PanelBody>
		</InspectorControls>
	];
}

@rogerlos
Copy link

(Doesn't it seem kind of nuts these oft-used core mimicking functions aren't part of Gutenberg? It does to me.)

I ended up cheating. I have a regular old JS file, which doesn't use JSX or react, which is a dependency of my block. Within it I stashed a function which is the REST caller, and what amounts to a "data store" for the returned post list, since my page typically has tens of blocks which use the exact same REST posts list in the SelectControl and it seems stupid to repeatedly ask for the same thing, but I'm not hip on the latest techniques, so...

function cheat() {
    this.cheat = this;
    this.types = [];
    this.get_posts = function () {
        return new Promise(
            function ( resolve ) {
                wp.apiFetch( { path : '/wp/v2/types' } ).then(
                    function ( types ) {
                        this.types = types;
                    }
                    wp.apiFetch( { path : '/wp/v1/custom/getposts' } ).then(
                        function ( res ) {
                            resolve( res )
                        }
                    )
                )
            }
        )
    };
    this.post_selector_opts = function () {
        let cheat = this.cheat;
        return new Promise(
            function ( resolve ) {
                cheat.get_posts()
                    .then(
                        function ( posts ) {
                            let opts = [{ key: 'none', label: 'Select Post', value: 0 }];
                            for ( const P of posts ) {
                                opts.push( { key: P.slug, label: P.title, value: P.id } );
                            }
                            resolve( opts );
                        }
                    )
            }
        )
    };
    this.init = function () {
        this.post_selector_opts().then( opts => {
            cheater.posts = opts;
        } )
    }
}

const cheater = { posts: [] };

let shared = new cheat();
shared.init();

and then within my block:

el(
    SelectControl,
    {
         options: cheater.posts,
    }
)

Kind of sleazy, sure, and definitely would make the facebook folks cry. Because the REST call is made when the editor loads, the select control always has the content available. This makes sense to me; if you know you're going to be making the exact same call potentially dozens of times, why would you not stash the results somewhere?

(FWIW, I'm going to ditch the select control since it's way too basic; without the ability to use headers within it to organize the list (as allowed by the HTML spec) it's incomprehensible to use for a post selector on a site with more than a dozen or so posts.)

@pbiron
Copy link

pbiron commented Nov 11, 2018

(Doesn't it seem kind of nuts these oft-used core mimicking functions aren't part of Gutenberg? It does to me.)

I completely agree that GB should include controls that mimic all of the wp_dropdown_xxx() funcs!!!

@pbiron
Copy link

pbiron commented Nov 11, 2018

Within it I stashed a function which is the REST caller, and what amounts to a "data store" for the returned post list, since my page typically has tens of blocks which use the exact same REST posts list in the SelectControl and it seems stupid to repeatedly ask for the same thing, but I'm not hip on the latest techniques, so...
if you know you're going to be making the exact same call potentially dozens of times, why would you not stash the results somewhere?

From what I can tell (by watching REST API hits against the server) with the way I wrote MyTaxonomySelect it seems that wp.data is caching the taxonomy terms I query for, because when a user does subsequent select/de-selects of the block (and hence, making my control appear/disappear) there are NOT additional REST API calls made against the server. But I could be wrong.

@pbiron
Copy link

pbiron commented Nov 11, 2018

The only problem with it (for my immediate need) is that:

  • when you select the block this is an InspectorControl for the dynamic options aren't yet populated.
  • you have to de-select the block and then re-select it
    • then the terms from the taxonomy appear

I suspect I can solve that problem by using a Promise, but since I don't really understand what a Promise is I'm not sure :-( @rogerlos, I'm gonna look at how you are using Promises and see if I can crib something from that. Thanx.

@rogerlos
Copy link

I clearly don't totally grok promises, either, as anyone who is an expert at them probably would not be thrilled with my code. My custom API endpoint returns posts by types, and I omitted some code above where I make sure that types I don't want are omitted from the selector, hence the layered REST calls.

And you are undoubtedly right about the cached call, but it's never super-clear to me exactly what process is caching what, so I tend to be a bit cautious. I do a lot of dev on a non-WP project where the only cache is what you make yourself, and that bleeds into this stuff.

@rogerlos
Copy link

rogerlos commented Nov 11, 2018

One thing which you might investigate: (I think that) InspectorControls code that falls within a PanelBody is not run until the panel is opened. So you might be able to work around the empty select control by ensuring that somewhere in your edit function you ask for the select options outside of the inspector panel being opened, if you're not already doing so:

edit: function ( props ) {
    const getopts = () => {
        return // code to get results;
    }
    let myopts = typeof( myopts ) === 'undefined' ? getopts() : myopts; // or whatever
    return(
        // your code, using myopts as options for SelectControl
    )
}

@rogerlos
Copy link

rogerlos commented Nov 11, 2018

Finally, I think what you really want to do upon the initial return from REST of your options list is to call the same "onChange" function that is called when someone makes a selection, to trigger all of the react stuff which happens onChange:

edit: function( props ) {
    const termChange = val => {
        props.setAttributes( { term: val } )
    }
    const getOpts = () => {
        let termList = []; // your code instead of []
        if ( typeof( props.attributes.term ) === 'undefined' )
            termChange( yourDefaultValue ); 
        return termList;
    }
    let myopts = typeof( myopts ) === 'undefined' ? getOpts() : myopts;
    return (
        // 'onChange' for term selector is termChange, 'options' is myopts
    )
}

@tribulant
Copy link

(Doesn't it seem kind of nuts these oft-used core mimicking functions aren't part of Gutenberg? It does to me.)

I ended up cheating. I have a regular old JS file, which doesn't use JSX or react, which is a dependency of my block. Within it I stashed a function which is the REST caller, and what amounts to a "data store" for the returned post list, since my page typically has tens of blocks which use the exact same REST posts list in the SelectControl and it seems stupid to repeatedly ask for the same thing, but I'm not hip on the latest techniques, so...

function cheat() {
    this.cheat = this;
    this.types = [];
    this.get_posts = function () {
        return new Promise(
            function ( resolve ) {
                wp.apiFetch( { path : '/wp/v2/types' } ).then(
                    function ( types ) {
                        this.types = types;
                    }
                    wp.apiFetch( { path : '/wp/v1/custom/getposts' } ).then(
                        function ( res ) {
                            resolve( res )
                        }
                    )
                )
            }
        )
    };
    this.post_selector_opts = function () {
        let cheat = this.cheat;
        return new Promise(
            function ( resolve ) {
                cheat.get_posts()
                    .then(
                        function ( posts ) {
                            let opts = [{ key: 'none', label: 'Select Post', value: 0 }];
                            for ( const P of posts ) {
                                opts.push( { key: P.slug, label: P.title, value: P.id } );
                            }
                            resolve( opts );
                        }
                    )
            }
        )
    };
    this.init = function () {
        this.post_selector_opts().then( opts => {
            cheater.posts = opts;
        } )
    }
}

const cheater = { posts: [] };

let shared = new cheat();
shared.init();

and then within my block:

el(
    SelectControl,
    {
         options: cheater.posts,
    }
)

Kind of sleazy, sure, and definitely would make the facebook folks cry. Because the REST call is made when the editor loads, the select control always has the content available. This makes sense to me; if you know you're going to be making the exact same call potentially dozens of times, why would you not stash the results somewhere?

(FWIW, I'm going to ditch the select control since it's way too basic; without the ability to use headers within it to organize the list (as allowed by the HTML spec) it's incomprehensible to use for a post selector on a site with more than a dozen or so posts.)

Very good man, well done. You nailed this with traditional Javascript and without the JSX.

For anyone wondering how to use SelectControl, it is a component. So you can use wp.components directly or define the component object before you use it at the top of the file:

const {SelectControl} = wp.components;

@megphillips91
Copy link

megphillips91 commented Aug 10, 2019

====> Update to comment
I just moved my declaration of journeyOptions up outside of the edit function into where all the other constants are declared and it solved the problem. Works like a charm.
====>
I set a constant within the edit function which makes the call to the RestAPI for the options. The chosen value of the select is saved and when block is updated, the proper value is saved and rendered as expected.

The only problem I am having is that onChange, the entire SelectControl disappears from the panel. Very strange. I can't seem to get past this road block. Any advice is appreciated. I think it has something to do with the options being set from this function.

Here I've pasted the code which is all from within my edit function

const journeyOptions = [];
wp.apiFetch( { path: '/journey-cxm/v1/data' } )
.then(
posts => posts.map(function(post){
//replace space with hyphen for class
let optionvalue = post.journey_path.replace(/\s/g, '-')
journeyOptions.push({value: optionvalue, label: post.journey_path})
}
)
);

function onChangeJourney( newJourney ) {
setAttributes( { journey: newJourney } );
}

<SelectControl
label={ __( 'Choose Journey:' ) }
value={ journey }
onChange={ onChangeJourney }
options={ journeyOptions }
/>

@strarsis
Copy link

strarsis commented May 11, 2020

Yes, I want to load data via the WordPress REST API (using @wordpress/api-fetch) and
then populate a SelectControl with these as options, but options doesn't accept a function.

@woodyhayday
Copy link

woodyhayday commented May 20, 2024

Dropping this here for those who need to solve this incorrectly in a hurry:

(Note, this is the wrong way of doing this as it's piggybacking off of the stylesheet of your block in a nefarious way, the above answer from @rogerlos is more legit, or this looks more like the 'proper' way of doing this, but sometimes needs must. Goes away and adds an issue to circle back around and make more solid once there's better docs.)

function yourProject_gutenberg_script_intercept( $tag, $handle, $src ) {
    

    if ( $handle == 'your-block-slug-style' ){

        // inject whatever vars you want to pass from php to js at point of loading
        $tag .= '<script>var your_var = ' . json_encode( $your_var ) . ';</script>';

    }

    return $tag;

};
add_filter( 'style_loader_tag', 'yourProject_gutenberg_script_intercept', 10, 3 );

Don't be like me 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants