Wordpress Gutenberg autocomplete - saving attributes - wordpress

I want to develop a custom block that will let the user pick some information from autocomplete. I manage to create the autocomplete component on edit function.
A user can select an item but i don't know how to handle the attribute save.
I'm trying to save the selected item as attribute package_name. I created the onChange function on Autocomplete component but event.target.value is undefined.
Here is my code from block.js
const { __ } = wp.i18n; // Import __() from wp.i18n
const { AlignmentToolbar,
BlockControls,
registerBlockType } = wp.blocks;
const { RichText } = wp.editor;
const { Autocomplete, } =wp.components;
const MyAutocomplete = () => {
const autocompleters = [
{
name: 'fruit',
triggerPrefix: '#',
options: [
{ name: 'Apple', id: 1 },
{ name: 'Orange', id: 2 },
{ name: 'Grapes', id: 3 },
{ name: 'test', id: 4 },
],
getOptionLabel: option => (
<span>
{ option.name }
</span>
),
getOptionKeywords: option => [ option.name ],
isOptionDisabled: option => option.name === 'Grapes',
getOptionCompletion: option => (
<abbr title={ option.name }>{ option.name }</abbr>
),
}
];
function onChangeAuto(newContent){
console.log('autocompletexx '+newContent);
}
function onSelectAuto(event){
console.log(event.target);
console.log( event.target.value);
}
return (
<div>
<Autocomplete completers={ autocompleters }>
{ ( { isExpanded, listBoxId, activeId } ) => (
<div
contentEditable
suppressContentEditableWarning
aria-autocomplete="list"
aria-expanded={ isExpanded }
aria-owns={ listBoxId }
aria-activedescendant={ activeId }
onChange={onChangeAuto }
onSelect={onSelectAuto}
>
</div>
) }
</Autocomplete>
<p class="autocomplete_p">Type # for triggering the autocomplete.</p>
</div>
);
};
registerBlockType( 'residence-gutenberg-block/membership-package-settings', {
title: __( 'Residence - Membership Package Settings' ), // Block title.
icon: 'shield',
category: 'common',
keywords: [
__( 'membership-package-settings' ),
],
attributes:{
package_id:{
type:'string',
select:'p'
},
package_name:{
type:'string',
},
},
edit: function( props ) {
const { attributes: {package_name}, className,setAttributes,isSelected } = props;
return (
<div className={ props.className }>
<form>
<label className="wpresidence_editor_label">Current Package: {package_name}</label>
<MyAutocomplete></MyAutocomplete>
</form>
</div>
);
},
save: function( props ) {
// Rendering in PHP
return null;
},
} );

Passing down onChange, onSelect to the div element won't work, because these attributes are only applicable to the form field elements (as input, select, etc.).
I checked the documentation and the source code and didn't find any details or official approaches for dealing with the case.
However, I'm seeing two possible approaches for getting the selected value:
1. Using Autocomplete onReplace prop
Looking into the Autocomplete's source code, I noticed that onSelect callback invokes onReplace prop with the selected option as array. It may not fit all the cases, but you can give it a try! It may be enough for your case! You can try to add your handler to the onReplace as follows:
<Autocomplete
onReplace={ option => { console.log(option) } }
completers={ autocompleters }>
{ ( { isExpanded, listBoxId, activeId } ) => (
<div
contentEditable
suppressContentEditableWarning
aria-autocomplete="list"
aria-expanded={ isExpanded }
aria-owns={ listBoxId }
aria-activedescendant={ activeId }
/>
) }
</Autocomplete>
2. Listen for <div /> changes manually
You can add onInput, onBlur listeners to the <div />, having an uncontrolled react div component and when the div's value is changed then we can keep the changed value in your parent component's state.
Here's a great discussion, which describes these technique: React.js: onChange event for contentEditable
The good think is that there's already a plugin (based on this discussion) that can do it for you: react-contenteditable.
Firstly you have to convert your <MyAutocomplete /> component to be a statefull (not functional) and then:
import ContentEditable from 'react-contenteditable'
// Boilerplate ...
<Autocomplete completers={ autocompleters }>
{ ( { isExpanded, listBoxId, activeId } ) => (
<ContentEditable
html={this.state.html}
onChange={this.handleChange}
contentEditable
suppressContentEditableWarning
aria-autocomplete="list"
aria-expanded={ isExpanded }
aria-owns={ listBoxId }
aria-activedescendant={ activeId }
/>
) }
</Autocomplete>
Conclusion
I'm surprised that in the Autocomplete's documentation there aren't any details for this case. I guess it's because of the following statement (27.08.2018):
Gutenberg is being developed on GitHub, and you can try an early beta
version today from the plugin repository. Though keep in mind it’s not
fully functional, feature complete, or production ready.
However, one of both mentioned approaches above will help you, until they provide a complete API to work with their components.
I would suggest you to keep wrapping the Wordpress's <Autocomplete /> with your own component - in order to easily refactor your code later, when they release the complete API.
If you have questions, feel free to comment below.

Related

Customize Embedded Zoom Meeting SDK with Component View in 2.2.0

When working with Embedded Zoom Component, the Zoom SDK return an element which you need to place it inside an html element
the problem is how to resize and position the returned component inside my code after rendering
const client = ZoomMtgEmbedded.createClient();
function getSignature(e) {
e.preventDefault();
// ... some code to get the signature
startMeetingZoomMtgEmbedded(response.signature);
}
function startMeetingZoomMtgEmbedded(signature) {
let meetingSDKElement = document.getElementById('meetingSDKElement');
client.init({
debug: true,
zoomAppRoot: meetingSDKElement,
language: 'en-US',
customize: {
meetingInfo: ['topic', 'host', 'mn', 'pwd', 'telPwd', 'invite', 'participant', 'dc', 'enctype'],
toolbar: {
buttons: [
{
text: 'Custom Button',
className: 'CustomButton',
onClick: () => {
console.log('custom button');
}
}
]
}
}
});
client.join({
apiKey: apiKey,
signature: signature,
meetingNumber: meetingNumber,
password: passWord,
userName: userName,
userEmail: userEmail,
tk: registrantToken,
success: (success) => {
console.log('success');
},
error: (error) => {
console.log(error);
}
});
}
return (
<div className="App">
<main>
<h1>Zoom Meeting SDK Sample React</h1>
{/* For Component View */}
<div id="meetingSDKElement"></div>
<button onClick={getSignature}>Join Meeting</button>
</main>
</div>
);
So my question is how to modify the style and the position of the component before or after the rendering of the code by the Zoom SDK.
For Resizing , You will find details in the following documentation link :
Zoom Documentation for resizing component view
For Positioning, You will find details in the following documentation link :
Zoom Documentation for positioning component view
The only way to resize camera view is editing #ZOOM_WEB_SDK_SELF_VIDEO id. So, you have to edit other classes also to make buttons, containers and etc resize like camera view does, but it is totally buggy and i don't think it is a good idea pay all this effort to a workaround, besides that, in next versions maybe they bring built in properties to do this job.
Just for example, this is the result when you change #ZOOM_WEB_SDK_SELF_VIDEO:
#ZOOM_WEB_SDK_SELF_VIDEO {
width: 720%;
height: 480%;
}
In general way, you can modify the style and position of your component by using reactive CSS styling.
In zoom way you can use (zoom web meeting SDK)
(a) "popper: {}" properties for positioning elements
(b) "viewSizes: {}" properties for default meeting canvas size
(c) for styling use "id" and "class" for reactive CSS styling
popper use:
client.init({
...
customize: {
video: {
popper: {
anchorElement: meetingSDKElement,
placement: 'top'
}
},
}
...
})
viewSizes use:
client.init({
...
customize: {
video: {
viewSizes: {
default: {
width: 1000,
height: 600,
}
}
},
}
...
})

How to pass attributes to child Gutenberg blocks in InnerBlocks template?

I'm trying to build a custom set of column blocks using InnerBlocks and am having trouble passing attributes to block templates. To start, I scaffolded a new plugin using Create-Guten-Block. I then created two blocks, one to serve as a row container, and one for individual columns.
Here is a simplified version of how I'm creating the main container (with two columns hard-coded for testing):
registerBlockType( 'wabe/multi-column-block', {
title: __( 'Multi-Column' ),
icon: 'columns',
category: 'common',
keywords: [
__( 'columns' ),
__( 'grid' ),
],
attributes: {
layout: {
type: 'string',
},
},
edit: () => {
return (
<div>
{ /* inspector controls for choosing a layout will go here */ }
<InnerBlocks
template={ [
[ 'wabe/multi-column-column', { columnwidth: '6' }, [
[ 'core/paragraph', { content: 'Insert column content here.' } ],
],
],
[ 'wabe/multi-column-column', { columnwidth: '6' }, [
[ 'core/paragraph', { content: 'Insert column content here.' } ],
],
],
] }
/>
</div>
);
},
save: () => {
// This is a dynamic block
return (
<InnerBlocks.Content />
);
},
} );
... and how I'm creating the individual column block:
registerBlockType( 'wabe/multi-column-column', {
title: __( 'Multi-Column Column' ),
icon: 'columns',
category: 'common',
keywords: [
__( 'columns' ),
__( 'grid' ),
],
attributes: {
columnwidth: {
type: 'string',
default: '',
},
},
edit: ( props ) => {
return (
<div>
<p>Width: { props.attributes.columnwidth }</p>
<InnerBlocks />
</div>
);
},
save: () => {
return (
<InnerBlocks.Content />
);
},
} );
I'll have a custom select for users to choose a column layout, and using that, will determine how many columns to include, and pass the "columnwidth" attribute to each column block. This will tell each column what CSS class to use.
The problem is no matter what I try, that "columnwidth" attribute just will not be passed to the column block. In fact, I can't get it to pass anything, even "className".
I've looked at the code for core columns/column blocks, and can't see what I'm doing wrong. Everything has gone great up until this giant roadblock. Any tips are appreciated.
The block code example you have provided shows you are setting up the InnerBlocks fine. The issue may be a simple oversight that you have missed importing the required dependancy of <InnerBlocks>.
I was able to build your block code successfully once I added the required imports:
import { registerBlockType } from '#wordpress/blocks';
import { __ } from '#wordpress/i18n';
import { InnerBlocks} from '#wordpress/block-editor';
Here is the result:
The code editor shows that your attributes are being passed and being saved.
If you are still encountering an issue, double check that you are clearing your browser cache before retesting and also check the browser console to see if any errors are present.
As you mentioned you have already reviewed the Gutenberg code for Columns/Column, a tip would be to look at how they do variations for creating the options in your Inspector Control. Hope this gets you back on track with creating your block.

Add value to existing WP Block Editor setting

I would like to add a 33% to the Wordpress Block "Button". So far it has 25%,50%,75% and 100%. Is it possible to insert my new value into the existing width selector?
I'm guessing Block Filters are the way to go.
I think I also found the way to get the settings object which might then help me to find out what I need to overwrite. However simply adding this code to my admin.js does not produce any output. Where would I need to load this?
const filterBlocks = (settings) => {
if (settings.name !== 'core/buttons') {
return settings
}
console.log(settings);
return settings;
}
Quick solution: Add a custom CSS class in the Buttons' block properties under "Advanced > Additional CSS class(es)" then define the custom width in your theme style.css
Detailed solution:
By using wp.hooks.addFilter() you can add a new control to the Button block with as many extra custom width options as you need. The Button blocks preset widths are defined within the function WidthPanel() of the blocks edit.js function:
function WidthPanel( { selectedWidth, setAttributes } ) {
...
return (
...
<ButtonGroup aria-label={ __( 'Button width' ) }>
{ [ 25, 50, 75, 100 ].map( ( widthValue ) => {
...
}
}
To add a new width value of 33% to the block, we need to add our own new button control to the InspectorControls and then use wp.hooks.addFilter() to add this to the existing core Button block, eg:
index.js
import { createHigherOrderComponent } from '#wordpress/compose';
import { Fragment } from '#wordpress/element';
import { InspectorControls } from '#wordpress/block-editor';
import { PanelBody, Button } from '#wordpress/components';
const withInspectorControls = createHigherOrderComponent((BlockEdit) => {
return (props) => {
const { setAttributes } = props;
let widthValue = 33; // must be a number
return (
<Fragment>
<BlockEdit {...props} />
<InspectorControls>
<PanelBody title="Custom Width">
<Button
key={widthValue}
isSmall
variant={widthValue}
onClick={() => setAttributes({ width: widthValue })}
>
{widthValue}%
</Button>
</PanelBody>
</InspectorControls>
</Fragment>
);
};
}, 'withInspectorControl');
wp.hooks.addFilter(
'editor.BlockEdit',
'core/button',
withInspectorControls
);
Next, a new additional css style needs to be added that (matches the existing width presets structure) for the new custom width, eg:
style.scss
$blocks-block__margin: 0.5em;
&.wp-block-button__width-33 {
width: calc(33.33% - #{ $blocks-block__margin });
}
And there you have it..
The easiest way to put all the code above together/working is to create your own Gutenberg block (and that in itself can be challenging if you aren't familiar with the process or ReactJS). I too have come across similiar challenges with Gutenberg, so I wanted to provide a detailed solution for this kind of issue that works.

Change parent component background on hover in reactJS

I have following React code
Code
What I would like is to when I hover my "E-commerce" picture App component background should change on "E-commerce" picture background.
So respectively and for other pictures.
I will be very grateful if you help me solve this problem.
Context, according to the React docs, should be used only for truly global state like current user or theme. Using context for components makes them less reusable.
updated code
Your component tree is App -> SolutionBox -> SolutionItem.
You want to "react" to an event in SolutionItem in App but there is SolutionBox inbetween them so you have to thread the event thru SolutionBox to App.
Step 1
Add a prop to SolutionItem called on OnHover, this will be a function call back that any parent component can use to react to changes.
function SolutionsSectionBoxItem({ solutionIMG, onHover }) {
let callOnHover = state => {
if (_.isFunction(onHover)) {
onHover(state);
}
};
return (
<div className="solutions-section-item-box">
<img
src={solutionIMG}
alt=""
onMouseEnter={() => {
callOnHover(true);
}}
onMouseLeave={() => {
callOnHover(false);
}}
className="solutions-section-item-img"
/>
</div>
);
}
Step 2
Add a prop to SolutionBoxItem called on BGChanged, this will again be a function call back that will be called when any solutionitem onhover happens. This function will take a menuName string and pass either the current menu name or default.
function SolutionsSectionBox({ onBGChanged }) {
let callBGChanged = menuName => {
if (_.isFunction(onBGChanged)) {
onBGChanged(menuName);
}
};
return (
<div className="solutions-section-box-box">
<SolutionItem
solutionIMG={Ecommerce}
onHover={state => {
callBGChanged(state === true ? "Ecommerce" : "default");
}}
/>
<SolutionItem
solutionIMG={SalesMarketing}
onHover={state => {
callBGChanged(state === true ? "SalesMarketing" : "default");
}}
/>
<SolutionItem
solutionIMG={Analytics}
onHover={state => {
callBGChanged(state === true ? "Analytics" : "default");
}}
/>
<SolutionItem
solutionIMG={Middleware}
onHover={state => {
callBGChanged(state === true ? "Middleware" : "default");
}}
/>
</div>
);
}
Step 3
In the App component listen for the changes. In here we now set state when ever the mouse enters or leaves a solution item. From here you have to change the background, you are using css to control the background url, this will be harder since you now need css class for each background type. You could use the bgImage state value to change the name of the extra css className like 'AppSalesMarketing', 'AppEcommerce', etc.
export default function App() {
const [bgImage, setbgImage] = useState(E);
const onBGChanged = menuName => {
setbgImage(menuName);
};
return (
<div className={`App ${bgImage === "default" ? "" : `App${bgImage}`}`}>
<SolutionBox onBGChanged={onBGChanged} />
</div>
);
}
In CSS
Leave the original App class but based on the bgImage value add an additional one using the name of the bgImage + App like below to cascade down the updated background-image value.
.AppEcommerce {
background-image: url(https://placekitten.com/600/600);
}
.AppSalesMarketing {
background-image: url(https://placekitten.com/500/800);
}
.AppAnalytics {
background-image: url(https://placekitten.com/800/500);
}
.AppMiddleware {
background-image: url(https://placekitten.com/700/700);
}
Extra
I added lodash to test that the incoming props are functions before I call them, it is good to do defensive programming because you never know who may use your component in the future.
let callBGChanged = menuName => {
if (_.isFunction(onBGChanged)) {
onBGChanged(menuName);
}
};
Two ways to solve the problem. One is passing down a function to update state, the other is to useContext. In this case it makes sense to use context because you are passing down a function through multiple components that do not care about the function.
First thing to do is make the background image dynamic in the div's style and use context:
// Put this outside the component
export const BackgroundContext = React.createContext(null);
// -- snip
const [backgroundImage, setBackgroundImage] = useState(Ecommerce);
const updateBackgroundImage = newImage => setBackgroundImage(newImage);
// -- snip
<BackgroundContext.Provider value={updateBackgroundImage}>
<div className="App" style={{ backgroundImage: `url(${backgroundImage})` }}>
{/* -- snip */}
</BackgroundContext.Provider>
Now in your SolutionsSectionBoxItem component you can import the background context:
import BackgroundContext from "../App";
Then using that context and react's mouseover api, update the selected background image:
const setBackgroundImage = useContext(BackgroundContext);
// -- snip
<img onMouseOver={() => setBackgroundImage(solutionIMG)} {/* -- snip -- */} />
You can read more here: https://reactjs.org/docs/hooks-faq.html#how-to-avoid-passing-callbacks-down

Selected ListItem value using SelectableContainerEnhance in Material-UI

I'm really new to ReactJS and trying to work with Material-UI components on a new Meteor app I'm working with. A classic use case has come to my needs: a list of items changes the UI when the user selects or not some ListItem. Surprisingly, I found that React isn't easy with parent-child component relations like that.
I tried to follow the Material-UI Docs, implementing SelectableList component like the docs suggests using the SelectableContainerEnhance class. Then I went this way:
const {ListItem, Avatar, Divider} = mui;
App = React.createClass({
mixins: [ReactMeteorData],
getMeteorData() {
return {
players: Players.find({}, { sort: { score: -1 } }).fetch()
}
},
render() {
return (
<SelectableList subheader="Players list">
{this.data.players.map((player) => {
return (
React.Children.toArray([
<Divider />,
<ListItem
value={player._id}
primaryText={player.name}
secondaryText={player.score}
leftAvatar={<Avatar>{player.name}</Avatar>} />
])
);
})}
</SelectableList>
<Divider />
{ true /* What to do now? */ ?
(<span>Thanks!</span>) :
(<span>Click a player to select</span>)}
);
}
});
Ok, the list items has become selectable. But how to know if any ListItem is selected? And how to get the value and adjust the UI according to it?
They talk about setting up a valueLink in the documentation.
<SelectableList
subheader="Players List"
valueLink={{
value: this.state.selectedIndex,
requestChange: this.handleUpdateSelectedIndex
}}>
And then define a handleUpdateSelectedIndex to set the state:
getInitialState() {
return {selectedIndex: 1};
},
handleUpdateSelectedIndex(e, index) {
this.setState({
selectedIndex: index,
});
},
This will give you this.state.selectedIndex on your App component that you can do whatever you need to do with it.

Resources