converting from blob to binary to save it to mongodb - 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,
})
}
}

Related

I want to use "el-upload" to upload images and documents, but I want to display the images

I want to use "el-upload" to upload images and documents, but I want to display the images. If "list-type='picture-card'" is used, the document will also be shown with an incorrect cover. So is there any way I can upload files that show images thumbnails, documents that show names?
enter image description here
<el-upload
action=""
ref="upload"
:auto-upload="false"
:on-change="onchange">
<el-button slot="trigger" size="small" type="primary">Click to Upload</el-button>
</el-upload>
<el-image
style="width: 100px; height: 100px"
:src="fileList[0]"
:preview-src-list="fileList">
</el-image>
data() {
return {
fileList: []
}
}, methods: {
onchange(file, fileList){
this.fileList.push(URL.createObjectURL(file.raw))
}
}
<template>
<div class="upload-file">
<el-upload
multiple
:action="uploadFileUrl"
:before-upload="handleBeforeUpload"
:file-list="fileList"
:limit="limit"
:on-error="handleUploadError"
:on-exceed="handleExceed"
:on-success="handleUploadSuccess"
:show-file-list="false"
:headers="headers"
class="upload-file-uploader"
ref="upload"
>
<!-- 上传按钮 -->
<el-button type="primary">选取文件</el-button>
</el-upload>
<!-- 上传提示 -->
<!-- <div class="el-upload__tip" v-if="showTip">
请上传
<template v-if="fileSize"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </template>
<template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template>
的文件
</div> -->
<!-- 文件列表 -->
<transition-group
class="upload-file-list el-upload-list el-upload-list--text"
name="el-fade-in-linear"
tag="ul"
>
<li
:key="file.uid"
class="el-upload-list__item ele-upload-list__item-content"
v-for="(file, index) in fileList">
<el-link
:href="`${baseUrl}${file.url}`"
:underline="false"
target="_blank"
>
<span class="el-icon-document"> {{ getFileName(file.name) }} </span>
</el-link>
<div class="ele-upload-list__item-content-action">
<el-link :underline="false" #click="handleDelete(index)" type="danger"
>删除</el-link
>
</div>
</li>
</transition-group>
</div>
</template>
<script setup>
import { getToken } from "#/utils/auth";
const props = defineProps({
modelValue: [String, Object, Array],
// 数量限制
limit: {
type: Number,
default: 1,
},
// 大小限制(MB)
fileSize: {
type: Number,
default: 2,
},
// 文件类型, 例如['png', 'jpg', 'jpeg']
fileType: {
type: Array,
default: () => ["png", "jpg", "gif"],
},
// 是否显示提示
isShowTip: {
type: Boolean,
default: true,
},
});
const { proxy } = getCurrentInstance();
const emit = defineEmits();
const number = ref(0);
const uploadList = ref([]);
const baseUrl = "URL HERE";
const uploadFileUrl = baseUrl + "Folder_URL"; //
const headers = ref({ Authorization: "Bearer " + getToken() });
const fileList = ref([]);
const showTip = computed(
() => props.isShowTip && (props.fileType || props.fileSize)
);
watch(
() => props.modelValue,
(val) => {
if (val) {
let temp = 1;
const list = Array.isArray(val) ? val : props.modelValue.split(",");
fileList.value = list.map((item) => {
if (typeof item === "string") {
item = { name: item, url: item };
}
item.uid = item.uid || new Date().getTime() + temp++;
return item;
});
} else {
fileList.value = [];
return [];
}
},
{ deep: true, immediate: true }
);
function handleBeforeUpload(file) {
if (props.fileType.length) {
let fileExtension = "";
if (file.name.lastIndexOf(".") > -1) {
fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1);
}
const isTypeOk = props.fileType.some((type) => {
if (file.type.indexOf(type) > -1) return true;
if (fileExtension && fileExtension.indexOf(type) > -1) return true;
return false;
});
if (!isTypeOk) {
proxy.$modal.msgError(
${props.fileType.join("/")}
);
return false;
}
}
if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize;
if (!isLt) {
proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
return false;
}
}
proxy.$modal.loading("正在上传文件,请稍候...");
number.value++;
return true;
}
function handleExceed() {
proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
}
function handleUploadError(err) {
proxy.$modal.msgError("上传文件失败");
}
// 上传成功回调
function handleUploadSuccess(res, file) {
uploadList.value.push({ name: res.fileName, url: res.fileName });
if (uploadList.value.length === number.value) {
fileList.value = fileList.value
.filter((f) => f.url !== undefined)
.concat(uploadList.value);
uploadList.value = [];
number.value = 0;
emit("update:modelValue", listToString(fileList.value));
proxy.$modal.closeLoading();
console.log("url", file);
}
}
// 删除文件
function handleDelete(index) {
fileList.value.splice(index, 1);
emit("update:modelValue", listToString(fileList.value));
}
// 获取文件名称
function getFileName(name) {
if (name.lastIndexOf("/") > -1) {
return name.slice(name.lastIndexOf("/") + 1);
} else {
return "";
}
}
// 对象转成指定字符串分隔
function listToString(list, separator) {
let strs = "";
separator = separator || ",";
for (let i in list) {
if (undefined !== list[i].url) {
strs += list[i].url + separator;
}
}
return strs != "" ? strs.substr(0, strs.length - 1) : "";
}
</script>
//tu
<style scoped lang="scss">
.upload-file-uploader {
margin-bottom: 5px;
}
.upload-file-list .el-upload-list__item {
border: 1px solid #e4e7ed;
line-height: 2;
margin-bottom: 10px;
position: relative;
}
.upload-file-list .ele-upload-list__item-content {
display: flex;
justify-content: space-between;
align-items: center;
color: inherit;
}
.ele-upload-list__item-content-action .el-link {
margin-right: 10px;
}
</style>

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);

testing input events with 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;

React Form validation displaying error

I am using React-Validation-Mixin together with Joi and Joi-Validation-Strategy to do some validations on a React Step/Wizard Form.
I have a parent FormStart Element that receives the state of its FormStep children through props.
The validation correctly signals that the input is required, but when I write a correct number in the browser (5 numbers as in PLZ/ZIP-Code), it will still signal that the input is invalid, even though the zip state shows a correct 5-digit number, so the next button never takes me to the next Form step.
class FormStart extends Component {
constructor(props) {
super(props);
this.state = {
step: 1,
zip: ""
}
this.goToNext = this.goToNext.bind(this);
}
goToNext() {
const { step } = this.state;
if (step !== 10) {
this.setState({ step: step + 1 });
if (step == 9) {
const values = {
zip: this.state.zip,
};
console.log(values);
// submit `values` to the server here.
}
}
}
handleChange(field) {
return (evt) => this.setState({ [field]: evt.target.value });
}
render(){
switch (this.state.step) {
case 1:
return <FormButton
onSubmit={this.goToNext}
/>;
//omitting the other 8 cases
case 9:
return <FormStep7
onSubmit={this.goToNext}
zip={this.state.zip}
onZipChange={this.handleChange('zip')}
/>;
case 10:
return <FormSuccess/>;
}
}
}
export default FormStart;
The React console shows that the zip state is correctly changed, and the Validation object also receives the same correct 5-digit zip and still holds the correct value onBlur.
class FormStep7 extends Component {
constructor(props) {
super(props);
this.validatorTypes = {
PLZ: Joi.number().min(5).max(5).required().label('PLZ').options({
language: {
number: {
base: 'wird benötigt',
min: 'muss {{limit}} Nummern enthalten',
max: 'muss {{limit}} Nummern enthalten'
}
}
})
};
this.getValidatorData = this.getValidatorData.bind(this);
this.getClasses = this.getClasses.bind(this);
this.renderHelpText = this.renderHelpText.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
getValidatorData() {
return {
PLZ: this.props.zip
};
}
getClasses(field) {
return classnames({
'form-control': true,
'has-error': !this.props.isValid(field)
});
}
renderHelpText(message) {
return (
<span className='help-block'>{message}</span>
);
}
handleSubmit(evt) {
evt.preventDefault();
const onValidate = (error) => {
if (error) {
//form has errors; do not submit
} else {
this.props.onSubmit();
}
};
this.props.validate(onValidate);
}
render() {
return (
<form role="form" onSubmit={this.handleSubmit}>
<div className='row'>
<div className="col-md-10 col-md-offset-1">
<div className='form-group'>
<label htmlFor="zip">
Einsatzort
</label>
<br />
<input className={this.getClasses('PLZ')} id="PLZ" placeholder="Meine PLZ" type="text" onChange={this.props.onZipChange} onBlur={this.props.handleValidation('PLZ')} value={this.props.zip} />
{this.renderHelpText(this.props.getValidationMessages('PLZ'))}
</div>
</div>
</div>
<div className='row'>
<div className="col-md-10 col-md-offset-1">
<button className="btn btn-green btn-block">Next</button>
</div>
</div>
</div>
</form>
);
}
}
FormStep7.propTypes = {
errors: PropTypes.object,
validate: PropTypes.func,
isValid: PropTypes.func,
handleValidation: PropTypes.func,
getValidationMessages: PropTypes.func,
clearValidations: PropTypes.func
};
export default validation(strategy)(FormStep7);
What am I doing wrong?
I found out that the issue was on Joi.number(). I changed the validation to match a Regex String pattern and then it worked.
this.validatorTypes = {
PLZ: Joi.string().regex(/^[0-9]{5}$/).label('PLZ').options({
language: {
string: {
regex: {
base: "mit 5 Nummern wird benötigt"
}
}
}
})
};

Setting state in react. Is there a better way to write this without warning errors?

I am working on a registration form on react. I am a bit stuck with the validation part of it.
As of now I am getting the following warnings four times on the console: "warning Do not mutate state directly. Use setState() react/no-direct-mutation-state."
I am guessing the reason I am getting these errors is because of statements like these "this.state.errors.firstName = "First name must be at least 2 characters.";" and like this"this.state.errors = {};" in my code.
However, I do not know how to make this better and eliminate the warnings. If you can provide a better way for me to do this that would be awesome. Any help will be highly appreciated. Thanks so much in advance!
import React, { Component } from 'react';
import {withRouter} from "react-router-dom";
import HeaderPage from './HeaderPage';
import Logo from './Logo';
import RegistrationForm from './RegistrationForm';
import axios from 'axios';
class Registration extends Component {
mixins: [
Router.Navigation
];
constructor(props) {
super(props);
this.state = {
firstName:'',
lastName:'',
email:'',
errors:{},
helpText: '',
helpUrl: '',
nextLink:''
};
this.setUserState = this.setUserState.bind(this);
this.registrationFormIsValid = this.registrationFormIsValid.bind(this);
this.saveUser = this.saveUser.bind(this);
}
setUserState(e){
const target = e.target;
const value = target.value;
const name = target.name;
this.setState({[name]: value});
//delete this line
console.log(this.state[name]);
}
registrationFormIsValid(){
var formIsValid = true;
this.state.errors = {};
//validate first name
if(this.state.firstName.length < 2){
this.state.errors.firstName = "First name must be at least 2 characters.";
formIsValid = false;
}
//validate last name
if(this.state.lastName.length < 2){
this.state.errors.lastName = "Last name must be at least 2 characters.";
formIsValid = false;
}
//validate email
if(this.state.email.length < 2){
this.state.errors.email = "Email must be at least 2 characters.";
formIsValid = false;
}
this.setState({errors : this.state.errors});
return formIsValid;
}
saveUser(e, { history }){
e.preventDefault();
// const errorWrappers = document.getElementsByClassName('input');
// for (var i=0; i < errorWrappers.length; i++) {
// const isError= errorWrappers[i].innerHTML;
// if (isError.length > 0){
// errorWrappers[i].previousSibling.className = "error-input"
// }
// }
if(!this.registrationFormIsValid()){
return;
}
const values = {
firstName: this.state.firstName,
lastName: this.state.lastName,
email: this.state.email,
password: this.state.password,
phone: this.state.phone,
address: this.state.address,
dob: this.state.birthday
}
if (this.props.userRole === 'instructor'){
axios.post(`/instructors`, values)
.then((response)=> {
//delete this line
console.log(response);
})
.catch((error) => {
console.log(error + 'something went wrooooong');
});
this.props.history.push("/success-instructor");
}else{
axios.post(`/students`, values)
.then((response)=> {
//delete this line
console.log(response);
})
.catch((error) => {
console.log(error + 'something went wrooooong');
});
if (this.props.parent === "false"){
this.props.history.push("/success-student");
}else{
this.props.history.push("/success-parent");
}
}
}
//end of validation
render() {
return (
<div className="Registration">
<div className="container menu buttons">
<HeaderPage/>
</div>
<div className="page container narrow">
<div className="cover-content">
<Logo/>
<div className="container">
<h2 className="page-title">{this.props.title}</h2>
<a className="helpLink" href={this.props.helpUrl}>{this.props.helpText}</a>
<div className="main-content background-white">
<RegistrationForm
userRole={this.props.userRole}
onChange={this.setUserState}
onSave={this.saveUser}
errors={this.state.errors}
/>
<br/>
<br/>
<br/>
</div>
</div>
</div>
</div>
</div>
);
}
}
export default withRouter(Registration);
Instead of
this.state.errors = {};
and
this.state.errors.lastName = "Last name must be at least 2 characters.";
use
this.setState({errors = {}});
this.setState({ errors: { lastName: "Last name must be at least 2 characters." } });
You need to avoid directly mutating the state.
The Warning itself answers the question. Please read the React Doc
carefully.
"warning Do not mutate state directly. Use setState()
react/no-direct-mutation-state."
Do not mutate state
Don't ever have code that directly changes state. Instead, create new object and change it. After you are done with changes update state with setState.
Instead of:
this.state.errors.someError1="e1";
this.state.errors.someError2="e2";
do this:
this.errorsObject=Object.assign({},this.state.errors,{someError1:"e1",someError2:"e2"};
and in the end:
this.setState({
errors:this.errorsObject
});
Object.assign lets us merge one object's properties into another one, replacing values of properties with matching names. We can use this to copy an object's values without altering the existing one.