WP DEV Section 25: Plugin: Multiple Choice Block Type (React)


WordPress

Updated Mar 14th, 2022

Starting Our Multichoice block

import {TextControl } from @wordpress/components

can use your own css flex or grid or use other wp comps like Flex, Flexblock, FlexItem, Button, Icon

Styling our Block

Add an index.scss file in the src folder and add the block className and

Add a className to the star Icon and nested class in the CSS

When only modifying one property, instead of adding a class just declare inline CSS in the JSX with:

style={{fontSize: “20px”}}

*note object in object and camelcase and value in quotes.

Event Handling & Updating Block Attributes

Update text-field value in editor in real-time after every keystroke with onChange prop

function EditComponent (props) {

  function updateQuestion(value) {
    props.setAttributes({question: value})
  }

return (
  // JSX here
)

}

Start working on answers area. Nneed a new attribute to work with defined in the wp.blocks.registerBlockType definition.

attributes: {
  question: {type: "string"},
  answers: {type: "array", default: [""]}
}

Go to JSX to make area come to life by cutting flex area and in its place adding curly brackets and loop through answers attribute

{props.attributes.answers.map(function (answer, index) {
  return (
    // paste back in flex area here and add a value={answer} to the    TextControl
  )
})}

Test with some hardcoded defaults but you will see you cannot edit because we need to give each one of these an onChange prop as a way to change state data. Add an “onChange” to the <TextControl> WP comp.

onChange={newValue => {
  //create copy of array to so we don't mutate
  const newAnswers = props.attributes.answers.concat([])
  newAnswers[index] = newValue
  props.setAttributes({answers: newAnswers})
}}

Complicated because we don’t want to mutate state and we only want to update a portion at a time. Create new copy of array we can mutate.

Change gears and make add answers button come to life. Give button an onClick prop with an ES6 arrow function right there with no parameter.

onClick={() => {
  props.setAttributes({answers: props.attributes.answers.concat([])})
}}

Make the delete buttons come to life by adding to the Delete button:

onClick={() => deleteAnswer(index)}

Create a matching function above overall JSX that filters through to create new array with everything except the answer you want to delete

function deleteAnswer(indexToDelete) {
  const newAnswers = props.attributes.answers.filter(function(x, index) {
    return index != indexToDelete
  })
  props.setAttributes({answers: newAnswers})
}

Focus New Field For Immediate Typing

Do you wish that whenever you click the “Add another answer” button the new text field was automatically focused so you could immediately begin typing? Here’s how we could achieve this.

First, let’s change the onClick function for our “Add Another Answer” button so that instead of concatenating an array with a single value of “” we concatenate an array with a single value of undefined.

This way, the only time an answer field will be undefined instead of an empty string is when you just recently clicked the button to add a new field. Now, we can add a prop named autoFocus to our component that you type into; for example:

That comparison will only evaluate as true when we just recently added a new field, so it will be automatically focused and ready to have a value typed into it.

Setting Up the Correct Answer

When you click on one a star to denote it is the correct answer.

Add a new attribute

correctAnswer: {type: "number", default: undefined}

Note that the reason the default is undefined instead of zero or null is because if the answer was the first item it would have the value of zero. Zero can be interpreted as false, so when we check to see if the user has set an answer there could be some room for confusion.

Find button with the star icon and on the opening tag add an onClick prop

onClick={() => markAsCorrect(index)}

Create the function with the matching name

function markAsCorrect(index) {
  props.setAttributes({correctAnswer: index})
}

We want to dynamically change The Icon’s icon attribute to be “star-filled” instead of “star-empty.”

icon={props.attributes.correctAnswer == index ? "star-filled" : "star-empty"}

Note that in the code above we cannot have “if statement” as statements are not allowed here and we can only have expressions, so we use a ternary operator.

Next we work on handling situation where we delete the item that is currently marked as correct answer so we change the correct answer back to undefined. In the deleteAnswer function add a new line.

if (indexToDelete == props.attributes.correctAnswer) {
  props.setAttributes({correctAnswer: undefined})
}

No answer marked we should disable the post “update” button using built-in WP function. Not as simple since there may be multiple instances of a block. We need to solve from a WP block editor as a whole perspective. WP gives us all the tools to do this.

Test in browser console the following code

wp.data.select("/core/block-editor").getBlocks()

This is awesome and very powerful.

Set up code that anytime a block changes none of the blocks have correct answer of undefined.

Write a function with a unique name up at the top of the file.

function ourStartFunction() {
  wp.data.subscribe(function() {
    console.log("hello")
  })
}

ourStartFunction()

By using subscribe we know we are always working with the latest version of the data.

function ourStartFunction() {
  wp.data.subscribe(function() {
    const results = wp.data.select("core/block-editor").getBlocks().filter(function(block) {
       return block.name == "ourplugin/are-you-paying-attention" && block.attributes.correctAnswer == undefined
    })
  })
}

ourStartFunction()

New array returned will be empty unless the logic is valid. So just inside the “ourStartFunction” create a variable to denote if the condition is met so we can change as we go.

let locked = false

And now just add the following lines at the current end of the same function:

if (results.length && locked == false) {
  locked = true
  wp.data.dispatch("core/editor").lockPostSaving("noanswer")
}


if (results.length && locked) {
  locked = false
  wp.data.dispatch("core/editor").unlockPostSaving("noanswer")
}

How to avoid needing to have unique function names?

Get rid of name and at the start of line add opening parentheses and at the end of the function a closing parentheses, then another opening and closing parentheses immediately after. This is called an IIFE, immediately invoked function expression.

(function() {
  // logic goes here
})()

Now we can have scoped variables, meaning limited to live only within our function and won’t mess up a globally scoped WP variable.

Next we make sure the answers attribute has just and empty string inside of an array as the default value.

How to Use React on the Front-End of WordPress

Clicking the correct answer shows a green or red overlay on the block.

Create a new “src/frontend.js” file

In package.json we need to tweak the “build” and “start” scripts

wp-scripts build src/index.js src/frontend.js


wp-scripts start src/index.js src/frontend.js

Let’s set up some CSS but we download from the resources in this lesson and move into the src folder, frontend.scss and import this into the js file. You should now see a frontend.css file created in you build folder.

Now write the necessary php to make sure it’s loaded. ONly want it tobe loaded if the current page needs it. This strategy may seem weird but it works, find the function “theHTML” and just above the “ob_start” add the following line:

wp_enqueue_script('attentionFrontend', plugin_dir_url(_FILE__) . 'build/frontend.js', array('wp-element'))

wp_enqueue_style(attentionFrontendStyles, plugin_dir_url(_FILE__) . 'build/frontend.css')

But this loads on front-end and back-end so wrap the code above in an if statement

if (!is_admin()) {
 // paste code here
}

Now if you are on a blog or archive page with lots of posts, WP is smart enough to not load the file again, will only load once. There are multiple ways of accomplishing the loading of these front-end files and this is just one of them.

Just under the call of “ob_start,” delete the “h3” rendering dummy data and replace with:

<div class="paying-attention-update-me"><div>

And in frontend.js store a variable

const divsToUpdate = document.querySelectorAll(".paying-attention-update-me")

divsToUpdate.forEach(function(div) {
  div.innerHTML = "hello"
})

Now we just want to use a React component in place of the div.innerHTML = “hello” testing approach.

We use react by creating a component function below:

function Quiz () {
  return (
    <div className="paying-attention-frontend">
    Hello From React
    </div>
  )
}

But we need to import a few things first besides just the CSS file:

import React from 'react'
import ReactDOM from 'react-dom'

Note that we don’t need to npm install the packages above as it is done with @wordpress/scripts and the dependency of “wp-element” that is aded when enqueueing the frontend files. “wp-element” is WP’s version of React. They have abstracted React into their own script. Effortless and seamless to use React across multiple plugins without making users download unnecessary code, we always point to the one copy of React WP downloads for us.

And now we leverage ReactDOM

divsToUpdate.forEach(function(div) {
  ReactDOM.render(<Quiz />, div)
  div.classList.remove("paying-attention-update-me")
})

Note we also remove the “update-me” placeholder class. This is not required but is helpful if in the future if you want to adapt the code so it runs on the fly, perhaps lazy-loading posts. We can keep track of which elements have already been hydrated with JS.

Passing Block Data From PHP into JavaScript/React

Content stored in inside of comments in DB. How do we gets this data, these attributes, into JS?

In the index.php file, in the “theHTML” function.

Not an official WP way to handle this? No guidance from them. So we need to get creative, and there is probably 10 different ways to do this. Here’s Brad’s way: Wrap in a “pre” tag, ” echo wp_json_encode($attributes) .”

<pre style="display: none;"><?php echo wp_json_encode($attributes) ?></pre>

Comment out the React.DOM.render line, without the “pre” tags, temporarily to see what PHP gives us from the DB and you will se it is in a format that slightly will confuse the JSON interpreter so that’s why we use the “pre” tags. Now there should be no problem parsing as data.

We also add an inline display of none so it is hidden but is still in DOM so the JS can still access it. Back in the “frontend.js” file we can parse the data:

const data = JSON.parse(div.querySelector("pre").innerHTML)

Now we can remove the commenting out of the ReactDOM.render line and tweak to be:

ReactDOM.render(<Quiz question={data.question}/>, div)

Now in the Quiz component definition we can pass props as a parameter:

function Quiz(props) {
  return (
  <div className="paying-attention-frontend">
    {props.question}
  </div>
  )
}

We can now test the front-end and we will see the correct text. But this isn’t the most efficient, manually declaring a prop for each attribute. So we remove question={data.question} or data={data} for just {..data}. This is called the spread operator.

// spread example

let a, b

let other = {c: "hello", d: "World"}

let newComnbined = {a, b, ...other}

Now we have what we need and can reference like:

<p>{props.question}</p>
<ul>
  {props.answers.map(function(answer) {
    return <li>{answer}</li>
  })}
</ul>

Tweak bg color temporarily

Letting Users Click On (Guess) An Answer

Add an “onClick” prop to the list-item in the map function in the previous lesson.

return <li onClick={() => {
  handleAnswer(index)
}}>

And create that function just inside the Quiz function component

function handleAnswer(index) {
  if (index == props.correctAnswer) {
    alert("Congrats")
  } else {
    alert("Sorry")
  }
}

Now down in the JSX just under the UL:

<div className="correct-message correct-message--is-visible">
  <p>That is correct!</p>
</div>

But we need to conditionally add the “is-visible” class.

Also want to add smiley face using the bootstrap icons SVG code from “icons.getboostrap.com/icons/” and copying the code in the “Copy HTML” block and paste right above the paragraph tag and remove the fill and change the width and height to 24 and change class to className.

Do the same for an incorrect answer, text and icon.

CSS animations is handling the timing and fading in and out.

To conditionally add the CSS modifier class, we can’t just grab from the DOM and add the class. In React we keep track of some state data for the question being “isCorrrect”

import {useState} from 'react'

const [isCorrect, setIsCorrect] = useState(undefined)

Now we can update the state instead of alerting

function handleAnswer(index) {
  if (index == props.correctAnswer) {
    setIsCorrect(true)
  } else {
    setIsCorrect(false)
  }
}

And we can dynamically add the className with this code:

<div classname={"correct-message" + (isCorrect == true ? " correct-message--visible" : "")}>

Note the parenthesis around the conditional, and the space to start the modifier class string.

<div classname={"incorrect-message" + (isCorrect === false ? " incorrect-message--visible" : "")}>

Note the triple equals sign on the false because he doesn’t want undefined to be interpreted as false.

At this point the animation works on the first attempt only because nothing clears the state back to undefined.

Attention To Detail

When a user gets the question wrong wait 2600ms until the animation finishes and then reset the state to undefined with the help of “useEffect”

import React, {useState, useEffect} from 'react'

useEffect(() => {
  if (isCorrect === false ) {
    setTimeout(() => {
      setIsCorrect(undefined)
    }, 2600)
  }
}, [isCorrect])

When the user gets the question correct: disable clickability on all answers:

return <li onClick={ isCorrect ==true ? undefined : () => handleAnswer(index)}>{answer}</li>

Add styling to the buttons when correct. Finish button styling before the animation finishes so add to the existing useEffect and create a nw piece of state:

if (isCorrect === true) {
  setTimeout(() => {
    setIsCorrectDelayed(true)
  }, 1000)
}

The new state:

const [isCorrectDelayed, setIsCorrectDelayed] = useState(undefined)

Add the icon to the list item:

return (
  <li onClick={ isCorrect ==true ? undefined : () => handleAnswer(index)}>
    { isCorrectDelayed === true && index == props.correctAnswer && (
      // paste svg code here
    )}
    {answer}
  </li>
)

Note the variation on the ternary operator.

Do the same for the red x by duplicating the code above and tweaking:

return (
  <li onClick={ isCorrect ==true ? undefined : () => handleAnswer(index)}>
    { isCorrectDelayed === true && index == props.correctAnswer && (
      // paste svg code for green checkmark here
    )}
    { isCorrectDelayed === true && index !== props.correctAnswer && (
      // paste svg code for red x here
    )}
    {answer}
  </li>
)

Give each list item a “className” so we can gray-out the incorrect answers:

return (
  <li className={(isCorrectDelayed === true && index == props.correctAnswer ? "no-click" : "") + (isCorrectDelayed === true && index != props.correctAnswer ? "fade-incorrect" : "")} onClick={ isCorrect ==true ? undefined : () => handleAnswer(index)}>
    { isCorrectDelayed === true && index == props.correctAnswer && (
      // paste svg code for green checkmark here
    )}
    { isCorrectDelayed === true && index !== props.correctAnswer && (
      // paste svg code for red x here
    )}
    {answer}
  </li>
)

Note: Inside of the className there are multiple ternary operators by putting each in parenthesis and separating with a plus sign.

Note about Animations / Transitions in React

In the following lesson you may have noticed that instead of dynamically adding / removing content from JSX / DOM I’m just letting all of the content be present the entire time and using CSS classes to make it visible or invisible.

I did this for the sake of simplicity, and because this is a course about WordPress and not React. If you are interested in learning more about React, and would want to write conditional JSX to actually add/remove content from the DOM at the appropriate time (without it appearing or disappearing abruptly in 1 millisecond without an animation/transition) I recommend checking out the popular React community package called React Transition Group.

Let Admin Choose Background Color of Block

Set up color picker in admin settings panel.

In the “index.js” file import

import {InspectorControls} from "@wordpress/block-editor"

And on the line above import PanelBody and PanelRow and ColorPicker

In the JSX just inside the “edit-block” div:

<InspectorControls>
  <PanelBody title="Background Color" initialOpen={true}>
    <PanelRow>
      <ColorPicker color={props.attributes.bgColor} onChange Complete={(x) => props.setAttributes({bgColor: x.hex})}/>
    </PanelRow>
  </PanelBody>
</InspectorControls>

Pleasantly surprising this is all it takes to create a right-hand side area.

Need an attribute to save the values. Name it anything:

bgColor: {type: "string", default: "#ebebeb"}

Now if we change the default value using the color picker and refresh it will save that new value.

At this point it is still not changing the editor block color. To do this add an inline styles to the main “paying-attention-edit-block” div.

style={{backgroundColor: props.attributes.bgColor}}

Awkward scrollbar for color picker and the default picker in general. We can use one from a 3rd party? It’s apparently pretty easy to use a 3rd party react package in WordPress?

casesandberg.github.io/react-color/

Use this package within the block type. In VSCode stop the current task and run this command:

npm install react-color

Now import this in.

import {ChromePicker} from 'react-color'

And then in the JSX update ColorPicker to ChromePicker and add another prop called “disableAlpha={true}”

To update the color chosen on the block in the frontend add the same inline style just tweaked a bit in the frontend.js file and add to the opening div in the JSX after the className=”are-you-paying-attention-frontend.”

style={{backgroundColor: props.bgColor}}

Block Text Alignment & Block Preview

Let owner choose text alignment

Menu once click onto the block.

index.js

import from wordpress lock editor BlockOCNtrols and AlihnmentToolbar

Add an attribute to store in db for if they want left center or right

default: "left"

Add in index.js

<BlockControls>

</BlockControls>

Add a value and an onChange

This works for backend. To add to the front-end go to frontedn.js and add another inline style set to the attribute value

Look through WP document for block controls.

Want to add a preview to our block type. In the “index.js” file, where we add an example as top level property of wp.blocks.registerBlockType

example: {
  attributes: {
    //many settings here...
  }
}

Interesting this is not an image but an actual rendering. pretty cool.