testing input events with react testing library - react-testing-library

I've created a small keypad app in react and I'm trying to test the input event on the app and for some reason I am not getting the expected result. I'm trying to test it to failure and success. The test I'm running is this below, I want to input 1995 (the correct combination), click the unlock button and ultimately have a message return Unlocked! but it only returns Incorrect Code! which should only happen if the code is incorrect or the input field is empty. But it shouldn't be empty as I have filled it out in the test..
here is a codesandbox: https://codesandbox.io/s/quirky-cloud-gywu6?file=/src/App.test.js:0-26
Any ideas?
test:
const setup = () => {
const utils = render(<App />);
const input = utils.getByLabelText("input-code");
return {
input,
...utils
};
};
test("It should return a successful try", async () => {
const { input, getByTestId } = setup();
await act(async () => {
fireEvent.change(input, { target: { value: "1995" } });
});
expect(input.value).toBe("1995");
await act(async () => {
fireEvent.click(getByTestId("unlockbutton"));
});
expect(getByTestId("status")).toHaveTextContent("Unlocked!");
});
the component I'm trying to test
import React, { useState, useEffect } from "react";
import Keypad from "./components/Keypad";
import "./App.css";
import "./css/Result.css";
function App() {
//correctCombination: 1995
const [result, setResult] = useState("");
const [locked, setLocked] = useState("Locked");
const [tries, setTries] = useState(0);
const [hide, setHide] = useState(true);
//Along with the maxLength property on the input,
// this is also needed for the keypad
useEffect(() => {
(function() {
if (result >= 4) {
setResult(result.slice(0, 4));
}
})();
}, [result]);
const onClick = button => {
switch (button) {
case "unlock":
checkCode();
break;
case "clear":
clear();
break;
case "backspace":
backspace();
break;
default:
setResult(result + button);
break;
}
};
const checkCode = () => {
if (result === "1995") {
setLocked("Unlocked!");
setTries(0);
} else if (tries === 3) {
setHide(false);
setLocked("Too many incorrect attempts!");
setTimeout(() => {
setHide(true);
}, 3000);
} else {
setLocked("Incorrect code!");
setTries(tries + 1);
}
};
const clear = () => {
setResult("");
};
const backspace = () => {
setResult(result.slice(0, -1));
};
const handleChange = event => {
setResult(event.target.value);
};
return (
<div className="App">
<div className="pin-body">
<h1>Pin Pad</h1>
<div className="status">
<h2 data-testid="status">{locked}</h2>
</div>
<div className="result">
<input
maxLength={4}
type="phone"
aria-label="input-code"
data-testid="inputcode"
placeholder="Enter code"
onChange={handleChange}
value={result}
/>
</div>
{hide ? <Keypad onClick={onClick} /> : false}
</div>
</div>
);
}
export default App;

Related

disabled submit button after submitting form data to server. in react native

I am new to react native. I have created a form from where I am sending some data to server. Now I want that to disabled submit button after user click on submit . once user submit data then after He unable to send data. means I want to avoid duplicate entry. please help. thanks. if possible also tell how to do it with functional component too.
here is my code
export default function Add(props) {
const { navigation } = props
const offset = (Platform.OS === 'android') ? -200 : 0;
const [AmazonError, setAmazonError] = useState([]);
const [Amazon, setAmazon] = useState('');
const [AmazonCNError, setAmazonCNError] = useState([]);
const [AmazonCN, setAmazonCN] = useState('');
const [AEPSError, setAEPSError] = useState([]);
const [AEPS, setAEPS] = useState('');
const [DMTError, setDMTError] = useState([]);
const [DMT, setDMT] = useState('');
const [BBPSError, setBBPSError] = useState([]);
const [BBPS, setBBPS] = useState('');
const [leadTagNumber, setLeadTagNumber] = useState([]);
const validateInputs = () => {
if (!Amazon.trim()) {
setAmazonError('Please Fill The Input')
return;
}
if (!AmazonCN.trim()) {
setAmazonCNError('Please Fill The Input')
return;
}
if (!AEPS.trim()) {
setAEPSError('Please Fill The Input')
return;
}
if (!DMT.trim()) {
setDMTError('Please Fill The Input')
return;
}
if (!BBPS.trim()) {
setBBPSError('Please Fill The Input')
return;
}
else
{
//+++++++++++++++++++++++++++++++++=submitting form data to api start+++++++++++++++++++++++++++++++++++
{
const leadTagNumber = props.route.params.leadTagNumber
AsyncStorage.multiGet(["application_id", "created_by",'leadTagNumber']).then(response => {
// console.log(response[0][0]) // Key1
console.log(response[0][1]) // Value1
// console.log(response[1][0]) // Key2
console.log(response[1][1]) // Value2
console.log(leadTagNumber)
fetch('https://xyxtech/Android_API_CI/uploaddata/tbservice_details', {
method: 'POST',
headers: {'Accept': 'application/json, text/plain, */*', "Content-Type": "application/json" },
// We convert the React state to JSON and send it as the POST body
body: JSON.stringify([{ data}])
})
.then((returnValue) => returnValue.json())
.then(function(response) {
console.log(response)
Alert.alert("File uploaded");
return response.json();
});
});
// event.preventDefault();
}
//+++++++++++++++++++++++++++++++++submitting form data to api end++++++++++++++++++++++++++++++++++++++
Alert.alert("success")
return;
//}
}
};
const handleAmazon = (text) => {
setAmazonError('')
setAmazon(text)
}
const handleAmazonCN= (text) => {
setAmazonCNError('')
setAmazonCN(text)
}
const handleAEPS= (text) => {
setAEPSError('')
setAEPS(text)
}
const handleDMT = (text) => {
setDMTError('')
setDMT(text)
}
const handleBBPS = (text) => {
setBBPSError('')
setBBPS(text)
}
return (
<View style={{flex: 1}}>
<ScrollView style={{flex: 1,}} showsVerticalScrollIndicator={false}>
<TextInput
maxLength={30}
placeholder="AEPS (Adhar enabled payment system) *"
style={styles.inputStyle}
onChangeText={(text)=>handleAEPS(text)}
defaultValue={AEPS}
value = {AEPS} />
<Text>{AEPSError}</Text>
</ScrollView>
<Button
style={styles.inputStyleB}
title="Submit"
color="#FF8C00"
onPress={() => validateInputs()}
/>
</View>
)
}
Set new state property to enable/disable button
const [disableButton, setDisableButton] = useState(false);
Now in your button component, add disabled property with disableButton state.
<Button
style={styles.inputStyleB}
title="Submit"
color="#FF8C00"
onPress={() => validateInputs()}
disabled={disableButton}
/>
Disable you button before fetching data
setDisableButton(true) //Add this
const leadTagNumber = props.route.params.leadTagNumber
Incase of fetch error or after fetching is complete, enable button again
setDisabledButton(false)

react-map-gl-draw addFeatures creating duplicates

Describe the Issue
When feature is deleted and new feature is added for vertices to update. The new feature is created twice. Every time this cycle is repeated.
Function deleteVertex have the the code of this problem.
Actual Result
new duplicate feature is added.
Expected Result
Only 1 new feature should be created.
Reproduce Steps
Add a feature. Remove the Feature any 1 index of coordinates. Remove the old Feature. Add the this new modified version of Feature using addFeatures.
Question
Also I want to know if there is any easy way to add or remove the vertices.
Code
const MapOverLay = ({
type,
viewport,
setViewport,
save,
close,
previousFeatures,
defaultViewPort,
}) => {
const editorRef = useRef(null);
const featureRef = useRef(null);
const locateIconRef = useRef(false);
const [previousFeature, setPreviousFeature] = useState(previousFeatures);
const [mapData, setMapData] = useState({
editor: {
type: previousFeatures.length ? 'Editing' : '',
mode: previousFeatures.length ? new EditingMode() : null,
selectedFeatureIndex: null,
},
});
const onSelect = map => {
if (map.selectedEditHandleIndex !== null) {
featureRef.current = {
type: 'selectedEditHandleIndex',
index: map.selectedEditHandleIndex,
featureIndex: map.selectedFeatureIndex,
};
}
else {
featureRef.current = {
type: 'selectedFeatureIndex',
index: map.selectedFeatureIndex,
};
}
};
const onUpdate = ({ editType, data }) => {
if (editType === 'addFeature') {
const temp_data = makeClone(mapData);
temp_data.editor.type = 'Editing';
temp_data.editor.mode = new EditingMode();
setMapData(temp_data);
}
if (previousFeatures.length) {
setPreviousFeature(data);
}
};
const deleteVertex = () => {
const feature = editorRef.current.getFeatures()[0];
if (feature.geometry.coordinates[0].length !== 2) {
feature.geometry.coordinates[0].splice(featureRef.current.index, 1);
editorRef.current.deleteFeatures(featureRef.current.featureIndex);
editorRef.current.addFeatures(feature);
console.log('Delete');
}
else {
editorRef.current.deleteFeatures(featureRef.current.featureIndex);
}
featureRef.current = null;
};
// console.log(editorRef.current);
const deleteFeature = () => {
if (previousFeature.length) {
setPreviousFeature([]);
}
else {
editorRef.current.deleteFeatures(featureRef.current.index);
}
save([]);
};
const onDelete = () => {
if (featureRef.current !== null) {
switch (featureRef.current.type) {
case 'selectedEditHandleIndex':
deleteVertex();
return;
case 'selectedFeatureIndex':
deleteFeature();
return;
default:
break;
}
}
};
const onSave = map => {
save(map.getFeatures());
};
const getDrawModeStyle = () => {
switch (type) {
case 'Polygon':
return style.polygon;
case 'Point':
return style.point;
}
};
const getDrawModePressedStyle = () => {
switch (type) {
case 'Polygon':
return style.pressed_polygon;
case 'Point':
return style.pressed_point;
}
};
const getDrawMode = () => {
switch (type) {
case 'Polygon':
return new DrawPolygonMode();
case 'Point':
return new DrawPointMode();
}
};
const onToolClick = () => {
if (editorRef.current.getFeatures().length === 0) {
const temp_data = makeClone(mapData);
if (type === temp_data.editor.type) {
temp_data.editor.type = '';
temp_data.editor.mode = null;
setMapData(temp_data);
}
else {
temp_data.editor.type = type;
temp_data.editor.mode = getDrawMode();
setMapData(temp_data);
}
}
};
const locate = map => {
locateIconRef.current = true;
const features = map.getFeatures();
if (features.length) {
const center = centerOfMass(features[0]);
setViewport(prevState => ({
...prevState,
zoom: 10,
longitude: center.geometry.coordinates[0],
latitude: center.geometry.coordinates[1],
}));
}
else {
defaultViewPort();
}
};
const viewPortChangeFromMap = nextViewport => {
locateIconRef.current = false;
setViewport(nextViewport);
};
const _renderDrawTools = () => {
// copy from mapbox
return (
<>
<div className='mapboxgl-ctrl-top-left'>
<div className={['mapboxgl-ctrl-group mapboxgl-ctrl', style.navigation_group_top_left].join(' ')}>
<button
className="mapbox-gl-draw_ctrl-draw-btn"
title="Save"
onClick={() => onSave(editorRef.current)}
/>
<button
id={getDrawModeStyle()}
className={
[
'mapbox-gl-draw_ctrl-draw-btn mapbox-gl-draw_polygon',
mapData.editor.type === type ? getDrawModePressedStyle() : '',
].join(' ')
}
title="Select"
onClick={() => onToolClick()}
/>
<button
className="mapbox-gl-draw_ctrl-draw-btn mapbox-gl-draw_trash"
title="Remove"
onClick={onDelete}
/>
</div>
</div>
<div className='mapboxgl-ctrl-top-right'>
<div className={['mapboxgl-ctrl-group mapboxgl-ctrl', style.navigation_group_top_right].join(' ')}>
<button
className="mapbox-gl-draw_ctrl-draw-btn"
title="Close"
onClick={close}
/>
</div>
</div>
<div className='mapboxgl-ctrl-bottom-right'>
<div className={['mapboxgl-ctrl-group mapboxgl-ctrl', style.navigation_group_bottom_right].join(' ')}>
<button
className="mapbox-gl-draw_ctrl-draw-btn"
title="Focus"
onClick={() => locate(editorRef.current)}
/>
</div>
</div>
</>
);
};
return (
<ReactMapGL
{...viewport}
mapStyle="mapbox://styles/giddyops/ckips5wdw61xb17qs3eorsoj9"
mapboxApiAccessToken={MAPBOX_TOKEN}
onViewportChange={viewPortChangeFromMap}
attributionControl={false}
transitionDuration={1000}
transitionInterpolator={locateIconRef.current ? new FlyToInterpolator() : null}
>
<Editor
ref={editorRef}
mode={mapData.editor.mode}
clickRadius={12}
features={previousFeature.length ? previousFeature : null}
onSelect={onSelect}
onUpdate={onUpdate}
editHandleShape={'circle'}
featureStyle={getFeatureStyle}
editHandleStyle={getEditHandleStyle}
onClick={e => console.log(e)}
/>
{_renderDrawTools()}
</ReactMapGL>
);
};
I have reviewed your code and found out that it could be issue that delete features was finished deleting way after than adding feature.
you can solve this issue by making the following changes:
make deleteVertex function async.
cloning the feature in the first line.
await the editorRef.current.deleteFeatures(featureRef.current.featureIndex);

Not closing camera in zxing

In the Zxing library, I want to close the camera when the user clicks cancel. So I used a button and add onclick event to it. it is calling resetReader method. I called this method after gets a barcode value or in the cancel button onclick event.If it is getting barcode values, this resetReader method works perfectly. if we cancel, the camera doesn't stop. Am I missing something?
const codeReader = new BrowserMultiFormatReader(hints);
const resetReader = () => {
codeReader.reset();
codeReader.stopContinuousDecode();
};
for those who haven't figured it out yet? I have found a solution to this problem. Harendrra's solution didn't work for me, but this one did in combination with usestate. For my project the code uses Bootstrap. So when I click on a button the Modal appears. The camera loads. When I click on the Close button the camera disappears. Hope this is a solutions for everyone, enjoy ;-)
export default function Example(props) {
// codeReader
const [codeReader, setReader] = useState(new BrowserMultiFormatReader());
const [videoInputDevices, setVideoInputDevices] = useState([]);
const [selectedVideoDevice, selectVideoDevice] = useState('');
useEffect(() => {
(async () => {
const videoInputDeviceList = await codeReader.listVideoInputDevices();
setVideoInputDevices(videoInputDeviceList);
if (videoInputDeviceList.length > 0 && selectedVideoDevice == null) {
selectVideoDevice(videoInputDeviceList[0].deviceId);
}
})();
}, [codeReader, selectedVideoDevice]);
const handleShow = () => {
setBrand('');
// Open modal.
setShow(true);
codeReader.decodeFromVideoDevice(selectedVideoDevice, 'videoElement', (res) => {
setCanClose(true);
if (res) {
const rawText = res.getText();
axios
.get(`https://world.openfoodfacts.org/api/v0/product/${rawText}.json`)
.then((result) => {
// set data
setBrand(result.data.product.brands);
// close modal
setShow(false);
// codeReader reset
codeReader.reset();
})
.catch((err) => console.log('error', err));
}
});
};
const handleClose = () => {
// codeReader reset.
setReader(codeReader.reset());
// Close modal
setShow(false);
// Set new codeReader.
// The solution for the error messages after the codeReader reset.
// This will build the codeReader for the next time.
setReader(new BrowserMultiFormatReader(hints));
};
return (
<Fragment>
<div className='py-2'>
<div>Brand: {brand}</div>
<Button variant='primary' onClick={handleShow}>
Launch static backdrop modal
</Button>
<Modal show={show} onHide={handleClose} backdrop='static' keyboard={false} centered id='scanProductModal'>
<Modal.Body>
<div
onChange={(event) => {
const deviceId = event.target.value;
selectVideoDevice(deviceId);
}}
>
<div className='button-group-top'>
<select className='form-select form-select-sm' aria-label='Default select example'>
{videoInputDevices &&
videoInputDevices.map((inputDevice, index) => {
return (
<option value={inputDevice.deviceId} key={index}>
{inputDevice.label || inputDevice.deviceId}
</option>
);
})}
</select>
</div>
<video id='videoElement' width='600' height='400' />
<Button className='btn btn-danger' onClick={handleClose}>
Close
</Button>
</div>
</Modal.Body>
</Modal>
</Fragment>
);
}
Yes, I resolved. You have to create codeReader object at the top of the Class. Try this code.
import "../App.css";
import { BrowserBarcodeReader } from "#zxing/library";
class Barcode extends React.Component {
codeReader = new BrowserBarcodeReader();
constructor(props) {
super(props);
this.state = { reader: {}, selectedDevice: "" };
this.startButton = this.startButton.bind(this);
this.resetButton = this.resetButton.bind(this);
this.getBarcode = this.getBarcode.bind(this);
}
componentDidMount() {
this.getBarcode();
}
startButton() {
console.log("start", this.codeReader);
this.codeReader
.decodeOnceFromVideoDevice(this.state.selectedDevice, "video")
.then(result => {
document.getElementById("result").textContent = result.text;
})
.catch(err => {
console.error(err.toString());
document.getElementById("result").textContent = err;
});
console.log(
`Started continous decode from camera with id ${this.state.selectedDevice}`
);
}
resetButton() {
this.codeReader && this.codeReader.reset();
document.getElementById("result").textContent = "";
}
getBarcode() {
let selectedDeviceId;
return this.codeReader.getVideoInputDevices().then(videoInputDevices => {
const sourceSelect = document.getElementById("sourceSelect");
selectedDeviceId = videoInputDevices[0].deviceId;
if (videoInputDevices.length > 1) {
videoInputDevices.forEach(element => {
const sourceOption = document.createElement("option");
sourceOption.text = element.label;
sourceOption.value = element.deviceId;
sourceSelect.appendChild(sourceOption);
});
sourceSelect.onchange = () => {
selectedDeviceId = sourceSelect.value;
};
const sourceSelectPanel = document.getElementById(
"sourceSelectPanel"
);
sourceSelectPanel.style.display = "block";
}
this.setState({
selectedDevice: selectedDeviceId
});
})
.catch(err => {
alert(err);
});
}
render() {
return (
<div>
<h2>Barcode</h2>
{Object.keys(this.codeReader).length > 0 && (
<div>
<div>
<button
className="button"
id="startButton"
onClick={this.startButton}
>
Start
</button>
<button
className="button"
id="resetButton"
onClick={this.resetButton}
>
Reset
</button>
</div>
<div>
<video
id="video"
width="600"
height="400"
style={{ border: "1px solid gray" }}
></video>
</div>
<label>Result:</label>
<pre>
<code id="result"></code>
</pre>
</div>
)}
</div>
);
}
}
export default Barcode; ```

converting from blob to binary to save it to mongodb

I get the object like that from the front end to my node express server.
{ picture: [ { preview: 'blob:http://localhost:3000/1f413443-83d8-499e-a432-9ac51a2592b7' } ],
name: 'fsdfs',
description: 'fdfd',
url: 'fdfd',
about: 'dfdf' }
i get this error :
TypeError: path must be a string or Buffer
at TypeError (native)
this is my function where i save to the mongodb
exports.create_a_project = function(req, res) {
console.log(req.body);
var new_project = new Project(req.body);
new_project.picture.data = fs.readFileSync(req.body.picture[0]);
new_project.picture.contentType = 'image/png';
new_project.save(function(err, project) {
if (err)
res.send(err);
res.json(project);
});
};
some how i need to convert the image i am receiving to be binary in order to save.
or i need to send it as a binary base64 from the client side it self.
my client side i use react redux Dropzone for sending my data.
here is my form and how it look like.
import React from 'react';
import {Field, reduxForm} from 'redux-form';
import Dropzone from 'react-dropzone';
const FILE_FIELD_NAME = 'picture';
const renderDropzoneInput = (field) => {
const files = field.input.value;
return (
<div>
<Dropzone
name={field.name}
onDrop={(filesToUpload, e) => field.input.onChange(filesToUpload)}
>
<div>Try dropping some files here, or click to select files to
upload.
</div>
</Dropzone>
{field.meta.touched &&
field.meta.error &&
<span className="error">{field.meta.error}</span>}
{files && Array.isArray(files) && (
<ul>
{files.map((file, i) => <li key={i}>{file.name}</li>)}
</ul>
)}
</div>
);
};
const validate = values => {
const errors = {};
if (!values.name) {
errors.name = 'Required';
} else if (values.name.length > 15) {
errors.name = 'Must be 15 characters or less';
}
if (!values.description) {
errors.description = 'Required';
} else if (values.description.length > 15) {
errors.description = 'Must be 75 characters or less';
}
if (!values.url) {
errors.url = 'Required';
} else if (values.url.length > 15) {
errors.url = 'Must be 15 characters or less';
}
if (!values.about) {
errors.about = 'Required';
} else if (values.about.length > 15) {
errors.about = 'Must be 15 characters or less';
}
if (!values.picture) {
errors.picture = 'Required';
} else if (values.picture.length > 15) {
errors.picture = 'Must be 15 characters or less';
}
// if (!values.email) {
// errors.email = 'Required';
// } else if (!/^[A-Z0-9._%+-]+#[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)) {
// errors.email = 'Invalid email address';
// }
// if (!values.age) {
// errors.age = 'Required';
// } else if (isNaN(Number(values.age))) {
// errors.age = 'Must be a number';
// } else if (Number(values.age) < 18) {
// errors.age = 'Sorry, you must be at least 18 years old';
// }
return errors;
};
const warn = values => {
const warnings = {};
// if (values.age < 19) {
// warnings.age = 'Hmm, you seem a bit young...';
// }
return warnings;
};
const renderField = ({input, label, type, meta: {touched, error, warning}}) => (
<div>
<label>{label}</label>
<div>
<input {...input} placeholder={label} type={type}/>
{touched && ((error && <span>{error}</span>) ||
(warning && <span>{warning}</span>))}
</div>
</div>
);
const SyncValidationForm = (props) => {
const {handleSubmit, pristine, reset, submitting} = props;
return (
<form onSubmit={handleSubmit}>
<Field name="name" type="text" component={renderField}
label="Name"/>
<Field name="description" type="text" component={renderField}
label="Description"/>
<Field name="url" type="text" component={renderField}
label="Url"/>
<Field name="about" type="text" component={renderField}
label="About"/>
{/*<Field name="picture" type="text" component={renderField}*/}
{/*label="Picture"/>*/}
<Field
name={FILE_FIELD_NAME}
component={renderDropzoneInput}
/>
{/*<Field name="email" type="email" component={renderField} label="Email"/>*/}
{/*<Field name="age" type="number" component={renderField} label="Age"/>*/}
<div>
<button type="submit" disabled={submitting}>Submit</button>
<button type="button" disabled={pristine || submitting}
onClick={reset}>Clear Values
</button>
</div>
</form>
);
};
export default reduxForm({
form: 'syncValidation', // a unique identifier for this form
validate, // <--- validation function given to redux-form
warn // <--- warning function given to redux-form
})(SyncValidationForm);
this is my service function which deals with the rest api.
const addProject = (newProject) => {
let data = JSON.stringify(newProject);
return axios.post('http://localhost:3008/projects', data, {
headers: {
'Content-Type': 'application/json',
}
}
).then(response => {
// console.log(response)
}).catch(error => {
console.log(error)
});
};
So I think your problem is in this line:
new_project.picture.data = fs.readFileSync(req.body.picture[0]);
And it's the mongoDB table column that you are inserting data into that is giving you that error. It's expecting a string or Buffer and you gave it a File object.
You can get a base64 byte string using what I posted here, which I'll try and integrate with your code, below:
You'd need to ensure you have a variable to collect your file(s). This is how I have the top of my page set up:
import React from 'react'
import Reflux from 'reflux'
import {Form, Card, CardBlock, CardHeader, CardText, Col, Row, Button } from 'reactstrap'
import actions from '~/actions/actions'
import DropZone from 'react-dropzone'
// stores
import SomeStore from '~/stores/someStore.jsx'
Reflux.defineReact(React)
export default class myUploaderClass extends Reflux.Component {
constructor(props) {
super(props);
this.state = {
attachments: [],
};
this.stores = [
SomeStore,
]
....
Then bind new functions:
....
this.getData = this.getData.bind(this);
this.processFile = this.processFile.bind(this);
this.errorHandler = this.errorHandler.bind(this);
this.progressHandler = this.progressHandler.bind(this);
} // close constructor
Then we work on supplying the bytes to attachments and sending it to new_project.picture.data. For me, I use a function that runs onDrop of the DropZone using onDrop={this.uploadFile}. I can't really tell what you're doing because I have no clue what filesToUpload refers to. My uploadFile looks like this:
uploadFile(event){
this.setState({
files: event,
});
document.getElementById('docDZ').classList.remove('dragover');
document.getElementById('progress').textContent = '000';
document.getElementById('progressBar').style.width = '0%';
this.state.files = event; // just for good measure
for (let i = 0; i < this.state.files.length; i++) {
const a = i + 1;
console.log('in loop, pass: ' + a);
const f = this.state.files[i];
this.getData(f); // this will load the file to SomeStore.state.attachments
}
}
and this would be the getData function ran for each file dropped/added to the DropZone:
getData(f) {
const reader = new FileReader();
reader.onerror = this.errorHandler;
reader.onprogress = this.progressHandler;
reader.onload = this.processFile(f);
reader.readAsDataURL(f);
}
Then processFile() from the onload runs:
processFile(theFile) {
return function(e) {
const bytes = e.target.result.split('base64,')[1];
const fileArray = [];
// *** Collect any existing attachments ***
// I found I could not get it from this.state, but had to use
// my store, instead
if (SomeStore.state.attachments.length > 0) {
for (let i=0; i < SomeStore.state.attachments.length; i++) {
fileArray.push(SomeStore.state.attachments[i]);
}
}
// Add the new one to this.state
fileArray.push(bytes);
// Update the state
SomeStore.setState({
attachments: fileArray,
});
// This seemed to automatically add it to this.state, for me.
}
}
Once you have that, you should be able to do:
new_project.picture.data = this.state.attachments[0];
If not, for some reason, you might try to call this inside exports.create_a_project(), as the first thing you do:
getData(req.body.picture[0]);
This might even work without having to modify your onDrop routine from what you have. And if this.state.attachments doesn't have anything, your SomeStore.state.attachments definitely should, assuming you have this saved to a folder called stores as someStore.jsx.
import Reflux from 'reflux'
import Actions from '~/actions/actions`
class SomeStore extends Reflux.Store
{
constructor()
{
super();
this.state = {
attachments: [],
};
this.listenables = Actions;
this.baseState = {
attachments: [],
};
}
onUpdateFields(name, value) {
this.setState({
[name]: value,
});
}
onResetFields() {
this.setState({
attachments: [],
});
}
}
const reqformdata = new SomeStore
export default reqformdata
Additional functions
errorHandler(e){
switch (e.target.error.code) {
case e.target.error.NOT_FOUND_ERR:
alert('File not found.');
break;
case e.target.error.NOT_READABLE_ERR:
alert('File is not readable.');
break;
case e.target.error.ABORT_ERR:
break; // no operation
default:
alert('An error occurred reading this file.');
break;
}
}
progressHandler(e) {
if (e.lengthComputable){
const loaded = Math.round((e.loaded / e.total) * 100);
let zeros = '';
// Percent loaded in string
if (loaded >= 0 && loaded < 10) {
zeros = '00';
}
else if (loaded < 100) {
zeros = '0';
}
// Display progress in 3-digits and increase bar length
document.getElementById("progress").textContent = zeros + loaded.toString();
document.getElementById("progressBar").style.width = loaded + '%';
}
}
And my DropZone & applicable progress indicator markup:
render(){
const dropZoneStyle = {
height: "34px",
width: "300px",
border: "1px solid #ccc",
borderRadius: "4px",
};
return (
<Form>
<Col xs={5}>
<DropZone type="file" id="docDZ"
onDrop={this.uploadFile}
onDropRejected={this.onDropRejected}
onClick={this.handleUploadClick}
onChange={this.handleChange}
onDragEnter={this.handleDragEnter}
onDragLeave={this.handleDragLeave}
accept=".doc, .docx, .gif, .png, .jpg, .jpeg, .pdf"
multiple="true"
maxSize={999000}
style={dropZoneStyle}>
{'Click HERE to upload or drop files here...'}
</DropZone>
<table id="tblProgress">
<tbody>
<tr>
<td><b><span id="progress">000</span>%</b> <span className="progressBar"><span id="progressBar" /></span></td>
</tr>
</tbody>
</table>
</Col>
</Row>
</Form>
)
} // close render
} // close class
And CSS:
.progressBar {
background-color: rgba(255, 255, 255, .1);
width: 100%;
height: 26px;
}
#progressBar {
background-color: rgba(87, 184, 208, .5);
content: '';
width: 0;
height: 26px;
}
Other functions you're missing:
handleUploadClick(){
return this.state;
}
handleChange(){
this.state.errors.fileError = "";
}
handleDragEnter(event){
event.preventDefault();
document.getElementById("docDZ").classList.add("dragover");
}
handleDragLeave(event){
event.preventDefault();
document.getElementById("docDZ").classList.remove("dragover");
}
onDropRejected(files){
const errors ={}
let isAlertVisible = false;
for(let i=0, j = files.length; i < j; i++){
const file = files[i];
const ext = file.name.split('.').pop().toLowerCase();
//console.log(ext)
if(this.isFileTypeValid(ext)===false){
errors.fileError = "Only image files (JPG, GIF, PNG), Word files (DOC, DOCX), and PDF files are allowed.";
isAlertVisible = true;
}
if(ext === "docx" || ext ==="gif" || ext ==="png" || ext ==="jpg" || ext ==="jpeg" || ext ==="pdf" || ext ==="doc" && file.size > 999000){
errors.fileError = "Exceeded File Size limit! The maximum file size for uploads is 999 KB.";
isAlertVisible = true;
}
this.setState({
"errors": errors,
"isAlertVisible": isAlertVisible,
})
}
}

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