Getting attributes of InnerBlocks and saving them to parent - reactjs

I have created a "Tabbed Panels" (tabbed content) block that is essentially just an InnerBlocks component that only allows the block "Panel". When you create a Panel you must give the Panel a heading which is then used in the Panel as well as the Tab button. So in my render function for Tabbed Panels, I need to pull the heading from the children Panel blocks.
There are a couple approaches I could use, like just using regex in the tabbed-panels-render.php function to search the children html for the proper attributes, but this doesn't seem like the best approach.
I think the simplest solution would be to listen for any changes to the Panel blocks and saves the changes (heading and id in this case) to the parent. My current approach is based off this discussion which uses hooks to listen for changes. That part seems to work fine, but I need to save the output somewhere so I'm saving them as attributes to the Tabbed Panels block. This seems to work fine at first, but putting the "setAttributes" method directly in the edit function leads to issues. If there are too many Tabbed Panel blocks on the page, then React throws a "too many renders" error.
Where should my "setAttributes" function live, or is there a better approach to passing data from child to parent? I thought about using the useDispatch hook in the child, but I need to check a lot of events (the heading changes, the block is reordered, the block is deleted, etc.)
Here are the relevant js and php files. There are some custom elements, but you should be able to parse it.
tabbed-panels.js
import { arraysMatch } from 'Components/utils.js'
const { InnerBlocks } = wp.blockEditor
const { createBlock } = wp.blocks
const { Button } = wp.components
const { useDispatch, useSelect } = wp.data
const { __ } = wp.i18n
export const tabbedPanels = {
name: 'my/tabbed-panels',
args: {
title: __('Tabbed Panels', '_ws'),
description: __('Tabbable panels of content.', '_ws'),
icon: 'table-row-after',
category: 'common',
supports: {
anchor: true
},
attributes: {
headings: {
type: 'array',
default: []
},
uids: {
type: 'array',
default: []
}
},
edit: props => {
const { setAttributes } = props
const { headings, uids } = props.attributes
const { insertBlock } = useDispatch('core/block-editor')
const { panelHeadings, panelUids, blockCount } = useSelect(select => {
const blocks = select('core/block-editor').getBlocks(props.clientId)
return {
panelHeadings: blocks.map(b => b.attributes.heading),
panelUids: blocks.map(b => b.clientId),
blockCount: select('core/block-editor').getBlockOrder(props.clientId).length
}
})
if (!arraysMatch(panelHeadings, headings)) {
setAttributes({ headings: panelHeadings })
}
if (!arraysMatch(panelUids, uids)) {
setAttributes({ uids: panelUids })
}
return (
<div className="block-row">
<InnerBlocks
allowedBlocks={ ['my/panel'] }
templateLock={ false }
renderAppender={ () => (
<Button
isSecondary
onClick={ e => {
insertBlock(createBlock('my/panel'), blockCount, props.clientId)
} }
>
{ __('Add Panel', '_ws') }
</Button>
) }
/>
</div>
)
},
save: props => {
return (
<InnerBlocks.Content />
)
}
}
}
tabbed-panels-render.php
<?php
function block_tabbed_panels($atts, $content) {
$atts['className'] = 'wp-block-ws-tabbed-panels ' . ($atts['className'] ?? '');
$headings = $atts['headings'] ?? '';
$uids = $atts['uids'] ?? '';
ob_start(); ?>
<div class="tabs" role="tablist">
<?php
foreach ($headings as $i=>$heading) : ?>
<button
id="tab-<?= $uids[$i]; ?>"
class="tab"
role="tab"
aria-selected="false"
aria-controls="panel-<?= $uids[$i]; ?>"
tabindex="-1"
>
<?= $heading; ?>
</button>
<?php
endforeach; ?>
</div>
<div class="panels">
<?= $content; ?>
</div>
<?php
return ob_get_clean();
}
panel.js
import ComponentHooks from 'Components/component-hooks.js'
const { InnerBlocks, RichText } = wp.blockEditor
const { __ } = wp.i18n
export const panel = {
name: 'my/panel',
args: {
title: __('Panel', '_ws'),
description: __('Panel with associated tab.', '_ws'),
icon: 'format-aside',
category: 'common',
supports: {
customClassName: false,
html: false,
inserter: false,
reusable: false
},
attributes: {
heading: {
type: 'string'
},
uid: {
type: 'string'
}
},
edit: props => {
const { setAttributes } = props
const { heading } = props.attributes
return (
<>
<ComponentHooks
componentDidMount={ () => setAttributes({ uid: props.clientId }) }
/>
<RichText
label={ __('Tab Name', '_ws') }
placeholder={ __('Tab Name', '_ws') }
tagName="h4"
onChange={ newValue => setAttributes({ heading: newValue }) }
value={ heading }
/>
<InnerBlocks
templateLock={ false }
/>
</>
)
},
save: props => {
return (
<InnerBlocks.Content />
)
}
}
}
panel-render.php
<?php
function block_panel($atts, $content) {
$uid = $atts['uid'] ?? '';
ob_start(); ?>
<div
id="panel-<?= $uid ?>"
class="panel"
role="tabpanel"
aria-labelledby="tab-<?= $uid; ?>"
tabindex="0"
hidden="hidden"
>
<?= $content; ?>
</div>
<?php
return ob_get_clean();
}

You can access children blocks from the parent and get the attributes (tab title in your case).
componentDidUpdate(previousProps, previousState) {
var myID = this.props.clientId;
var tabs_title = [];
this.myBlock = wp.data.select('core/block-editor').getBlock(myID);
this.myBlock.innerBlocks.map(block => {
tabs_title.push( block.attributes.title );
});
this.props.setAttributes({ 'tabs_title': tabs_title });
}

Related

Gutenberg Block Variation Picker not working

I'm trying to add the BlockVariationPicker like in the WordPress Github example:
import { useSelect } from '#wordpress/data';
import {
__experimentalBlockVariationPicker as BlockVariationPicker,
store as blockEditorStore,
} from '#wordpress/block-editor';
const MyBlockVariationPicker = ( { blockName } ) => {
const variations = useSelect(
( select ) => {
const { getBlockVariations } = select( blocksStore );
return getBlockVariations( blockName, 'block' );
},
[ blockName ]
);
return <BlockVariationPicker variations={ variations } />;
};
In my edit function I'm adding:
{ MyBlockVariationPicker }
The block variation picker does not show.
I have already registered my bloc variations with scope block:
registerBlockVariation(
'my/testimonial',
[
{
name: 'testimonial-1',
title: 'Testimonial 1',
scope: ['block'],
attributes: {
example: 'testimonial-1'
},
},
{
name: 'testimonial-2',
title: 'Testimonial 2',
scope: ['block'],
attributes: {
example: 'testimonial-2'
},
}
]
);
The block variations should show in { MyBlockVariationPicker } but the don't. Unfortunately there isn't much documentation about this. How can we render the variations of a block using the Block Variation Picker as shown in the Github example?
Both the Columns and Query block use __experimentalBlockVariationPicker and its a really nice component/UI and I agree, it there aren't many examples of how to use it, most likely as its still 'experimental' and still likely to change.
I found that both the Columns and Query blocks display the BlockVariationPicker by checking if the current block (by clientId) contains any InnerBlocks; if there are none, the BlockVariationPicker is shown. When using this component in your own block, you will need some attribute or property to check whether or not a variation has been selected.
I've put together a basic/working example using the structure of your my/testimonial block + variations and based on how the BlockVariationPicker is implemented in Columns block:
import { get } from 'lodash';
import { useSelect } from '#wordpress/data';
import { registerBlockType, registerBlockVariation, store as blocksStore } from '#wordpress/blocks';
import { useBlockProps, __experimentalBlockVariationPicker as BlockVariationPicker } from '#wordpress/block-editor';
// Create our own BlockVariationPicker
const MyBlockVariationPicker = ({ name, setAttributes }) => { // Note: We need "name" and "setAttributes" from edit() props
const { blockType, variations, defaultVariation } = useSelect(
(select) => {
const { getBlockVariations, getBlockType, getDefaultBlockVariation } = select(blocksStore);
return {
blockType: getBlockType(name),
defaultVariation: getDefaultBlockVariation(name, 'block'),
variations: getBlockVariations(name, 'block')
};
},
[name]
);
return <BlockVariationPicker
variations={variations}
icon={get(blockType, ['icon', 'src'])}
label={get(blockType, ['title'])}
onSelect={(nextVariation = defaultVariation) => {
if (nextVariation.attributes) {
setAttributes(nextVariation.attributes); // Use setAttributes to set the selected variation attributes
}
}}
/>;
};
// Register the Block Variations
registerBlockVariation(
'my/testimonial',
[
{
name: 'testimonial-1',
title: 'Testimonial 1',
icon: 'admin-comments', // Added icon so the variation is visibly different (optional)
scope: ['block'],
attributes: {
example: 'testimonial-1'
},
isDefault: true
},
{
name: 'testimonial-2',
title: 'Testimonial 2',
icon: 'admin-links',
scope: ['block'],
attributes: {
example: 'testimonial-2'
},
}
]
);
registerBlockType('my/testimonial', {
title: 'My Testimonial',
keywords: ['testimonial'],
icon: 'admin-post',
attributes: {
example: {
type: "string", // no default set, example is "undefined"
}
},
edit(props) {
const { attributes, setAttributes } = props;
// If example is undefined, show Variation Picker
if (attributes.example === undefined) {
return (
<MyBlockVariationPicker {...props} />
);
}
// Otherwise show the Editor
return (<div {...useBlockProps()}><h2>{attributes.example}</h2></div>);
},
save: ({ attributes }) => {
return <div {...useBlockProps.save()}><h2>{attributes.example}</h2></div>;
}
})
If you build the above javascript, the resulting block allows you to pick from the two variations on insertion:

How to render one object from map instead all of them?

I have a profile object in the state of my react app, which contains an array of 6 objects.
I want to be able to render these objects separately.
{
this.state.profiles.map(profile => (
<div key={profile.mainCompanyID}>
{profile.name}
{profile.description}
</div>
))
}
The code above will display all 6 names/descriptions. But I want the power to be able to only map through one of the objects in the array, not all of them.
Any ideas?
filter the array before map
renderBasedOnId = id =>{
const { profiles } = this.state
return profiles.filter(x => x.id === id).map(item => <div>{item.name}</div>)
}
render(){
return this.renderBasedOnId(this.state.selectedId) //just an example of usage
}
You can filter out the data and then apply map.
Working Example - https://codesandbox.io/s/funny-shirley-q9s9j
Code -
function App() {
const profileData = [
{ id: 1, name: "Tom" },
{ id: 2, name: "Dick" },
{ id: 3, name: "Harry" },
{ id: 4, name: "Nuts" }
];
const selectedProfile = profileData.filter(x => x.name === "Harry");
return (
<div className="App">
<h1>Test Filter and map in jsx</h1>
{selectedProfile.map(x => (
<li>
{x.id} - {x.name}
</li>
))}
</div>
);
}
okay you can do it this way
{
this.state.profiles.map(profile => {
if (profile.mainCompanyID === id) { // id you want to match to
return (
<div key={profile.mainCompanyID}>
{profile.name}
{profile.description}
</div>)
} else {
return null
}
})
}
Hope it helps
{ this.state.profiles.map((profile, key) => {
(key===0)
?
return(
<div key={profile.key}>
<p>Name:{profile.name}</p>
<p>Description:{profile.description}</p>
</div>
)
:
return null;
})
}

Shuffle.js implementation with React.js

I'm trying to activate shuffle.js component functionality (search, filter and sort) with react.js. However, the documentation on the website is very limited. I know that I need to add a search input and some buttons to do what I want, yet I'm not sure how to connect my search box input and other button events to manipulate the photogrid (or other elements within a container) that is being rendered by react.
I have imported shuffle.js as node module and initialised it on the react page. The basic code that they provide seems to be working and displays the photo grid, however, that's pretty much it. I also want to implement the search, filtering and sorting functionality but there isn't documentation on how to do that in react.js. The code below shows the photogrid implementation but nothing else.
import React, {Component} from "react";
import Shuffle from 'shufflejs';
class PhotoGrid extends React.Component {
constructor(props) {
super(props);
const grayPixel = 'data:image/gif;base64,R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==';
const blackPixel = 'data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=';
const greenPixel = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mO02Vz4HwAE9AJhcLBN6AAAAABJRU5ErkJggg==';
this.state = {
photos: [{
id: 4,
src: grayPixel
},
{
id: 5,
src: blackPixel
},
{
id: 6,
src: greenPixel
},
],
searchTerm: '',
sortByTitle: '',
sortByDate: '',
sortByPopularity: '',
filterCategory: ''
};
this.filters = {
cat1: [],
cat2: [],
};
this.wb = this.props.dataWB;
this.element = React.createRef();
this.sizer = React.createRef();
this._handleSearchKeyup = this._handleSearchKeyup.bind(this);
this._handleSortChange = this._handleSortChange.bind(this);
this._handleCategory1Change = this._handleCategory1Change.bind(this);
this._handleCategory2Change = this._handleCategory2Change.bind(this);
this._getCurrentCat1Filters = this._getCurrentCat1Filters.bind(this);
this._getCurrentCat2Filters = this._getCurrentCat2Filters.bind(this);
}
/**
* Fake and API request for a set of images.
* #return {Promise<Object[]>} A promise which resolves with an array of objects.
*/
_fetchPhotos() {
return new Promise((resolve) => {
setTimeout(() => {
resolve([{
id: 4,
username: '#stickermule',
title:'puss',
date_created: '2003-09-01',
popularity: '233',
category1:'animal',
category2:'mammals',
name: 'Sticker Mule',
src: 'https://images.unsplash.com/photo-1484244233201-29892afe6a2c?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=800&h=600&fit=crop&s=14d236624576109b51e85bd5d7ebfbfc'
},
{
id: 5,
username: '#prostoroman',
date_created: '2003-09-02',
popularity: '232',
category1:'industry',
category2:'mammals',
title:'city',
name: 'Roman Logov',
src: 'https://images.unsplash.com/photo-1465414829459-d228b58caf6e?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=800&h=600&fit=crop&s=7a7080fc0699869b1921cb1e7047c5b3'
},
{
id: 6,
username: '#richienolan',
date_created: '2003-09-03',
popularity: '231',
title:'nature',
category1:'art',
category2:'insect',
name: 'Richard Nolan',
src: 'https://images.unsplash.com/photo-1478033394151-c931d5a4bdd6?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=800&h=600&fit=crop&s=3c74d594a86e26c5a319f4e17b36146e'
}
]);
}, 300);
});
}
_whenPhotosLoaded(photos) {
return Promise.all(photos.map(photo => new Promise((resolve) => {
const image = document.createElement('img');
image.src = photo.src;
if (image.naturalWidth > 0 || image.complete) {
resolve(photo);
} else {
image.onload = () => {
resolve(photo);
};
}
})));
}
_handleSortChange(evt) {
var value = evt.target.value.toLowerCase();
function sortByDate(element) {
return element.getAttribute('data-created');
}
function sortByPopularity(element) {
return element.getAttribute('data-popularity');
}
function sortByTitle(element) {
return element.getAttribute('data-title').toLowerCase();
}
let options;
if (value == 'date-created') {
options = {
reverse: true,
by: sortByDate,
};
} else if (value == 'title') {
options = {
by: sortByTitle,
};
} else if (value == 'popularity') {
options = {
reverse: true,
by: sortByPopularity,
};
} else if (value == 'default') {
this.shuffle.filter('all');
} else {
options = {};
}
this.shuffle.sort(options);
};
_getCurrentCat1Filters = function () {
return this.filters.cat1.filter(function (button) {
return button.classList.contains('active');
}).map(function (button) {
console.log('button value: '+button.getAttribute('data-value'))
return button.getAttribute('data-value');
});
};
_getCurrentCat2Filters = function () {
return this.filters.cat2.filter(function (button) {
return button.classList.contains('active');
}).map(function (button) {
console.log('button value: '+button.getAttribute('data-value'))
// console.log('button value: '+button.getAttribute('data-value'))
return button.getAttribute('data-value');
});
};
_handleCategory1Change = function (evt) {
var button = evt.currentTarget;
console.log(button)
// Treat these buttons like radio buttons where only 1 can be selected.
if (button.classList.contains('active')) {
button.classList.remove('active');
} else {
this.filters.cat1.forEach(function (btn) {
btn.classList.remove('active');
});
button.classList.add('active');
}
this.filters.cat1 = this._getCurrentCat1Filters();
console.log('current cat contains : '+this.filters.cat1);
this.filter();
};
/**
* A color button was clicked. Update filters and display.
* #param {Event} evt Click event object.
*/
_handleCategory2Change = function (evt) {
var button = evt.currentTarget;
// Treat these buttons like radio buttons where only 1 can be selected.
if (button.classList.contains('active')) {
button.classList.remove('active');
} else {
this.filters.cat2.forEach(function (btn) {
btn.classList.remove('active');
});
button.classList.add('active');
}
this.filters.cat2 = this._getCurrentCat2Filters();
console.log('current cat contains : '+this.filters.cat2);
this.filter();
};
filter = function () {
if (this.hasActiveFilters()) {
this.shuffle.filter(this.itemPassesFilters.bind(this));
} else {
this.shuffle.filter(Shuffle.ALL_ITEMS);
}
};
itemPassesFilters = function (element) {
var cat1 = this.filters.cat1;
var cat2 = this.filters.cat2;
var cat1 = element.getAttribute('data-category1');
var cat2 = element.getAttribute('data-category2');
// If there are active shape filters and this shape is not in that array.
if (cat1.length > 0 && !cat1.includes(cat1)) {
return false;
}
// If there are active color filters and this color is not in that array.
if (cat2.length > 0 && !cat2.includes(cat2)) {
return false;
}
return true;
};
/**
* If any of the arrays in the `filters` property have a length of more than zero,
* that means there is an active filter.
* #return {boolean}
*/
hasActiveFilters = function () {
return Object.keys(this.filters).some(function (key) {
return this.filters[key].length > 0;
}, this);
};
_handleSearchKeyup(event) {
this.setState({
searchTerm: event.target.value.toLowerCase()
}, () => {
this.shuffle.filter((element) => {
return element.dataset.name.toLowerCase().includes(this.state.searchTerm) || element.dataset.username.toLowerCase().includes(this.state.searchTerm);
})
})
}
componentDidMount() {
// The elements are in the DOM, initialize a shuffle instance.
this.shuffle = new Shuffle(this.element.current, {
itemSelector: '.js-item',
sizer: this.sizer.current,
});
// Kick off the network request and update the state once it returns.
this._fetchPhotos()
.then(this._whenPhotosLoaded.bind(this))
.then((photos) => {
this.setState({
photos
});
});
}
componentDidUpdate() {
// Notify shuffle to dump the elements it's currently holding and consider
// all elements matching the `itemSelector` as new.
this.shuffle.resetItems();
}
componentWillUnmount() {
// Dispose of shuffle when it will be removed from the DOM.
this.shuffle.destroy();
this.shuffle = null;
}
render() {
return (
<div>
<div id='searchBar'>
<input type="text" className='js-shuffle-search' onChange={ this._handleSearchKeyup } value={ this.state.searchTerm } />
</div>
<div id='gridActions'>
<h2>Filter By cat 1</h2>
<button onClick={ this._handleCategory1Change } value='all'>All</button>
<button onClick={ this._handleCategory1Change } value='art'>Art</button>
<button onClick={ this._handleCategory1Change } value='industry'>Industry</button>
<button onClick={ this._handleCategory1Change } value='animal'>Animal</button>
<h2>Filter By cat 2</h2>
<button onClick={ this._handleCategory2Change } value='all'>All</button>
<button onClick={ this._getCurrentCat1Filters } value='mammals'>Mammals</button>
<button onClick={ this._getCurrentCat2Filters } value='insects'>Insects</button>
<h2>Sort By</h2>
<button onClick={ this._handleSortChange } value='default'>Default</button>
<button onClick={ this._handleSortChange } value='date-created'>By Date</button>
<button onClick={ this._handleSortChange } value='title'>By Title</button>
<button onClick={ this._handleSortChange } value='popularity'>By Popularity</button>
</div>
<div ref={ this.element } id='grid' className="row my-shuffle-container shuffle"> {
this.state.photos.map(image =>
<PhotoItem { ...image } />)}
<div ref={ this.sizer } className="col-1#xs col-1#sm photo-grid__sizer"></div>
</div>
</div>
);
}
}
function PhotoItem({id, src, category1, category2, date_created, popularity, title, name, username }) {
return (
<div key={id}
className="col-lg-3 js-item"
data-name={name}
data-title={title}
data-date-created={date_created}
data-popularity={popularity}
data-category1={category1}
data-cetagory2={category2}
data-username={username}>
<img src={src} style={{width : "100%",height :"100%"}}/>
</div>
)
}
export default PhotoGrid;
The photogrid right now does nothing, just displays photos which I'm unable to search, filter and sort.
Only judging by the documentation, I haven't tried it yet, but should work.
The instance of Shuffle has a filter method, which takes a string, or an array of strings, to filter the elements by "groups", or a callback function to perform more complicated search. You should call this.shuffle.filter after updating the state of your component, i.e.:
_handleSearchKeyup(event){
this.setState({searchTerm : event.target.value}, () => {
this.shuffle.filter((element) => { /* use this.state.searchTerm to return matching elements */ } );
})
}
Edited after building a fiddle.
The callback function looks at data-name and data-username attributes to check if they contain the search string
_handleSearchKeyup(event){
this.setState({searchTerm : event.target.value.toLowerCase()}, () => {
this.shuffle.filter((element) => {
return (
element.dataset.name.toLowerCase().includes(this.state.searchTerm) ||
element.dataset.username.toLowerCase().includes(this.state.searchTerm)
);
})
})
}
For the above to work you also need to add these attributes to the DOM nodes, so update the PhotoItem component:
function PhotoItem({ id, src, name, username }) {
return (
<div key={id}
className="col-md-3 photo-item"
data-name={name}
data-username={username}>
<img src={src} style={{width : "100%",height :"100%"}}/>
</div>
)
}
In opposition to pawel's answer I think that this library operates on DOM. It makes this not react friendly.
Classic input handlers saves values within state using setState method. As an effect to state change react refreshes/updates the view (using render() method) in virtual DOM. After that react updates real DOM to be in sync with virtual one.
In this case lib manipulates on real DOM elements - calling render() (forced by setState()) will overwritte earlier changes made by Shuffle. To avoid that we should avoid using setState.
Simply save filter and sorting parameters directly within component instance (using this):
_handleSearchKeyup(event){
this.searchTerm = event.target.value;
this.shuffle.filter((element) => { /* use this.searchTerm to return matching elements */ } );
}
Initialize all the params (f.e. filterCategories, searchTerm, sortBy and sortOrder) in constructor and use them in one this.shuffle.filter() call (second parameter for sort object) on every parameter change. Prepare some helper to create combined filtering function (mix of filtering and searching), sorting is far easier.
setState can be used for clear all filters button - forced rerendering - remember to clear all parameters within handler.
UPDATE
For sorting order declare
this.reverse = true; // in constructor
this.orderBy = null;
handlers
_handleSortOrderChange = () => {
this.reverse = !this.reverse
// call common sorting function
// extracted from _handleSortChange
// this._commonSortingFunction()
}
_handleSortByChange = (evt) => {
this.orderBy = evt.target.value.toLowerCase();
// call common sorting function
// extracted from _handleSortChange
// this._commonSortingFunction()
}
_commonSortingFunction = () => {
// you can declare sorting functions in main/component scope
let options = { reverse: this.reverse }
const value = this.orderBy;
if (value == 'date-created') {
options.by = sortByDate // or this.sortByDate
} else if (value == 'title') {
options.by = sortByTitle
//...
//this.shuffle.sort(options);
You can also store ready options sorting object in component instance (this.options) updated by handlers. This value can be used by _commonSortingFunction() to call this.shuffle.sort but also by filtering functions (second parameter).
reversing button (no need to bind)
<button onClick={this._handleSortOrder}>Reverse order</button>
UPDATE 2
If you want to work with 'normal' react, setState you can move (encapsulate) all the filtering (searchBar, gridActions) into separate component.
State update will force rerendering only for 'tools', not affecting elements managed in real DOM by shuffle (parent not rerendered). This way you can avoid manual css manipulations ('active') by using conditional rendering (plus many more possibilities - list active filters separately, show order asc/desc, show reset only when sth changed etc.).
By passing this.shuffle as prop you can simply invoke search/filter/sort in parent.

how to return a string from map using reactjs

i have map function which i want to return html string and join to another string and display on modal. i am passing it through function params. The map seems to return array of objects and i am unsure how to convert this to string
let found = [];
found.push({source:'x',destination:'x',protocol:'x',ports:'x'});
found.push({source:'y',destination:'y',protocol:'y',ports:'y'});
let flows = found.map(function(name, index){
return <li key={index.toString()} >{name.source.toString()} {name.destination.toString()} {name.protocol.toString()} {name.ports.toString()}</li>
}).reduce((prev, curr) => [prev, ', ', curr]);
showConfirm('',`You are about to approve a local market exemption. Click to confirm! <ul>${flows}</ul>`,()=>{submitApprove(args);self.props.clearModal()},()=>self.props.clearModal(),confirmOptions);
showConfirm open the following modal
class ModalConfirm extends Component {
...
render() {
let yesDisabled = false;
let yesText = 'Yes';
let noDisabled = false;
let noText = 'No';
if (typeof this.props.modals.confirmOptions !== 'undefined') {
yesDisabled = (typeof this.props.modals.confirmOptions.confirmYesDisabled !== 'undefined') ? this.props.modals.confirmOptions.confirmYesDisabled : false;
yesText = (typeof this.props.modals.confirmOptions.confirmYesText !== 'undefined') ? this.props.modals.confirmOptions.confirmYesText : 'Yes';
noDisabled = (typeof this.props.modals.confirmOptions.confirmNoDisabled !== 'undefined') ? this.props.modals.confirmOptions.confirmNoDisabled : false;
noText = (typeof this.props.modals.confirmOptions.confirmNoText !== 'undefined') ? this.props.modals.confirmOptions.confirmNoText : 'No';
}
let bodyWrapper = 'Are you sure you want to confirm?';
if (typeof this.props.modals.confirmMsg === 'string') {
if (this.props.modals.confirmMsg.length > 0 || this.props.modals.confirmMsg !== null) {
bodyWrapper = Parser(this.props.modals.confirmMsg);
}
bodyWrapper = <div className="row-fluid" style={{ wordWrap: 'break-word' }} dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(bodyWrapper) }} />;
}
return (
<Modal id="myModalReduxConfirm" className="modal fade" role="dialog" show={this.props.modals.showConfirm}>
{this.renderTitle()}
<Modal.Body>
{bodyWrapper}
</Modal.Body>
<Modal.Footer>
<Button bsClass="btn btn-success btn-sm" onClick={this.props.handleConfirmYes} disabled={yesDisabled}>{yesText}</Button>
<Button bsClass="btn btn-default btn-sm" onClick={this.props.handleConfirmNo} disabled={noDisabled}>{noText}</Button>
</Modal.Footer>
</Modal>
);
}
}
...
result
You are about to approve . Click to confirm! <ul>[object Object],, ,[object Object],, ,[object Object],, ,[object Object],, ,[object Object],, ,[object Object]</ul>
UPDATE
i changed to the following now it does not pass string but just displays object.
self.props.showConfirm('',() => flows,()=>{submitApprove(args);self.props.clearModal()},()=>self.props.clearModal(),confirmOptions);
console.log(this.props.modals.confirmMsg());
displays
{store: undefined, title: "", body: ƒ, show: true, handleConfirmYes: ƒ, …}body: ƒ ()clearModal: ƒ ()confirmOptions: {confirmYesText: "Confirm", confirmYesDisabled: false, confirmNoText: "Cancel", confirmNoDisabled: false}handleConfirmNo: ƒ ()handleConfirmYes: ƒ ()modals: {showConfirm: true, confirmTitle: "", confirmMsg: ƒ, handleConfirmYes: ƒ, handleConfirmNo: ƒ, …}show: truestore: undefinedtitle: ""__proto__: Object
main.bundle.js:200439
[object Object][object Object][object Object][object Object][object Object][object Object]
You can see the body shows f and when i log it, it does not interpret the object
Your JSX elements cannot be rendered in arrays that mixes them with strings. And using reduce with JSX will often lead to headaches.
A quick solution to your problem would be to map your array directly and check if the element is not the last of the array to add the , :
const found = [
{
name: {
source: "eirglerk",
destination: "zlekjrnzi"
}
},
{
name: {
source: "lkcyukyuk",
destination: "uylcl"
}
},
{
name: {
source: "trutrurt",
destination: "culcul"
}
}
]
const App = props => (
<p>
You are about to approve . Click to confirm!
<ul>
{found.map(({ name, index }, i) =>
<li key={index}> {name.source} {name.destination} {name.protocol} {name.ports}{i !== found.length - 1 && ','}</li>
)}
</ul>
</p>
)
ReactDOM.render(<App/>, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.5.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.5.2/umd/react-dom.production.min.js"></script>
<div id='root'>
Or by keeping your code, you can do the following if you encapsulter the comma into a JSX element :
const found = [
{
name: {
source: "eirglerk",
destination: "zlekjrnzi"
}
},
{
name: {
source: "lkcyukyuk",
destination: "uylcl"
}
},
{
name: {
source: "trutrurt",
destination: "culcul"
}
}
]
const App = props => (
<p>
You are about to approve . Click to confirm!
<ul>
{found
.map(({ name, index }) => <li key={index}> {name.source} {name.destination} {name.protocol} {name.ports}</li>)
.reduce((prev, elem) => [prev, <span>,</span>, elem])}
</ul>
</p>
)
ReactDOM.render(<App/>, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.5.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.5.2/umd/react-dom.production.min.js"></script>
<div id='root'>
You will need to ass some CSS to keep your commas inline though
Assume if I have found1, found2 and found3, based on the clicked button change I switch and map to loop the structure?
const found1 = [
{
name: {
source: "eirglerk",
destination: "zlekjrnzi"
}
},
{
name: {
source: "lkcyukyuk",
destination: "uylcl"
}
},
{
name: {
source: "trutrurt",
destination: "culcul"
}
}
]
const found2 = [
{
name: {
source: "eirglerk",
destination: "zlekjrnzi"
}
},
{
name: {
source: "lkcyukyuk",
destination: "uylcl"
}
},
{
name: {
source: "trutrurt",
destination: "culcul"
}
}
]
var foundtype = "found1"
JSON.foundtype.map(() => {
//Load found1 data
}

Some problems with vue router reload and components updating

I'm actually working on a VueJs project where I meet some problems.
The 1st is that when I'm reloading page using parameters, I'm throwed back to the root page and I don"t understand why. I'm actually using the vue webpack template and lazy loading for my routes templates.
The 2nd problem is that I'm making a page of comments for my project who's are generated from a array of object. When the user has done posting his comment a new array is loaded from the database and put in the data of the parent as following :
parentComponent -> data -> array of objects -> each object = a component
So I used this.$set to put datas but it has not worked,I used a foreach loop and now I'm just replacing the array like this :
oldArray = new Array
But it doesn't work and my child components don't want to update even with forceUpdate().Maybe it's something very silly but I didn't found the answer.
If someone could have the answer that would be very nice to share it.
Here is my parent code:
<template>
<div class='comments'>
<div v-if="comments.length > 0" class="comments__block">
<comment-block v-for="comment in comments" :key="comment.Id" :comment.sync="comment" :updateHandler="loadComments"></comment-block>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'Comments',
data () {
return {
page: 1,
comments: [],
numberPages: 0,
resultEmpty: false
}
},
components: {
'comment-block': commBlock,
},
computed: {
methods: {
loadComments (nextPage = 1) {
if (isLogged) {
axios
.get(`API query if logged)
.then(response => {
if (Success) {
if (Comments) {
this.comments = Comments
this.resultEmpty = false
} else {
this.resultEmpty = true
}
}
})
.catch(e => {
console.log(e)
})
} else {
axios
.get(`url if not logged`)
.then(response => {
if (Success) {
if (Comments) {
this.comments = []
Comments.forEach(c => {
this.comments.push(c)
})
this.resultEmpty = false
} else {
this.resultEmpty = true
}
}
})
.catch(e => {
console.log(e)
})
}
}
},
mounted () {
this.loadComments()
},
watch: {
'$route' (to, from) {
this.loadComments()
}
},
}
</script>
And for the children
<template>
<div class='comment-block'>
<div class="comment-block__header">
<div class="comment-block__username">
{{userName}}
</div>
<div v-if="isLogged" class="comment-block__likes">
<input v-model="like" class="comment-block__radio" type="radio" name="likes" id="like" value="Y"/><label class="comment-block__radio-label comment-block__radio-label--like" for="like" v-bind:class="{'comment-block__radio-label--liked': like == 'Y'}" #click="toggle('Y')">{{nbLikes}}<v-icon large color="white">keyboard_arrow_up</v-icon></label>
<input v-model="like" class="comment-block__radio" type="radio" name="likes" id="dislike" value="N"/><label class="comment-block__radio-label comment-block__radio-label--dislike" for="dislike" v-bind:class="{'comment-block__radio-label--liked': like == 'N'}" #click="toggle('N')">{{nbDislikes}}<v-icon large color="white">keyboard_arrow_down</v-icon></label>
<input v-model="like" class="comment-block__radio" type="radio" name="likes" id="disapproved" value="D"/><label class="comment-block__radio-label comment-block__radio-label--disapproved" for="disapproved" v-bind:class="{'comment-block__radio-label--liked': like == 'D'}" #click="toggle('D')">{{nbDisapproved}}<v-icon large color="white">report</v-icon></label>
</div>
<div v-else class="comment-block__likes">
<button class="comment-block__button" #click="displayMessage = ! displayMessage" id="like"><p class="comment-block__radio-p comment-block__radio-p--like" for="like">{{comment.NbLikes}}<v-icon large color="white">keyboard_arrow_up</v-icon></p></button>
<button class="comment-block__button" #click="displayMessage = ! displayMessage" id="dislike"><p class="comment-block__radio-p comment-block__radio-p--dislike" for="dislike">{{comment.NbDislikes}}<v-icon large color="white">keyboard_arrow_down</v-icon></p></button>
<button class="comment-block__button" #click="displayMessage = ! displayMessage" id="disapproved"><p class="comment-block__radio-p comment-block__radio-p--disapproved" for="disapproved">{{comment.NbDisapproved}}<v-icon large color="white">report</v-icon></p></button>
</div>
</div>
<div class="comment-block__body">
<div cols="12">
<div class="comment-block__commentary">
{{commentary }}
</div>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'Comments',
data () {
return {
id: this.comment.idComment,
userName: this.comment.UserName,
commentary: this.comment.Commentary,
nbLikes: this.comment.NbLikes,
nbDislikes: this.comment.NbDislikes,
nbDisapproved: this.comment.NbDisapproved,
displayedLikes: this.nbLikes,
displayedDislikes: this.nbDislikes,
displayedDisapproved: this.nbDisapproved,
lastLike: this.comment.LastLike,
like: this.comment.LastLike,
displayMessage: false
}
},
props: {
comment: {type: Object, required: true},
updateHandler: {type: Function, required: true}
},
computed: {
isLogged () {
if (this.$store.state.jwt.token) {
return true
}
return false
}
},
methods: {
submitLike () {
var payload = {
IdComment: this.id,
Like: this.like
}
var header = {
headers: {
Authorization: this.$store.state.jwt.token
}
}
axios
.post('someUrl', payload, header)
.then(resp => {
if (Success) {
this.updateHandler()
}
})
.catch(e => {
console.log(e)
})
},
toggle (value) {
if (value === this.like) {
this.like = ''
}
}
},
watch: {
like (newVal, oldVal) {
if (newVal !== oldVal) {
this.submitLike()
switch (oldVal) {
case 'Y' : this.nbLikes--
break
case 'N' : this.nbDislikes--
break
case 'D' : this.nbDisapproved--
break
}
switch (newVal) {
case 'Y' : this.nbLikes++
break
case 'N' : this.nbDislikes++
break
case 'D' : this.nbDisapproved++
break
}
}
if (oldVal === newVal) {
this.like = ''
}
},
comment (newVal) {
this.$forceUpdate()
}
}
}
</script>
PS: Sorry for my bad english if it is.

Resources