Sorry if I am asking a beginner's level question. I am new to React.js and recently I have been trying to grasps the concepts by following this tutorial:
JustDjango
What I am trying to accomplish is creating a login form which uses redux to store the states, my code is as follows :
import React from 'react';
import { Form, Icon, Input, Button, Spin } from 'antd/lib';
import { connect } from 'react-redux';
import { NavLink } from 'react-router-dom';
import * as actions from '../store/actions/auth';
const FormItem = Form.Item;
const antIcon = <Icon type="loading" style={{ fontSize: 24 }} spin />;
class NormalLoginForm extends React.Component {
handleSubmit = (e) => {
e.preventDefault();
this.props.form.validateFields((err, values) => {
if (!err) {
this.props.onAuth(values.userName, values.password);
this.props.history.push('/');
}
});
}
render() {
let errorMessage = null;
if (this.props.error) {
errorMessage = (
<p>{this.props.error.message}</p>
);
}
const { getFieldDecorator } = this.props.form;
return (
<div>
{errorMessage}
{
this.props.loading ?
<Spin indicator={antIcon} />
:
<Form onSubmit={this.handleSubmit} className="login-form">
<FormItem>
{getFieldDecorator('userName', {
rules: [{ required: true, message: 'Please input your username!' }],
})(
<Input prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />} placeholder="Username" />
)}
</FormItem>
<FormItem>
{getFieldDecorator('password', {
rules: [{ required: true, message: 'Please input your Password!' }],
})(
<Input prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />} type="password" placeholder="Password" />
)}
</FormItem>
<FormItem>
<Button type="primary" htmlType="submit" style={{marginRight: '10px'}}>
Login
</Button>
Or
<NavLink
style={{marginRight: '10px'}}
to='/signup/'> signup
</NavLink>
</FormItem>
</Form>
}
</div>
);
}
}
const WrappedNormalLoginForm = Form.useForm()(NormalLoginForm);
const mapStateToProps = (state) => {
return {
loading: state.loading,
error: state.error
}
}
const mapDispatchToProps = dispatch => {
return {
onAuth: (username, password) => dispatch(actions.authLogin(username, password))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(WrappedNormalLoginForm);
The error traceback shows that the error is coming from :
76 | const WrappedNormalLoginForm = Form.useForm()(NormalLoginForm);
77 |
78 | const mapStateToProps = (state) => {
79 | return {
Some google search on this particular error shows that this error has something to do with hooks being defined in a classed based component , however i do not understand why :
const mapStateToProps = (state) => {......
is considered a hook
Will greatly appreciate anybody's help!
React hooks only used by functional components. You used class components.
Shortly, Form.useForm() the method is only used functional components, you can read it from this link below:
https://ant.design/components/form/
Related
The fireEvent.submit(form) call doesn't seem to be submitting the form. How can I submit the form?
BasicInfo/index.test.tsx
import { fireEvent, render, screen, waitFor } from '#testing-library/react';
import userEvent from '#testing-library/user-event';
import BasicInfo from '.';
describe('BasicInfo', () => {
it('can submit form', async () => {
const handleSubmit = jest.fn();
render(<BasicInfo onSubmit={handleSubmit} />);
const user = userEvent.setup();
await user.type(screen.getByLabelText('Event title'), 'Event title 1');
await user.type(screen.getByLabelText('Featured game'), 'Scrabble');
await user.type(screen.getByLabelText('Venue location'), '21 2nd St.');
await user.type(screen.getByLabelText('Event starts'), '2017-06-01T08:30');
await user.type(screen.getByLabelText('Event ends'), '2017-06-01T08:30');
const form = screen.getByTestId<HTMLFormElement>('basic-info');
fireEvent.submit(form);
await waitFor(() => {
expect(handleSubmit).toHaveBeenCalledWith({
title: 'Event title 1',
game: { name: 'Scrabble' },
address: '21 2nd St.',
starts_at: '2017-06-01T08:30',
ends_at: '2017-06-01T08:30',
});
});
});
});
BasicInfo/index.tsx
import { Form, Formik, FormikErrors, FormikHelpers, FormikProps } from 'formik';
import Image from 'next/image';
import Link from 'next/link';
import { useLocation } from '../../context/location';
import { Event } from '../../hooks/useEvents';
import useGames, { Game } from '../../hooks/useGames';
import useMapbox, { Feature, Coords } from '../../hooks/useMapbox';
import useThrottle from '../../hooks/useThrottle';
import { parseAddress } from '../../lib/helpers';
import AddressItem from '../AddressItem';
import AutoComplete from '../AutoComplete';
import Button from '../Button';
import FormSection from '../FormSection';
import InputGroup from '../InputGroup';
import GameItem from '../GameItem';
export interface BasicInfoValues {
title: string;
game: Game;
address: string;
starts_at: string;
ends_at: string;
coords: string;
}
interface Props {
event?: Event;
initialValues?: BasicInfoValues;
onSubmit: (values: BasicInfoValues) => Promise<void>;
}
function BasicInfo({ event, initialValues: initialVals, onSubmit }: Props) {
const { coords } = useLocation();
const { data: places, forward, getStaticMapUrl } = useMapbox();
const { data: games, search, leanGame } = useGames();
const throttle = useThrottle();
const initialValues: BasicInfoValues = initialVals || {
title: '',
game: { name: '' },
address: '',
starts_at: '',
ends_at: '',
coords: '',
};
const validate = (values: BasicInfoValues) => {
const errors: FormikErrors<BasicInfoValues> = {};
if (values.title.length < 3) {
errors.title = 'Must be at least 3 characters';
}
if (values.title.length > 50) {
errors.title = 'Must be less than 50 characters';
}
if (!values.game.name) {
errors.game = 'Must select a game' as FormikErrors<Game>;
}
if (!values.coords) {
errors.coords = 'Must select a location';
}
if (!values.starts_at) {
errors.starts_at = 'Required';
}
if (!values.ends_at) {
errors.ends_at = 'Required';
}
return errors;
};
const handleSubmit = async function (
values: BasicInfoValues,
formikHelpers: FormikHelpers<BasicInfoValues>
) {
try {
await onSubmit(values);
formikHelpers.setSubmitting(false);
} catch (error) {
console.error(error);
}
};
const renderLocation = function ({
setFieldValue,
values,
handleChange,
errors,
touched,
handleBlur,
}: FormikProps<BasicInfoValues>) {
const handleEditLocationClick = function () {
setFieldValue('address', '');
setFieldValue('coords', '');
};
const { coords: coordinates, address } = values;
if (coordinates && address) {
const staticMapUrl = getStaticMapUrl({
coords: JSON.parse(coordinates),
width: 600,
height: 165,
});
const { street, city } = parseAddress(address);
return (
<div className="basic-info__location">
<div className="basic-info__location__image-container">
<Image
src={staticMapUrl}
alt="static map"
layout="fill"
objectFit="fill"
/>
</div>
<div className="basic-info__location__address-container">
<div className="basic-info__location__address-container__address">
<span className="basic-info__location__street">{street}</span>
<span className="basic-info__location__city">{city}</span>
</div>
<Button
text="Edit location"
color="secondary"
size="small"
onClick={handleEditLocationClick}
/>
</div>
</div>
);
} else {
return (
<AutoComplete<Feature>
name="address"
value={values.address}
onChange={(e) => {
if (!handleChange) return;
handleChange(e);
throttle.wait(() => {
if (!e.target.value) return;
if (!coords) return;
forward({
coords,
q: e.target.value,
});
}, 500);
}}
label="Venue location"
placeholder="Search for a venue or address"
items={places}
Input={InputGroup}
itemRenderer={(item) => <AddressItem placeName={item.place_name} />}
onItemClick={(item) => {
if (!setFieldValue) return;
setFieldValue('address', item.place_name);
setFieldValue('coords', JSON.stringify(item.center));
}}
onBlur={handleBlur}
error={errors.coords && touched.address ? errors.coords : undefined}
/>
);
}
};
return (
<Formik
initialValues={initialValues}
onSubmit={handleSubmit}
validate={validate}
>
{(formikProps) => {
const {
values,
handleChange,
setFieldValue,
errors,
touched,
handleBlur,
} = formikProps;
return (
<Form id="basic-info" className="basic-info" data-testid="basic-info">
<FormSection
title="Basic Info"
description="Name your event and tell gamers what game will be played. Add
details that highlight what makes it unique."
icon="segment"
>
<InputGroup
name="title"
value={values.title}
onChange={handleChange}
onBlur={handleBlur}
label="Event title"
placeholder="Be clear and descriptive"
error={errors.title && touched.title ? errors.title : undefined}
/>
<AutoComplete<Game>
name="game.name"
value={values.game.name}
onBlur={handleBlur}
onChange={(e) => {
handleChange(e);
throttle.wait(() => {
if (!e.target.value) return;
search({ name: e.target.value, limit: 5 });
}, 500);
}}
label="Featured game"
placeholder="Search games"
Input={InputGroup}
items={games}
itemRenderer={(item) => <GameItem game={item} />}
onItemClick={(item) => {
const game = leanGame(item);
setFieldValue('game', game);
}}
error={
errors.game && touched.game
? (errors.game as string)
: undefined
}
/>
<span>
Need game ideas?{' '}
<Link href="/" passHref>
<a className="link">Browse games by category</a>
</Link>
</span>
</FormSection>
<FormSection
title="Location"
description="Help gamers in the area discover your event and let attendees know where to show up."
icon="map"
>
{renderLocation(formikProps)}
</FormSection>
<FormSection
title="Date and time"
description="Tell gamers when your event starts and ends so they can make plans to attend."
icon="date_range"
>
<InputGroup
name="starts_at"
value={values.starts_at}
onChange={handleChange}
onBlur={handleBlur}
label="Event starts"
placeholder="Search for a venue or address"
type="datetime-local"
// icon="calendar_today"
error={
errors.starts_at && touched.starts_at
? errors.starts_at
: undefined
}
/>
<InputGroup
name="ends_at"
value={values.ends_at}
onChange={handleChange}
onBlur={handleBlur}
label="Event ends"
placeholder="Search for a venue or address"
type="datetime-local"
// icon="calendar_today"
error={
errors.ends_at && touched.ends_at ? errors.ends_at : undefined
}
/>
</FormSection>
</Form>
);
}}
</Formik>
);
}
export default BasicInfo;
My form validation was preventing me from submitting
So i set up mobx and the files show no error in visual studio code and the react client app compile just fine and it shows the list of Departments but there is also a button thats not working its set up all fine and the onClick event is all fine but it doesnt work I had problems with mobx because its greater than version 6 or idk its version so i had to add the construvtor to display the list
Department store
import {observable, action, makeObservable} from 'mobx';
import { createContext } from 'react';
import agent from '../api/agent';
import { IDepartment } from '../models/department';
class DepartmentStore {
#observable departments: IDepartment[] = [];
#observable selectedDepartment: IDepartment | undefined;
#observable loadingInitial = false;
#observable editMode =false;
constructor() {
// Just call it here
makeObservable(this);
}
#action loadDepartments= () => {
this.loadingInitial = true;
agent.Departments.list()
.then(departments => {
departments.forEach((department) => {
this.departments.push(department);
})
}).finally(() => this.loadingInitial = false);
};
#action selectDepartment = (id: string) => {
this.selectedDepartment = this.departments.find(d => d.id === id);
this.editMode = false;
}
}
export default createContext(new DepartmentStore());
so everything here shows no error below ill display the DepartmentList code where it contains the button thats now functioning
import { observer } from "mobx-react-lite";
import React, { SyntheticEvent, useContext } from "react";
import { Item, Button, Segment } from "semantic-ui-react";
import { IDepartment } from "../../../app/models/department";
import DepartmentStore from "../../../app/stores/departmentStore";
interface IProps {
deleteDepartment: (event: SyntheticEvent<HTMLButtonElement>,id: string) => void;
submitting: boolean;
target: string;
}
export const DepartmentList: React.FC<IProps> = ({
deleteDepartment,
submitting,
target
}) => {
const departmentStore = useContext(DepartmentStore);
const {departments, selectDepartment} = departmentStore;
return (
<Segment clearing>
<Item.Group divided>
{departments.map((department) => (
<Item key={department.id}>
<Item.Content style={{ display: "flex" }}>
<Item.Header style={{ width: "100%", marginTop: "1em" }}>
{department.name}
</Item.Header>
<Item.Extra>
<Button
name={department.id}
loading={target === department.id && submitting}
onClick={(e) => deleteDepartment(e, department.id)}
content="Delete"
color="red"
floated="right"
/>
<Button
onClick={() => selectDepartment(department.id)}
content="View"
color="blue"
floated="right"
/>
</Item.Extra>
</Item.Content>
</Item>
))}
</Item.Group>
</Segment>
);
};
export default observer(DepartmentList)
;
I have a problem. I want to create instant search, without any search button, that when i'm typing e.g. more than 3 letters, my results will be instant show below.
My code:
<template>
<div class="nav-scroller py-1 mb-2">
<div class="nav d-flex justify-content-between">
<input v-model="keyword" class="form-control" type="text" placeholder="Search" aria-label="Search">
<div v-bind:key="result.id" v-for="result in results">
<p>Results are: {{ result.title }}</p>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'Home',
components: {
},
data() {
return {
keyword: '',
results: [],
}
},
methods: {
getResults() {
axios.get("http://127.0.0.1:8000/api/v1/books/?search="+this.keyword)
.then(res => (this.results = res.data))
.catch(err => console.log(err));
}
},
created() {
this.getResults()
}
}
</script>
Now my 'keyword' parameter is probably not passed to the url, because when I refresh the page, all records from APi are the results.
Could you help me?
You should either call method when input changes
<input v-model="keyword" #input="getResults">
and method:
getResults() {
if (this.keyword.length > 3)
axios.get("http://127.0.0.1:8000/api/v1/books/?search="+this.keyword)
.then(res => (this.results = res.data))
.catch(err => console.log(err));
}
}
Or watcher can be used. When keyword changes watcher will call getResults method.
watch: {
keyword: "getResults"
}
Use watcher for the keyword value update.
Whenever keyword is more than 3 letters, request the getResults() method to search.
export default {
name: 'Home',
components: {
},
data() {
return {
keyword: '',
results: [],
}
},
watch: {
keyword: function(newVal) {
if (newVal.length >2) {
this.getResults();
}
}
},
methods: {
getResults() {
axios.get("http://127.0.0.1:8000/api/v1/books/?search="+this.keyword)
.then(res => (this.results = res.data))
.catch(err => console.log(err));
}
},
created() {
this.getResults()
}
}
I have a component that switch Language of a nuxtjs application using nuxt-i18n as follows
<template>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link langpicker">{{ $t("language_picker") }} </a>
<div class="navbar-dropdown is-hidden-mobile">
<div>
<nuxt-link
v-if="currentLanguage != 'en'"
class="navbar-item"
:to="switchLocalePath('en')"
>
<img src="~/static/flags/us.svg" class="flagIcon" /> English
</nuxt-link>
<nuxt-link
v-if="currentLanguage != 'el'"
class="navbar-item"
:to="switchLocalePath('el')"
>
<img src="~/static/flags/el.svg" class="flagIcon" /> Ελληνικά
</nuxt-link>
</div>
</div>
</div>
</template>
<script>
export default {
name: "LangPicker",
computed: {
currentLanguage() {
return this.$i18n.locale || "en";
}
}
};
</script>
I want to write a Unit Test that test the correct language switch on 'nuxt-link' click.
So far I have the following
import { mount, RouterLinkStub } from "#vue/test-utils";
import LangPicker from "#/components/layout/LangPicker";
describe("LangPicker with locale en", () => {
let cmp;
beforeEach(() => {
cmp = mount(LangPicker, {
mocks: {
$t: msg => msg,
$i18n: { locale: "en" },
switchLocalePath: msg => msg
},
stubs: {
NuxtLink: RouterLinkStub
}
});
});
it("Trigger language", () => {
const el = cmp.findAll(".navbar-item")
});
});
cmp.find(".navbar-item") return an empty object.
I don't know how I must set up to "trigger" the click event.
const el = cmp.findAll(".navbar-item")[1].trigger("click");
make sure your find selector is correct.
const comp = cmp.find(".navbar-item");
comp.trigger('click');
you can use chrome dev tools selector utility.
Refer this link for detailed information.
I am working on an app which was created with the Vue loader's webpack template.
I included testing with Karma as an option when creating the project, so it was all set up and I haven't changed any of the config.
The app is a Github user lookup which currently consists of three components; App.vue, Stats.vue and UserForm.vue. The stats and form components are children of the containing app component.
Here is App.vue:
<template>
<div id="app">
<user-form
v-model="inputValue"
#go="submit"
:input-value="inputValue"
></user-form>
<stats
:username="username"
:avatar="avatar"
:fave-lang="faveLang"
:followers="followers"
></stats>
</div>
</template>
<script>
import Vue from 'vue'
import axios from 'axios'
import VueAxios from 'vue-axios'
import _ from 'lodash'
import UserForm from './components/UserForm'
import Stats from './components/Stats'
Vue.use(VueAxios, axios)
export default {
name: 'app',
components: {
UserForm,
Stats
},
data () {
return {
inputValue: '',
username: '',
avatar: '',
followers: [],
faveLang: '',
urlBase: 'https://api.github.com/users'
}
},
methods: {
submit () {
if (this.inputValue) {
const api = `${this.urlBase}/${this.inputValue}`
this.fetchUser(api)
}
},
fetchUser (api) {
Vue.axios.get(api).then((response) => {
const { data } = response
this.inputValue = ''
this.username = data.login
this.avatar = data.avatar_url
this.fetchFollowers()
this.fetchFaveLang()
}).catch(error => {
console.warn('ERROR:', error)
})
},
fetchFollowers () {
Vue.axios.get(`${this.urlBase}/${this.username}/followers`).then(followersResponse => {
this.followers = followersResponse.data.map(follower => {
return follower.login
})
})
},
fetchFaveLang () {
Vue.axios.get(`${this.urlBase}/${this.username}/repos`).then(reposResponse => {
const langs = reposResponse.data.map(repo => {
return repo.language
})
// Get most commonly occurring string from array
const faveLang = _.chain(langs).countBy().toPairs().maxBy(_.last).head().value()
if (faveLang !== 'null') {
this.faveLang = faveLang
} else {
this.faveLang = ''
}
})
}
}
}
</script>
<style lang="stylus">
body
background-color goldenrod
</style>
Here is Stats.vue:
<template>
<div class="container">
<h1 class="username" v-if="username">{{username}}</h1>
<img v-if="avatar" :src="avatar" class="avatar">
<h2 v-if="faveLang">Favourite Language: {{faveLang}}</h2>
<h3 v-if="followers.length > 0">Followers ({{followers.length}}):</h3>
<ul v-if="followers.length > 0">
<li v-for="follower in followers">
{{follower}}
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'stats',
props: [
'username',
'avatar',
'faveLang',
'followers'
]
}
</script>
<style lang="stylus" scoped>
h1
font-size 44px
.avatar
height 200px
width 200px
border-radius 10%
.container
display flex
align-items center
flex-flow column
font-family Comic Sans MS
</style>
And here is UserForm.vue:
<template>
<form #submit.prevent="handleSubmit">
<input
class="input"
:value="inputValue"
#input="updateValue($event.target.value)"
type="text"
placeholder="Enter a GitHub username..."
>
<button class="button">Go!</button>
</form>
</template>
<script>
export default {
props: ['inputValue'],
name: 'user-form',
methods: {
updateValue (value) {
this.$emit('input', value)
},
handleSubmit () {
this.$emit('go')
}
}
}
</script>
<style lang="stylus" scoped>
input
width 320px
input,
button
font-size 25px
form
display flex
justify-content center
</style>
I wrote a trivial test for UserForm.vue which test's the outerHTML of the <button>:
import Vue from 'vue'
import UserForm from 'src/components/UserForm'
describe('UserForm.vue', () => {
it('should have a data-attribute in the button outerHTML', () => {
const vm = new Vue({
el: document.createElement('div'),
render: (h) => h(UserForm)
})
expect(vm.$el.querySelector('.button').outerHTML)
.to.include('data-v')
})
})
This works fine; the output when running npm run unit is:
UserForm.vue
✓ should have a data-attribute in the button outerHTML
However, when I tried to write a similarly simple test for Stats.vue based on the documentation, I ran into a problem.
Here is the test:
import Vue from 'vue'
import Stats from 'src/components/Stats'
// Inspect the generated HTML after a state update
it('updates the rendered message when vm.message updates', done => {
const vm = new Vue(Stats).$mount()
vm.username = 'foo'
// wait a "tick" after state change before asserting DOM updates
Vue.nextTick(() => {
expect(vm.$el.querySelector('.username').textContent).toBe('foo')
done()
})
})
and here is the respective error when running npm run unit:
ERROR LOG: '[Vue warn]: Error when rendering root instance: '
✗ updates the rendered message when vm.message updates
undefined is not an object (evaluating '_vm.followers.length')
I have tried the following in an attempt to get the test working:
Change how the vm is created in the Stats test to be the same as the UserForm test - same error is returned
Test individual parts of the component, for example the textContent of a div in the component - same error is returned
Why is the error referring to _vm.followers.length? What is _vm with an underscore in front? How can I get around this issue to be able to successfully test my component?
(Repo with all code: https://github.com/alanbuchanan/vue-github-lookup-2)
Why is the error referring to _vm.followers.length? What is _vm with an underscore in front?
This piece of code is from the render function that Vue compiled your template into. _vm is a placeholder that gets inserted automatically into all Javascript expressions when vue-loader converts the template into a render function during build - it does that to provide access to the component.
When you do this in your template:
{{followers.length}}
The compiled result in the render function for this piece of code will be:
_vm.followers.length
Now, why does the error happen in the first place? Because you have defined a prop followers on your component, but don't provide any data for it - therefore, the prop's value is undefined
Solution: either you provide a default value for the prop:
// Stats.vue
props: {
followers: { default: () => [] }, // function required to return fresh object
// ... other props
}
Or you propvide acual values for the prop:
// in the test:
const vm = new Vue({
...Stats,
propsData: {
followers: [/* ... actual data*/]
}
}).$mount()