Gutenberg Block Development: Only one RichText content is saving - gutenberg-blocks

I added two RichText components in my block.
registerBlockType( 'hallmark/gray-content-container', {
title: __( 'Gray Content Container' ),
icon: 'grid-view',
category: 'hallmark-blocks',
keywords: [
__( 'Hallmark gray content' ),
__( 'Hallmark' ),
__( 'Gray content container' ),
],
attributes:{
contentHeading: {
type: 'string',
source: 'children',
selector: 'h1,h2,h3,h4,h5,h6'
},
textContent: {
type: 'string'
}
},
edit: function( props ) {
var textContent = props.attributes.textContent;
var contentHeading = props.attributes.contentHeading;
function onChangeTextContent( content ) {
props.setAttributes( { textContent: content } );
}
function onChangeHeading (heading) {
props.setAttributes( { contentHeading: heading} );
}
return (
<div className={ props.className }>
<label className="editor-content-section-label">Content for gray section</label>
<RichText
tagName="h1"
value={contentHeading}
onChange={onChangeHeading}
placeholder={ __( 'Add a heading' ) }
keepPlaceholderOnFocus
/>
<RichText
tagName="p"
className={props.className}
onChange={onChangeTextContent}
value={textContent}
placeholder={ __( 'Add content' ) }
keepPlaceholderOnFocus
/>
</div>
);
},
save: function( props ) {
//return null;
return(
<div className={props.className}>
<div className="gray-bg">
<div className="constrain content">
<RichText.Content tagName="h1" value={ attributes.contentHeading } />
<RichText.Content tagName="p" value={ attributes.textContent } />
</div>
</div>
</div>
);
},
} );
I tried two different approaches to save the data.
Using default save() function
save: function( props ) {
return(
<div className={props.className}>
<div className="gray-bg">
<div className="constrain content">
<RichText.Content tagName="h1" value={ attributes.contentHeading } />
<RichText.Content tagName="p" value={ attributes.textContent } />
</div>
</div>
</div>
);
},
Saving it in PHP:
Using render_callback method (Using return null; from block's default save() function.
register_block_type( 'hallmark/white-content-container', array(
'render_callback' => 'hall_render_white_content'
) );
function hall_render_white_content( $atts ) {
$heading = $atts['contentHeading'];
$raw_content = $atts['textContent'];
$full_content = $heading . $raw_content;
// var_dump($full_content);
$content = hall_clean_shortcode_block_content( $full_content );
return '<div class="gray-bg"><div class="constrain content">' . $content . '</div></div>';
}
atts['contentHeading'] element does not exist at all in $atts array. When I check var_dump( $attas ); it has textContentelement present.
The problem is both approaches are only saving the textContent. contentHeading is not at all saving.
What I am missing?

Try setting
attributes:{
contentHeading: {
type: 'string',
source: 'children',
selector: 'h1'
},
textContent: {
type: 'string'
selector: 'p'
}
},
I think the selectors have to exactly match what is set in the save method.
<div className="constrain content">
<RichText.Content tagName="h1" value={ attributes.contentHeading } />
<RichText.Content tagName="p" value={ attributes.textContent } />
</div>
I think you also need a unique selector, so if you had two RichText paragraphs, you could do
textContentA: {
type: 'string'
selector: 'p.content-a'
}
textContentB: {
type: 'string'
selector: 'p.content-b'
}

For debugging use console.log(props.attributes) inside your edit function and observe if the values of contentHeading is changing or not when you edit. edit() function will be called each time if the state or props of component changes. As per my lucky guess the source of contentHeading should be 'text' instead of children.

Related

How to handle two api calls in react-typescript ant-design

interface SingleCustomerShipmentData {
_id: string;
shipping_type: string;
origin_port_city: string;
destination_port_city: string;
shipment_pickup_date: string;
}
interface SingleCustomerData {
id: string;
first_name: string;
last_name: string;
email: string;
phone: string;
Avatar: string;
}
const columns: ColumnsType<SingleCustomerShipmentData> = [
{
title: 'Shipment Type',
dataIndex: 'shipping_type',
key: 'shipping_type',
render: (shipping_type, data) => {
return (
<div>
<img className='customers-avatar' src={data.Avatar} alt="" />
{shipping_type}
</div>
)
},
},
{
title: 'Origin',
dataIndex: 'origin_port_city',
key: 'origin_port_city',
},
{
title: 'Destination',
dataIndex: 'destination_port_city',
key: 'destination_port_city',
},
{
title: 'Shipment Date',
dataIndex: 'shipment_pickup_date',
key: 'shipment_pickup_date',
},
{
title: 'Shipping ID',
dataIndex: '_id',
key: '_id',
},
{
title: '',
key: 'action',
render: () => {
return (
<Space size="middle"
<Link to="/shipment-details" className='space-action-green'>View Details</Link>
</Space>
)
},
},
];
const Shipments: React.FC = () => {
const [shipmentData, setShipmentData] = useState(null);
const [customersData, setCustomersData] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
getSingleCustomer()
}, [])
useEffect(() => {
getSingleCustomerShipment()
}, [])
const getSingleCustomer = () => {
setLoading(true);
return makeAPICall({
path: `get_single_customer/123456789`,
method: "GET",
})
.then((data) => {
setCustomersData(data);
setLoading(false);
console.log(data);
})
.catch((err) => {
setLoading(false);
console.log(err);
});
}
const getSingleCustomerShipment = () => {
setLoading(true);
return makeAPICall({
path: `get_single_customer_shipments/123456789`,
method: "GET"
}).then((data) => {
setShipmentData(data);
console.log(data);
}).catch((err) => {
setLoading(false);
console.log(err);
})
}
return (
<SiderLayout>
<div className="site-layout-background" style={{ padding: "40px 40px", minHeight: 710 }}>
<Link className='shipment-back' to="/">
<img src={ArrowLeft} alt="" />
</Link>
{loading ? "loading..." : (
<>
<div className='shipment__container'>
<div className='shipment__container--image'>
<img src={data.Avatar} alt="" style={{ marginRight: 10 }} />
<div>
<p className='bold'>{first_name}</p>
<p>{email}</p>
<p>{phone}</p>
</div>
</div>
<div>
<span className='shipment__container--edit'>Edit</span>
</div>
</div>
<div className='shipment__border'>
<div className='shipment-spacing'>
<button className='new-shipment'>Add New Shipment {" "} +</button>
<select className='shipment-select' name="" id="">
<option value="">Shipment Type</option>
</select>
<select className='shipment-select' name="" id="">
<option value="">Shipment Date</option>
</select>
</div>
<div className='shipment-search'>
<form className="nosubmit">
<input className="nosubmit" type="search" placeholder="Search by shipment ID, Destination" />
</form>
</div>
</div>
<Table columns={columns} dataSource={data} />
</>
)}
</div>
</SiderLayout>
So I'm having these two api calls to be made on the same page. However I created two interface because both would definitely be needed on the it. But I don't really know how I can achieve that, because it is given an error message cannot find name data. So I brought it here for any person with an idea of it to help me out.
const [shipmentData, setShipmentData] = useState(null);
const [customersData, setCustomersData] = useState(null);
these shouldn't start as a null value. Because AntD Table requires an array to render.
const [shipmentData, setShipmentData] = useState([]);
const [customersData, setCustomersData] = useState([]);
should solve the problem you are facing.

antd Form.Item accepts only one child

I've created a little Fiddle to illustrate the issue: https://stackblitz.com/edit/react-avejvc-mmhqda?file=index.js
This form works:
<Form initialValues={{ surname: 'Mouse'}}>
<Form.Item name="surname">
<Input />
</Form.Item>
</Form>
This form doesn't:
<Form initialValues={{ surname: 'Mouse'}}>
<Form.Item name="surname">
<Input />
{null}
</Form.Item>
</Form>
The only difference is that the Form.Item in the second form has two children.
Is there an intention behind this?
In case anyone wonders why I am asking. So sth like this is breaking the form:
<Form.Item name={name}>
{type==="string" && <Input />}
{type==="integer" && <InputNumber />}
</Form.Item>
The official documentation here gives examples of using multiple children in one Form.Item.
<Form.Item label="Field">
<Form.Item name="field" noStyle><Input /></Form.Item> // that will bind input
<span>description</span>
</Form.Item>
You appear to have a problem with what you are putting in the Form.Item, ie. {null} may not be allowed.
I found a solution and have a better understanding now of what is going on.
From the docs (https://ant.design/components/form/#Form.Item):
After wrapped by Form.Item with name property, value(or other property defined by valuePropName) onChange(or other property defined by trigger) props will be added to form controls, the flow of form data will be handled by Form
There is a working example in the docs too, here is the codepen: https://codepen.io/pen?&editors=001
const { useState } = React;;
const { Form, Input, Select, Button } = antd;
const { Option } = Select;
const PriceInput = ({ value = {}, onChange }) => {
const [number, setNumber] = useState(0);
const [currency, setCurrency] = useState('rmb');
const triggerChange = (changedValue) => {
onChange?.({
number,
currency,
...value,
...changedValue,
});
};
const onNumberChange = (e) => {
const newNumber = parseInt(e.target.value || '0', 10);
if (Number.isNaN(number)) {
return;
}
if (!('number' in value)) {
setNumber(newNumber);
}
triggerChange({
number: newNumber,
});
};
const onCurrencyChange = (newCurrency) => {
if (!('currency' in value)) {
setCurrency(newCurrency);
}
triggerChange({
currency: newCurrency,
});
};
return (
<span>
<Input
type="text"
value={value.number || number}
onChange={onNumberChange}
style={{
width: 100,
}}
/>
<Select
value={value.currency || currency}
style={{
width: 80,
margin: '0 8px',
}}
onChange={onCurrencyChange}
>
<Option value="rmb">RMB</Option>
<Option value="dollar">Dollar</Option>
</Select>
</span>
);
};
const Demo = () => {
const onFinish = (values) => {
console.log('Received values from form: ', values);
};
const checkPrice = (_, value) => {
if (value.number > 0) {
return Promise.resolve();
}
return Promise.reject(new Error('Price must be greater than zero!'));
};
return (
<Form
name="customized_form_controls"
layout="inline"
onFinish={onFinish}
initialValues={{
price: {
number: 0,
currency: 'rmb',
},
}}
>
<Form.Item
name="price"
label="Price"
rules={[
{
validator: checkPrice,
},
]}
>
<PriceInput />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
};
ReactDOM.render(<Demo />, mountNode);

React: Trying to set selection to drop down option

I setup a component that is basically a drop down and I am trying to figure out how to set it to where when I submit the form....its set on that one option. When I submit it now, it sends all the options to the backend instead of just the one I selected.
Here is my Category component
import React, { Component } from 'react'
class Categories extends Component{
handleCatChange = (event) => {
this.setState({category: event.target.value}) <------this should set the state to whatever is selected
}
render(){
let categories = this.props.category
let value = this.props.value
let optionItems = categories.map((cat,index) => {
return <option key={index} value={value}>{cat.category}</option>
})
return (
<div>
<select onchange={this.handleCatChange} value={this.props.category}>
{this.props.category ? optionItems : <p>Loading....</p>}
</select>
</div>
)
}
}
export default Categories
And here is RecipeInput Component with form
import React, { Component } from 'react'
import Categories from './Categories.js'
class RecipeInput extends Component{
constructor(props){
super(props)
this.state = {
category: [],
name:'',
ingredients: '',
chef_name: '',
origin: ''
}
}
componentDidMount(){
let initialCats = [];
const BASE_URL = `http://localhost:10524`
const CATEGORIES_URL =`${BASE_URL}/categories`
fetch(CATEGORIES_URL)
.then(resp => resp.json())
.then(data => {
initialCats = data.map((category) => {
return category
})
this.setState({
category: initialCats
})
});
}
handleSubmit = (event) =>{
event.preventDefault();
this.props.postRecipes(this.state)
this.setState({
name:'',
ingredients: '',
chef_name: '',
origin: ''
})
}
render(){
return(
<div>
<form onSubmit={this.handleSubmit}>
<Categories category={this.state.category} value={this.state.category}/>
<div>
<label for='name'>Recipe Name:</label>
<input type='text' value={this.state.name} onChange={this.handleNameChange} />
</div>
<div>
<label for='name'>Country Origin:</label>
<input type='text' value={this.state.origin} onChange={this.handleOriginChange} />
</div>
<div>
<label for='name'>Chef Name:</label>
<input type='text' value={this.state.chef_name} onChange={this.handleChefChange} />
</div>
<div>
<label for='name'>Ingredients:</label>
<textarea value={this.state.ingredients} onChange={this.handleIngChange} />
</div>
<input value='submit' type='submit'/>
</form>
</div>
)
}
}
export default RecipeInput
And here is the error that is produced on submission(Its Rails btw)
I tired a few ways but haven't quite wrapped my head around using a component as a dropdown. What do I need to do?
Here is my backend code that creates the record on the api
def create
recipe = Recipe.create(recipe_params)
if recipe.save
render json: recipe
else
render json: { error: "Couldn't save" }
end
end
private
def recipe_params
params.permit(:category_id,:name,:ingredients,:chef_name,:origin,category_attribute:[:category])
end
Also my postRecipe function
export const postRecipes = (recipe)=>{
const BASE_URL = `http://localhost:10524`
const RECIPES_URL =`${BASE_URL}/recipes`
const config = {
method: "POST",
body:JSON.stringify(recipe),
headers: {
"Accept": "application/json",
"Content-type": "application/json"
}
}
//category field
return(dispatch)=>{
fetch(RECIPES_URL,config)
.then(response => response.json())
.then(resp => {
dispatch({
type: 'Add_Recipe',
payload:{
// category:resp.category,
name: resp.name,
ingredients: resp.ingredients,
chef_name: resp.chef_name,
origin: resp.origin,
categoryId: resp.categoryId
}
})
})
//.then(response => <Recipe />)
.catch((error) => console.log.error(error))
}
}
Code Edit due to change in question:
Access selectedValue while sending to the server
class Categories extends Component {
render() {
...
let optionItems = categories.map((cat, index) => {
return (
<option key={index} value={index}>
{cat.category}
</option>
);
});
...
}
}
class RecipeInput extends Component{
constructor(props){
super(props)
this.state = {
category: [],
name:'',
ingredients: '',
chef_name: '',
origin: ''
selectedValue: {}
}
}
handleSubmit(id){
this.setState({
selectedValue: this.state.category[id]
)}
}
...
}
You're passing onChange and value from Input Component but you're not using them in Categories Component.
Add onChange and value property to tag.
here is reference
import "./styles.css";
import React, { Component } from "react";
class Categories extends Component {
render() {
let categories = this.props.category;
let onChange = this.props.onChange;
let optionItems = categories.map((cat, index) => {
return (
<option key={index} value={cat.category}>
{cat.category}
</option>
);
});
return (
<div>
<select onChange={(e) => onChange(e.target.value)}>
{this.props.category.length ? optionItems : null}
</select>
</div>
);
}
}
export default function App() {
const onChange = (value) => {
console.log(value);
};
return (
<Categories
onChange={onChange}
category={[{ category: "1st" }, { category: "2nd" }]}
/>
);
}
I've updated the code.
If you need to try it online you can refer my Sandbox
https://codesandbox.io/s/stackoverflow-qno-65730813-j32ce

How to use Radio groups inside Antd table?

I want to do this: each row is a Radio group, each cell is a Radio button, like the picture:
An example of Radio group is like:
<Radio.Group onChange={this.onChange} value={this.state.value}>
<Radio value={1}>A</Radio>
<Radio value={2}>B</Radio>
<Radio value={3}>C</Radio>
<Radio value={4}>D</Radio>
</Radio.Group>
But I don't know how to add a Radio group to wrap each Antd table row?
My current code is:
renderTable() {
let columns = [];
columns.push(
{
title: '',
dataIndex: 'name',
key: 'name',
width: '45vw',
},
);
this.props.task.options.forEach((option, i) => {
columns.push(
{
title: option,
dataIndex: option,
key: option,
className: 'choice-table-column',
render: x => {
return <Radio value={0} />
},
},
);
});
let rowHeaders = [];
this.props.task.extras.forEach((extra, i) => {
rowHeaders.push(
{"name": `${i + 1}. ${extra}`},
);
});
// How can I pass a className to the Header of a Table in antd / Ant Design?
// https://stackoverflow.com/questions/51794977/how-can-i-pass-a-classname-to-the-header-of-a-table-in-antd-ant-design
const tableStyle = css({
'& thead > tr > th': {
textAlign: 'center',
},
'& tbody > tr > td': {
textAlign: 'center',
},
'& tbody > tr > td:first-child': {
textAlign: 'left',
},
});
return (
<div>
<Table className={tableStyle} columns={columns} dataSource={rowHeaders} size="middle" bordered pagination={false} />
</div>
);
}
I don't think it is possible to use radio group for each row, however you can achieve it in a traditional way.
Here is code sample
https://codesandbox.io/s/goofy-benz-12kv5
class App extends React.Component {
state = {
task: { options: [1, 2, 3, 4, 5], extras: [6, 7, 8, 9, 10] },
selected: {}
};
onRadioChange = e => {
let name = e.currentTarget.name;
let value = e.currentTarget.value;
this.setState({
...this.state,
selected: { ...this.state.selected, [name]: value }
});
};
onSubmit = () => {
console.log(this.state.selected);
this.setState({
...this.state,
selected: {}
});
};
render() {
let columns = [];
columns.push({
title: "",
dataIndex: "name",
key: "name",
width: "45vw"
});
this.state.task.options.forEach((option, i) => {
columns.push({
title: option,
key: option,
render: row => {
return (
<input
type="radio"
checked={this.state.selected[row.name] == option}
onChange={this.onRadioChange}
name={row.name}
value={option}
/>
);
}
});
});
let rowHeaders = [];
this.state.task.extras.forEach((extra, i) => {
rowHeaders.push({ name: `${i + 1}.${extra}` });
});
return (
<div>
<Button onClick={this.onSubmit} type="primary">
{" "}
Submit
</Button>
<Table
columns={columns}
dataSource={rowHeaders}
size="middle"
bordered
pagination={false}
/>
<Tag color="red">Selected options</Tag>
<br />
{JSON.stringify(this.state.selected)}
</div>
);
}
}
hi there i had the same problem and base on new updates on antd this way of using is easier
<Table
rowSelection={{
type: "radio",
getCheckboxProps: (record) => {
console.log("record", record);
},
}}
pagination={{ hideOnSinglePage: true }}
columns={columns}
dataSource={data}
/>
example : https://ant.design/components/table/#components-table-demo-row-selection
for hiding table header : https://newbedev.com/javascript-antd-table-hide-table-header-code-example
hope its usefull

Paginator not works when i fetch data from API into a mat-table. Mat table is generated dynamically

I'm creating my mat-table dynamically where is pass columns through an array and data is also rendering dynamically.
Mat-paginator was working fine with the local array. But when i fetch data from an API it doesn't respond.
I've tried different things but didn't find any solution.
Here is my code from the Html file.
`
<div class="container">
<div class="tableSearchRow">
<div id="searchTitle">
<button (click)="toggle()" [hidden]="show" class="searchButton">{{name}}<i class="material-icons searchIconImg"> search </i></button>
<mat-form-field *ngIf="!isPrepositionChecked" >
<input id="textupercase" matInput (keyup)="applyFilter($event.target.value)" placeholder="Search">
</mat-form-field>
</div>
<div id="searchIcon">
<button mat-raised-button (click)="openDialog()">+Add</button>
</div>
</div>
<div class="row">
<mat-table #table [dataSource]="dataSource" >
<ng-container *ngFor="let column of columns" [cdkColumnDef]="column.columnDef">
<mat-header-cell *cdkHeaderCellDef>{{ column.header }}</mat-header-cell>
<mat-cell *matCellDef="let commonCode">{{column.cell(commonCode)}}</mat-cell>
</ng-container>
<ng-container cdkColumnDef="Actions">
<mat-header-cell *cdkHeaderCellDef> Actions </mat-header-cell>
<mat-cell *matCellDef="let commonCode; let i = index">
<button mat-button (click)="updateDataDialog(i,commonCode)"><i class="material-icons blue">border_color</i></button>enter code here
<button mat-button (click)="delete(commonCode.Code)"><i class="material-icons red">delete</i></button>
<button mat-button routerLink="commonCodeValues"><i class="material-icons" >blur_circular</i></button>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns" ></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns;" ></mat-row>
</mat-table>
<mat-paginator [pageSizeOptions]="[5,10, 20]"></mat-paginator>
</div>
</div>
`
Code of Js File
`
let fields: any[] = [
{
type: 'text',
name: 'Code',
label: 'Code',
value: '',
required: true,
},
{
type: 'text',
name: 'Name',
label: 'Name',
value: '',
required: true,
},
{
type: 'text',
name: 'Description',
label: 'Description',
value: '',
required: true,
},
];
#Component({
selector: 'app-common-codes',
templateUrl: './common-codes.component.html',
styleUrls: ['../setupStyle.css']
})
export class CommonCodesComponent implements OnInit {
#ViewChild(MatPaginator) paginator: MatPaginator;
dataSource: any;
name="commonCode";
Employee: any = [];
// Columns Of Table
columns = [
{ columnDef: 'Code', header: 'Code', cell: (element: any) => `${element.Code}`},
{ columnDef: 'Name', header: 'Name', cell: (element: any) => `${element.Name}`},
{ columnDef: 'Description', header: 'Description', cell: (element: any) => `${element.Description}`},
];
displayedColumns = this.columns.map(c => c.columnDef).concat(['Actions']);
constructor(private dialog: MatDialog, private fb: FormBuilder, private setupService: SetupService) {
this.dataSource = new MatTableDataSource(this.json.transfer)
// this.refresh()
}
ngOnInit() {
// console.log(this.displayData());
}
ngAfterViewInit() {
this.refresh();
this.dataSource.paginator = this.paginator;
}
// Filter For Table
applyFilter(filterValue: string) {
this.dataSource.filter = filterValue.trim().toLowerCase();
}
// Function to refresh data inside table
refresh(){
this.dataSource = new MatTableDataSource(JSON.parse(localStorage.getItem(this.name )));
}
// Function for search bar
toggle(){
this.show=true;
this.isPrepositionChecked=false;
}
// Function For Adding New Table
openDialog(){
let regiForm = this.fb.group({
'Code' : ['', Validators.required],
'Name' : ['', Validators.required],
'Description' : ['', Validators.required],
});
this.setupService.openDialog(regiForm, fields, this.name).afterClosed().subscribe(res => {
this.refresh()
})
}
// Function for update Data inside table
updateDataDialog(index, obj){
let regiForm = this.fb.group({
'Code' : [obj.Code, Validators.required],
'Name' : [obj.Name, Validators.required],
'Description' : [obj.Description, Validators.required],
});
this.setupService.updateDialog(regiForm, fields, this.name, index ).afterClosed().subscribe(res => {
this.refresh();
})
}
// Function to remove data from table
delete(id){
this.setupService.deleteData().afterClosed().subscribe(res => {
if(res){
let items = JSON.parse(localStorage.getItem(this.name));
for (var i =0; i<=items.length; i++) {
if (items[i].Code == id) {
console.log("id FOUND")
items.splice(i,1)
break;
}
else {
console.log("id doesn't match")
}
}
items = JSON.stringify(items);
localStorage.setItem(this.name, items);
this.refresh();
}
})
}
// displayData(){
// this.setupService.getData().subscribe((res : {}) => {
// this.Employee = res;
// });
// }
}
`

Resources