Gutenberg custom block, function/attributes not defined - javascript

I receive the console error
ReferenceError: selectImage is not defined
at edit (index.js?fb2e:95)
I thought selectImage was defined in the following Gutenberg block:
/**
* Block dependencies
*/
import icon from './icon';
import './style.scss';
/**
* Internal block libraries
*/
const { __ } = wp.i18n;
const { registerBlockType } = wp.blocks;
const {
RichText,
MediaUpload,
BlockControls,
BlockAlignmentToolbar,
} = wp.editor
/**
* Register block
*/
export default registerBlockType(
'jsforwpblocks/heroblock',
{
title: __( 'Hero Block', 'jsforwpblocks' ),
description: __( 'Large block with hero image, text and buttons', 'jsforwpblocks' ),
category: 'common',
icon: {
background: 'rgba(254, 243, 224, 0.52)',
src: icon,
},
keywords: [
__( 'Banner', 'jsforwpblocks' ),
__( 'Call to Action', 'jsforwpblocks' ),
__( 'Message', 'jsforwpblocks' ),
],
attributes: {
message: {
type: 'array',
source: 'children',
selector: '.message-body',
},
blockAlignment: {
type: 'string',
default: 'wide',
},
imgUrl: {
type: 'string',
default: 'http://placehold.it/500'
}
},
getEditWrapperProps( { blockAlignment } ) {
if ( 'left' === blockAlignment || 'right' === blockAlignment || 'full' === blockAlignment ) {
return { 'data-align': blockAlignment };
}
},
selectImage(value) {
console.log(value);
setAttributes({
imgUrl: value.sizes.full.url,
})
},
edit: props => {
const { attributes: { message, blockAlignment }, className, setAttributes } = props;
const onChangeMessage = message => { setAttributes( { message } ) };
return (
<div className={ className }>
<BlockControls>
<BlockAlignmentToolbar
value={ blockAlignment }
onChange={ blockAlignment => setAttributes( { blockAlignment } ) }
/>
</BlockControls>
<RichText
tagName="div"
multiline="p"
placeholder={ __( 'Add your custom message', 'jsforwpblocks' ) }
onChange={ onChangeMessage }
value={ message }
/>
<div className="media">
<MediaUpload
onSelect={selectImage}
render={ ({open}) => {
return <img
src={attributes.imgUrl}
onClick={open}
/>;
}}
/>
</div>
</div>
);
},
save: props => {
const { attributes: { message, blockAlignment, imgUrl } } = props;
return (
<div
className={classnames(
`align${blockAlignment}`
)}
style={backgroundImage={imgUrl}}
>
<h2>{ __( 'Call to Action', 'jsforwpblocks' ) }</h2>
<div class="message-body">
{ message }
</div>
</div>
);
},
},
);
EDIT
If I move the function down into the edit function, the error disappears:
edit: props => {
const { attributes: { message, blockAlignment }, className, setAttributes } = props;
const onChangeMessage = message => { setAttributes( { message } ) };
function selectImage(value) {
console.log(value);
setAttributes({
imgUrl: value.sizes.full.url,
})
}
return (
<div className={ className }>
However, I receive a new error:
ReferenceError: attributes is not defined
at Object.render (index.js:101)
Line 101 is the last line of:
save: props => {
const { attributes: { message, blockAlignment, imgUrl } } = props;
return (
<div
className={classnames(
The updated code is here (pastebin.com).
Help appreciated.

I think you have to add className in save to your props object destructering like you did in edit:
const { attributes: { message, blockAlignment, imgUrl }, className } = props;
Two more things:
A few lines down you're using class, I'd change this to className as well
If I use classnames in a custom block I always add it to my imports:
import classnames from 'lodash/classnames'
Haven't actually tried if it would work without importing it.

I just quickly checked your block (the original version from pastebin - without my former edits) in my setup and I had the same error. But the error doesn't refer to save but to edit. What helped was adding imgUrl to your attributes destructering in edit (same as in save):
const { attributes: { message, blockAlignment, imgUrl }, className, setAttributes } = props;
and then only use imgUrl in your MediaUpload return src like that:
return <img src={imgUrl} onClick={open} />;

As an addition to your second file on Pastebin try the following replacement for your save function:
save: props => {
const { attributes: { message, blockAlignment, imgUrl } } = props;
const divStyle = {
backgroundImage: 'url(' + imgUrl + ')',
};
return (
<div
className={classnames(
`align${blockAlignment}`
)}
style={divStyle}
>
<h2>{ __( 'Call to Action', 'jsforwpblocks' ) }</h2>
<div className="message-body">
{ message }
</div>
</div>
);
},
And the classnames import actually only worked like that (but might depend on how you set your dependencies):
import classnames from 'classnames'

Related

Child component onClick parameter doesn't work in parent component

I set button onClick as a parameter in the child component and drag and use onClick in the parent component.
The child component Room .
type Props = {
items?: [];
onClick?: any;
}
const Room = ({ onClick, items: [] }: Props) => {
return (
<div>
{items.length ? (
<>
{items.map((item: any, index: number) => {
return (
<>
<button key={index} onClick={() => { console.log('hi'); onClick }}>{item.name}</button>
</>
)
}
</>
)
</div>
)
}
This is the parent component.
const LoadingRoom = () => {
const handleWaitingRoomMove = (e: any) => {
e.preventDefault();
console.log('Hello Move')
}
return (
<>
<Room
items={[
{
name: "Waiting Room",
onClick: {handleWaitingRoomMove}
},
{
name: "Host Room",
},
]}
>
</Room>
</>
)
}
I want to call parent component's onClick handleWaitingRoomMove but it's not getting called.
However, console.log('hi') on the button is called normally whenever the button is clicked. Only button is not called. Do you know why?
onlick is a attribute to the child element. so move it outside of the items array
<Room
onClick={handleWaitingRoomMove}
items={[
{
name: "Waiting Room",
},
{
name: "Host Room",
},
]}
>
In the child, missing () for onClick
onClick={(ev) => { console.log('hi'); onClick(ev) }}
Demo
You are passing onClick in items, not direct props
<button key={index} onClick={item.onClick}>{item.name}</button>
so your component will be
type Props = {
items?: [];
}
const Room = ({ items: [] }: Props) => {
return (
<div>
{items.length ? (
<>
{items.map((item: any, index: number) => {
return (
<>
<button key={index} onClick={item.onClick}>{item.name}</button>
</>
)
}
</>
)
</div>
)
}
It would probably be more advantageous to have one handler that does the work, and use that to identify each room by type (using a data attribute to identify the rooms). That way you keep your data and your component logic separate from each other. If you need to add in other functions at a later stage you can.
const { useState } = React;
function Example({ data }) {
// Handle the room type by checking the value
// of the `type` attribute
function handleClick(e) {
const { type } = e.target.dataset;
switch (type) {
case 'waiting': console.log('Waiting room'); break;
case 'host': console.log('Host Room'); break;
case 'guest': console.log('Guest Room'); break;
default: console.log('Unknown room'); break;
}
}
// Keep the room data and the handler separate
return (
<div>
{data.map(obj => {
return (
<Room
key={obj.id}
data={obj}
handleClick={handleClick}
/>
);
})}
</div>
);
}
// Apply a data attribute to the element
// you're clicking on, and just call the handler
// `onClick`
function Room({ data, handleClick }) {
const { type, name } = data;
return (
<button
data-type={type}
onClick={handleClick}
>{name}
</button>
);
}
const data = [
{ id: 1, type: 'waiting', name: 'Waiting Room' },
{ id: 2, type: 'host', name: 'Host Room' },
{ id: 3, type: 'guest', name: 'Guest Room' }
];
ReactDOM.render(
<Example data={data} />,
document.getElementById('react')
);
button:not(:last-child) { margin-right: 0.25em; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>

Can't get pass the -> TypeError: Cannot read property 'id' of undefined at production build

Long story short, I have a table where I've build a searching functionality using the following code to search in the object arrays and update the state using hooks.
const SearchView = (e) => {
e.preventDefault();
console.log(e.target.value)
if (!e.target.value) {
setSearchDataSuggestions([])
setSearchField(null)
} else {
setSearchField(true)
props.setSeachingView(true)
for (const row of Object.values(rows)) {
Object.keys(row).forEach(function (item, index) {
if (typeof(row[item]) === 'string' && row[item].toLowerCase().includes(e.target.value.toLowerCase().trim())) {
setSearchDataSuggestions([...search_data_suggestions, (rows[index]) ])
}
});
}
}
}
The code works as fine in the npm start but wont work at production build ... Whenever I try to type something into the text field (where the SearchView() is called onChange) it crashes on a blank page along the following console error:
Console Error
I've no idea where this error is telling me ... Since there's no .id being passed whatsoever in my code. By any chance, I'm posting the entire code here anyways.
Can someone please help me identify where is the bug trigger and why it works fine in a dev server and not in prod !??!
Full code goes below:
import React, { useState, useEffect } from 'react';
import DataTable from 'react-data-table-component';
import moment from 'moment'
import TextField from '#material-ui/core/TextField';
import InputAdornment from '#material-ui/core/InputAdornment';
const LookUpTable = (props) => {
const [error, setError] = useState(null);
const [isLoaded, setIsLoaded] = useState(false);
const [rows, setItems] = useState([]);
const [search_data_suggestions, setSearchDataSuggestions] = useState([]);
const [accessToken] = useState(props.accessToken)
const [isSearching] = useState(props.isSearching);
const [search_field, setSearchField] = useState(null);
const success_validation = row_success => {
if (row_success) {
return "True";
} else {
return "False";
}
};
const columns = [
{
name: 'Timestamp',
selector: 'timestamp',
sortable: true,
format: row => moment(row.timestamp).format('lll'),
},
{
name: 'Project ID',
selector: 'project_id',
sortable: true,
},
{
name: 'Client Name',
selector: 'client_name',
sortable: true,
right: true,
},
{
name: 'Client Email',
selector: 'client_email',
sortable: true,
right: true,
},
{
name: 'Client Mobile',
selector: 'client_mobile',
sortable: true,
},
{
name: 'Sent By',
selector: 'sent_by',
sortable: true,
},
{
name: 'Success',
selector: 'success_validation',
sortable: true,
right: true,
format: row => success_validation(row.success),
},
];
const ExpanableComponent = ({ data }) => {
return(
<div>
<p></p>
<ul>
<li><strong>Report Filename: </strong>{data.file_name} </li>
<li><strong>Project Description: </strong>{data.project_desc} </li>
<li><strong>Project Owner's Name: </strong>{data.project_owner_name} </li>
<li><strong>Project Owner's Email: </strong>{data.project_owner_email} </li>
</ul>
</div>
)
}
const SearchView = (e) => {
e.preventDefault();
console.log(e.target.value)
// setSearchValue(e.target.value.toLowerCase())
// const values = (Object.values(rows));
if (!e.target.value) {
setSearchDataSuggestions([])
setSearchField(null)
} else {
setSearchField(true)
props.setSeachingView(true)
for (const row of Object.values(rows)) {
Object.keys(row).forEach(function (item, index) {
// console.log('[' + index + '] ' + 'keys: ' + item + ' values : ' + row[item]);
if (typeof(row[item]) === 'string' && row[item].toLowerCase().includes(e.target.value.toLowerCase().trim())) {
setSearchDataSuggestions([...search_data_suggestions, (rows[index])])
// console.log('Got search as: ' + e.target.value + ' and updated with the row: ' + (rows[index]) )
}
});
}
}
}
const CustonSearchViewRender = () => {
if (search_field) {
return (
<DataTable
columns={columns}
data={search_data_suggestions}
highlightOnHover={true}
pagination={true}
fixedHeader={true}
expandableRows={true}
expandableRowsComponent={<ExpanableComponent />}
striped={true}
/>
)
}
}
// Note: the empty deps array [] means
// this useEffect will run once
// similar to componentDidMount()
useEffect(() => {
fetch("/lookup/", {
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`,
}
}
)
.then(res => res.json())
.then(
(result) => {
setIsLoaded(true);
setItems(result);
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
(error) => {
setIsLoaded(true);
setError(error);
}
)
}, [accessToken])
if (error) {
return <div>Error: {error.message}</div>;
} else if (!isLoaded) {
return <div>Loading...</div>;
} else {
return (
<div className='lookup-table container-fluid'>
<div className="search-bar container">
<br></br>
<h4 className="search-bar-title">Past 90 days report table lookup ...</h4>
<TextField
label="Search me"
onChange={e => SearchView(e)}
variant="outlined"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<i className="fas fa-search"></i>
</InputAdornment>
)
}}
/>
</div>
{isSearching && !search_field?
<DataTable
columns={columns}
data={rows}
highlightOnHover={true}
pagination={true}
fixedHeader={true}
expandableRows={true}
expandableRowsComponent={<ExpanableComponent />}
striped={true}
/> : null}
{search_field ? <CustonSearchViewRender/> : null}
</div>
);
}
}
export default LookUpTable;
Thank you so much.

Gutenberg repeater blocks using ESNEXT

I've been working on creating my own custom Gutenberg repeater block with a text & link input field. I've only seen ES5 samples like this and this. I've been working on creating my own version of those samples for close to 8 hours now and I'm stuck.
I'm here because I want to be pointed in the right direction (help is greatly needed).
Here is the code I currently have. I don't know where to start with the ES5 -> ESNEXT conversion.
Edit: I forgot to say that I'm trying to avoid using ACF for this
// Importing code libraries for this block
import { __ } from '#wordpress/i18n';
import { registerBlockType } from '#wordpress/blocks';
import { RichText, MediaUpload, InspectorControls } from '#wordpress/block-editor';
import { Button, ColorPicker, ColorPalette, Panel, PanelBody, PanelRow } from '#wordpress/components';
registerBlockType('ccm-block/banner-block', {
title: __('Banner Block'),
icon: 'format-image', // from Dashicons → https://developer.wordpress.org/resource/dashicons/.
category: 'layout', // E.g. common, formatting, layout widgets, embed.
keywords: [
__('Banner Block'),
__('CCM Blocks'),
],
attributes: {
mediaID: {
type: 'number'
},
mediaURL: {
type: 'string'
},
title: {
type: 'array',
source: 'children',
selector: 'h1'
},
content: {
type: 'array',
source: 'children',
selector: 'p'
},
bannerButtons: {
type: 'array',
source: 'children',
selector: '.banner-buttons',
},
items: {
type: 'array',
default: []
}
},
/**
* The edit function relates to the structure of the block when viewed in the editor.
*
* #link https://wordpress.org/gutenberg/handbook/block-api/block-edit-save/
*
* #param {Object} props Props.
* #returns {Mixed} JSX Component.
*/
edit: (props) => {
const {
attributes: { mediaID, mediaURL, title, content, bannerButtons },
setAttributes, className
} = props;
const onSelectImage = (media) => {
setAttributes({
mediaURL: media.url,
mediaID: media.id,
});
};
const onChangeTitle = (value) => {
setAttributes({ title: value });
};
const onChangeContent = (value) => {
setAttributes({ content: value });
};
const onChangeBannerButtons = (value) => {
setAttributes({ bannerButtons: value });
};
// console.log(items);
// var itemList = items.sort
return (
<div className={className}>
<div id="#home-banner">
<MediaUpload
onSelect={onSelectImage}
allowedTypes="image"
value={mediaID}
render={({ open }) => (
<Button className={mediaID ? 'image-button' : 'button button-large'} onClick={open}>
{!mediaID ? __('Upload Image', 'ccm-blocks') : <img src={mediaURL} alt={__('Featured Image', 'ccm-blocks')} />}
</Button>
)}
/>
<RichText
tagName="h1"
placeholder={__('Insert Title Here', 'ccm-blocks')}
className={className}
onChange={onChangeTitle}
value={title}
/>
<RichText
tagName="p"
placeholder={__('Insert your short description here...', 'ccm-blocks')}
className={className}
onChange={onChangeContent}
value={content}
/>
<RichText
tagName="ul"
multiline="li"
className="banner-buttons"
placeholder={ __('Add a banner button link (max of 2)', 'ccm-blocks') }
onChange={ onChangeBannerButtons }
value={ bannerButtons }
/>
</div>
</div>
);
},
/**
* The save function determines how the different attributes should be combined into the final markup.
* Which is then serialised into the post_content.
*
* #link https://wordpress.org/gutenberg/handbook/block-api/block-edit-save/
*
* #param {Object} props Props.
* #returns {Mixed} JSX Frontend HTML.
*/
save: (props) => {
return (
<div className={ props.className }>
<div id="home-banner" style={{backgroundImage: `url(${ props.attributes.mediaURL })`}}>
<div class="container">
<div class="row">
<div class="col-12">
<div class="content-inner">
<RichText.Content tagName="h1" className={ props.className } value={ props.attributes.title } />
<RichText.Content tagName="p" className={ props.className } value={ props.attributes.content } />
<RichText.Content tagName="ul" className="banner-buttons" value={ props.attributes.bannerButtons } />
</div>
</div>
</div>
</div>
</div>
</div>
);
},
});
Edit 2: Here's my failed take on it
// Importing code libraries for this block
import { __ } from '#wordpress/i18n';
import { registerBlockType } from '#wordpress/blocks';
import { RichText, MediaUpload, InspectorControls } from '#wordpress/block-editor';
import { Button, ColorPicker, ColorPalette, Panel, PanelBody, PanelRow } from '#wordpress/components';
/**
* Register the Block
*
* #link https://wordpress.org/gutenberg/handbook/block-api/
* #param {string} name name.
* #param {Object} settings settings.
* #return {?WPBlock} The block, otherwise `undefined`.
*/
registerBlockType('ccm-block/banner-block', {
title: __('Banner Block'),
icon: 'format-image', // from Dashicons → https://developer.wordpress.org/resource/dashicons/.
category: 'layout', // E.g. common, formatting, layout widgets, embed.
keywords: [
__('Banner Block'),
__('CCM Blocks'),
],
attributes: {
mediaID: {
type: 'number'
},
mediaURL: {
type: 'string'
},
title: {
type: 'array',
source: 'children',
selector: 'h1'
},
content: {
type: 'array',
source: 'children',
selector: 'p'
},
bannerButtons: {
type: 'array',
source: 'children',
selector: '.banner-buttons',
},
items: {
source: 'query',
default: [],
selector: '.item',
query: {
title: {
type: 'string',
source: 'text',
selector: '.title'
},
index: {
type: 'number',
source: 'attribute',
attribute: 'data-index'
}
}
}
},
/**
* The edit function relates to the structure of the block when viewed in the editor.
*
* #link https://wordpress.org/gutenberg/handbook/block-api/block-edit-save/
*
* #param {Object} props Props.
* #returns {Mixed} JSX Component.
*/
edit: (props) => {
const {
attributes: { mediaID, mediaURL, title, content, bannerButtons, items },
setAttributes, className
} = props;
const onSelectImage = (media) => {
setAttributes({
mediaURL: media.url,
mediaID: media.id,
});
};
const onChangeTitle = (value) => {
setAttributes({ title: value });
};
const onChangeContent = (value) => {
setAttributes({ content: value });
};
const onChangeBannerButtons = (value) => {
setAttributes({ bannerButtons: value });
};
// Clone an array of objects
function _cloneArray(arr) {
if (Array.isArray(arr)) {
for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) {
arr2[i] = arr[i];
}
return arr2;
} else {
return Array.from(arr);
}
}
// return repeater items
var itemList = items.sort(function(a, b){
return a.index - b.index;
}).map(function(item){
console.log(item);
return(
<RichText
tagName="h1"
placeholder={ __('Test', 'ccm-blocks') }
value={ item.title }
onChange={ function(value){
var newObject = Object.assign({}, item, {
title: value
});
setAttributes({
items: [].concat(_cloneArray(items.filter(function(itemFilter){
return itemFilter.index != item.index;
})), [newObject])
});
} }
/>
);
});
//
console.log(itemList);
return (
<div className={className}>
<div id="#home-banner">
<RichText
className="item-list"
tagName="h1"
value={ itemList }
/>
<Button
className="button add-row"
onClick={ function(){
setAttributes({
items: [].concat(_cloneArray(items), [{
index: items.length,
title: ""
}])
});
} }
>
Add a button
</Button>
<MediaUpload
onSelect={onSelectImage}
allowedTypes="image"
value={mediaID}
render={({ open }) => (
<Button className={mediaID ? 'image-button' : 'button button-large'} onClick={open}>
{!mediaID ? __('Upload Image', 'ccm-blocks') : <img src={mediaURL} alt={__('Featured Image', 'ccm-blocks')} />}
</Button>
)}
/>
<RichText
tagName="h1"
placeholder={__('Insert Title Here', 'ccm-blocks')}
className={className}
onChange={onChangeTitle}
value={title}
/>
<RichText
tagName="p"
placeholder={__('Insert your short description here...', 'ccm-blocks')}
className={className}
onChange={onChangeContent}
value={content}
/>
<RichText
tagName="ul"
multiline="li"
className="banner-buttons"
placeholder={ __('Add a banner button link (max of 2)', 'ccm-blocks') }
onChange={ onChangeBannerButtons }
value={ bannerButtons }
/>
</div>
</div>
);
},
/**
* The save function determines how the different attributes should be combined into the final markup.
* Which is then serialised into the post_content.
*
* #link https://wordpress.org/gutenberg/handbook/block-api/block-edit-save/
*
* #param {Object} props Props.
* #returns {Mixed} JSX Frontend HTML.
*/
save: (props) => {
return (
<div className={ props.className }>
<div id="home-banner" style={{backgroundImage: `url(${ props.attributes.mediaURL })`}}>
<div class="container">
<div class="row">
<div class="col-12">
<div class="content-inner">
<RichText.Content tagName="h1" className={ props.className } value={ props.attributes.title } />
<RichText.Content tagName="p" className={ props.className } value={ props.attributes.content } />
<RichText.Content tagName="ul" className="banner-buttons" value={ props.attributes.bannerButtons } />
</div>
</div>
</div>
</div>
</div>
</div>
);
},
});
I've figured it out!! After countless hours of tinkering with it here's what I came up with. It's a rough version of what I want but it definitely works! Here's a link for one of the tutorials I found link.
// Importing code libraries for this block
import { __ } from '#wordpress/i18n';
import { registerBlockType } from '#wordpress/blocks';
import { RichText } from '#wordpress/block-editor';
import { Button } from '#wordpress/components';
// Register the block
registerBlockType( 'test-block/custom-repeater-block', {
title: __('Repeater Block'),
icon: 'layout',
category: 'layout',
keywords: [
__('Custom Block'),
],
attributes: {
info: {
type: 'array',
selector: '.info-wrap'
}
},
// edit function
edit: (props) => {
const {
attributes: { info = [] },
setAttributes, className
} = props;
const infoList = (value) => {
return(
value.sort((a, b) => a.index - b.index).map(infoItem => {
return(
<div className="info-item">
<Button
className="remove-item"
onClick={ () => {
const newInfo = info.filter(item => item.index != infoItem.index).map(i => {
if(i.index > infoItem.index){
i.index -= 1;
}
return i;
} );
setAttributes({ info: newInfo });
} }
>×</Button>
<h3>Number {infoItem.index}</h3>
<RichText
tagName="h4"
className="info-item-title"
placeholder="Enter the title here"
value={infoItem.title}
style={{ height: 58 }}
onChange={ title => {
const newObject = Object.assign({}, infoItem, {
title: title
});
setAttributes({
info: [...info.filter(
item => item.index != infoItem.index
), newObject]
});
} }
/>
<RichText
tagName="p"
className="info-item-description"
placeholder="Enter description"
value={infoItem.description}
style={{ height: 58 }}
onChange={ description => {
const newObject = Object.assign({}, infoItem, {
description: description
});
setAttributes({
info: [...info.filter(
item => item.index != infoItem.index
), newObject]
});
} }
/>
</div>
)
})
)
}
return(
<div className={className}>
<div className="info-wrap">{infoList(info)}</div>
<Button onClick={title => {
setAttributes({
info: [...info, {
index: info.length,
title: "",
description: ""
}]
});
}}>Add Item</Button>
</div>
);
},
// save function
save: (props) => {
const info = props.attributes.info;
const displayInfoList = (value) => {
return(
value.map( infoItem => {
return(
<div className="info-item">
<RichText.Content
tagName="h4"
className="info-item-title"
value={infoItem.title}
style={{ height: 58 }}
/>
</div>
)
} )
)
}
return(
<div className={props.className}>
<div className="info-wrap">{ displayInfoList(info) }</div>
</div>
);
}
} )
I just created an example with the query attribute, you can find it here: github repo
I believe that query is the right way to go when working on more complex blocks.
So first attribute part:
attributes: {
services: {
type: "array",
source: "query",
default: [],
selector: "section .card-block",
query: {
index: {
type: "number",
source: "attribute",
attribute: "data-index",
},
headline: {
type: "string",
selector: "h3",
source: "text",
},
description: {
type: "string",
selector: ".card-content",
source: "text",
},
},
},
},
Then edit.js part:
import produce from "immer";
import { __ } from "#wordpress/i18n";
import "./editor.scss";
import { RichText, PlainText } from "#wordpress/block-editor";
// import { useState } from "#wordpress/element";
export default function Edit({ attributes, setAttributes, className }) {
const services = attributes.services;
const onChangeContent = (content, index, type) => {
const newContent = produce(services, (draftState) => {
draftState.forEach((section) => {
if (section.index === index) {
section[type] = content;
}
});
});
setAttributes({ services: newContent });
};
return (
<>
{services.map((service) => (
<>
<RichText
tagName="h3"
className={className}
value={service.headline}
onChange={(content) =>
onChangeContent(content, service.index, "headline")
}
/>
<PlainText
className={className}
value={service.description}
onChange={(content) =>
onChangeContent(content, service.index, "description")
}
/>
</>
))}
<input
className="button button-secondary"
type="button"
value={__("Add Service", "am2-gutenberg")}
onClick={() =>
setAttributes({
services: [
...attributes.services,
{ headline: "", description: "", index: services.length },
],
})
}
/>
</>
);
}
and finally, saving part:
export default function save({ attributes, className }) {
const { services } = attributes;
return (
<section className={className}>
{services.length > 0 &&
services.map((service) => {
return (
<div className="card-block" data-index={service.index}>
<h3>{service.headline}</h3>
<div className="card-content">{service.description}</div>
</div>
);
})}
</section>
);
}

how to display group messages

Please help me, I'm new in react. I'm rendering values from nested object. Each object has title and message property. Titles can be same. I want display messages under title. If title same as in previos object do not display it , only once. But in my case it displays after each message.
my object:
arrayOfMessages=[
{
title: 'cars',
message: 'toyota'
},
{
title: 'cars',
message: 'ford'
},
{
title: 'cars',
message: 'bmw'
},
{
title: 'bikes',
message: 'suzuki'
},
{
title: 'bikes',
message: 'bmw'
},
]
expected output:
title
message
message
message
title2
message
message
in my case:
title
message
title
message
title
message
title2
message
title2
message
<div>
{arrayOfMessages.map((item, idx) => {
const {
message,
title
} = item
return (
<div key={idx} className="message-content">
<p>{title}</p>
<p>{message}</p>
</div>
)
})}
</div>
One approach would be to group items of arrayOfMessages by the title field, to achieve the required rendering result by using the native .reduce() method:
const arrayOfMessages=[
{
title: 'cars',
message: 'toyota'
},
{
title: 'cars',
message: 'ford'
},
{
title: 'cars',
message: 'bmw'
},
{
title: 'bikes',
message: 'suzuki'
},
{
title: 'bikes',
message: 'bmw'
},
];
/* Use reduce() to group items of arrayOfMessages by title */
const groupedMessages = arrayOfMessages.reduce((groups, item) => {
/* Find group for the title of current item */
const group = groups.find(group => group.title === item.title);
/* If matching group found, add message of item to it's messages array */
if(group) {
group.messages.push(item.message);
}
/* Otherwise, add a new group for this title */
else {
groups.push({ title : item.title, messages : [] });
}
return groups;
}, [])
console.log(groupedMessages);
Using the code above, you could then revise your render() method to render the title once for each item category:
<div>
{ groupedMessages.map((group, idx0) => (<div key={idx0} className="message-content">
<h2>{ group.title }</h2>
{ group.messages.map((message, idx1) => (<p key={idx1}>{message}</p>)) }
</div>))
}
</div>
Hope that helps!
Your object is not well structured, should be like this:
arrayOfMessages=[
{
title:"cars",
description:["toyota","ford","bmw"]
},
{
title:"bike",
description:["suzuki","bmw"]
}
]
then you can implement your code like this :
{
arrayOfMessages.map((message, idx) => {
const { description, title } = message
return (
<div key={idx} className="message-content">
<p>{title}</p>
{description.map((desc, index) => (
<p key={index}>desc</p>
))}
</div>
)
})
}
You should make up your data first.
...
let renderMessages = {}
arrayOfMessages.forEach(message => {
if (!renderMessages[message.title]) {
renderMessages[message.title] = {
title: message.title,
messages: [message.message]
}
} else {
renderMessages[message.title].messages.push(message.message)
}
})
return (
<div>
{Object.keys(renderMessages).map(key => {
let msg = renderMessages[key]
return <div key={key} className="message-content">
<p>{msg.title}</p>
{msg.messages.map(content => <p key={content}>{content}</p>)}
</div>
})}
</div>
)
You could group the messages based on the title using reduce. Then loop through the entries of the merged object to get the desired format
const arrayOfMessages=[{title:'cars',message:'toyota'},{title:'cars',message:'ford'},{title:'cars',message:'bmw'},{title:'bikes',message:'suzuki'},{title:'bikes',message:'bmw'},]
const grouped = arrayOfMessages.reduce((acc, { title, message }) => {
acc[title] = acc[title] || [];
acc[title].push(message);
return acc;
}, {})
console.log(grouped)
Here's a live demo:
class Sample extends React.Component {
render() {
const arrayOfMessages = [{ title: 'cars', message: 'toyota' }, { title: 'cars', message: 'ford' }, { title: 'cars', message: 'bmw' }, { title: 'bikes', message: 'suzuki' }, { title: 'bikes', message: 'bmw' },]
const grouped = arrayOfMessages.reduce((acc, { title, message }) => {
acc[title] = acc[title] || [];
acc[title].push(message);
return acc;
}, {})
return (
<div>
{
Object.entries(grouped).map(([title, messages], i1) => (
<div key={i1}>
<h1>{title}</h1>
{ messages.map((message, i2) => (<p key={i2}>{message}</p>)) }
</div>)
)
}
</div>
);
}
}
// Render it
ReactDOM.render(
<Sample />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="react"></div>
try this
render(){
//title watcher
let watcher = {};
return <div>
{arrayOfMessages.map((item, idx) => {
const {
message,
title
} = item
if(!watcher[title]){
//update watcher with title
//if watcher does not contain your title return title and message
watcher[title]=title;
return (
<div key={idx} className="message-content">
<p>{title}</p>
<p>{message}</p>
</div>
)
} else {
//if watcher found with title return your message
<div key={idx} className="message-content">
<p>{message}</p>
</div>
}
})}
</div>
}

React - Focus on an "input" element (rendered as an element of a list) in the DOM vanishes after keypress

I want the focus on an "input" element to remain after keypress
The Code:
import React, { Component } from 'react'
class App extends Component {
render() {
return (
<NewContactBox/>
)
}
}
class NewContactBox extends Component {
constructor(props) {
super(props)
this.state = {
name: '',
email: '',
phone: '',
}
this.fieldRefs = {
name: React.createRef(),
email: React.createRef(),
phone: React.createRef(),
}
}
hc = (ev, type, ref) => {
this.setState({
[type]: ev.target.value
})
// console.log(ref)
ref.focus()
}
render() {
const ContactInput = ({fields}) => (
fields.map((f) => (
<input
key={f.id}
className={`p-1 my-3 w-100 mr-3 ${f.id}`}
size="16px"
placeholder={f.placeholder}
value={this.state[f.id]}
ref={this.fieldRefs[f.id]}
onChange={(e) => this.hc(e, f.id, this.fieldRefs[f.id].current)}
/>
))
)
return (
<ContactInput
fields={[
{ placeholder: "Name", id: 'name' },
{ placeholder: "Phone number", id: 'phone' },
{ placeholder: "Email", id: 'email' },
]}
/>
)
}
}
export default App
I've tried
Change01 - declaring the refs inside the Input tag in another way
Change02 - not passing the ref to onChange explicitly and then accessing the ref directly from this.fieldrefs
constructor(props) {
this.fieldRefs = {} // Change01
}
hc = (ev, type) => { //Change02
this.setState({
[type]: ev.target.value
})
// console.log(this.fieldRefs[type].current)
this.fieldRefs[type].current.focus()
}
...
<input
...
ref={(el) => this.fieldRefs[f.id] = el} //Change01
onChange={(e) => this.hc(e, f.id)} //Change02
/>
But it didn't help, and after every keypress, the body element became the active element.
Maybe you need to move your ContactInput declaration outside of your render(), else you will be recreating a new component every time it rerenders. For example,
render() {
return (
<this.ContactInput
fields={[
{ placeholder: "Name", id: 'name' },
{ placeholder: "Phone number", id: 'phone' },
{ placeholder: "Email", id: 'email' },
]}
/>
)
}
ContactInput = ({fields}) => (
// If react complains about returning multiple component, add this React.Fragment short syntax
<React.Fragment>
{fields.map((f) => (
<input
key={f.id}
className={`p-1 my-3 w-100 mr-3 ${f.id}`}
size="16px"
placeholder={f.placeholder}
value={this.state[f.id]}
ref={this.fieldRefs[f.id]}
onChange={(e) => this.hc(e, f.id, this.fieldRefs[f.id].current)}
/>
))}
<React.Fragment/>
)

Categories

Resources