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>
);
}
Related
I have some nested data that needs to generate a form of checkboxes dynamically. The "Tasks" data, needs a parent checkbox, as per MaterialUI's docs under "Indeterminate" in their Checkboxes example . I'm struggling to understand how to apply their example in conjunction with my code.
Current data used to generate dynamic checkboxes:
const availableFilters = useMemo(
() => [
{
title: "Status",
filterOptions: [
{ label: "Ready for Review"},
{ label: "Ready for Techcheck"},
],
},
{
title: "Offices",
filterOptions:
[
{ label: "London" },
{ label: "Berlin"},
}],
},
{
title: "Tasks",
filterGroups:
[
{
title: "3D"
filterOptions: [
{label: "Animation"},
{label: "Lighting"},
],
},
{
title: "Comp"
filterOptions: [
{label: "Compositing"},
{label: "Prep"}
],
},
],
},
{
title: "Creator",
filterOptions: [{ label: "Alex"}, { label: "John"}],
},
],
[filterInfo, taskGroups]
);
Getting confused rather easily with the nesting and some recursive typescript stuff.
This is the handlerFunction in the parent component(AddtoReviewMenu) with "Lodash":
const [checkedValues, setCheckedValues] = useState<{[key: string]: string[];}>({});
const handleCheckboxChange = useCallback(
(checked: boolean, title: string, value: string) => {
if(checked) {
if (Object.keys(checkedValues).includes(title)) {
setCheckedValues({
...checkedValues,
[title]: [...checkedValues[title], value],
});
} else {
setCheckedValues({
...checkedValues,
[title]: [value],
});
} else {
setCheckedValues({
...checkedValues,
[title]: _(checkedValues[title])
.filter((c) => c !== value)
.value(),
});
}
},
[checkedValues]
);
Here is the child component that populates the the checkboxes based on the data:
import React from "react";
import IconButton, { IconButtonProps } from "#mui/material/IconButton";
import Box, { FormControl, Stack } from "#mui/material/";
interface ExpandMoreProps extends IconButtonProps {
expand: Boolean;
}
const ExpandMore = styled((props: ExpandMoreProps) => {
const { expand, ...other } = props;
return <IconButton {...other} />;
});
interface Props extends FilterGroup {
handleCheckboxChange: (
checked: boolean,
title: string,
value: string
) => void;
}
export default function FilterOptionGroup(props: Props) {
const { filterGroups, title, handleCheckboxChange } = props;
const [expanded, setExpanded] = useState(true);
const handleExpandClick = () => {
setExpanded(!expanded);
};
return (
<Box>
<FormControl>
<Stack>
<ExpandMore expand={expanded} onClick={handleExpandClick}>
<ArrowUpIcon />
</ExpandMore>
<FormLabel> {title} </FormLabel>
</Stack>
<Collapse in={expanded}>
{filterOptions
? filterOptions?.map((item) => (
<FormControlLabel
control={
<Checkbox
onChange={(event, checked) =>
handleCheckboxChange(checked, title, item.label)
}
/>
}
label={item.label}
value={item.label}
/>
))
: filterGroups?.map((item) => (
<FilterOptionGroup
title={item.title}
filterOptions={item.filterOptions}
filterGroups={item.filterGroups}
handleCheckboxChange={handleCheckboxChange}
/>
))}
</Collapse>
</FormControl>
</Box>
);
}
And a "Types.ts" file:
export interface FilterGroup {
title: string;
filterOptions?: FilterOption[];
filterGroups?: FilterGroup[];
}
export interface FilterOption {
label: string;
}
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'
I was practicing a React and tried to build Arya's kill list. I played around and implemented some features, one supposed to change a person when double clicked. In state I have array (list of people) of array <--- and I want to setState to this array.
I tried to use a ternary operator which suppose to change state from false to true and opposite. But it doesn't work.
Here are examples of a code with solutions I tried to implement:
class App extends React.Component {
state = {
toKill: [
{ name: 'Cersei Lannister',
formDouble: false
},
{ name: 'Ser Ilyn Payne',
formDouble: false
},
{ name: 'The Mountain',
formDouble: false
},
{ name: 'The Hound',
formDouble: false
}
],
}
doubleClickHandler = (index) => {
this.setState({
toKill: this.state.toKill.map(obj =>
obj[index].formDouble ? false : true)
})
// console.log(this.state.toKill[index].formDouble)
// this.state.toKill[index].formDouble === false ?
// this.setState({
// [this.state.toKill[index].formDouble]: true
// }) : this.setState({
// [this.state.toKill[index].formDouble]: false
// })
}
(...)
<div>
{this.state.toKill.map((toKill, index) => {
return <ToKill
double ={() => this.doubleClickHandler(index)}
formDouble={toKill.formDouble}
click ={() => this.deleteToKillHandler(index)}
key={index + toKill.name}
name={toKill.name}
cause={toKill.cause}
img={toKill.img} />
})}
</div>
In doubleClickHandler you can see what I tried to implement and it didn't work.
Here is toKill component:
const ToKill = (props) => {
return (
<div className="hero-card" onDoubleClick={props.double}>
{props.formDouble !== true? <h1>test</h1>
: <>
<div className="hero-img-container">
<img
src={props.img}
alt={props.name}
className="hero-img"
/>
</div>
<div className="hero-desc">
<h3 className="hero-name">{props.name}</h3>
<p className="hero-cause">{props.cause}</p>
<button onClick={props.click}>Delete</button>
</div>
</>
}
</div>
)
}
So what I expect is once I double click on a specific element for example 'The mountain' it will show me it's profile while the rest will show <h1>test</h1>.
Most probably your doubleClickHandler is incorrect. As I understand you want to set formDouble to true for only single element, which you've clicked. In such case doubleClickHandler should be
doubleClickHandler = (index) => {
this.setState({
toKill: this.state.toKill.map((obj, objIndex) =>
objIndex === index ? {...obj, formDouble: true} : {...obj, formDouble: false})
})
}
A snippet using this code:
class App extends React.Component {
state = {
toKill: [
{ name: "Cersei Lannister", formDouble: false },
{ name: "Ser Ilyn Payne", formDouble: false },
{ name: "The Mountain", formDouble: false },
{ name: "The Hound", formDouble: false }
]
};
doubleClickHandler = index => {
this.setState({
toKill: this.state.toKill.map((obj, objIndex) =>
objIndex === index
? { ...obj, formDouble: true }
: { ...obj, formDouble: false }
)
});
};
render() {
return (
<div>
{this.state.toKill.map((toKill, index) => {
return (
<ToKill
double={() => this.doubleClickHandler(index)}
formDouble={toKill.formDouble}
click={() => this.deleteToKillHandler(index)}
key={index + toKill.name}
name={toKill.name}
cause={toKill.cause}
img={toKill.img}
/>
);
})}
</div>
);
}
}
const ToKill = props => {
return (
<div className="hero-card" onDoubleClick={props.double}>
{props.formDouble !== true ? (
<h1>test</h1>
) : (
<React.Fragment>
<div className="hero-img-container">
<img src={props.img} alt={props.name} className="hero-img" />
</div>
<div className="hero-desc">
<h3 className="hero-name">{props.name}</h3>
<p className="hero-cause">{props.cause}</p>
<button onClick={props.click}>Delete</button>
</div>
</React.Fragment>
)}
</div>
);
};
ReactDOM.render(<App />, document.getElementById("app"));
<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="app"></div>
I'm trying to show an icon based on whether or not the current user Id equals the accordion Id.
i.e. I'm trying to let users who made a card be the ones that can edit the card.
This starts to happen where it says //Update icon in code.
The record successfully updates (I can see in the database, and when you refresh the page, it shows correctly without errors), but when the component tries to re-render, I get this error: https://imgur.com/a/YVTDg82
I feel like this is something obvious, and I'm missing something easy.
import React from "react"
import PropTypes from "prop-types"
import { Accordion, Icon, Input, Form, Button, Modal, Dropdown } from 'semantic-ui-react'
//components
import UpdateCard from "./UpdateCard"
import LikeUnlike from "./LikeUnlike"
class Card extends React.Component {
constructor(props) {
super(props)
this.state = {
activeIndex: null,
search: ''
}
}
updateSearch = (event) => {
this.setState({ search: event.target.value.substr(0, 20) })
}
//Opens and closes the accordion
handleClick = (e, titleProps) => {
const { index } = titleProps
const { activeIndex } = this.state
const newIndex = activeIndex === index ? -1 : index
this.setState({ activeIndex: newIndex })
}
render() {
// const options = [
// { key: 'css', text: 'CSS', value: 'css' },
// { key: 'html', text: 'HTML', value: 'html' },
// { key: 'javascript', text: 'Javascript', value: 'javascript' },
// { key: 'rails', text: 'Rails', value: 'rails' },
// { key: 'react', text: 'React', value: 'react' },
// { key: 'ruby', text: 'Ruby', value: 'ruby' },
// ]
const { activeIndex } = this.state
const { showEditMenu } = this
let filteredCards = this.props.librarys.filter(
(library) => {
return library.title.toLowerCase().indexOf(this.state.search.toLowerCase()) !== -1 || library.desc.toLowerCase().indexOf(this.state.search.toLowerCase()) !== -1
}
)
return (
<React.Fragment>
<div className='search-bar'>
<Input fluid icon={<Icon name='search' inverted circular link />} value={this.state.search} onChange={this.updateSearch} placeholder="Search Syntaxes" />
</div>
<ul>
{filteredCards.map((librarys, index) => {
return (
<div key={index} >
<Accordion>
<Accordion.Title >
<Icon
name='dropdown'
active={activeIndex === index}
index={index}
onClick={this.handleClick}
/>
<Icon name='trash alternate'
onClick={() => {
this.props.handleDelete(librarys.id)
}}
/>
{
//Update Icon
librarys.user.id === this.props.currentUser.id ?
<UpdateCard
handleUpdate={this.props.handleUpdate}
libraryId={librarys.id}
likes={librarys.likes}
librarys={librarys}
/>
: ''
}
Title: {librarys.title} Likes: {librarys.likes}
<LikeUnlike
handleUpdate={this.props.handleUpdate}
libraryId={librarys.id}
librarys={librarys}
/>
</Accordion.Title>
<Accordion.Content active={activeIndex === index}>
Description: <br></br>
{librarys.desc} <br></br>
Markdown: <br></br>
{librarys.markdown}
</Accordion.Content>
</Accordion>
</div>
)
})}
</ul>
</React.Fragment>
);
}
}
export default Card
You need to do a null check before comparing the values. Like below:
librarys.user && librarys.user.id === this.props.currentUser.id ?
// same stuff here
So, I have a property (fields), within which I wish to change the value of an element (countries). Alerting the value of countries currently displays the value 2, but I want to change the value to 100, so that re-alerting fields.countries.value, after the change, displays the new value.
How do I do this?
import type { State } from '../../common/types';
import DynamicField from './DynamicField';
import R from 'ramda';
import React from 'react';
import buttonsMessages from '../../common/app/buttonsMessages';
import linksMessages from '../../common/app/linksMessages';
import { FormattedMessage } from 'react-intl';
import { ValidationError } from '../../common/lib/validation';
import { connect } from 'react-redux';
import { fields } from '../../common/lib/redux-fields';
import {
Block,
Box,
Button,
Checkbox,
FieldError,
Flex,
Form,
Heading,
Input,
PageHeader,
Pre,
Radio,
Select,
Space,
Title,
View,
} from '../app/components';
// The example of dynamically loaded editable data.
// cato.org/publications/commentary/key-concepts-libertarianism
const keyConceptsOfLibertarianism = [
'Individualism',
'Individual Rights',
'Spontaneous Order',
'The Rule of Law',
'Limited Government',
'Free Markets',
'The Virtue of Production',
'Natural Harmony of Interests',
'Peace',
].map((concept, index) => ({
id: index,
name: concept,
}));
// Proof of concept. Country list will be read from firebase
const countryArray = [
{ label: 'Select Country', value: 0 },
{ label: 'France', value: 2 },
{ label: 'England', value: 4 },
{ label: 'Swizterland', value: 8 },
{ label: 'Germany', value: 16 },
{ label: 'Lithuania', value: 32 },
{ label: 'Romania', value: 64 },
].map((countryName, index) => ({
id: index,
name: countryName,
}));
// Dynamically create select list
const countryOptions = [];
countryArray.map(countryItem =>
countryOptions.push({ label: countryItem.name.label, value: countryItem.name.value }),
);
// Proof of concept. Country list will be read from firebase
const cityArray = [
{ label: 'Select City', value: 0 },
{ label: 'London', value: 50 },
{ label: 'Paris', value: 75 },
].map((cityName, index) => ({
id: index,
name: cityName,
}));
// Dynamically create select list
const cityOptions = [];
cityArray.map(cityItem =>
cityOptions.push({ label: cityItem.name.label, value: cityItem.name.value }),
);
// Proof of concept. Country list will be read from firebase
const gymArray = [
{ label: 'Select Gym', value: 0 },
{ label: 'Virgin Sport', value: 23 },
{ label: 'Sports Direct', value: 45 },
].map((gymName, index) => ({
id: index,
name: gymName,
}));
// Dynamically create select list
const gymOptions = [];
gymArray.map(gymItem =>
gymOptions.push({ label: gymItem.name.label, value: gymItem.name.value }),
);
type LocalState = {
disabled: boolean,
error: ?Object,
submittedValues: ?Object,
};
class FieldsPage extends React.Component {
static propTypes = {
fields: React.PropTypes.object.isRequired,
dynamicFields: React.PropTypes.object,
// getCities: React.PropTypes.object,
};
state: LocalState = {
disabled: false,
error: null,
submittedValues: null,
};
onFormSubmit = () => {
const { dynamicFields, fields } = this.props;
const values = {
...fields.$values(),
concepts: {
...dynamicFields,
},
};
// This is just a demo. This code belongs to Redux action creator.
// Disable form.
this.setState({ disabled: true });
// Simulate async action.
setTimeout(() => {
this.setState({ disabled: false });
const isValid = values.name.trim();
if (!isValid) {
const error = new ValidationError('required', { prop: 'name' });
this.setState({ error, submittedValues: null });
return;
}
this.setState({ error: null, submittedValues: values });
fields.$reset();
}, 500);
};
handleSelectedCountryChange = () => {
// Pass in the selected country value to get associated cites
const { fields, getCities } = this.props;
getCities('country', fields.$values());
};
/*
handleSelectedCityChange = (event => {
// Pass in the selected city value to get associated gyms
this.setState({secondLevel: event.target.value});
});
*/
render() {
const { fields } = this.props;
const { disabled, error, submittedValues } = this.state;
return (
<View>
<Title message={linksMessages.fields} />
<PageHeader
description="New clients enter their gym details here."
heading="New user entry form."
/>
<Form onSubmit={this.onFormSubmit}>
<Input
{...fields.name}
aria-invalid={ValidationError.isInvalid(error, 'name')}
disabled={disabled}
label="Your Name"
maxLength={100}
type="text"
/>
<FieldError error={error} prop="name" />
<Heading alt>Key Concepts of Libertarianism</Heading>
<Block>
<Flex wrap>
{keyConceptsOfLibertarianism.map(item =>
<Box mr={1} key={item.id}>
<DynamicField
disabled={disabled}
item={item}
path={['fieldsPage', 'dynamic', item]}
/>
</Box>,
)}
</Flex>
</Block>
<Block>
<Checkbox
{...fields.isLibertarian}
checked={fields.isLibertarian.value}
disabled={disabled}
label="I'm libertarian"
/>
<Checkbox
{...fields.isAnarchist}
checked={fields.isAnarchist.value}
disabled={disabled}
label="I'm anarchist"
/>
</Block>
<Block>
<Flex>
<Radio
{...fields.gender}
checked={fields.gender.value === 'male'}
disabled={disabled}
label="Male"
value="male"
/>
<Space x={2} />
<Radio
{...fields.gender}
checked={fields.gender.value === 'female'}
disabled={disabled}
label="Female"
value="female"
/>
<Space x={2} />
<Radio
{...fields.gender}
checked={fields.gender.value === 'other'}
disabled={disabled}
label="Other"
value="other"
/>
</Flex>
</Block>
<Block>
<Select
{...fields.countries}
disabled={disabled}
label="Countries"
onChange={this.handleSelectedCountryChange}
options={countryOptions}
/>
</Block>
<Block>
<Select
{...fields.cities}
disabled={disabled}
label="Cities"
// onChange={this.handleSelectedCityChange}
options={cityOptions}
/>
</Block>
<Block>
<Select
{...fields.gyms}
disabled={disabled}
label="Gyms"
// onChange={this.handleSelectedCityChange}
options={gymOptions}
/>
</Block>
{/*
Why no multiple select? Because users are not familiar with that.
Use checkboxes or custom checkable dynamic fields instead.
*/}
<Button disabled={disabled} type="submit">
<FormattedMessage {...buttonsMessages.submit} />
</Button>
{submittedValues &&
<Pre>
{JSON.stringify(submittedValues, null, 2)}
</Pre>
}
</Form>
</View>
);
}
}
FieldsPage = fields({
path: 'fieldsPage',
fields: [
'countries',
'cities',
'gyms',
'gender',
'isAnarchist',
'isLibertarian',
'name',
],
getInitialState: () => ({
countries: '0',
cities: '0',
gyms: '0',
gender: 'male',
isAnarchist: false,
isLibertarian: false,
}),
})(FieldsPage);
export default connect(
(state: State) => ({
dynamicFields: R.path(['fieldsPage', 'dynamic'], state.fields),
}),
)(FieldsPage);
=====================================================================
fields.js
/* #flow weak */
import R from 'ramda';
import React from 'react';
import invariant from 'invariant';
import { resetFields, setField } from './actions';
type Path = string | Array<string> | (props: Object) => Array<string>;
type Options = {
path: Path,
fields: Array<string>,
getInitialState?: (props: Object) => Object,
};
const isReactNative =
typeof navigator === 'object' &&
navigator.product === 'ReactNative'; // eslint-disable-line no-undef
// Higher order component for huge fast dynamic deeply nested universal forms.
const fields = (options: Options) => (WrappedComponent) => {
const {
path = '',
fields = [],
getInitialState,
} = options;
invariant(Array.isArray(fields), 'Fields must be an array.');
invariant(
(typeof path === 'string') ||
(typeof path === 'function') ||
Array.isArray(path)
, 'Path must be a string, function, or an array.');
return class Fields extends React.Component {
static contextTypes = {
store: React.PropTypes.object, // Redux store.
};
static getNormalizePath(props) {
switch (typeof path) {
case 'function': return path(props);
case 'string': return [path];
default: return path;
}
}
static getFieldValue(field, model, initialState) {
if (model && {}.hasOwnProperty.call(model, field)) {
return model[field];
}
if (initialState && {}.hasOwnProperty.call(initialState, field)) {
return initialState[field];
}
return '';
}
static lazyJsonValuesOf(model, props) {
const initialState = getInitialState && getInitialState(props);
// http://www.devthought.com/2012/01/18/an-object-is-not-a-hash
return options.fields.reduce((fields, field) => ({
...fields,
[field]: Fields.getFieldValue(field, model, initialState),
}), Object.create(null));
}
static createFieldObject(field, onChange) {
return isReactNative ? {
onChangeText: (text) => {
onChange(field, text);
},
} : {
name: field,
onChange: (event) => {
// Some custom components like react-select pass the target directly.
const target = event.target || event;
const { type, checked, value } = target;
const isCheckbox = type && type.toLowerCase() === 'checkbox';
onChange(field, isCheckbox ? checked : value);
},
};
}
state = {
model: null,
};
fields: Object;
values: any;
unsubscribe: () => void;
onFieldChange = (field, value) => {
const normalizedPath = Fields.getNormalizePath(this.props).concat(field);
this.context.store.dispatch(setField(normalizedPath, value));
};
createFields() {
const formFields = options.fields.reduce((fields, field) => ({
...fields,
[field]: Fields.createFieldObject(field, this.onFieldChange),
}), {});
this.fields = {
...formFields,
$values: () => this.values,
$setValue: (field, value) => this.onFieldChange(field, value),
$reset: () => {
const normalizedPath = Fields.getNormalizePath(this.props);
this.context.store.dispatch(resetFields(normalizedPath));
},
};
}
getModelFromState() {
const normalizedPath = Fields.getNormalizePath(this.props);
return R.path(normalizedPath, this.context.store.getState().fields);
}
setModel(model) {
this.values = Fields.lazyJsonValuesOf(model, this.props);
options.fields.forEach((field) => {
this.fields[field].value = this.values[field];
});
this.fields = { ...this.fields }; // Ensure rerender for pure components.
this.setState({ model });
}
componentWillMount() {
this.createFields();
this.setModel(this.getModelFromState());
}
componentDidMount() {
const { store } = this.context;
this.unsubscribe = store.subscribe(() => {
const newModel = this.getModelFromState();
if (newModel === this.state.model) return;
this.setModel(newModel);
});
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
return (
<WrappedComponent {...this.props} fields={this.fields} />
);
}
};
};
export default fields;