0

I'm working to translate a custom shortcode to a Gutenberg-block but am stuck on two issues. This post first treats the current situation, the next heading shows the problems I'm running into in greater detail.

Current situation

I'm using the shortcode (syntax and code below) to display resume-items on my website. It includes fields for a title, organisation, location, duration, number of hours per week and/or GPA and outputs as follows. There are links to some of the attributes. Shortcode output

Syntax of the shortcode (removed part of the description for brevity) is as follows:

[cv-item naam='<a href="https://www.tudelft.nl/en/education/programmes/masters/aerospace-engineering/msc-aerospace-engineering/" target="_blank" rel="noopener">Aerospace Engineering (MSc)</a>' organisatie='<a href="http://www.tudelft.nl/en/" target="_blank" rel="noopener">Delft University of Technology</a>' locatie='Delft, The Netherlands' duur='09/2014 - current' status='open' cijfer='7.7/10' cijfer_datum='11/2018'] Double-track specialisation (180 EC, three years) in the [...] on the other. [/cv-item]

Shortcode definition is as follows:

function cv_item( $atts, $content = null ) {
$atts_completed = shortcode_atts( array(
    'naam'         => null,
    'organisatie'  => null,
    'locatie'      => null,
    'duur'         => null,
    'uren'         => null,
    'status'       => 'closed',
    'cijfer'       => null,
    'cijfer_datum' => null,
), $atts );

$naam         = $atts_completed['naam'];
$organisatie  = $atts_completed['organisatie'];
$locatie      = $atts_completed['locatie'];
$duur         = $atts_completed['duur'];
$uren         = $atts_completed['uren'];
$status       = $atts_completed['status'];
$cijfer       = $atts_completed['cijfer'];
$cijfer_datum = $atts_completed['cijfer_datum'];

static $id = 1;
$id ++;

$output = '<div class="cv-item" id="cv-item-' . $id . '">';

/* Uit-/inklap-knop */
if ( $status == 'open' ) {
    $toggle_class = 'fa-arrow-up';
} else {
    $toggle_class = 'fa-arrow-down';
}

if ( $content != null ) {
    $output .= '<i class="fa ' . $toggle_class . ' cv-item-toggle" id="cv-item-' . $id . '"></i>';
}

/* Eerste regel, met naam, bedrijf en locatie */
$output .= '<span class="cv-item-title">';
$output .= '<strong>' . $naam . '</strong>';

if ( $organisatie != null ) {
    $output .= ', <span class="organisation">' . $organisatie . '</span>';

    if ( $locatie != null ) {
        $output .= ' (' . $locatie . ')';
    }
}
$output .= '</span>';

/* Tweede regel, met duur en aantal uren per week */
if ( $duur != null || $uren != null || $cijfer != null ) {
    $output .= '<span class="cv-item-details">';

    if ( $duur != null ) {
        $output .= $duur;
    }

    if ( $uren != null | $cijfer != null ) {
        if ( $duur != null ) {
            $output .= ' | ';
        }

        if ( $uren != null ) {
            $output .= $uren . __( ' uur per week', 'peerlings' );
        }

        if ( $cijfer != null ) {
            $output .= $cijfer;

            if ( $cijfer_datum != null ) {
                $output .= ' (' . __( 'per', 'peerlings', 'Cijfer XX per YY' ) . ' ' . $cijfer_datum . ')';
            }
        }

    }

    $output .= '</span>';
}

/* Omschrijving */
if ( $content != null ) {
    $output .= '<div class="cv-item-description ' . $status . '">' . $content . '</div>';
}

/* Closing */
$output .= '</div>';

return $output; }

As you can see, there are a bunch of conditionals in the function that processes the shortcode. Also, some separators (like the comma between item title and organisation) are added, as are a bunch of wrapper-spans used for styling purposes (such as span.cv-item-title and span.details). I have trouble replicating this in Gutenberg.

New situation, problems and solution attempts

I've been working on adjusting this custom profile block to suit my needs. So far, that has worked reasonably well (full block.js-code here), but I'm stuck on two problems:

  • Replicating the conditionals
  • Adding the separators and span-wrappers

Conditionals

In the original code, values from the block settings (in the right sidebar) are displayed conditionally, per the code below (extract from return-statement from the edit- and save-functions):

el('div', {className: 'organic-profile-social'},
    attributes.nameURL && el('a', {
        className: 'social-link',
        href: attributes.nameURL,
        target: '_blank'
    },
    el('i', {className: 'fa fa-facebook'})
    ),
    attributes.organisationURL && el('a', {
        className: 'social-link',
        href: attributes.organisationURL,
        target: '_blank'
    },
    el('i', {className: 'fa fa-twitter'})
    )
)

The same logic, however, doesn't apply to regular fields, as shown below (extract from return-statement from the save-function):

el('span', {},
    attributes.grade && el('span', {
        className: 'grade',
        value: attributes.grade
    }),
    attributes.gradeDate && el('span', {
        className: 'grade-date',
        value: attributes.gradeDate
    }
    )
)

This outputs the grade and grade-date spans regardless of their content. Also, the span that is supposed to wrap the these two, turns up as an empty span preceding them:

<span></span><span class="grade"></span><span class="grade-date"></span>

Of course, I can just move all conditional fields to the settings-panel in the sidebar, but that (to me) defeats the entire WYSIWYG-on-the-back-end-approach that Gutenberg is (insofar I understand correctly) all about.

Question: Is it even possible to achieve what I have in mind, and how can I do so?

Separators, wrappers and intermediate text

I thought I had this to work, by adding the separators using helper-functions that set a particular separator, or set it to null if the surrounding values are empty, like so:

if (typeof props.attributes.grade !== 'undefined' && typeof props.attributes.gradeDate !== 'undefined') {
    var separatorGradeGradeDatePre = ' (' + i18n.__('per')
    var separatorGradeGradeDateSuf = ')'
} else {
    var separatorGradeGradeDatePre = null
    var separatorGradeGradeDateSuf = null
}

I then added these two the return-statement of the save-function. That worked well one way, but not the way back. Put differently: if the edit-function goes to read the post_content-HTML, it comes across text that isn't specified in the edit-function and throws an error.

Idea's I've thought of so far - as well as my reasons for not implementing (yet):

  • Including the separators in the edit-function, too, and then hiding these using editor-specific CSS. Haven't implemented, because I think it's ugly (to say the least) to include content that I'm then (in all instances) going to hide using CSS.
  • Not including the separators in the save-function, but adding them using CSS content-properties. Even more ugly than the previous option, especially as it will require a nasty PHP-CSS-mashup to include the conditional statements.

Question: Are there any other ways to achieve this, or should I just go for one of my ideas posted above?

1 Answer 1

1

Although a little different than I had initially anticipated, I have found/created a solution to my two problems.

Conditionals

I have opted to move non-required input to the sidebar, using the InspectorControls-element. Reason for this is that I realised that it would be impossible to hide a field (in the editor-part of the screen) that was left blank (required to show an accurate front-end representation), while still being able to edit that field.

For example, this is the definition of a textfield in the sidebar.

el(TextControl, {
    type: 'text',
    value: attr.location,
    label: __('Location', 'peerlings-blocks'),
    onChange: function (newLocation) {
        props.setAttributes({location: newLocation})
    }
}),

This is (in the edit- and save-parts) the code used for displaying it, if the field has been used

attr.location && el('span', {className: 'location'}, attr.location),

The reason why I originally didn't want to move these details to the sidebar was that I thought they then wouldn't show in the editor-preview, but using the code above, that was possible to achieve. Problem solved.

Separators and wrappers

This took some more fiddling to get to work, for which I had a few revelations:

  • The selector (defined as part of any attribute) looks at the HTML extracted from post_content to find the value of a particular attribute. If this selection finds other HTML than expected, the block validation fails.
  • The edit-part is used to display the block-'preview' in the Wordpress Dashboard. Also, it reads the HTML from post_content, when a block is edited again. On the other hand, the save-part transforms the attribute values to a HTML that is sent to (and saved in) post_content. Essentially, save encodes the block to HTML, edit decodes the HTML to attributes (found in the selectors discussed previously). This explains why minor differences between the two (in the parts found by the selectors) immediately invalidate the block.

Given the above, I placed the separators and wrappers outside of the elements that are selected to retrieve the attributes. That resulted in some extra wrapper elements, but does the trick. To illustrate, the shortcode had the following HTML output:

<span class="cv-item-details">08/2013 - current | 6 hours per week</span>

Now, that line looks like this:

<span class="resume-item-details">
    <span class="duration">
        08/2013 – current
    </span> 
    | 
    <span class="hours">
        <span class="data">
            6
        </span>
     hours per week
    </span>
</span>

The problems I had originally envisioned (separators in between block-level input fields) were essentially solved by moving a number of input fields to the sidebar (as outlined above). Separators between input fields that do remain are only included in the save-part, and that works as intended - as shown in the code below.

edit-part:

el('span', {className: 'resume-item-title'},
    el(RichText, {
        key: 'editable',
        tagName: 'strong',
        className: 'name',
        placeholder: __('Name', 'peerlings-blocks'),
        keepPlaceholderOnFocus: true,
        value: attr.name,
        onChange: function (newName) {
            props.setAttributes({name: newName})
        }
    }),
    el(RichText, {
        key: 'editable',
        className: 'organisation',
        placeholder: __('Organisation', 'peerlings-blocks'),
        keepPlaceholderOnFocus: true,
        value: attr.organisation,
        onChange: function (newOrganisation) {
            props.setAttributes({organisation: newOrganisation.replace('<br>', '')})
        }
    })
),

save-part (note the comma between the two RichText-parts, conditional on attr.organisation not being empty):

el('span',
    {className: 'resume-item-title'},
    el(RichText.Content, {
        tagName: 'strong',
        className: 'name',
        value: attr.name
    }),
    attr.organisation && ', ',
    el(RichText.Content, {
        tagName: 'span',
        className: 'organisation',
        value: attr.organisation
    })
),

You can find the full JS-file of the block (at least, the current version) here.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.