Gutenberg Block -> Media Upload - Array of different images - reactjs

Someone could help me create array for images inside attributes?
At the moment I did:
attributes: {
Bg_URL: { type: 'string' },
Bg_ID: { type: 'number' },
Bg_ALT: { type: 'string'},
icon_1_URL: { type: 'string' },
icon_1_ID: { type: 'number' },
icon_1_ALT: { type: 'string'},
icon_2_URL: { type: 'string' },
icon_2_ID: { type: 'number' },
icon_2_ALT: { type: 'string'},
icon_3_URL: { type: 'string' },
icon_3_ID: { type: 'number' },
icon_3_ALT: { type: 'string'},
},
Then Update function:
const onSelectImage = (name) => (e) => {
setAttributes({
[name+'_URL']: e.url,
[name+'_ID']: e.id,
[name+'_ALT']: e.alt,
});
};
And MediaUpload:
<MediaUpload
onSelect={onSelectImage('Bg')}
type="image"
value={attributes.Bg_ID}
render={({open}) => (
<figure className={!attributes.Bg_ID ? ' image-button' : ' image-preview'} onClick={open} >
{!attributes.Bg_ID ? __("Upload Image") : <img src={attributes.Bg_URL} />}
</figure>
)}
/>
So ok, I did one function to update the specific image in attributes, but I would like to have images array there, which would store all block images, and from onSelect I could pass name/id/key of that image to store, which I would call
images.key.url
Ideally I would see the same for RichText, where I don't need to define each of it separate, but would have a array "content", and there something like this?
value={ attributes.content.heading }
onChange={ ( content.heading ) => setAttributes( { content.heading } ) }
where .heading don't need to be defined earlier, it is when adding the content.
Have anyone had the same idea and could help to archive that?
Thanks.

I would suggest storing the images in an array attribute. Then you can use the MediaUpload component to create and edit a "gallery".
The onSelectMeida function:
const onSelectMedia = (media) => {
setAttributes({
images: [{
id: media.id,
url: media.url,
alt: media.alt,
}],
});
};
The MediaUpload component:
<MediaUploadCheck>
<MediaUpload
multiple={ true }
gallery={ true }
onSelect={ (media) => onSelectMedia(media) }
allowedTypes={ ['image'] }
accept="image/*"
value={ images.map(item => item.id) }
render={ ({open}) => {
return (
<Fragment>
<Button
isPrimary={ true }
onClick={ (event) => {
event.stopPropagation();
open();
} }
>
{ images.length > 0 ? __('Edit Images', 'pb') : __('Select Images', 'pb') }
</Button>
</Fragment>
);
} }
/>
</MediaUploadCheck>
Then you can render the images using a map:
{ images.length > 0 && images.map(image => {
<img
key={ image.id }
src={ image.url }
alt={ image.alt }
/>
}) }

Related

How do I remove duplicate code from titles.map and titles.slice?

How do I remove duplicate code from titles.map and titles.slice?
how should be handled with en effect to set the facets that should be displayed. Functionality works as expected, I just want to remove duplicated code.
import { useState } from "react";
const App = () => {
const titles = [
{ id: "0", name: "Title" },
{ id: "5637144579", name: "Miss" },
{ id: "5637144576", name: "Mr." },
{ id: "5637145326", name: "MrandMrs." },
{ id: "5637144577", name: "Mrs." },
{ id: "5637144578", name: "Ms." },
{ id: "5637145330", name: "Br." },
{ id: "5637145327", name: "Dame" },
{ id: "5637144585", name: "Dr." },
{ id: "5637145331", name: "Fr." },
{ id: "5637144582", name: "I" },
];
const [isAllFacets, setIsAllFacets] = useState(false);
const MAX_FACET_COUNT = 5;
const visibleFacetCount = titles.length - 1 === MAX_FACET_COUNT ?
titles.length :
MAX_FACET_COUNT;
const showAllFacet = () => { setIsAllFacets(!isAllFacets); };
return (<>
{isAllFacets ?
titles.map((title: any) => {
return <div key={title.id}>{title.name}</div>;
}) :
titles.slice(0, visibleFacetCount).map((title) => {
return <div key={title.id}>{title.name}</div>;
})}
{titles.length > visibleFacetCount && (<>
{!isAllFacets ? (
<button onClick={showAllFacet}>show all</button>
) : (
<button onClick={showAllFacet}>show less</button>
)}
</>)}
</>);
};
export default App;
One option would be to use the conditional operator to slice the entire existing array in case isAllFacets is false - instead of alternating over the whole JSX to return, alternate over only the index to slice.
A similar approach can be used to simplify your <button> text.
return (
<>
{
titles
.slice(0, isAllFacets ? titles.length : visibleFacetCount)
.map(title => <div key={title.id}>{title.name}</div>)
}
{titles.length > visibleFacetCount && <button onClick={showAllFacet}>show{isAllFacets ? ' all' : ' less'}</button>}
</>
);
You can achieve your goal with useMemo hook. Prepare the data to display and render it. Value will be recalculated when anything inside depsArray is changed.
const titlesToDisplay = useMemo(() => {
return isAllFacets ? titles : titles.slice(0, visibleFacetCount);
}, [titles, isAllFacets, visibleFacetCount]);
return (
<>
{titlesToDisplay.map((title) => {
return <div key={title.id}>{title.name}</div>;
})}
{titles.length > visibleFacetCount && (
<>
{!isAllFacets ? (
<button onClick={showAllFacet}>show all</button>
) : (
<button onClick={showAllFacet}>show less</button>
)}
</>
)}
</>
);
you only need to put all together :
titles.slice(0, isAllFacets?titles.length:visibleFacetCount).map((title) => { return <div key={title.id}>{title.name}
</div>; })
Create a renderTitle function which returns the title div
const renderTitle = title => <div key={title.id}>{title.name}</div>
Then pass it to both .map invocations
titles.map(renderTitle)
and
titles.slice(0, visibleFacetCount).map(renderTitle)
And for your button, simplify it with:
<button onClick={showAllFacet}>show {isAllFacets ? "less" : "all"}</button>

How to properly change the boolean inside of object in array?

So, I'm trying toggle the Icon based on the isBadData per email data in the object of array. But I can't seem to find out how could save it back to the state so it can update the Icon image in LeadProfileComponent.
This is what it looks like:
checkIcon = isBadData: false
crossIcon = isBadData: true
Heres my code:
// ModalComponent.js
const [leadProfile, setLeadProfile] = useState([
{
id: 'd114877b-074b-4aa2-a3f0-3b9446885336',
firstName: 'wqe',
lastName: 'wqe',
name: 'wqe wqe',
email: [
{
type: 'personal',
address: 'qwe#hotmail.com',
valid_since: '2010-05-09',
isBadData: true,
},
{
type: 'personal',
address: 'wqe#hotmail.com',
valid_since: '2017-03-09',
isBadData: true,
},
{
type: 'personal',
address: 'wqe#aol.com',
valid_since: '2009-01-12',
isBadData: true,
},
],
},
]);
<LeadProfileComponent leadProfile={leadProfile} setLeadProfile={setLeadProfile} />
// LeadProfileComponent.js
const LeadProfileComponent = (props) => {
const handleChildEmail = (email, index) => {
props.setLeadProfile((prev: any) => {
const value = { ...prev[0].email[index] };
console.log('inside value');
console.log(value);
value.isBadData = !value.isBadData;
console.log(value);
// return prev;
return [value];
});
console.log('props.leadProfile');
console.log(props.leadProfile);
};
return (
<>
{
props.leadProfile.map((lead, index) => (
return(
<>
{lead.email.map(() => {
return (
<button
id="btnCheck"
onClick={() => {
handleChildEmail(email, index);
}}
>
<img
src={
email.isBadData !== true
? checkIcon
: closeIcon
}
/>
</button>
)
})}
</>
)
}
</>
);
}
Heres what it looks like when you console log inside of handChildEmail function:
As you can see, I was able to change the inside boolean of email[0], but I cant save it back to the leadProfile state since I have a missing part in the destructuring part
Break your components in smaller parts, and manage each email individually
LeadProfileEmailComponent.js
const LeadProfileEmailComponent = ({ initialEmailData, ...props }) => {
const [emailData, setEmailData] = useState(initialEmailData);
return (
<button
id="btnCheck"
onClick={() => {
setEmailData({
...emailData,
isBadData: !emailData.isBadData
});
}}
>
<img
src={
emailData.isBadData !== true
? checkIcon
: closeIcon
}
/>
</button>
)
}
Change this in LeadProfileComponent:
{lead.email.map((email) => {
return (
<LeadProfileEmailComponent initialEmailData={email} />
)
})}
The downside is, the state of the parent component will not be updated. However this is standard design pattern practise, you should not rely on the parent component data for this.

SPFx Reactjs Cannot Read Properties of Undefined

I am using React to develop with SPfx for SharePoint online and am trying to rebuild a past solution I made, so that I can utilize the property pane of sharepoint to edit the webpart. I am getting an error of:
ERROR:
Cannot read properties of undefined (reading 'fileAbsoluteUrl')
when trying to render the grid. However, using the Edge developer tools, I can see the array being initialized with the information from the Property Pane.
If anybody could help explain why the code is returning an undefined, I would greatly appreciate it. Or if you have experience using PnP with SPFx.
This is my .tsx code:
export default class GridLayout extends React.Component<IGridLayoutProps, {}> {
public render(): React.ReactElement<IGridLayoutProps> {
const {
description,
isDarkTheme,
environmentMessage,
hasTeamsContext,
userDisplayName,
gridItems
} = this.props;
let arr = [];
arr.push(this.props.gridItems);
let firstItem = arr.slice(0, 1);
let firstRow = arr.slice(1, 6);
let items = arr.slice(6);
console.log(firstItem, firstRow, items);
if (this.props.gridItems && this.props.gridItems.length > 0) {
var gridFirstItem = firstItem.map(el => {
<div className={`${styles.tile1}`}>
<div key={el.Title} className={`${styles.galleryframe1}`} style={{ backgroundColor: '#2f2f2f' }}>
<a href={el.Hyperlink ? el.Hyperlink : '#'}>
<img className={`${styles.galleryimg}`} src={el && el.length > 0 ? el.filePicker.fileAbsoluteUrl : ''} />
</a>
</div>
</div>
});
console.log('first grid item good...')
var gridFirstRow = firstRow.map(el => {
<div className={`${styles.tile1}`}>
<div key={el.Title} className={`${styles.galleryframe1}`} style={{ backgroundColor: '#0069f8' }}>
<a href={el.Hyperlink ? el.Hyperlink : '#'}>
<img className={`${styles.galleryimg}`} src={el && el.length > 0 ? el.filePicker.fileAbsoluteUrl : ''} />
</a>
</div>
</div>
});
console.log('first row good...')
var restOfItems = items.map(el => {
<div className={`${styles.tile}`}>
<div key={el.Title} className={`${styles.galleryframe}`} style={{ backgroundColor: '#e64856' }}>
<a href={el.Hyperlink ? el.Hyperlink : '#'}>
<img className={`${styles.galleryimg}`} src={el && el.length > 0 ? el.filePicker.fileAbsoluteUrl : ''} />
</a>
</div>
</div>
});
console.log('grid good')
} else {
return (
<div className={`${styles.label}`}>Use Property Pane Editor to Add Tiles.</div>
)
}
return (
<body>
<div className={`${styles.footer}`} >Our Mission: Provide the fullest possible accounting for our missing personnel to their families and the nation.</div>
{/* Renders the grid */}
<div className={`${styles.grid}`}>
{gridFirstItem}
{gridFirstRow}
{restOfItems}
</div>
</body>
);
}
}
This is the code involving the property pane and the array:
PropertyFieldCollectionData("gridItems", {
key: "gridItemsFieldId",
label: "Grid Data",
panelHeader: "Grid Data Panel",
manageBtnLabel: "Manage grid data",
value: this.properties.gridItems,
fields: [
{
id: "Title",
title: "Item Title",
type: CustomCollectionFieldType.string,
required: true,
},
{
id: "Description",
title: "Item Description",
type: CustomCollectionFieldType.string,
},
{
id: "Hyperlink",
title: "Link to Open",
type: CustomCollectionFieldType.url,
required: true,
},
{
id: "filePicker",
title: "Select File",
type: CustomCollectionFieldType.custom,
onCustomRender: (
field,
value,
onUpdate,
item,
itemId,
onError
) => {
return React.createElement(FilePicker, {
context: this.context,
key: itemId,
buttonLabel: "Select File",
onChange: (filePickerResult: IFilePickerResult[]) => {
console.log('changing....', field);
onUpdate(field.id, filePickerResult[0]);
this.context.propertyPane.refresh();
this.render();
},
onSave: (filePickerResult: IFilePickerResult[]) => {
console.log('saving....', field);
if (filePickerResult && filePickerResult.length > 0) {
console.log('filePickerResult && filePickerResult.length > 0');
if (filePickerResult[0].fileAbsoluteUrl == null) {
console.log('filePickerResult[0].fileAbsoluteUrl == null');
filePickerResult[0].downloadFileContent().then(async r => {
let fileresult = await this.web.getFolderByServerRelativeUrl(`${this.context.pageContext.site.serverRelativeUrl}/SiteAssets/SitePages`).files.addUsingPath(filePickerResult[0].fileName, r, true);
filePickerResult[0].fileAbsoluteUrl = `${this.context.pageContext.site.absoluteUrl}/SiteAssets/SitePages/${fileresult.data.Name}`;
console.log('saving....', filePickerResult[0]);
onUpdate(field.id, filePickerResult[0]);
this.context.propertyPane.refresh();
this.render();
});
} else {
console.log('saving....', filePickerResult[0]);
onUpdate(field.id, filePickerResult[0]);
this.context.propertyPane.refresh();
this.render();
}
}
},
hideLocalUploadTab: false,
hideLocalMultipleUploadTab: true,
hideLinkUploadTab: false,
});
},
required: false,
},
],
disabled: false,
},

Dynamic render react child component

How can i dynamic render react child component? Now that looks like this and its works.
<CustomFieldArea>
{(ExampleCustomFields || []).map((e: {
field: string;
CustomComponent: 'Text' | 'TextArea'
}) => {
if (e?.CustomComponent === 'Text') {
return (
<CustomFieldArea.Text
name={e?.field}
/>
)
}
if (e?.CustomComponent === 'TextArea') {
return (
<CustomFieldArea.TextArea
name={e?.field}
/>
)
}
})}
</CustomFieldArea>
Here is the output I’m looking for:
<CustomFieldArea>
{(ExampleCustomFields || []).map((e: {
field: string;
CustomComponent: 'Text' | 'TextArea'
}) => {
return (
<CustomFieldArea[e?.CustomComponent]
name={e?.field}
/>
)
})}
</CustomFieldArea>
But it doesnt work. How can i using <CustomFieldArea[e?.CustomComponent] label={e?.title}> like this.
Are you want something like render props ?
<DataProvider render={data => (
<h1>Hello, {data.target}</h1>
)}/>
<Mouse children={mouse => (
<p>Current mouse position: {mouse.x}, {mouse.y}</p>
)}/>
Read more here
if render props isn't that you want then Use HOC's
const menu = [
{ title: 'Home', icon: 'HomeIcon' },
{ title: 'Notifications', icon: 'BellIcon' },
{ title: 'Profile', icon: 'UserIcon' },
]
const Icon = (props) => {
const { name } = props
let icon = null
if (name === 'HomeIcon') icon = HomeIcon
if (name === 'BellIcon') icon = BellIcon
if (name === 'UserIcon') icon = UserIcon
return React.createElement(icon, { ...props })
}
Read more here
Helpful links
First
Second

How to make some values default vaslues and enable and other no default and disbale in an admin dialog which activate contact form fields

I have a React app with an admin panel and one of the dialogs is for contact form fields.
Essentially in the contact form dialog there are switches which all by default are enable and those turn on the fields in the main app.
The screenshot shows the fields how are in the default way all turned on.
Right now the issue here is that the last field preferredContactWay should be off by default.
My solution to this problem was as follow but is a bad solution will be needed a more efficient one and less redundant code. A better solution is what I seek to my problem.
I created a second object called availableContactFields and removed from the original defaultContactFields preferredContactWay field.
const defaultContactFields = {
name: {
name: 'name',
type: 'name',
},
email: {
name: 'email',
type: 'email',
confirm: true,
},
phone: {
name: 'phone',
type: 'phone',
},
preferredContactHours: {
name: 'preferredContactHours',
type: 'select',
options: ['8-20', '8-12', '12-16', '16-20'],
defaultValue: '8-20',
},
};
const availableContactFields = {
name: {
name: 'name',
type: 'name',
},
email: {
name: 'email',
type: 'email',
confirm: true,
},
phone: {
name: 'phone',
type: 'phone',
},
preferredContactHours: {
name: 'preferredContactHours',
type: 'select',
options: ['8-20', '8-12', '12-16', '16-20'],
defaultValue: '8-20',
},
preferredContactWay: {
name: 'preferredContactWay',
type: 'select',
options: ['Phone call', 'SMS', 'Email'],
defaultValue: 'Phone call',
},
};
Then I have the next part function and I'm passing the default fields to have as default what should on and so the preferredContact way remains off initially
function EditContactFormDialog({ closeDialog, handleEdit, editItem }) {
const classes = useStyles();
const [disabled, setDisabled] = useState(false);
const [internalValue, setInternalValue] = useState(
get(editItem, 'value') || Object.values(defaultContactFields),
);
console.log({ internalValue });
function toggleEnabled(name, enabled) {
console.log(name, enabled);
const onlyEnabled = key =>
(internalValue.some(val => val.name === key) && key !== name) ||
(key === name && enabled);
const toConfig = key =>
internalValue.find(val => val.name === key) ||
availableContactFields[key];
setInternalValue(
supported
.map(itm => itm.name)
.filter(onlyEnabled)
.map(toConfig),
);
}
function toggleConfirm(name, confirm) {
setInternalValue(
internalValue.map(val => (val.name === name ? { ...val, confirm } : val)),
);
}
function toggleOptions(name, options) {
setInternalValue(
internalValue.map(val => (val.name === name ? { ...val, options } : val)),
);
}
the last part is the return of the component the map is taking this
const supported = [
{ name: 'name' },
{ name: 'email', confirmable: true },
{ name: 'phone', confirmable: true },
{ name: 'preferredContactHours' },
{ name: 'preferredContactWay' },
];
Then here the switches are rendered
return (
<Dialog open>
<DialogTitle className={classes.dialogTitle}>
Customize contact form fields
</DialogTitle>
<DialogContent>
<Table>
<TableBody>
{supported.map(({ name, confirmable }) => {
const value = internalValue.find(item => item.name === name);
console.log({ internalValue });
const isEnabled = Boolean(value);
console.log('boolean ->', { value }, ': ', { name }, ' : ', {
isEnabled,
});
const isConfirm = isEnabled && value.confirm;
const { options } = availableContactFields[name];
const valueOptions = get(value, 'options', []);
console.log(name, value);
return (
<TableRow key={name}>
<TableCell>
<FormControlLabel
control={
<Switch
color="primary"
checked={isEnabled}
onChange={({ target: { checked } }) =>
toggleEnabled(name, checked)
}
/>
}
label={name}
/>
</TableCell>
<TableCell>
{confirmable && (
<FormControlLabel
disabled={!isEnabled}
control={
<Checkbox
checked={isConfirm}
onChange={({ target: { checked } }) =>
toggleConfirm(name, checked)
}
/>
}
label="Confirm"
/>
)}
{options && (
<FormControl
disabled={!isEnabled}
className={classes.fullWidth}
>
<InputLabel>Options</InputLabel>
<Select
multiple
value={valueOptions}
onChange={({ target }) =>
toggleOptions(name, target.value)
}
renderValue={selected => selected.join(', ')}
>
{options.map(option => (
<MenuItem key={option} value={option}>
<Checkbox
checked={valueOptions.includes(option)}
/>
<ListItemText primary={option} />
</MenuItem>
))}
</Select>
</FormControl>
)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</DialogContent>
<DialogActions style={{ justifyContent: 'space-between' }}>
<Button disabled={disabled} onClick={closeDialog}>
Cancel
</Button>
<Button
color="primary"
variant="contained"
disabled={disabled || internalValue.length < 1}
onClick={() => {
setDisabled(true);
handleEdit(internalValue);
}}
>
{disabled && <CircularProgress size={18} />}
Save
</Button>
</DialogActions>
</Dialog>
);
}
Answering my own question as I found a solution and could be ok at least doing the same as before.
To avoid having redundant code I added the following 2 lines inside the beginning of the component
const availableContactFields = { ...defaultContactFields };
delete availableContactFields.preferredContactWay;
const [internalValue, setInternalValue] = useState(
get(editItem, 'value') || Object.values(availableContactFields),
);
In this way I'm deleting from the default fields the preferredContactWay and then the switch is kept off in default session.
Not sure is the right solution but at this moment work

Resources