Αρχιτεκτονικά project: ο κώδικας του block type


Μέσα στο φάκελο src/archmeta-info-block δημιουργούμε ένα νέο αρχείο με όνομα editor.js και για να το συμπεριλάβουμε στο project, θα το πρέπει να το ενσωματώσουμε σε αυτό. Άρα στο αρχείο src/editor.js προσθέτουμε την ακόλουθη γραμμή:

import './archmeta-info-block/editor.js';

Τώρα κάθε φορά που χρησιμοποιούμε μία από τις εξής εντολές του npm (npm run build ή npm run start) θα συμπεριλάβουμε και τον κώδικα του αρχείου src/archmeta-info-block/editor.js στο project μας. Σε αυτό το αρχείο λοιπόν, θα προσθέσουμε αρκετό κώδικα, οπότε στη συνέχεια θα επικεντρωθούμε σε συγκεκριμένα σημεία και ακολούθως θα συμπεριλάβουμε όλο τον κώδικα.

Τα imports

Στην κορυφή του αρχείου, ορίζουμε ποιες συναρτήσεις θα συμπεριλάβουμε από εξωτερικά αρχεία.

import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps } from '@wordpress/block-editor';
import { useSelect } from '@wordpress/data';
import { useEntityProp } from '@wordpress/core-data';
import { TextControl } from '@wordpress/components';
  • Η registerBlockType μας επιτρέπει να ορίσουμε ένα καινούριο block type για να μπορεί να χρησιμοποιηθεί στο περιβάλλον του editor (client-side).
  • Τα block, μπορούν να χρησιμοποιήσουν πολλές δυνατότητες στο περιβάλλον του editor. Για κάποιες από αυτές, ο editor προσθέτει attributes και αυτά είναι προσβάσιμα στο περιβάλλον του editor (και αποθηκεύονται κατά την αποθήκευση ενός block) χρησιμοποιώντας το Support API και είναι διαθέσιμα μέσω ενός αντικειμένου που μας επιστρέφεται από την χρησιμοποίηση ενός custom React hook, του useBlockProps.
  • Η useEntityProp, είναι άλλο ένα custom React hook. Από την έκδοση 5.4 του WordPress, έχει αντικαταστήσει τα attributes με source: meta, ως η μέθοδος που χρησιμοποιούμε για να προσπελάσουμε τα metadata ενός post, page, ή όπως στην περίπτωσή μας, ενός custom post type.
  • Η useSelect είναι άλλο ένα custom React hook, που μας βοηθάει να προσπελαύνουμε δεδομένα του WordPress όσο είμαστε στον editor και απλοποιεί αυτή την δυνατότητα σε σχέση με την withSelect από την έκδοση 5.9 του WordPress.
  • Τέλος, η TextControl είναι ένα από τα έτοιμα component που μας παρέχει το WordPress.

Η συνάρτηση save (dynamic blocks)

Όταν χρησιμοποιούμε την registerBlockType (στο αρχείο που περιέχει τον κώδικα JavaScript), δίνουμε ως παράμετρο και ένα αντικείμενο που περιγράφει το block type. Καθώς τις περισσότερες πληροφορίες (π.χ. τα attributes) τα ορίζουμε στο αρχείο block.json, εδώ θα ορίσουμε τα πεδία edit και save. Το καθένα από αυτά έχει την τιμή μιας συνάρτησης, που θα καλούμε είτε στον editor κατά την χρήση ενός block αυτού του block type (η τιμή της edit), είτε κατά την αποθήκευση του block (η τιμή της save).

Η συνάρτηση της τιμής save επιστρέφει μια τιμή που ως συνήθως είναι ο κώδικας HTML που θα αποθηκευτεί ως μέρος του content του post στο οποίο χρησιμοποιούμε το block. Ταυτόχρονα, όταν ο χρήστης πάει να επεξεργαστεί το post, ο editor εκτελεί για κάθε block την συνάρτηση αυτή, και το αποτέλεσμα που επιστρέφει πρέπει να είναι το ίδιο με αυτό που είναι αποθηκευμένο στην βάση δεδομένων, αφού και οι τιμές των attributes, του block είναι και αυτές οι ίδιες. Περισσότερα και πιο αναλυτικά για αυτό το μηχανισμό μπορούμε να βρούμε στην τεκμηρίωσή του.

Κάθε φορά που αλλάζουμε την συνάρτηση που καλείται κατά την αποθήκευση του block, το WordPress έχει ένα μηχανισμό για να μην δημιουργείται πρόβλημα από αυτή την αλλαγή. Κάποιες φορές όμως θέλουμε να μπορούμε να αλλάζουμε τον κώδικα του block type, χωρίς να πρέπει να ανανεώσουμε κάνοντας εκ νέου αποθήκευση σε όλα τα block αυτού του block type στον ιστότοπο που τα χρησιμοποιούμε. Το WordPress μας παρέχει αυτή τη δυνατότητα μέσω των dynamic blocks. Η συνάρτηση αποθήκευσης επιστρέφει απλά την τιμή null, και έτσι παρακάμπτουμε όλο το μηχανισμό ελεγχου (validation). Έτσι το πεδίο save έχει την παρακάτω τιμή:

    save: () => {
        return null;
    }

Η render_callback

Ο κώδικας HTML για αυτό το block type, παράγεται πλέον από τον server, μέσω της συνάρτησης PHP που έχουμε δώσει σαν τιμή στην render_callback κατά την καταχώρηση του block type. Ουσιαστικά τα dynamic blocks μας δίνουν τη δυνατότητα να αλλάζουμε τον κώδικα σε ένα αρχείο PHP, και να βλέπουμε άμεσα τις αλλαγές σε όλα τα σημεία που χρησιμοποιούμε blocks αυτού του block type. Ένας μηχανισμός παρόμοιος με τα template parts στα κλασικά themes του WordPress. Στην render_callback έχουμε δώσει την ακόλουθη τιμή:

'render_callback' => function($attributes) {
                    return ArchitectureProject::MetaBlockFrontEndTemplatePartHTML($attributes);
                },

ώστε για λόγους οργάνωσης του κώδικα, να καλούμε μια μέθοδο της class ArchitectureProject (στο αρχείο src/ArchitectureProject.php), περνώντας σε αυτήν τις τιμές των attributes του block, ως παράμετρο. Η μέθοδος αυτή υλοποιείται ως εξής (σημείωση: τα στυλ κανονικά θα τα είχαμε σε ένα εξωτερικό αρχείο CSS, τώρα είναι inline στον κώδικα για απλοποίηση σε αυτό το σημείο, ώστε να επικεντρωθούμε στην συνάρτηση που δίνουμε ως τιμή στην render_callback):

    public static function MetaBlockFrontEndTemplatePartHTML($attributes) {
        $wrapper_attributes = get_block_wrapper_attributes();
        if ( ! empty($attributes['archprojectId'])) {
            $id = $attributes['archprojectId'];
        } else {
            $id = get_the_ID();
        }
        $metadata = get_post_meta( $id, 'dh_project_info', true );
        ob_start(); ?>
            <div <?php echo esc_attr($wrapper_attributes); ?>>
                <table style="border-collapse: collapse">
                    <thead>
                        <th style="padding:0 2em;border: 1px solid grey">Location</th>
                        <th style="padding:0 2em;border: 1px solid grey">Year</th>
                        <th style="padding:0 2em;border: 1px solid grey">Type</th>
                    </thead>
                    <tbody>
                        <td style="padding:0 2em;border: 1px solid grey"><?php echo esc_html( $metadata['location'] ); ?></td>
                        <td style="padding:0 2em;border: 1px solid grey"><?php echo esc_html( $metadata['year'] ); ?></td>
                        <td style="padding:0 2em;border: 1px solid grey"><?php echo esc_html( $metadata['type'] ); ?></td>
                </table>
            </div>
        <?php return ob_get_clean();
    }

Ας προσέξουμε ότι στο εξωτερικό HTML tag (το wrapping block element) χρησιμοποιούμε την τιμή που επιστρεφει η συνάρτηση get_block_wrapper_attributes(). Αυτή προσθέτει στο element για παράδειγμα ένα id, ή ένα class που έχουμε προσθέσει στο block μας στο περιβάλλον του editor, και επιτρέπει να λειτουργήσει το block type σωστα σύμφωνα με το Support API.

Η συνάρτηση edit

Μιας και η συνάρτηση που καθορίζει την συμπεριφορά του block type στο περιβάλλον του editor, αποτελεί σχεδόν του σύνολο του αρχείου src/archmeta-info-block/editor.js, σε αυτό το σημείο, παραθέτω το σύνολο του κώδικα του αρχείου (αν και κάποια μικρά κομματια δίνονται και πιο πάνω).

import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps } from '@wordpress/block-editor';
import { useSelect } from '@wordpress/data';
import { useEntityProp } from '@wordpress/core-data';
import { TextControl } from '@wordpress/components';

registerBlockType('dh-architect-plugin/archmeta-info-block', {
    edit: ( props ) => {       
        const blockProps = useBlockProps();
        const postType = useSelect(
			( select ) => select( 'core/editor' ).getCurrentPostType(),
			[]
		);

        if ( postType === 'dh_at_archproj' ) {
            const currentPost = useSelect(
                ( select ) => select( 'core/editor' ).getCurrentPost(),
                []
            );
            const currentPostId = currentPost['id'];
    
            const [ meta, setMeta ] = useEntityProp( 'postType', postType, 'meta', currentPostId );
            const myMeta = meta['dh_project_info'];
    
            const updateMetaLocation = (newLocation) => {
                const newMeta = Object.assign({...meta['dh_project_info']}, {location: newLocation.toString()});
                setMeta( { ...meta, dh_project_info: newMeta } );
            };
    
            const updateMetaYear = (newYear) => {
                const newMeta = Object.assign({...meta['dh_project_info']}, {year: newYear.toString()});
                setMeta( { ...meta, dh_project_info: newMeta } );
            };
    
            const updateMetaType = (newType) => {
                const newMeta = Object.assign({...meta['dh_project_info']}, {type: newType.toString()});
                setMeta( { ...meta, dh_project_info: newMeta } );
            };
        
            return (
                <div { ...blockProps }>
                    <TextControl
                        label="Location"
                        value={ ( myMeta && myMeta.location ) ? myMeta.location : "" }
                        onChange={ updateMetaLocation }
                    />
                    <TextControl
                        label="Year"
                        value={ ( myMeta && myMeta.year ) ? myMeta.year : "" }
                        onChange={ updateMetaYear }
                    />
                    <TextControl
                        label="Type"
                        value={ ( myMeta && myMeta.type ) ? myMeta.type : "" }
                        onChange={ updateMetaType }
                    />
                </div>
            )
        } else {
            const allArchitectureProjects = useSelect( select => {
                return select('core').getEntityRecords('postType', 'dh_at_archproj', {per_page: -1})
            })

            if ( ! allArchitectureProjects ) {
                return <div>Loading architecture projects... Please wait.</div>
            } else {
                return (
                    <div { ...blockProps }>
                        <select onChange = { event => props.setAttributes({archprojectId: event.target.value}) }>
                            <option value="">Select an architecture project</option>
                            { allArchitectureProjects.map( archproj => {
                                return (
                                    <option value={archproj.id} selected = {props.attributes.archprojectId == archproj.id}>
                                        {archproj.title.rendered}
                                    </option>
                                )
                            })}
                        </select>
                    </div>
                )
            }
        }
    },
    save: () => {
        return null;
    }
});

Το βασικό σημείο που πρέπει να προσέξουμε, είναι ότι στην περίπτωση που το block χρησιμοποιείται σε ένα architecture project, τότε εμφανίζει τις τιμές location, year, type (τα metadata) του αρχιτεκτονικού project δίνοντας τη δυνατότητα στο χρήστη να αλλάξει αυτές τις τιμές, ενώ αν ο χρήστης εισάγει ένα block από αυτό το block type σε ένα post που δεν είναι αρχιτεκτονικό project, τότε ο χρήστης μπορεί να επιλέξει από μια λίστα με όλα τα projects, πιο θέλει, για να εμφανίζονται σε αυτό το σημείο οι τιμές για αυτό το project (χωρίς βέβαια δυνατότητα αλλαγής των τιμών αυτών).