Updating state based on different button clicks - javascript

I am trying to make a survey tool with firebase and react and I am implementing a template feature, so now one of the templates is working, when user clicks on create new survey, I have various buttons shown to them like: blank, temp1,temp2,temp3,temp4 based on whatever is clicked a clickhandler is triggered, now how do I update the content of the state based on which template i am getting? below is my code:
<button onClick={props.clicked} className={classes.Board}>
Board Survey
</button>
<button onClick={props.chair} className={classes.Chair}>
Chairperson Survey
</button>
<button onClick={props.member} className={classes.Member}>
Board Member Survey
</button>
And now i use
clicked={this.clickHandler}
The code to fetch survey template:
const fetchSurvey = async () => {
const res = await axios.get(
"/surveys/ckb0kmt3n000e3g5rmjnfw4go/content/questions.json"
);
return res.data;
};
Where "ckb0kmt3n000e3g5rmjnfw4go" is the survey id
const content = {
title: "Your Survey Tite",
subTitle: "Your Survey Description",
creatorDate: new Date(),
lastModified: new Date(),
questions: fetchSurvey().then(questions => {
this.setState(state => ({
...state,
content: {
...state.content,
questions
}
}));
}),
submitting: true
};
this.setState({
_id: newId(),
content: content,
userId: this.props.user_Id
});
}
Now how to do i update the questions key to run different functions based on which templates i choose on the previous page?
ClickHandler Code:
clickHandler = () => {
const newSurvey = { ...this.state };
axios
.put(
"/surveys/" + this.state._id + ".json?auth=" + this.props.token,
newSurvey
)
.then(res => {
this.props.onGetSurveyId(res.data._id);
})
.catch(error => {
console.log(error);
});
};
File1:
import React from "react";
import Button from "#material-ui/core/Button";
import Icon from "#material-ui/core/Icon";
import { makeStyles } from "#material-ui/core/styles";
import classes from "./NewSurvey.module.css";
import { Link } from "react-router-dom";
import Dialog from "#material-ui/core/Dialog";
import DialogActions from "#material-ui/core/DialogActions";
import DialogContent from "#material-ui/core/DialogContent";
import DialogTitle from "#material-ui/core/DialogTitle";
import InputLabel from "#material-ui/core/InputLabel";
import Input from "#material-ui/core/Input";
import MenuItem from "#material-ui/core/MenuItem";
import FormControl from "#material-ui/core/FormControl";
import Select from "#material-ui/core/Select";
import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
const NewSurvey = props => {
const [open, setOpen] = React.useState(false);
const [template, setTemplate] = React.useState("");
const handleChange = event => {
setTemplate(Number(event.target.value) || "");
};
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const notify = () =>
toast.info("Creating Survey....Please Wait", {
position: "top-right",
autoClose: 5000,
hideProgressBar: true,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined
});
return (
<div className={classes.CreateSurvey}>
<ToastContainer
position="top-right"
autoClose={5000}
hideProgressBar
newestOnTop
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
/>
<Dialog
disableBackdropClick
disableEscapeKeyDown
open={open}
onClose={handleClose}
className={classes.Dialog}
>
<DialogTitle className={classes.DialogTitle}>
Create New Survey
</DialogTitle>
<h5 className={classes.Info}>
Choose a template or start with a blank survey
</h5>
<DialogContent>
<button
onClick={function(event) {
props.clicked();
notify();
}}
className={classes.Blank}
>
Blank Survey
</button>
<button onClick={props.board} className={classes.Board}>
Board Survey
</button>
<button onClick={props.chair} className={classes.Chair}>
Chairperson Survey
</button>
<button onClick={props.member} className={classes.Member}>
Board Member Survey
</button>
</DialogContent>
<DialogActions>
<button onClick={handleClose} className={classes.CancelButton}>
Cancel
</button>
</DialogActions>
</Dialog>
<button
className={classes.NewSurvey}
onClick={handleClickOpen}
disabled={props.isLoading || props.error}
>
<Icon className={classes.Icon}>add_circle_outline</Icon> Create New
Survey
</button>
</div>
);
};
export default NewSurvey;
File 2:
import React, { Component } from "react";
import { withRouter } from "react-router-dom";
import { connect } from "react-redux";
import newId from "../../../ulitity/idGenerator";
import axios from "../../../axios-order";
import * as actionTypes from "../../../Store/actions/actionsTypes";
import NewSurveyView from "../../../Components/NewSurvey/NewSurvey";
import { InitQuestions } from "../../../ulitity/constants/Questions";
class NewSurvey extends Component {
state = {
_id: "",
userId: "",
content: {
title: "Your Survey Title",
subTitle: "",
creatorDate: "",
lastModified: ""
}
};
componentDidMount() {
var sampleQuestion;
var questionObj;
sampleQuestion = InitQuestions["SINGLE_LINE_TEXT"]();
questionObj = {
[sampleQuestion._id]: sampleQuestion
};
var orderQuestions;
let questions = [sampleQuestion._id];
orderQuestions = questions.map(questionId => questionObj[questionId]);
const fetchBoardMember = async () => {
const res = await axios.get(
"/surveys/ckb0kmt3n000e3g5rmjnfw4go/content/questions.json"
);
return res.data;
};
const fetchBoard = async () => {
const res = await axios.get(
"/surveys/ckb24goc800013g5r7j8igrk3/content/questions.json"
);
return res.data;
};
const fetchChair = async () => {
const res = await axios.get(
"/surveys/ckb24gt3s00053g5rrf60353r/content/questions.json"
);
return res.data;
};
const content = {
title: "Your Survey Title",
subTitle: "Your Survey Description",
creatorDate: new Date(),
lastModified: new Date(),
questions: fetchBoard().then(questions => {
this.setState(state => ({
...state,
content: {
...state.content,
questions
}
}));
}),
submitting: true
};
this.setState({
_id: newId(),
content: content,
userId: this.props.user_Id
});
}
clickHandler = () => {
const newSurvey = { ...this.state };
axios
.put(
"/surveys/" + this.state._id + ".json?auth=" + this.props.token,
newSurvey
)
.then(res => {
this.props.onGetSurveyId(res.data._id);
})
.catch(error => {
console.log(error);
});
};
render() {
return (
<div>
<NewSurveyView
clicked={this.clickHandler}
board={this.boardHandler}
chair={this.chairHandler}
member={this.memberHandler}
isLoading={this.props.loading}
error={this.props.isError}
/>
</div>
);
}
}
const mapStateToProps = state => {
return {
user_Id: state.auth.userId,
token: state.auth.token,
surveyId: state.surveyEditer.survey.id
};
};
const mapDispatchToProps = dispatch => {
return {
onGetSurveyId: surveyId =>
dispatch({ type: actionTypes.GET_SURVEY_ID, surveyId: surveyId })
};
};
export default withRouter(
connect(mapStateToProps, mapDispatchToProps)(NewSurvey)
);

Related

useRef undefined error using nextjs and remirror editor component

I'm using NextJS and Remirror Editor plugin. I get an error that setContent is undefined on the index page where the editor is loaded. I want to add an "external" button after the Component is loaded to exit the text. The component is dynamically externally loaded. I'm really unsure how to make the external button/text change to work.
Index.tsx:
import { NextPage, GetStaticProps } from 'next';
import dynamic from 'next/dynamic';
import { WysiwygEditor } from '#remirror/react-editors/wysiwyg';
import React, { useRef, useEffect } from 'react';
const TextEditor = dynamic( () => import('../text-editor'), { ssr: false } );
import natural from "natural";
export interface EditorRef {
setContent: (content: any) => void;
}
type Props = {
aaa:string
bbb:string
}
const Home: NextPage< Props > = (props) => {
//hook call ref to use editor externally
const ref = useRef<EditorRef | null>(null);
const {aaa,bbb} = props;
return (
<>
<ul>
{aaa} </ul>
Next.js Home Page
<TextEditor ref={ref} />
{
useEffect(()=>{
ref.current!.setContent({content: "testing the text has changed"})
},[])}
<button onClick={() => ref.current.setContent({content: "testing the text has changed"})}>Set another text button 2</button>
</>
);
};
var stringWordNet = "";
export const getStaticProps = async ():Promise<GetStaticPropsResult<Props>> => {
var wordnet = new natural.WordNet();
wordnet.lookup('node', function(results) {
stringWordNet = String(results[0].synonyms[1]);
});
return {
props:{
aaa:stringWordNet,
bbb:"bbb"
}
}
};
export default Home;
text-editor.tsx
import 'remirror/styles/all.css';
import { useEffect, useState, forwardRef, Ref, useImperativeHandle, useRef } from 'react';
import { BoldExtension,
ItalicExtension,
selectionPositioner,
UnderlineExtension, MentionAtomExtension } from 'remirror/extensions';
import { cx } from '#remirror/core';
import {
EditorComponent,
FloatingWrapper,
MentionAtomNodeAttributes,
Remirror,
useMentionAtom,
useRemirror, ThemeProvider, useRemirrorContext
} from '#remirror/react';
import { css } from "#emotion/css";
const styles = css`
background-color: white;
color: #101010;
border: 1px solid #ccc;
border-radius: 4px;
padding: 4px;
`;
const ALL_USERS = [
{ id: 'wordnetSuggestion1', label: 'NotreSuggestion1' },
{ id: 'wordnetSuggestion2', label: 'NotreSuggestion2' },
{ id: 'wordnetSuggestion3', label: 'NotreSuggestion3' },
{ id: 'wordnetSuggestion4', label: 'NotreSuggestion4' },
{ id: 'wordnetSuggestion5', label: 'NotreSuggestion5' },
];
const MentionSuggestor: React.FC = () => {
return (
<FloatingWrapper positioner={selectionPositioner} placement='bottom-start'>
<div>
{
ALL_USERS.map((option, index) => (
<li> {option.label}</li>
))
}
</div>
</FloatingWrapper>
);
};
const DOC = {
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'New content',
},
],
},
],
};
//Start Imperative Handle Here
export interface EditorRef {
setContent: (content: any) => void;
}
const ImperativeHandle = forwardRef((_: unknown, ref: Ref<EditorRef>) => {
const { setContent } = useRemirrorContext({
autoUpdate: true,
});
// Expose content handling to outside
useImperativeHandle(ref, () => ({ setContent }));
return <></>;
});
//Make content to show.
const TextEditor = forwardRef((_: unknown, ref: Ref<EditorRef>) => {
const editorRef = useRef<EditorRef | null>(null);
const { manager, state, getContext } = useRemirror({
extensions: () => [
new MentionAtomExtension()
],
content: '<p>I love Remirror</p>',
selection: 'start',
stringHandler: 'html',
});
return (
<>
<button
onMouseDown={(event) => event.preventDefault()}
onClick={() => editorRef.current!.setContent(DOC)}
></button>
<div className='remirror-theme'>
<Remirror manager={manager} initialContent={state}>
<EditorComponent />
<MentionSuggestor />
<ImperativeHandle ref={editorRef} />
</Remirror>
</div>
</>
);
});
export default TextEditor;

Reset table using reset button

I'm new React developer(mainly with hooks but did not find good example with hooks), here i have antd table with search functionality, my question is when user writes something in search then user gets different result, how to cancel that search by clicking 'Reset' button ?
my code:
https://codesandbox.io/s/antd-table-filter-search-forked-mqhcn?file=/src/EventsSection/EventsSection.js
You can add an id to your input into TitleSearch.js:
<Search
id='IDYOUWANT'
placeholder="Enter Title"
onSearch={onSearch}
onChange={onChange}
style={{ width: 200 }}
/>
And add event into EventsSection.js
ResetInput = () => {
const input = document.getElementById('IDYOUWANT');
input.value = '';
this.handleSearch('');
}
....
<button
onClick={this.ResetInput}
>Reset</button>
Change IDYOUWANT with your id
run this code
Created a new function for reset value and trigger it from reset button.
function:
resetValue = () =>{
this.setState({
eventsData: eventsData
});
}
And trigger from button
<button onClick={this.resetValue}>Reset</button>
all code::
import React, { Component } from "react";
import styles from "./style.module.css";
import { EventsTable } from "../EventsTable";
import { StatusFilter } from "../StatusFilter";
import { TitleSearch } from "../TitleSearch";
const eventsData = [
{
key: 1,
title: "Bulletproof EP1",
fileType: "Atmos",
process: "match media",
performedBy: "Denise Etridge",
operationNote: "-",
updatedAt: "26/09/2018 17:21",
status: "complete"
},
{
key: 2,
title: "Dexter EP2",
fileType: "Video",
process: "Compliance",
performedBy: "Dane Gill",
operationNote: "passed",
updatedAt: "21/09/2018 12:21",
status: "inProgress"
}
];
class EventsSection extends Component {
constructor(props) {
super(props);
this.state = {
eventsData
};
}
handleFilter = (key) => {
const selected = parseInt(key);
if (selected === 3) {
return this.setState({
eventsData
});
}
const statusMap = {
1: "complete",
2: "inProgress"
};
const selectedStatus = statusMap[selected];
const filteredEvents = eventsData.filter(
({ status }) => status === selectedStatus
);
this.setState({
eventsData: filteredEvents
});
};
handleSearch = (searchText) => {
const filteredEvents = eventsData.filter(({ title }) => {
title = title.toLowerCase();
return title.includes(searchText);
});
this.setState({
eventsData: filteredEvents
});
};
handleChange = (e) => {
const searchText = e.target.value;
const filteredEvents = eventsData.filter(({ title }) => {
title = title.toLowerCase();
return title.includes(searchText);
});
this.setState({
eventsData: filteredEvents
});
};
resetValue = () =>{
this.setState({
eventsData: eventsData
});
}
render() {
return (
<section className={styles.container}>
<header className={styles.header}>
<h1 className={styles.title}>Events</h1>
<button onClick={this.resetValue}>Reset</button>
<TitleSearch
onSearch={this.handleSearch}
onChange={this.handleChange}
className={styles.action}
/>
</header>
<EventsTable eventsData={this.state.eventsData} />
</section>
);
}
}
export { EventsSection };
Here is what i did in order to solve it:
i added onClick on the button
<button onClick={this.resetSearch}>Reset</button>
Then in the function i put handleSearch to '', by doing this it reset the table:
resetSearch = () =>{
this.handleSearch('')
}

Meteor react tutorial - "update failed: Access denied" after running meteor remove insecure

I've been following along with the tutorial, and i had been having some issues, but i was able to solve all of them on my own - but now I've come to this point. i ran the "meteor remove insecure" and i was pretty sure i updated my tasks.js correctly to reflect my meteor methods. i changed the import on my main.js and TaskForm.jsx and App.jsx
EDIT
**
The error i am receiving does not show up in vsCode, the error only shows in the console. but, interestingly, if you look at my methods, you see the warning message is supposed to say "Not Authorized", however the warning that appears in the console says "Update failed: Access denied"
MOST of my variables are named exactly the same as in the tutorial, some are not... and that is probably adding a layer of confusion on top of the learning process... for example i have Task, Tasks, tasksList, and taskList, are all different variables... i am aware i should make those more legible, just trying to make it "work" for now.
tasks.js:
import { Mongo } from 'meteor/mongo';
import { check } from 'meteor/check';
export const Tasks = new Mongo.Collection('taskList');
Meteor.methods({
'taskList.insert'(text) {
check(text, String);
if (!this.userId) {
throw new Meteor.Error('Not authorized.');
}
Tasks.insert({
text,
createdAt: new Date,
owner: this.userId,
username: Meteor.users.findOne(this.userId).username
})
},
'taskList.remove'(taskId) {
check(taskId, String);
if (!this.userId) {
throw new Meteor.Error('Not authorized.');
}
Tasks.remove(taskId);
},
'taskList.setChecked'(taskId, isChecked) {
check(taskId, String);
check(isChecked, Boolean);
if (!this.userId) {
throw new Meteor.Error('Not authorized.');
}
Tasks.update(taskId, {
$set: {
isChecked
}
});
}
});
App.jsx:
import React, { useState } from 'react';
import { useTracker } from 'meteor/react-meteor-data';
import _ from 'lodash';
import { Task } from './Task';
import { Tasks } from '/imports/api/tasks';
import { TaskForm } from './TaskForm';
import { LoginForm } from './LoginForm';
const toggleChecked = ({ _id, isChecked }) => {
Tasks.update(_id, {
$set: {
isChecked: !isChecked
}
})
};
const deleteTask = ({ _id }) => Tasks.remove(_id);
const logoutFunction = (e) => {
Meteor.logout(e)
}
export const App = () => {
const filter = {};
const [hideCompleted, setHideCompleted] = useState(false);
if (hideCompleted) {
_.set(filter, 'isChecked', false);
}
const { tasksList, incompleteTasksCount, user } = useTracker(() => ({
tasksList: Tasks.find(filter, { sort: { createdAt: -1 } }).fetch(),
incompleteTasksCount: Tasks.find({ isChecked: { $ne: true }}).count(),
user: Meteor.user(),
}));
if (!user) {
return (
<div className="simple-todos-react">
<LoginForm/>
</div>
);
}
return (
<div className="simple-todos-react">
<button onClick ={logoutFunction}>Log Out</button>
<h1>Flight List ({ incompleteTasksCount })</h1>
<div className="filters">
<label>
<input
type="checkbox"
readOnly
checked={ Boolean(hideCompleted) }
onClick={() => setHideCompleted(!hideCompleted)}
/>
Hide Completed
</label>
</div>
<ul className="tasks">
{ tasksList.map(task1 => <Task
key={ task1._id }
task={ task1 }
onCheckboxClick={toggleChecked}
onDeleteClick={deleteTask}/>) }
</ul>
<TaskForm user={user}/>
</div>
);
};
TaskForm.jsx:
import React, { useState } from 'react';
import { Tasks } from '/imports/api/tasks';
export const TaskForm = ({ user }) => {
const [text, setText] = useState("");
const handleSubmit = () => {
if (!text) return;
Tasks.insert({
text: text.trim(),
createdAt: new Date(),
isChecked: false,
owner: user._id,
});
setText("");
};
return (
<form className="task-form" onSubmit={handleSubmit}>
<input
type="text"
placeholder="Type to add new tasks"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<button type="submit">Add Task</button>
</form>
);
};
main.js:
import { Meteor } from 'meteor/meteor';
import { Tasks } from '/imports/api/tasks';
function insertTask({ text }) {
Tasks.insert({text});
}
Meteor.startup(() => {
if (!Accounts.findUserByUsername('meteorite')) {
Accounts.createUser({
username: 'meteorite',
password: 'password'
});
}
if (Tasks.find().count() === 0) { //this is for basic data that will never render once app is live.
[
{text:'updated THE Firstttt Task again this wont show'},
{text:'the Second Task'},
{text:'update 1 Third Task'},
{text:'Fourth Task'},
{text:'Fifth Task'},
{text:'Sixth Task'},
{text:'Seventh Task'}
].forEach(eachTask=>{insertTask(eachTask)})
}
});
Task.jsx:
import React from 'react';
import classnames from 'classnames';
export const Task = ({ task, onCheckboxClick, onDeleteClick }) => {
const classes = classnames('task', {
'checked': Boolean(task.isChecked)
});
return (
<li className={classes}>
<button onClick={ () => onDeleteClick(task) }>×</button>
<span>{ task.text }</span>
<input
type="checkbox"
checked={ Boolean(task.isChecked) }
onClick={ () => onCheckboxClick(task) }
readOnly
/>
</li>
);
};
I think i figured out SOME of it, but still having issues. These are my methods.
tasks.js:
import { Mongo } from 'meteor/mongo';
import { check } from 'meteor/check';
// export default new Mongo.Collection('taskList');
export const Tasks = new Mongo.Collection('tasks');
Meteor.methods({
'tasks.insert'(text) {
check(text, String);
if (!this.userId) {
throw new Meteor.Error('Not authorized.');
}
Tasks.insert({
text,
createdAt: new Date,
owner: this.userId,
username: Meteor.users.findOne(this.userId).username
})
},
'tasks.remove'(taskId) {
check(taskId, String);
if (!this.userId) {
throw new Meteor.Error('Not authorized.');
}
Tasks.remove(taskId);
},
'tasks.setChecked'(taskId, isChecked) {
check(taskId, String);
check(isChecked, Boolean);
if (!this.userId) {
throw new Meteor.Error('Not authorized.');
}
Tasks.update(taskId, {
$set: {
isChecked
}
});
}
});
those above are my methods.
below are my calls to those methods.
the ONLY ONE THAT WORKS is delete.
any ideas why the others are wrong?
App.jsx:
import React, { useState } from 'react';
import { useTracker } from 'meteor/react-meteor-data';
import _ from 'lodash';
import { Task } from './Task';
import { Tasks } from '/imports/api/tasks';
import { TaskForm } from './TaskForm';
import { LoginForm } from './LoginForm';
const toggleChecked = ({ _id }) => Meteor.call('tasks.setChecked', _id)
const deleteTask = ({ _id }) => Meteor.call('tasks.remove',_id);
const logoutFunction = (e) => {
Meteor.logout(e)
}
export const App = () => {
const filter = {};
const [hideCompleted, setHideCompleted] = useState(false);
if (hideCompleted) {
_.set(filter, 'isChecked', false);
}
const { tasksList, incompleteTasksCount, user } = useTracker(() => ({
tasksList: Tasks.find(filter, { sort: { createdAt: -1 } }).fetch(),
incompleteTasksCount: Tasks.find({ isChecked: { $ne: true }}).count(),
user: Meteor.user(),
}));
if (!user) {
return (
<div className="simple-todos-react">
<LoginForm/>
</div>
);
}
return (
<div className="simple-todos-react">
<button onClick ={logoutFunction}>Log Out</button>
<h1>Flight List ({ incompleteTasksCount })</h1>
<div className="filters">
<label>
<input
type="checkbox"
readOnly
checked={ Boolean(hideCompleted) }
onClick={() => setHideCompleted(!hideCompleted)}
/>
Hide Completed
</label>
</div>
<ul className="tasks">
{ tasksList.map(task1 => <Task
key={ task1._id }
task={ task1 }
onCheckboxClick={toggleChecked}
onDeleteClick={deleteTask}/>) }
</ul>
<TaskForm user={user}/>
</div>
);
};
so again, function deleteTask works as expected.
however, function toggleChecked gives me the following error:
errorClass {message: "Match error: Expected boolean, got undefined", path: "", sanitizedError: errorClass, errorType: "Match.Error", stack: "Error: Match error: Expected boolean, got undefine…ea528700c66dd42ddcc29ef7434e9e62b909dc14:3833:16)"}errorType: "Match.Error"message: "Match error: Expected boolean, got undefined"path: ""sanitizedError: errorClass {isClientSafe: true, error: 400, reason: "Match failed", details: undefined, message: "Match failed [400]", …}stack: "Error: Match error: Expected boolean, got undefined↵ at check (http://localhost:3000/packages/check.js?hash=75acf7c24e10e7b3e7b30bb8ecc775fd34319ce5:76:17)↵ at MethodInvocation.tasks.setChecked (http://localhost:3000/app/app.js?hash=7e0d6e119e929408da1c048d1448a91b43b1a759:55:5)↵ at http://localhost:3000/packages/ddp-client.js?hash=5333e09ab08c9651b0cc016f95813ab4ce075f37:976:25↵ at Meteor.EnvironmentVariable.EVp.withValue (http://localhost:3000/packages/meteor.js?hash=857dafb4b9dff17e29ed8498a22ea5b1a3d6b41d:1207:15)↵ at Connection.apply (http://localhost:3000/packages/ddp-client.js?hash=5333e09ab08c9651b0cc016f95813ab4ce075f37:967:60)↵ at Connection.call (http://localhost:3000/packages/ddp-client.js?hash=5333e09ab08c9651b0cc016f95813ab4ce075f37:869:17)↵ at toggleChecked (http://localhost:3000/app/app.js?hash=7e0d6e119e929408da1c048d1448a91b43b1a759:149:17)↵ at onClick (http://localhost:3000/app/app.js?hash=7e0d6e119e929408da1c048d1448a91b43b1a759:318:20)↵ at HTMLUnknownElement.callCallback (http://localhost:3000/packages/modules.js?hash=ea528700c66dd42ddcc29ef7434e9e62b909dc14:3784:14)↵ at Object.invokeGuardedCallbackDev (http://localhost:3000/packages/modules.js?hash=ea528700c66dd42ddcc29ef7434e9e62b909dc14:3833:16)"proto: Error
Completely answered.
Updated my TaskForm.jsx submit function to:
const handleSubmit = () => {
if (!text) return;
Meteor.call('tasks.insert',text)
};
and updated my App.jsx to:
const toggleChecked = ({ _id, isChecked }) => Meteor.call('tasks.setChecked', _id, isChecked)
const deleteTask = ({ _id }) => Meteor.call('tasks.remove',_id);

How to Jest test use of lodash.get in React component?

Error
TypeError: Cannot read property 'length' of undefined
My App component is making use of import get from 'lodash.get' https://lodash.com/docs/4.17.11#get
I'm using get inside my render function like so:
const getLabel = (listings, label) => {
const componentsMap = {
Deliveries: Delivery,
Dispensaries: Dispensary,
Doctors: Doctor
};
const DynamicIcon = componentsMap[label];
if (get(listings, 'listings').length) {
return (
<div key={label}>
<DynamicIcon fill={DARK_GRAY} /> <strong> {label} </strong>
</div>
);
}
return <div />;
};
App.test.js
import React from 'react'
import { shallow } from 'enzyme'
import toJson from 'enzyme-to-json'
import { AppJest } from './App'
import listingsMock from '../__test__/mocks/listings-mock.json';
// Mock the services.
const mockLocate = jest.fn();
const mockDisplayListing = jest.fn();
jest.mock('../actions', () => ({
locate: () => mockLocate(),
displayListing: () => mockDisplayListing()
}));
describe('<App /> component', () => {
describe('when rendering', () => {
const wrapper = shallow(<AppJest
listings={listingsMock}
locate={mockLocate}
displayListing={mockDisplayListing}
/>);
it('should render a component matching the snapshot', () => {
const tree = toJson(wrapper);
expect(tree).toMatchSnapshot();
expect(wrapper).toHaveLength(1);
});
});
});
I assumed it was because I wasn't mocking listings and passing it into the props of the shallow wrapper, but I added the mock.
listings-mock.json
{
"bottom_right": {
"latitude": 32.618865,
"longitude": -96.555516
},
"id": 1390,
"latitude": 32.78143692016602,
"listings": [
{
"avatar_image": {
"small_url": "https://images.weedmaps.com/deliveries/000/028/448/avatar/square_fill/1510581750-1507658638-Knox_Medical_Logo.png"
},
"city": "Dallas",
"distance": 2,
"id": 28448,
"license_type": "medical",
"name": "Knox Medical (Delivery Now Available)",
"online_ordering": {
"enabled_for_pickup": false,
"enabled_for_delivery": false
},
"package_level": "listing_plus",
"rating": 5,
"region_id": 1390,
"retailer_services": [
"delivery"
],
"slug": "knox-medical-dallas",
"state": "TX",
"static_map_url": "https://staticmap.weedmaps.com/static_map/13/32.7736/-96.795108/402/147/map.png",
"wmid": 459977538
}
],
"longitude": -96.7899169921875,
"name": "Dallas",
"region_path": "united-states/texas/dallas",
"slug": "dallas",
"top_left": {
"latitude": 33.016492,
"longitude": -96.999319
}
}
App.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import get from 'lodash.get';
import { locate, displayListing } from '../actions';
import Header from './header';
import Hero from './hero';
import Ripple from './partials/ripple';
import ListingCards from './listing_cards';
import Delivery from '../icons/delivery';
import Dispensary from '../icons/dispensary';
import Doctor from '../icons/doctor';
import { DARK_GRAY } from '../constants/colors';
import {
AppWrapper,
AppContent,
ListingGroups,
} from './styles';
const regionTypes = ['delivery', 'dispensary', 'doctor'];
const regionLabels = {
delivery: 'Deliveries',
dispensary: 'Dispensaries',
doctor: 'Doctors',
};
export class App extends Component {
constructor(props) {
super(props);
this.state = {
loadingTimer: 0,
isLocatingStarted: false,
geoCoords: null,
width: 0
};
this.locateMe = this.locateMe.bind(this);
this.gotoListing = this.gotoListing.bind(this);
}
componentDidMount() {
// Fetch geolocation ahead of time.
navigator.geolocation.getCurrentPosition(position =>
this.setState({ geoCoords: position.coords }));
this.updateWindowDimensions();
window.addEventListener("resize", this.updateWindowDimensions);
}
componentWillUnmount() {
window.removeEventListener("resize", this.updateWindowDimensions);
}
updateWindowDimensions = () => this.setState({ width: window.innerWidth });
locateMe() {
console.log('locateMe')
const { dispatch } = this.props;
const { geoCoords } = this.state;
if (navigator.geolocation && !geoCoords) {
navigator.geolocation.getCurrentPosition(position =>
dispatch(locate(position.coords)));
} else {
dispatch(locate(geoCoords));
}
this.setState({ isLocatingStarted: true });
};
gotoListing(listing) {
const { dispatch } = this.props;
dispatch(displayListing(listing));
const link = `/listing/${listing.wmid}`;
this.props.history.push(link);
}
render() {
const { isLocating, location, regions, error } = this.props;
const { isLocatingStarted, width } = this.state;
const { state_abv: state } = location !== null && location;
const isLoading = isLocatingStarted && isLocating;
const getLabel = (listings, label) => {
const componentsMap = {
Deliveries: Delivery,
Dispensaries: Dispensary,
Doctors: Doctor
};
const DynamicIcon = componentsMap[label];
if (get(listings, 'listings').length) {
return (
<div key={label}>
<DynamicIcon fill={DARK_GRAY} /> <strong> {label} </strong>
</div>
);
}
return <div />;
};
return (
<AppWrapper>
<Header history={this.props.history} />
<Hero
location={location}
isLocating={isLocating}
locateMe={this.locateMe}
/>
{ isLoading ? <Ripple /> :
<AppContent>
{error && <div> {error.message} </div>}
{regions && (
<React.Fragment>
{regionTypes.map(regionType => (
<ListingGroups key={regionType}>
<h2>
{getLabel(regions[regionType], regionLabels[regionType])}
</h2>
<ListingCards
listings={get(regions[regionType], 'listings')}
state={state}
isMobileSize={width < 769}
gotoListing={this.gotoListing}
/>
</ListingGroups>
))}
</React.Fragment>
)}
</AppContent>
}
</AppWrapper>
);
}
}
const mapStateToProps = state => state.location;
App.propTypes = {
isLocating: PropTypes.bool.isRequired,
location: PropTypes.object,
regions: PropTypes.object,
dispatch: PropTypes.any,
error: PropTypes.object,
};
App.defaultProps = {
isLocating: false,
location: {},
regions: {},
error: {},
};
export const AppJest = App
export default connect(mapStateToProps)(App);
Ah I needed to finish adding in all the props to the wrapper component:
import React from 'react'
import { shallow } from 'enzyme'
import toJson from 'enzyme-to-json'
import { AppJest } from './App'
import Header from './header';
import Hero from './hero';
import Ripple from './partials/ripple';
import listingsMock from '../__test__/mocks/listings-mock.json';
// Mock the services.
const mockLocate = jest.fn();
const mockDisplayListing = jest.fn();
const mockGeolocation = {
getCurrentPosition: jest.fn(),
watchPosition: jest.fn()
};
global.navigator.geolocation = mockGeolocation;
jest.mock('../actions', () => ({
locate: () => mockLocate(),
displayListing: () => mockDisplayListing()
}));
describe('<App /> component', () => {
describe('when rendering', () => {
const wrapper = shallow(<AppJest
navigator={mockGeolocation}
isLocating={false}
location={null}
regions={null}
dispatch={null}
error={null}
listings={listingsMock}
locate={mockLocate}
displayListing={mockDisplayListing}
/>);
it('should render a component matching the snapshot', () => {
const tree = toJson(wrapper);
expect(tree).toMatchSnapshot();
expect(wrapper).toHaveLength(1);
expect(wrapper.find(Header)).toHaveLength(1);
expect(wrapper.find(Hero)).toHaveLength(1);
});
});
});
Required props:
App.propTypes = {
isLocating: PropTypes.bool.isRequired,
location: PropTypes.object,
regions: PropTypes.object,
dispatch: PropTypes.any,
error: PropTypes.object,
};
App.defaultProps = {
isLocating: false,
location: {},
regions: {},
error: {},
};

How to target specific element after mapping and passing onClick function as props

I am facing such problem, i got my array of records fetched from an API, mapped it into single elements and outputting them as single components. I have function which changes state of parent Component, passes value to child component and child component should hide/show div content after button is clicked.
Of course. It is working, but partially - my all divs are being hidden/shown. I have set specific key to each child component but it doesn't work.
App.js
import React, { Component } from 'react';
import './App.css';
import axios from 'axios';
import countries from '../../countriesList';
import CitySearchForm from './CitySearchForm/CitySearchForm';
import CityOutput from './CityOutput/CityOutput';
import ErrorMessage from './ErrorMessage/ErrorMessage';
class App extends Component {
state = {
country: '',
error: false,
cities: [],
infoMessage: '',
visible: false
}
getCities = (e) => {
e.preventDefault();
const countryName = e.target.elements.country.value.charAt(0).toUpperCase() + e.target.elements.country.value.slice(1);
const countryUrl = 'https://api.openaq.org/v1/countries';
const wikiUrl ='https://en.wikipedia.org/w/api.php?action=query&prop=extracts&exintro&explaintext&format=json&category=city&redirects&origin=*&titles=';
const allowedCountries = new RegExp(/spain|germany|poland|france/, 'i');
if (allowedCountries.test(countryName)) {
axios
.get(countryUrl)
.then( response => {
const country = response.data.results.find(el => el.name === countryName);
return axios.get(`https://api.openaq.org/v1/cities?country=${country.code}&order_by=count&sort=desc&limit=10`)
})
.then( response => {
const cities = response.data.results.map(record => {
return { name: record.city };
});
cities.forEach(city => {
axios
.get(wikiUrl + city.name)
.then( response => {
let id;
for (let key in response.data.query.pages) {
id = key;
}
const description = response.data.query.pages[id].extract;
this.setState(prevState => ({
cities: [...prevState.cities, {city: `${city.name}`, description}],
infoMessage: prevState.infoMessage = ''
}))
})
})
})
.catch(error => {
console.log('oopsie, something went wrong', error)
})
} else {
this.setState(prevState => ({
infoMessage: prevState.infoMessage = 'This is demo version of our application and is working only for Spain, Poland, Germany and France',
cities: [...prevState.cities = []]
}))
}
}
descriptionTogglerHandler = () => {
this.setState((prevState) => {
return { visible: !prevState.visible};
});
};
render () {
return (
<div className="App">
<ErrorMessage error={this.state.infoMessage}/>
<div className="form-wrapper">
<CitySearchForm getCities={this.getCities} getInformation={this.getInformation} countries={countries}/>
</div>
{this.state.cities.map(({ city, description }) => (
<CityOutput
key={city}
city={city}
description={description}
show={this.state.visible}
descriptionToggler={this.descriptionTogglerHandler} />
))}
</div>
);
}
}
export default App;
CityOutput.js
import React, { Component } from 'react';
import './CityOutput.css';
class CityOutput extends Component {
render() {
const { city, descriptionToggler, description, show } = this.props;
let descriptionClasses = 'output-record description'
if (show) {
descriptionClasses = 'output-record description open';
}
return (
<div className="output">
<div className="output-record"><b>City:</b> {city}</div>
<button onClick={descriptionToggler}>Read more</button>
<div className={descriptionClasses}>{description}</div>
</div>
)
}
};
export default CityOutput;
Put the visible key and the toggle function in the CityOutput instead of having it in the parent
import React, { Component } from "react";
import "./CityOutput.css";
class CityOutput extends Component {
state = {
visible: true
};
descriptionTogglerHandler = () => {
this.setState({ visible: !this.state.visible });
};
render() {
const { city, description } = this.props;
let descriptionClasses = "output-record description";
if (this.state.visible) {
descriptionClasses = "output-record description open";
}
return (
<div className="output">
<div className="output-record">
<b>City:</b> {city}
</div>
<button onClick={() => this.descriptionTogglerHandler()}>Read more</button>
<div className={descriptionClasses}>{description}</div>
</div>
);
}
}
export default CityOutput;
There are two ways of how I would approach this,
The first one is setting in your state a key property and check and compare that key with the child keys like:
state = {
country: '',
error: false,
cities: [],
infoMessage: '',
visible: false.
currKey: 0
}
descriptionTogglerHandler = (key) => {
this.setState((prevState) => {
return { currKey: key, visible: !prevState.visible};
});
};
// then in your child component
class CityOutput extends Component {
render() {
const { city, descriptionToggler, description, show, currKey, elKey } = this.props;
let descriptionClasses = 'output-record description'
if (show && elKey === currKey) {
descriptionClasses = 'output-record description open';
}
return (
<div className="output">
<div className="output-record"><b>City:</b> {city}</div>
<button onClick={() => descriptionToggler(elKey)}>Read more</button>
<div className={descriptionClasses}>{description}</div>
</div>
)
}
};
The other way is to set an isolated state for every child component
class CityOutput extends Component {
constructor(props) {
this.state = {
show: false
}
}
function descriptionToggler() {
const {show} = this.state;
this.setState({
show: !show
})
}
render() {
const { city, descriptionToggler, description } = this.props;
let descriptionClasses = 'output-record description'
if (this.state.show) {
descriptionClasses = 'output-record description open';
}
return (
<div className="output">
<div className="output-record"><b>City:</b> {city}</div>
<button onClick={descriptionToggler}>Read more</button>
<div className={descriptionClasses}>{description}</div>
</div>
)
}
};
I hope this helps ;)

Categories

Resources