Currency field on Field ( redux-form ) for mobile app - forms

I am currently developing a react mobile app.
The issue is that I want to add '£' currency symbol to a form-redux field with type=number.
Code
Form
import React from 'react';
import Card from "../../pandle-ui/Card";
import cssmodules from 'react-css-modules'
import styles from './form.cssmodule.scss'
import {Field} from "redux-form";
import CardTitleTextField from "../../form_fields/CardTitleTextField";
import {CardText, Divider, IconButton} from "material-ui";
import CardFieldRow from "../../layout/CardFieldRow";
import {TextFieldBase} from "../../form_fields/TextField";
import CurrencyField from "../../form_fields/CurrencyField";
import {CardActions, FlatButton} from "material-ui";
import DoneIcon from 'material-ui/svg-icons/action/done'
import {normalizePercentage} from "../../../util/currency_helpers";
class Form extends React.Component {
render() {
const {config, cancelButton, submit, wrapperRef} = this.props
return (
<div styleName="wrapper" ref={wrapperRef} >
<Card>
<DescriptionField />
<CardText style={ { paddingTop: 0 } }>
<QuantityField />
<UnitsField show={config.showUnits} />
<PriceField />
<DiscountField show={config.showDiscount} />
<Totals showTax={config.showTax} />
</CardText>
<Divider />
<CardActions style={{ textAlign: 'right', padding: '0 8px' }}>
{cancelButton}
<DoneButton onClick={() => submit()} />
</CardActions>
</Card>
</div>
)
}
}
Form.displayName = 'PagesLineItemsForm';
Form.propTypes = {};
Form.defaultProps = {};
export default cssmodules(Form, styles)
const DescriptionField = (props) =>
<Field
label="Description"
name="description"
component={CardTitleTextField}
{...props}
/>
const QuantityField = (props) =>
<CardFieldRow label="Quantity">
<Field name="quantity" type="number" component={TextFieldBase} {...props} />
</CardFieldRow>
const UnitsField = ({ show, ...props }) => {
if(show){
return <CardFieldRow label="Unit Type">
<Field name="unit" component={TextFieldBase} {...props} />
</CardFieldRow>
} else {
return null
}
}
const PriceField = (props) =>
<CardFieldRow label="Price">
<Field name="price" type="number" component={CurrencyField} {...props} />
</CardFieldRow>
const DiscountField = ({ show, props }) => {
if(show){
return <CardFieldRow label="Discount (%)">
<Field
name="discount_percentage"
component={TextFieldBase}
type="number"
normalize={normalizePercentage}
{...props}
/>
</CardFieldRow>
} else {
return null
}
}
const Totals = ({ showTax }) => {
if(showTax){
return <div>
<NetAmountField />
<TaxAmountField />
<TotalField />
</div>
} else {
return <TotalField name="net_amount" />
}
}
const NetAmountField = (props) =>
<CardFieldRow label="Net Amount">
<Field name="net_amount" component={CurrencyField} {...props} />
</CardFieldRow>
const TaxAmountField = (props) => {
return <CardFieldRow label="Tax Amount">
<Field name="tax_amount" component={CurrencyField} {...props} />
</CardFieldRow>;
}
const TotalField = (props) =>
<CardFieldRow label="Total">
<Field name="total_amount" disabled={true} component={CurrencyField} {...props} />
</CardFieldRow>
const DoneButton = (props) => <IconButton {...props}><DoneIcon /></IconButton>
CurrencyField.js
import React from 'react'
import {TextFieldBase} from "./TextField";
import {formatCurrency} from "../../util/formatters";
import {truncateCurrency} from "../../util/currency_helpers";
import {createComponentLogger} from "../../util/logging";
export default class CurrencyField extends React.Component {
constructor(props){
super(props)
log('Constructor called with props', props)
this.state = { value: props.value, focused : false }
this.baseInputPassProps = this.getBaseInputPassProps()
}
getBaseInputPassProps(){
return {
onChange: (e) => this.onChange(truncateCurrency(e.target.value)),
onBlur: (e) => this.onBlur(truncateCurrency(e.target.value)),
onFocus: (e) => this.onFocus()
}
}
onChange(value){
log('onChange called with', value)
safeParse(value, parsed => {
if(log('should update state', this.shouldUpdateStateValue(value))){
this.setState({ value })
this.props.input.onChange(parsed*100)
}
})
}
onBlur(value){
log('onBlur called with', value)
safeParse(value, parsed => {
this.props.input.onBlur(parsed*100)
})
}
onFocus(){
log('onFocus called')
this.props.input.onFocus()
}
shouldUpdateStateValue(value){
return this.state.value !== value
}
componentWillReceiveProps(nextProps){
const {value} = nextProps.input
log('will receive props', nextProps)
if(log('should update props value', this.shouldUpdatePropsValue(value))){
this.setState({
value: log('updating state.value', (value/100).toString())
})
}
}
shouldUpdatePropsValue(value){
const {input, meta: {active}} = this.props
return !active && input.value !== value
}
render(){
log('Render called')
return <TextFieldBase
{...log('Props for TextField', this.getTextFieldProps())}
/>
}
getTextFieldProps(){
const {value, ...props} = this.props
return Object.assign({}, props, this.getPassProps())
}
getPassProps(){
return { input: log('Input pass props', this.getInputPassProps()) }
}
getInputPassProps(){
return Object.assign({}, this.props.input, this.baseInputPassProps, {
value: log('Value for input pass props', this.getValue())
})
}
getValue(){
if(this.props.meta.active){
return log('active, state', this.state.value)
} else if(this.props.input.value) {
return log('inactive, formatted', this.getFormattedValue())
} else {
return ''
}
}
getFormattedValue() {
return (this.props.input.name === 'price') ?
this.props.input.value / 100
:
formatCurrency(
null,
log('for currency formatter', this.props.input.value / 100)
)
}
}
function safeParse(value, fn){
const parsed = Number(value)
log(`parsed ${value} as `, parsed)
!isNaN(parsed) && fn(parsed)
}
CurrencyField.displayName = 'CurrencyField'
const log = createComponentLogger(CurrencyField.displayName)
formatters.js
import numeral from 'numeral'
import 'numeral/locales/en-gb'
import moment from 'moment'
numeral.locale('en-gb');
export function formatCurrencyPence(number){
return (number > 0) ? formatCurrency(null, number/100) : '-'
}
export function formatCurrency(symbol='£', number){
return numeral(number).format('($0,0.00)')
}
export function formatAmount(number){
return numeral(number).format('-0,0.00')
}
export function formatInvoiceProps(item){
return [
formatInvoiceDates,
formatInvoiceAmounts
].reduce((memo, fn) => fn(memo), item)
}
const formatInvoiceDates = createFormatter(
formatServerDate,
'date',
'date-due'
)
function formatServerDate(date){
return formatMomentDate(serverToMomentDate(date))
}
function serverToMomentDate(date){
return moment(date, 'YYYY-MM-DD')
}
/**
* #param date {moment}
*/
export function formatMomentDate(date){
return (date.isValid() && date.format('DD/MM/YYYY')) || '-'
}
export function formatDate(date){
return formatMomentDate(moment(date))
}
const formatInvoiceAmounts = createFormatter(
formatInvoiceAmount,
'net-amount',
'tax-amount',
'total-amount'
)
function formatInvoiceAmount(amount){
return formatCurrency(null, amount)
}
function createFormatter(formatter, ...keys){
return ob => formatProps(ob, formatter, keys)
}
function formatProps(ob, formatter, keys){
return keys.reduce(formatProp(formatter), ob)
}
function formatProp(formatter){
return (ob, k) => {
const v = ob[k]
return v ? Object.assign({}, ob, { [k] : formatter(v, ob) }) : ob
}
}
This code uses number pad when the user tap in the field which is fine, but how can I format that?
Also, how can I prevent users to insert negative numbers?
Thx in advance.

Related

mui-datatable - update content of table by click a button possible?

I painstakingly put together a mui-datatable that displays the content of a CSV file.
Now I want to read another CSV file when a button is clicked.
Unfortunately, I'm a complete beginner - and I don't really know how to implement it.
I tried the following - but unfortunately it doesn't work that way.
Can someone help me with that?
As a 2nd step, I want to show a mui-alert for 3 seconds when I click on a cell. At the same time, a filter with the content of the cell is set. I have already successfully implemented this functionality using the setTimeout function. Strangely enough, only if I remove a filter within the 3 seconds "setTimeout" and then add a new filter, both filters (the one already removed and the new filter) are added again. Somehow the "setTimeout" function seems to be blocking further input in the mui-datatable....
Here is my code:
import {
Typography,
AppBar,
Button,
CssBaseline,
Toolbar,
Container,
Grid,
} from "#mui/material";
import React, { useState} from "react";
import { BackupTable, Refresh } from "#mui/icons-material";
import useStyles from "./styles";
//Tabelle
import MUIDataTable, { ExpandButton } from "mui-datatables";
import { TableRow, TableCell } from "#mui/material";
import { createTheme, ThemeProvider } from "#mui/material/styles";
import total from "./test.csv";
import lost1 from "./test.csv";
import lost2 from "./test.csv";
//const App = () => {
function App() {
const { classes } = useStyles();
const [file, setFile] = useState();
const [array, setArray] = useState([]);
const csvFileToArray = (string) => {
const csvHeader = string.slice(0, string.indexOf("\n")).split(",");
const csvRows = string.slice(string.indexOf("\n") + 1).split("\n");
const array = csvRows.map((i) => {
const values = i.split(",");
const obj = csvHeader.reduce((object, header, index) => {
object[header] = values[index];
return object;
}, {});
return obj;
});
setArray(array);
setCols(initFilter(Object.keys(Object.assign({}, ...array))));
};
const headerKeys = Object.keys(Object.assign({}, ...array));
let [selectedFilter, setSelectedFilter] = useState([new Array()]);
let [cols, setCols] = useState();
//Tabelle
const components = {
ExpandButton: function (props) {
return <ExpandButton {...props} />;
},
};
var removedFilterList=[];
const options = {
filter: true,
filterType: "multiselect",
onFilterChange: (changedColumn, filterList) => {
cols = modifyFilter(cols, filterList);
},
onFilterChipClose: (index, removedFilter, filterList) => {
removedFilterList = filterList;
selectedFilter = filterList;
},
responsive: "standard",
selectableRowsOnClick: false,
rowHover: true,
expandableRows: false,
expandableRowsHeader: false,
expandableRowsOnClick: true,
selectableRows: "none",
rowsPerPage: 100,
filterList: [],
selectableRowsHideCheckboxes: false,
onCellClick: (rowData, rowMeta) => {
onFilter(rowData, rowMeta);
},
};
const theme = createTheme({
overrides: {
MUIDataTableSelectCell: {
expandDisabled: {
// Soft hide the button.
visibility: "hidden",
},
},
MUIDataTableBodyCell: {
styleOverrides: {
root: {
backgroundColor: "#FF0000",
},
},
},
},
});
const initFilter = (cols) => {
//add options to columns
for (let i = 0; i < cols.length; i++) {
cols[i] = {
name: cols[i],
options: {
filterList: []
}
}
}
return cols;
}
let modifyFilter = (colss, newFilterArray) => {
for (let i = 0; i < colss.length; i++) {
colss[i].options.filterList = newFilterArray[i];
}
return colss;
}
const onFilter = (value, rowMeta) => {
let filteredCols = [...cols];
if (value !== "All") {
if (selectedFilter.length < rowMeta.colIndex + 1) {
for (let i = selectedFilter.length; i < rowMeta.colIndex + 1; i++) {
selectedFilter.push(new Array());
}
}
let schonVorhanden = selectedFilter[rowMeta.colIndex].indexOf(value);
if (schonVorhanden == "-1") {
selectedFilter[rowMeta.colIndex].push(value);
}
else {
console.log(value, "schon vorhanden");
}
}
filteredCols[rowMeta.colIndex].options.filterList = selectedFilter[rowMeta.colIndex];
setCols(filteredCols);
};
//---------------------------
//TEST
const ImportSamples = () => {
let input = total;
if(file == "lost1"){
input=lost1;
}
if (array.length == 0) {
fetch(input)
.then((r) => r.text())
.then((text) => {
csvFileToArray(text);
});
}
};
const doSetFile = (wert) => {
setFile(wert);
ImportSamples();
Refresh();
}
ImportSamples();
return (
<>
<CssBaseline />
<AppBar position="relative">
<Toolbar>
<BackupTable className={classes.icon} />
<Typography variant="h6">SampleProject Tabelle</Typography>
</Toolbar>
</AppBar>
<main>
<div className={classes.container}>
<Container maxWidth="sm">
<Typography
variant="h2"
align="center"
color="textPrimary"
gutterBottom
>
SampleProject
</Typography>
<Typography
variant="h5"
align="center"
color="textSecondary"
paragraph
>
Viewer
</Typography>
<div className={classes.button}>
<Grid container spacing={2} justifyContent="center">
<Grid item>
<Button variant="contained" color="primary" onClick={() => doSetFile('lost1')}>
Erste Aktion
</Button>
</Grid>
<Grid item>
<Button variant="outlined" color="primary">
Zweite Aktion
</Button>
</Grid>
</Grid>
</div>
</Container>
</div>
<div className={classes.container}>
<Container maxWidth="false">
<ThemeProvider theme={theme}>
<MUIDataTable
title={"SampleProject"}
data={array}
columns={cols}
components={components}
options={options}
/>
</ThemeProvider>
</Container>
</div>
</main>
<footer className={classes.footer}>
<Typography variant="h6" align="center" gutterBottom>
Footer
</Typography>
<Typography variant="subtitle1" align="center" color="textSecondary">
Something here to give the footer a purpose!
</Typography>
</footer>
</>
);
}
export default App;

React-native: How can I auto update the mainscreen after adding a new item

I'm basically new to React-native and I'm trying to integrate it with MongoDB, apollo-graphql to implement a basic chat app.
I need to update the screen automatically and show the newly created group when I add a new group. Now what happens is, when I create the group, I need to reload the app every time to show the updation made.
GroupScreen.tsx
const MY_GROUPS = gql`
query chatRooms {
chatRooms {
id
name
createdAt
imageUri
}
}
`;
export default function GroupScreen() {
const [groups, setGroups] = useState(null);
const { data, error, loading } = useQuery(MY_GROUPS);
useEffect(() => {
if (error) {
Alert.alert("Something went Wrong! Please reload.");
}
}, [error]);
useEffect(() => {
if (data) {
//console.log(data);
setGroups(data.chatRooms);
}
}, [data]);
return (
<View style={styles.container}>
<FlatList
style={{ width: "100%" }}
data={groups}
renderItem={({ item }) => <GroupListItem chatRoom={item} />}
keyExtractor={(item) => item.id}
/>
<NewGroupButtonItem />
</View>
);
}
NewGroupButtonItem.tsx
const CREATE_CHATROOM = gql`
mutation Mutation(
$createChatRoomName: String!
$createChatRoomImageUri: String
) {
createChatRoom(
name: $createChatRoomName
imageUri: $createChatRoomImageUri
) {
id
name
imageUri
createdAt
users {
id
name
}
}
}
`;
const NewGroupButtonItem = () => {
const [modalVisible, setModalVisible] = useState(false);
const [groupName, setGroupName] = useState("");
const [groupPic, setGroupPic] = useState(null);
const [newGroup, { data, error, loading }] = useMutation(CREATE_CHATROOM);
const onPress = () => {
setGroupName("");
setGroupPic(null);
setModalVisible(!modalVisible);
};
const onPressSave = () => {
newGroup({
variables: {
createChatRoomName: groupName,
createChatRoomImageUri: groupPic,
},
});
setModalVisible(!modalVisible);
};
return (
<View style={styles.container}>
<Modal animationType="fade" transparent={true} visible={modalVisible}>
<TouchableOpacity
style={styles.touchableContainer}
activeOpacity={1}
onPress={() => setModalVisible(!modalVisible)}
>
<View style={styles.mainContainer}>
<View style={styles.innerContainer}>
<Pressable
onPress={() => {
console.warn("Clicked Image!");
}}
>
<Image source={{}} style={styles.avatar} />
</Pressable>
<TextInput
placeholder={"Group Name"}
style={styles.inputBox}
value={groupName}
onChangeText={setGroupName}
/>
<Pressable
onPress={() => {
console.warn("Clicked Emojies!");
}}
>
<Entypo name="emoji-flirt" size={30} color="#37474f" />
</Pressable>
</View>
{!groupName ? (
<Text style={styles.saveButton} onPress={onPress}>
Cancel
</Text>
) : (
<Text style={styles.saveButton} onPress={onPressSave}>
Save
</Text>
)}
</View>
</TouchableOpacity>
</Modal>
<TouchableOpacity onPress={onPress}>
<MaterialIcons name="group-add" size={30} color="white" />
</TouchableOpacity>
</View>
);
};
export default NewGroupButtonItem;

How to prevent repeating code of form validation

I created the form for multiple inputs, where the specific input data shall be validated at the time of data entry and once again for all data just before the submission of the form to the backend.
The conditions to submit: all fields are mandatory and the data is valid.
My program works, but I don't like that I'm repeating the validation code in 2 places: in ErrorOutput and hadleSubmit.
In ErrorOutput I check the data and, if necessary, display an error message.
In handleSubmit I just check the data without displaying of error message and if all data is valid, I confirm submitting.
How can I improve my example to prevent the repetition of this code, but the data validation was also at the time of data entry and before submission?
import React from 'react'
import { render } from 'react-dom'
const ErrorOutput = props => {
let name = props.name
let inputValue = props.case
let submit = props.submit
// Data validation
if (name === 'firstName') {
if (!inputValue.match(/^[a-zA-Z]+$/) && inputValue.length > 0) {
return <span>Letters only</span>
} else if (submit && inputValue.length === 0) {
return <span>Required</span>
}
return <span></span>
}
if (name === 'telNo') {
if(!inputValue.match(/^[0-9]+$/) && inputValue.length > 0) {
return <span>Numbers only</span>
} else if (submit && inputValue.length === 0) {
return <span>Required</span>
}
return <span></span>
}
}
class App extends React.Component {
constructor(props){
super(props)
this.state = {
firstName: '',
telNo: '',
submit: false
}
}
handleSubmit(e){
e.preventDefault()
let submit = true
let error = true
const { firstName, telNo } = this.state
this.setState ({submit: submit})
// Repeat the data validation before submission
if (firstName === '' || !firstName.match(/^[a-zA-Z]+$/)) {
error = true
} else if (telNo === '' || !telNo.match(/^[0-9]+$/)) {
error = true
} else {
error = false
}
// Submited if all data is valid
if (!error) {
// send data
return alert('Success!')
}
}
handleValidation(e) {
this.setState({
[e.target.name]: e.target.value
})
}
render() {
return (
<form onSubmit={this.handleSubmit.bind(this)}>
<div>
<label>
First name:
</label>
<input
type='text'
name ='firstName'
value = {this.state.firstName}
onChange = {this.handleValidation.bind(this)}
/>
<ErrorOutput case={this.state.firstName} name={'firstName'} submit = {this.state.submit} />
</div>
<div>
<label>
Phone number:
</label>
<input
type='tel'
name ='telNo'
value = {this.state.telNo}
onChange = {this.handleValidation.bind(this)}
/>
<ErrorOutput case={this.state.telNo} name={'telNo'} submit = {this.state.submit} />
</div>
<button>
Submit
</button>
</form>
)
}
}
render(
<App />,
document.getElementById('root')
)
You could extract a FormItem component:
class FormItem extends React.Component {
render() {
return (
<div>
<label>
{this.props.label}
</label>
<input
{...this.props.input}
/>
<ErrorOutput
case={this.props.input.value}
name={this.props.input.name}
submit={this.props.onSubmit}
/>
</div>
);
}
}
and use it in your App:
render() {
return (
<form onSubmit={this.handleSubmit.bind(this)}>
<FormItem label='First name:' input={{
type: 'text'
name: 'firstName'
value: this.state.firstName,
onChange: this.handleValidation.bind(this)
}}
onSubmit={this.state.submit}
/>
<FormItem label='Phone number:' input={{
type:'tel'
name :'telNo'
value : {this.state.telNo}
onChange : {this.handleValidation.bind(this)}
}}
onSubmit={this.state.submit}
/>
<button>
Submit
</button>
</form>
)
}
this is where libraries like react-final-form and redux-form become handy.
UPD
ErrorOutput component should not validate anything, it is not a responsibility of a component. Instead, you could validate your values on inputs blur event and before submit:
class App extends React.Component {
constructor(props){
super(props)
this.state = {
firstName: '',
telNo: '',
submit: false,
errors: {},
invalid: false,
}
}
handleSubmit(e){
e.preventDefault()
if (this.validate()) {
// handle error
} else {
// submit
}
}
validate = () => {
const { firstName, telNo } = this.state
const errors = {}
let invalid = false;
if (firstName === '' || !firstName.match(/^[a-zA-Z]+$/)) {
errors.firstName = 'first name is required'
invalid = true;
} else if (telNo === '' || !telNo.match(/^[0-9]+$/)) {
telNo.telNo = 'telNo is required'
invalid = true;
}
this.setState({
invalid,
errors,
})
return invalid;
}
render() {
return (
<form onSubmit={this.handleSubmit.bind(this)}>
<FormItem label='First name:' input={{
type: 'text',
name: 'firstName',
value: this.state.firstName,
onChange: e => this.setState({ firstName: e.target.value }),
onBlur: () => this.validate(),
}}
/>
<FormItem label='Phone number:' input={{
type: 'tel',
name: 'telNo',
value: this.state.telNo,
onChange: e => this.setState({ telNo: e.target.value }),
onBlur: () => this.validate(),
}}
/>
<button>
Submit
</button>
</form>
)
}
}
and FormItem and ErrorOutput:
const ErrorOutput = ({ error }) => <span>{error}</span>
class FormItem extends React.Component {
render() {
return (
<div>
<label>
{this.props.label}
</label>
<input
{...this.props.input}
/>
{this.props.error && <ErrorOutput error={this.props.error} />}
</div>
);
}
}
const ErrorOutput = ({ errorText }) => <span>{errorText}</span>;
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
firstName: "",
telNo: "",
submit: false,
errors: {} //Add errors object to the state.
};
}
handleSubmit(e) {
e.preventDefault();
const errors = this.validateData();
if (Object.keys(errors).length === 0) {
alert("Success");
}
//else errors exist
this.setState({ errors });
}
validateData = () => {
let errors = {};
const { firstName, telNo } = this.state; // read the values to validate
if (firstName.length === 0) {
errors.firstName = "Required";
} else if (firstName.length > 0 && !firstName.match(/^[a-zA-Z]+$/)) {
errors.firstName = "Letters only";
}
if (telNo.length === 0) {
errors.telNo = "Required";
} else if (telNo.length > 0 && !telNo.match(/^[0-9]+$/)) {
errors.telNo = "Numbers only";
}
return errors;
};
handleValidation(e) {
this.setState({
[e.target.name]: e.target.value
});
}
render() {
const { errors } = this.state; // read errors from the state
return (
<form onSubmit={this.handleSubmit.bind(this)}>
<div>
<label>First name:</label>
<input
type="text"
name="firstName"
value={this.state.firstName}
onChange={this.handleValidation.bind(this)}
/>
{errors.firstName && <ErrorOutput errorText={errors.firstName} />}
</div>
<div>
<label>Phone number:</label>
<input
type="tel"
name="telNo"
value={this.state.telNo}
onChange={this.handleValidation.bind(this)}
/>
{errors.telNo && <ErrorOutput errorText={errors.telNo} />}
</div>
<button>Submit</button>
</form>
);
}
}
render(<App />, document.getElementById("root"));

React Leaflet: setting a GeoJSON's style dynamically

I'm trying to change a GeoJSON component's style dynamically based on whether its ID matches a selector.
The author of the plugin refers to the Leaflet documentation, which says that the style should be passed as a function. Which I'm doing, but no dice.
My component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as actions from '../../actions';
import { Marker, Popup, GeoJSON } from 'react-leaflet';
import { centroid } from '#turf/turf';
const position = geoJSON => {
return centroid(geoJSON).geometry.coordinates.reverse();
};
export class PlotMarker extends Component {
render() {
const {
id,
name,
geoJSON,
zoomLevel,
selectedPlot,
plotBeingEdited
} = this.props;
const markerPosition = position(geoJSON);
let style = () => {
color: 'blue';
};
if (selectedPlot === id) {
style = () => {
color: 'red';
};
}
if (zoomLevel > 14) {
return (
<GeoJSON
id={id}
data={geoJSON}
style={style}
onClick={() => {
this.props.selectPlot(id);
}}
/>
);
}
return (
<Marker
id={id}
className="marker"
position={markerPosition}
onClick={() => {
this.props.selectPlot(id);
}}>
<Popup>
<span>{name}</span>
</Popup>
</Marker>
);
}
}
function mapStateToProps(state) {
return {
selectedPlot: state.plots.selectedPlot,
plotBeingEdited: state.plots.plotBeingEdited,
zoomLevel: state.plots.zoomLevel
};
}
export default connect(mapStateToProps, actions)(PlotMarker);
OK, got it. It had to do with the way I was defining the style function. This doesn't work:
let style = () => {
color: 'blue';
};
if (selectedPlot === id) {
style = () => {
color: 'red';
};
}
if (zoomLevel > 14) {
return (
<GeoJSON
id={id}
data={geoJSON}
style={style}
onClick={() => {
this.props.selectPlot(id);
}}
/>
);
}
This works:
let style = () => {
return {
color: 'blue'
};
};
if (selectedPlot === id) {
style = () => {
return {
color: 'red'
};
};
}
if (zoomLevel > 14) {
return (
<GeoJSON
id={id}
data={geoJSON}
style={style}
onClick={() => {
this.props.selectPlot(id);
}}
/>
);
}

custom button in inline toolbar that calls custom function

I want to combine the inline toolbar with the example from draft.js to insert a link into the editor.
Thanks to the draft.js plugin FAQ I am able to add a decorator to the draft-js-plugin editor which inserts a link on a button click.
Now I want to put this button inside the inline-toolbar from draft-js-plugins. That doesn't seem to work. This is what I've done so far:
Editor.jsx
...
const inlineToolbarPlugin = createInlineToolbarPlugin({
theme: {buttonStyles, toolbarStyles},
structure: [..., LinkButton]
});
const {InlineToolbar} = inlineToolbarPlugin;
const plugins = [inlineToolbarPlugin];
class RMSEditor extends Component {
...
render() {
return (
<div>
<div className={editorStyles.editor}>
<Editor
editorState={this.state.editorState}
onChange={this.onChange}
plugins={plugins}
decorators={this.decorator}
ref={(element) => {
this.editor = element;
}}
readOnly={this.state.readOnly}
/>
<InlineToolbar />
</div>
</div>
);
}
LinkButton.jsx
import classNames from "classnames/bind";
import React from "react";
import {Glyphicon} from "react-bootstrap";
import styles from "./buttonStyles.less";
const cx = classNames.bind(styles);
const LinkButton = () => {
return (
<div className={cx('buttonWrapper')} onClick={(e) => {
console.log("Div", e);
}}>
<button className={cx('button')} onClick={(e) => {
console.log("Button", e);
}}><Glyphicon glyph="link"/></button>
</div>
)
};
export default LinkButton;
This way, I have managed to get a Button that shows up in the inline toolbar. But when I clicked on that button, nothing happens. I expacted that one of the onClick handlers will fire but that is not the case.
Full source code
Here you can find my full sourcecode as I don't want to put only the relevant parts directly under the questions. Please notice that the code will not work in the run snipped thingy as I have no clue how to get it to work there without setting up the hole wabpack thing.
MentionsEditor.jsx
import {CompositeDecorator, EditorState, RichUtils} from "draft-js";
import {BoldButton, CodeButton, ItalicButton, UnderlineButton, UnorderedListButton} from "draft-js-buttons";
import createInlineToolbarPlugin from "draft-js-inline-toolbar-plugin";
import {defaultSuggestionsFilter} from "draft-js-mention-plugin"; // eslint-disable-line import/no-unresolved
import Editor from "draft-js-plugins-editor"; // eslint-disable-line import/no-unresolved
import React, {Component, PropTypes} from "react";
import {Button} from "react-bootstrap";
import buttonStyles from "./buttonStyles";
import editorStyles from "./editorStyles";
import LinkButton from "./InsertLinkButton";
import toolbarStyles from "./toolbarStyles";
const inlineToolbarPlugin = createInlineToolbarPlugin({
theme: {buttonStyles, toolbarStyles},
structure: [BoldButton, ItalicButton, UnderlineButton, CodeButton, UnorderedListButton, LinkButton]
});
const {InlineToolbar} = inlineToolbarPlugin;
const plugins = [inlineToolbarPlugin];
class RMSEditor extends Component {
constructor(props) {
super(props);
}
decorator = [
{
strategy: function findLinkEntities(contentBlock, callback, contentState) {
contentBlock.findEntityRanges(
(character) => {
const entityKey = character.getEntity();
return (
entityKey !== null &&
contentState.getEntity(entityKey).getType() === 'LINK'
);
},
callback
);
},
component: function (props) {
const {url} = props.contentState.getEntity(props.entityKey).getData();
return (
<a href={url}>
{props.children}
</a>
);
}
}
];
state = {
editorState: EditorState.createEmpty(),
};
onChange = (editorState) => {
this.setState({
editorState,
});
};
editorLink = function (props) {
const {url} = props.contentState.getEntity(props.entityKey).getData();
return (
<a href={url} style={{color: "blue"}}>
{props.children}
</a>
);
};
focus = () => {
this.editor.focus();
};
insertLink = (e) => {
e.preventDefault();
const {editorState} = this.state;
const contentState = editorState.getCurrentContent();
const contentStateWithEntity = contentState.createEntity(
'LINK',
'MUTABLE',
{url: "https://example.com"}
);
const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
const newEditorState = EditorState.set(editorState, {currentContent: contentStateWithEntity});
this.setState({
editorState: RichUtils.toggleLink(
newEditorState,
newEditorState.getSelection(),
entityKey
)
}, () => {
setTimeout(() => {
this.focus()
}, 0);
});
};
render() {
return (
<div>
<Button onClick={this.insertLink}>insert URL</Button>
<div className={editorStyles.editor}>
<Editor
editorState={this.state.editorState}
onChange={this.onChange}
plugins={plugins}
decorators={this.decorator}
ref={(element) => {
this.editor = element;
}}
readOnly={this.state.readOnly}
/>
<InlineToolbar />
</div>
</div>
);
}
}
RMSEditor.propTypes = {
stakeholder: PropTypes.object.isRequired
};
export default RMSEditor;
LinkButton.jsx
import classNames from "classnames/bind";
import React from "react";
import {Glyphicon} from "react-bootstrap";
import styles from "./buttonStyles.less";
const cx = classNames.bind(styles);
const LinkButton = () => {
return (
<div className={cx('buttonWrapper')} onClick={(e) => {
console.log("Div", e);
}}>
<button className={cx('button')} onClick={(e) => {
console.log("Button", e);
}}><Glyphicon glyph="link"/></button>
</div>
)
};
export default LinkButton;
This works for me:
const MyButton = (props) => {
return (
<div className={props.className} onMouseDown={(e)=>e.preventDefault()}>
<button onClick={() => props.createComment('Hello')}>
Comment
</button>
</div>
)
}
const inlineToolbarPlugin = createInlineToolbarPlugin({
structure: [props => <MyButton {...props} createComment={(text) => alert(text)} />]
});
const { InlineToolbar } = inlineToolbarPlugin;
export default class MyComponent extends Component {
constructor(props) {
super(props);
}
render() {
const { editorState } = this.state;
return (
<div className="editor">
<Editor
editorState={editorState}
onChange={this.onChange}
plugins={[inlineToolbarPlugin]}
ref={(element) => { this.editor = element; }}
/>
<InlineToolbar />
</div>
)
}
}
Basically, this is what you need to do:
1) import the inline toolbar plugin (bonus there is a separator in there)
import createInlineToolbarPlugin, {Separator} from 'draft-js-inline-toolbar-plugin'
2) instantciate the plugin like this:
const inlineToolbarPlugin = createInlineToolbarPlugin({
structure: [
BoldButton,
ItalicButton,
UnderlineButton,
CodeButton,
Separator,
],
})
2.1) you can get those components and more from draft-js-buttons
3) add to the array more components (let's add a video button component)
AddVideoButton
this is what it looks like
import React, { Component, PropTypes } from 'react'
import classnames from 'classnames'
import { AtomicBlockUtils, RichUtils } from 'draft-js'
import createVideoPlugin from 'draft-js-video-plugin'
const {types} = createVideoPlugin()
export default class AddVideoButton extends Component {
static propTypes = {
getEditorState: PropTypes.func,
setEditorState: PropTypes.func,
style: PropTypes.shape(),
theme: PropTypes.shape(),
}
// toggleStyle(event){
// const { setEditorState, getEditorState, style } = this.props
//
// event.preventDefault()
// setEditorState(
// RichUtils.toggleInlineStyle(
// getEditorState(),
// style
// )
// )
// }
toggleVideo(event){
const { setEditorState, getEditorState } = this.props
event.preventDefault()
const style = { width: 100}
const editorState = getEditorState()
const currentContent = editorState.getCurrentContent()
const selectionState = editorState.getSelection()
const anchorKey = selectionState.getAnchorKey()
const currentContentBlock = currentContent.getBlockForKey(anchorKey)
const start = selectionState.getStartOffset()
const end = selectionState.getEndOffset()
const src = currentContentBlock.getText().slice(start, end)
if (RichUtils.getCurrentBlockType(editorState) === types.ATOMIC) {
return editorState
}
const contentState = editorState.getCurrentContent()
const contentStateWithEntity = contentState.createEntity(
types.VIDEOTYPE,
'IMMUTABLE',
{ src, style }
)
const entityKey = contentStateWithEntity.getLastCreatedEntityKey()
const newEditorState = AtomicBlockUtils.insertAtomicBlock(editorState, entityKey, ' ')
return setEditorState(newEditorState)
}
render(){
const { theme, style, getEditorState } = this.props
const styleIsActive = () => getEditorState().getCurrentInlineStyle().has(style)
return (
<div
className={theme.buttonWrapper}
onMouseDown={(e)=>e.preventDefault()}
>
<button
className={classnames({
[theme.active]:styleIsActive(),
[theme.button]:true,
})}
onClick={::this.toggleVideo}
>
<svg height="24" viewBox="0 0 120 100" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="M111.374,20.922c-3.151-3.159-7.557-5.128-12.374-5.125H29c-4.817-0.002-9.222,1.966-12.375,5.125 c-3.159,3.152-5.128,7.557-5.125,12.375v40.038c-0.002,4.818,1.966,9.223,5.125,12.375c3.152,3.158,7.557,5.129,12.375,5.125h70 c4.817,0.004,9.224-1.967,12.375-5.125c3.159-3.152,5.128-7.557,5.125-12.375V33.296C116.503,28.479,114.534,24.074,111.374,20.922z M104.624,78.959c-1.454,1.447-3.413,2.328-5.624,2.33H29c-2.211-0.002-4.17-0.883-5.625-2.33c-1.447-1.455-2.328-3.414-2.33-5.625 V33.296c0.002-2.211,0.883-4.17,2.33-5.625c1.455-1.447,3.413-2.328,5.625-2.33h70c2.211,0.002,4.17,0.883,5.625,2.33 c1.447,1.455,2.327,3.413,2.329,5.625v40.038C106.952,75.545,106.072,77.504,104.624,78.959z" fill="#232323"/><path d="M77.519,50.744L57.45,39.161c-0.46-0.266-0.971-0.397-1.483-0.397c-0.513,0-1.023,0.131-1.484,0.397 c-0.918,0.528-1.483,1.509-1.483,2.569v23.171c0,1.061,0.565,2.04,1.483,2.57c0.46,0.267,0.971,0.396,1.484,0.396 c0.513,0,1.023-0.13,1.483-0.396l20.069-11.586c0.918-0.531,1.482-1.51,1.482-2.571C79.001,52.253,78.437,51.274,77.519,50.744z" fill="#232323"/>
<path d="M0 0h24v24H0z" fill="none" />
</svg>
</button>
</div>
)
}
}
or
you can imitate anything out of the draft-js-buttons
Let me know if I helped or if you have any other questions