WordPress / gutenberg-examples

Examples for extending WordPress/Gutenberg with blocks.
1.2k stars 316 forks source link

dynamic select control... #34

Open SatishKumarPrajapati opened 6 years ago

SatishKumarPrajapati commented 6 years ago

is there any method to create dynamic select control ?

ajitbohra commented 6 years ago

@SatishKumarPrajapati you mean adding dynamic items to select control ?

SatishKumarPrajapati commented 6 years ago

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

ajitbohra commented 6 years ago

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

SatishKumarPrajapati commented 6 years ago

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 commented 6 years ago

@ajitbohra is there any example for dynamic select control ?

ajitbohra commented 6 years ago

@SatishKumarPrajapati

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

FadiZahhar commented 6 years ago

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 commented 5 years ago

I came up with a workaround:

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

pbiron commented 5 years ago

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:

Any help in solving that problem would be greatly appreciated!

pbiron commented 5 years ago

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 commented 5 years ago

(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 commented 5 years ago

(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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

(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 commented 5 years ago

====> 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 commented 4 years ago

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 commented 4 months ago

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 😄