mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-01-08 21:02:25 -05:00
feat(curriculum): add labs-react-forms (#59320)
Co-authored-by: Jessica Wilkins <67210629+jdwilkin4@users.noreply.github.com> Co-authored-by: Tom <20648924+moT01@users.noreply.github.com>
This commit is contained in:
@@ -2022,7 +2022,10 @@
|
||||
"Open up this page to review concepts around the basics of HTML elements, semantic HTML, tables, forms and accessibility."
|
||||
]
|
||||
},
|
||||
"qpra": { "title": "30", "intro": [] },
|
||||
"qpra": {
|
||||
"title": "30",
|
||||
"intro": []
|
||||
},
|
||||
"lecture-understanding-computer-internet-and-tooling-basics": {
|
||||
"title": "Understanding Computer, Internet, and Tooling Basics",
|
||||
"intro": [
|
||||
@@ -3426,7 +3429,10 @@
|
||||
"Open up this page to review all of the concepts taught including variables, strings, booleans, functions, objects, arrays, debugging, working with the DOM and more."
|
||||
]
|
||||
},
|
||||
"kagw": { "title": "258", "intro": [] },
|
||||
"kagw": {
|
||||
"title": "258",
|
||||
"intro": []
|
||||
},
|
||||
"lecture-introduction-to-javascript-libraries-and-frameworks": {
|
||||
"title": "Introduction to JavaScript Libraries and Frameworks",
|
||||
"intro": [
|
||||
@@ -3479,16 +3485,28 @@
|
||||
"In these lecture videos, you will learn about working with state and responding to events with React."
|
||||
]
|
||||
},
|
||||
"rmpy": { "title": "268", "intro": [] },
|
||||
"dbta": { "title": "269", "intro": [] },
|
||||
"rmpy": {
|
||||
"title": "268",
|
||||
"intro": []
|
||||
},
|
||||
"dbta": {
|
||||
"title": "269",
|
||||
"intro": []
|
||||
},
|
||||
"lecture-understanding-effects-and-referencing-values-in-react": {
|
||||
"title": "Understanding Effects and Referencing Values in React",
|
||||
"intro": [
|
||||
"In these lecture videos, you will learn about effects and referencing values with React."
|
||||
]
|
||||
},
|
||||
"xdyh": { "title": "270", "intro": [] },
|
||||
"vjgg": { "title": "272", "intro": [] },
|
||||
"xdyh": {
|
||||
"title": "270",
|
||||
"intro": []
|
||||
},
|
||||
"vjgg": {
|
||||
"title": "272",
|
||||
"intro": []
|
||||
},
|
||||
"review-react-state-and-hooks": {
|
||||
"title": "React State and Hooks Review",
|
||||
"intro": [
|
||||
@@ -3508,12 +3526,15 @@
|
||||
"In these lecture videos, you will learn about working with forms in React."
|
||||
]
|
||||
},
|
||||
"sgau": { "title": "276", "intro": [] },
|
||||
"lab-currency-converter": {
|
||||
"title": "Build a Currency Converter",
|
||||
"sgau": {
|
||||
"title": "276",
|
||||
"intro": []
|
||||
},
|
||||
"lab-event-rsvp": {
|
||||
"title": "Build an Event RSVP",
|
||||
"intro": [
|
||||
"For this lab, you'll build a currency converter app.",
|
||||
"You'll use React state, memoization, and controlled components to convert between currencies."
|
||||
"In this lab, you'll build an Event RSVP form using React.",
|
||||
"You'll practice using the useState hook to manage form input and display user responses."
|
||||
]
|
||||
},
|
||||
"lecture-working-with-data-fetching-and-memoization-in-react": {
|
||||
@@ -3522,15 +3543,27 @@
|
||||
"In these lecture videos, you will learn about data fetching and memoization in React."
|
||||
]
|
||||
},
|
||||
"ffpt": { "title": "279", "intro": [] },
|
||||
"lrof": { "title": "280", "intro": [] },
|
||||
"ffpt": {
|
||||
"title": "279",
|
||||
"intro": []
|
||||
},
|
||||
"lab-currency-converter": {
|
||||
"title": "Build a Currency Converter",
|
||||
"intro": [
|
||||
"For this lab, you'll build a currency converter app.",
|
||||
"You'll use React state, memoization, and controlled components to convert between currencies."
|
||||
]
|
||||
},
|
||||
"lecture-routing-react-frameworks-and-dependency-management-tools": {
|
||||
"title": "Routing, React Frameworks, and Dependency Management Tools",
|
||||
"intro": [
|
||||
"In these lecture videos, you will learn about routing in React, React frameworks, and dependency management tools."
|
||||
]
|
||||
},
|
||||
"vyzp": { "title": "281", "intro": [] },
|
||||
"vyzp": {
|
||||
"title": "281",
|
||||
"intro": []
|
||||
},
|
||||
"lecture-react-strategies-and-debugging": {
|
||||
"title": "React Strategies and Debugging",
|
||||
"intro": [
|
||||
@@ -3611,11 +3644,26 @@
|
||||
"In these lecture videos, you will learn what TypeScript is and how to use it."
|
||||
]
|
||||
},
|
||||
"trvf": { "title": "293", "intro": [] },
|
||||
"kwmg": { "title": "294", "intro": [] },
|
||||
"nodx": { "title": "295", "intro": [] },
|
||||
"erfj": { "title": "296", "intro": [] },
|
||||
"muyw": { "title": "297", "intro": [] },
|
||||
"trvf": {
|
||||
"title": "293",
|
||||
"intro": []
|
||||
},
|
||||
"kwmg": {
|
||||
"title": "294",
|
||||
"intro": []
|
||||
},
|
||||
"nodx": {
|
||||
"title": "295",
|
||||
"intro": []
|
||||
},
|
||||
"erfj": {
|
||||
"title": "296",
|
||||
"intro": []
|
||||
},
|
||||
"muyw": {
|
||||
"title": "297",
|
||||
"intro": []
|
||||
},
|
||||
"review-typescript": {
|
||||
"title": "Typescript Review",
|
||||
"intro": [
|
||||
@@ -3633,8 +3681,14 @@
|
||||
"Review the Front End Libraries concepts to prepare for the upcoming quiz."
|
||||
]
|
||||
},
|
||||
"rdzk": { "title": "301", "intro": [] },
|
||||
"vtpz": { "title": "302", "intro": [] },
|
||||
"rdzk": {
|
||||
"title": "301",
|
||||
"intro": []
|
||||
},
|
||||
"vtpz": {
|
||||
"title": "302",
|
||||
"intro": []
|
||||
},
|
||||
"workshop-bash-boilerplate": {
|
||||
"title": "Build a Boilerplate",
|
||||
"intro": [
|
||||
@@ -3652,7 +3706,10 @@
|
||||
"title": "Bash Commands Quiz",
|
||||
"intro": ["Test what you've learned bash commands with this quiz."]
|
||||
},
|
||||
"voks": { "title": "306", "intro": [] },
|
||||
"voks": {
|
||||
"title": "306",
|
||||
"intro": []
|
||||
},
|
||||
"workshop-database-of-video-game-characters": {
|
||||
"title": "Build a Database of Video Game Characters",
|
||||
"intro": [
|
||||
@@ -3678,7 +3735,10 @@
|
||||
"Test what you've learned on relational databases with this quiz."
|
||||
]
|
||||
},
|
||||
"pexz": { "title": "311", "intro": [] },
|
||||
"pexz": {
|
||||
"title": "311",
|
||||
"intro": []
|
||||
},
|
||||
"workshop-bash-five-programs": {
|
||||
"title": "Build Five Programs",
|
||||
"intro": [
|
||||
@@ -3696,7 +3756,10 @@
|
||||
"title": "Bash Scripting Quiz",
|
||||
"intro": ["Test what you've learned on bash scripting in this quiz."]
|
||||
},
|
||||
"tkgg": { "title": "315", "intro": [] },
|
||||
"tkgg": {
|
||||
"title": "315",
|
||||
"intro": []
|
||||
},
|
||||
"workshop-sql-student-database-part-1": {
|
||||
"title": "Build a Student Database: Part 1",
|
||||
"intro": [
|
||||
@@ -3746,7 +3809,10 @@
|
||||
"title": "Bash and SQL Quiz",
|
||||
"intro": ["Test what you've learned in this quiz on Bash and SQL."]
|
||||
},
|
||||
"eeez": { "title": "324", "intro": [] },
|
||||
"eeez": {
|
||||
"title": "324",
|
||||
"intro": []
|
||||
},
|
||||
"workshop-castle": {
|
||||
"title": "Build a Castle",
|
||||
"intro": [
|
||||
@@ -3762,7 +3828,10 @@
|
||||
"title": "Nano Quiz",
|
||||
"intro": ["Test what you've learned on Nano with this quiz ."]
|
||||
},
|
||||
"rhhl": { "title": "328", "intro": [] },
|
||||
"rhhl": {
|
||||
"title": "328",
|
||||
"intro": []
|
||||
},
|
||||
"workshop-sql-reference-object": {
|
||||
"title": "Build an SQL Reference Object",
|
||||
"intro": [
|
||||
@@ -3904,7 +3973,10 @@
|
||||
"title": "Placeholder - waiting for title",
|
||||
"intro": [""]
|
||||
},
|
||||
"lab-budget-app": { "title": "Build a Budget App", "intro": [""] },
|
||||
"lab-budget-app": {
|
||||
"title": "Build a Budget App",
|
||||
"intro": [""]
|
||||
},
|
||||
"review-classes-and-objects": {
|
||||
"title": "Classes and Objects Review",
|
||||
"intro": [
|
||||
@@ -3950,7 +4022,10 @@
|
||||
"title": "Placeholder - Waiting for title",
|
||||
"intro": [""]
|
||||
},
|
||||
"lab-placeholder-oop-3": { "title": "", "intro": [""] },
|
||||
"lab-placeholder-oop-3": {
|
||||
"title": "",
|
||||
"intro": [""]
|
||||
},
|
||||
"review-object-oriented-programming": {
|
||||
"title": "Object Oriented Programming Review",
|
||||
"intro": [
|
||||
@@ -4001,8 +4076,14 @@
|
||||
"title": "Build a Bisection Method",
|
||||
"intro": [""]
|
||||
},
|
||||
"workshop-merge-sort": { "title": "Build a Merge Sort", "intro": [""] },
|
||||
"lab-quick-sort": { "title": "Build a Quick Sort", "intro": [""] },
|
||||
"workshop-merge-sort": {
|
||||
"title": "Build a Merge Sort",
|
||||
"intro": [""]
|
||||
},
|
||||
"lab-quick-sort": {
|
||||
"title": "Build a Quick Sort",
|
||||
"intro": [""]
|
||||
},
|
||||
"lab-selection-sort": {
|
||||
"title": "Build a Selection Sort",
|
||||
"intro": [""]
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: Introduction to the Build an Event RSVP
|
||||
block: lab-event-rsvp
|
||||
superBlock: full-stack-developer
|
||||
---
|
||||
|
||||
## Introduction to the Build an Event RSVP
|
||||
|
||||
For this lab you will use <code>React.useState</code> to manage a form in a React component.
|
||||
16
curriculum/challenges/_meta/lab-event-rsvp/meta.json
Normal file
16
curriculum/challenges/_meta/lab-event-rsvp/meta.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "Build an Event RSVP",
|
||||
"usesMultifileEditor": true,
|
||||
"dashedName": "lab-event-rsvp",
|
||||
"superBlock": "full-stack-developer",
|
||||
"challengeOrder": [
|
||||
{
|
||||
"id": "67d936de7055982b02baf186",
|
||||
"title": "Build an Event RSVP"
|
||||
}
|
||||
],
|
||||
"helpCategory": "JavaScript",
|
||||
"isUpcomingChange": false,
|
||||
"blockLayout": "link",
|
||||
"blockType": "lab"
|
||||
}
|
||||
@@ -0,0 +1,917 @@
|
||||
---
|
||||
id: 67d936de7055982b02baf186
|
||||
title: Build an Event RSVP
|
||||
challengeType: 25
|
||||
dashedName: build-an-event-rsvp
|
||||
demoType: onClick
|
||||
---
|
||||
|
||||
# --description--
|
||||
|
||||
**Objective:** Fulfill the user stories below and get all the tests to pass to complete the lab.
|
||||
|
||||
**User Stories:**
|
||||
|
||||
1. You should create a form with fields for name, email, number of attendees, dietary preferences, and an option to indicate if you are bringing additional guests.
|
||||
|
||||
2. You should have a text input field where you would enter your name (mandatory).
|
||||
|
||||
3. You should have an email input field where you would enter your email address (mandatory). The form should validate the format to ensure it is a proper email.
|
||||
|
||||
4. You should have a number input field where you would enter the number of attendees in the form (mandatory), and the number should not be less than one.
|
||||
|
||||
5. You should have a text input field where you would enter your dietary preferences, and this information should be optional.
|
||||
|
||||
6. You should be able to check or uncheck a checkbox to indicate whether you are bringing additional guests.
|
||||
|
||||
7. You should have a button which submits the form, and the form should not cause the page to reload upon submission.
|
||||
|
||||
8. You should see a confirmation message displayed below the form after submitting, followed by the details provided (name, email, number of attendees, dietary preferences, and optional additional guests).
|
||||
|
||||
Here is an example message after submitting the form:
|
||||
|
||||
```markdown
|
||||
RSVP Submitted!
|
||||
Name: John Doe
|
||||
Email: example@example.com
|
||||
Number of attendees: 2
|
||||
Dietary preferences: None
|
||||
Bringing additional guests: Yes
|
||||
```
|
||||
|
||||
# --hints--
|
||||
|
||||
You should have one `form` element to hold all your form content.
|
||||
|
||||
```js
|
||||
const forms = document.querySelectorAll('form');
|
||||
assert.equal(forms.length, 1);
|
||||
```
|
||||
|
||||
You should have two `input` elements of type `text`.
|
||||
|
||||
```js
|
||||
const inputs = document.querySelectorAll('form input[type="text"]');
|
||||
assert.equal(inputs.length, 2);
|
||||
```
|
||||
|
||||
You should have one `input` element of type `email`.
|
||||
|
||||
```js
|
||||
try {
|
||||
const inputs = document.querySelectorAll('form input[type="email"]');
|
||||
assert.equal(inputs.length, 1);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw e;
|
||||
}
|
||||
```
|
||||
|
||||
You should have one `input` element of type `number`.
|
||||
|
||||
```js
|
||||
try {
|
||||
const inputs = document.querySelectorAll('form input[type="number"]');
|
||||
assert.equal(inputs.length, 1);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw e;
|
||||
}
|
||||
```
|
||||
|
||||
You should have one `input` element of type `checkbox`.
|
||||
|
||||
```js
|
||||
try {
|
||||
const inputs = document.querySelectorAll('form input[type="checkbox"]');
|
||||
assert.equal(inputs.length, 1);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw e;
|
||||
}
|
||||
```
|
||||
|
||||
One `input[type="text"]` element should be required.
|
||||
|
||||
```js
|
||||
try {
|
||||
const input = document.querySelectorAll('form input[type="text"][required]');
|
||||
assert.equal(input.length, 1);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw e;
|
||||
}
|
||||
```
|
||||
|
||||
The `input[type="email"]` element should be required.
|
||||
|
||||
```js
|
||||
try {
|
||||
const input = document.querySelector('form input[type="email"][required]');
|
||||
assert.exists(input);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw e;
|
||||
}
|
||||
```
|
||||
|
||||
The `input[type="number"]` element should be required.
|
||||
|
||||
```js
|
||||
try {
|
||||
const input = document.querySelector('form input[type="number"][required]');
|
||||
assert.exists(input);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw e;
|
||||
}
|
||||
```
|
||||
|
||||
Changing the value of the `input[type="text"]` elements should update component state.
|
||||
|
||||
```js
|
||||
async () => {
|
||||
try {
|
||||
const abuseState = __helpers.spyOn(React, 'useState');
|
||||
const script = [...document.querySelectorAll('script')]
|
||||
.find(s => s.dataset.src === 'index.jsx')
|
||||
.innerText.replace('_React.useState', 'abuseState');
|
||||
|
||||
const exports = {};
|
||||
const a = eval(script);
|
||||
|
||||
const s = await __helpers.prepTestComponent(exports.EventRSVPForm);
|
||||
const inp = s.querySelector('input[type="text"]');
|
||||
|
||||
await React.act(async () => {
|
||||
inp.value = 'John Doe';
|
||||
const ev = new InputEvent('change', { bubbles: true, cancelable: false });
|
||||
inp[Object.keys(inp).find(k => k.startsWith('__reactProps'))].onChange({
|
||||
...ev,
|
||||
target: inp
|
||||
});
|
||||
});
|
||||
|
||||
// For all state in `abuseState.returns`, there will be a multiple of two calls - one before the change, and one after.
|
||||
const alteredStates = abuseState.returns;
|
||||
const initialStates = alteredStates.splice(
|
||||
0,
|
||||
abuseState.returns.length / 2
|
||||
);
|
||||
|
||||
const stateChanged = initialStates.some((s, i) => {
|
||||
try {
|
||||
assert.deepEqual(s[0], alteredStates[i][0]);
|
||||
return false;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
abuseState.restore();
|
||||
assert.isTrue(stateChanged);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Changing the value of the `input[type="email"]` elements should update component state.
|
||||
|
||||
```js
|
||||
async () => {
|
||||
try {
|
||||
const abuseState = __helpers.spyOn(React, 'useState');
|
||||
const script = [...document.querySelectorAll('script')]
|
||||
.find(s => s.dataset.src === 'index.jsx')
|
||||
.innerText.replace('_React.useState', 'abuseState');
|
||||
|
||||
const exports = {};
|
||||
const a = eval(script);
|
||||
|
||||
const s = await __helpers.prepTestComponent(exports.EventRSVPForm);
|
||||
const inp = s.querySelector('input[type="email"]');
|
||||
|
||||
await React.act(async () => {
|
||||
inp.value = 'fcc@freecodecamp.org';
|
||||
const ev = new InputEvent('change', { bubbles: true, cancelable: false });
|
||||
inp[Object.keys(inp).find(k => k.startsWith('__reactProps'))].onChange({
|
||||
...ev,
|
||||
target: inp
|
||||
});
|
||||
});
|
||||
|
||||
// For all state in `abuseState.returns`, there will be a multiple of two calls - one before the change, and one after.
|
||||
const alteredStates = abuseState.returns;
|
||||
const initialStates = alteredStates.splice(
|
||||
0,
|
||||
abuseState.returns.length / 2
|
||||
);
|
||||
|
||||
const stateChanged = initialStates.some((s, i) => {
|
||||
try {
|
||||
assert.deepEqual(s[0], alteredStates[i][0]);
|
||||
return false;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
abuseState.restore();
|
||||
assert.isTrue(stateChanged);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Changing the value of the `input[type="number"]` elements should update component state.
|
||||
|
||||
```js
|
||||
async () => {
|
||||
try {
|
||||
const abuseState = __helpers.spyOn(React, 'useState');
|
||||
const script = [...document.querySelectorAll('script')]
|
||||
.find(s => s.dataset.src === 'index.jsx')
|
||||
.innerText.replace('_React.useState', 'abuseState');
|
||||
|
||||
const exports = {};
|
||||
const a = eval(script);
|
||||
|
||||
const s = await __helpers.prepTestComponent(exports.EventRSVPForm);
|
||||
const inp = s.querySelector('input[type="number"]');
|
||||
|
||||
await React.act(async () => {
|
||||
inp.value = 2;
|
||||
const ev = new InputEvent('change', { bubbles: true, cancelable: false });
|
||||
inp[Object.keys(inp).find(k => k.startsWith('__reactProps'))].onChange({
|
||||
...ev,
|
||||
target: inp
|
||||
});
|
||||
});
|
||||
|
||||
// For all state in `abuseState.returns`, there will be a multiple of two calls - one before the change, and one after.
|
||||
const alteredStates = abuseState.returns;
|
||||
const initialStates = alteredStates.splice(
|
||||
0,
|
||||
abuseState.returns.length / 2
|
||||
);
|
||||
|
||||
const stateChanged = initialStates.some((s, i) => {
|
||||
try {
|
||||
assert.deepEqual(s[0], alteredStates[i][0]);
|
||||
return false;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
abuseState.restore();
|
||||
assert.isTrue(stateChanged);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Changing the value of the `input[type="checkbox"]` elements should update component state.
|
||||
|
||||
```js
|
||||
async () => {
|
||||
try {
|
||||
const abuseState = __helpers.spyOn(React, 'useState');
|
||||
const script = [...document.querySelectorAll('script')]
|
||||
.find(s => s.dataset.src === 'index.jsx')
|
||||
.innerText.replace('_React.useState', 'abuseState');
|
||||
|
||||
const exports = {};
|
||||
const a = eval(script);
|
||||
|
||||
const s = await __helpers.prepTestComponent(exports.EventRSVPForm);
|
||||
const inp = s.querySelector('input[type="checkbox"]');
|
||||
|
||||
await React.act(async () => {
|
||||
inp.checked = true;
|
||||
const ev = new InputEvent('change', { bubbles: true, cancelable: false });
|
||||
inp[Object.keys(inp).find(k => k.startsWith('__reactProps'))].onChange({
|
||||
...ev,
|
||||
target: inp
|
||||
});
|
||||
});
|
||||
|
||||
// For all state in `abuseState.returns`, there will be a multiple of two calls - one before the change, and one after.
|
||||
const alteredStates = abuseState.returns;
|
||||
const initialStates = alteredStates.splice(
|
||||
0,
|
||||
abuseState.returns.length / 2
|
||||
);
|
||||
|
||||
const stateChanged = initialStates.some((s, i) => {
|
||||
try {
|
||||
assert.deepEqual(s[0], alteredStates[i][0]);
|
||||
return false;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
abuseState.restore();
|
||||
assert.isTrue(stateChanged);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Submitting the form should not result in the page reloading.
|
||||
|
||||
```js
|
||||
async () => {
|
||||
try {
|
||||
// Ideally, `window.onload` would be watched. However, the frame runner disables submissions causing window reloads.
|
||||
// So, the test needs to manually check for the `.preventDefault` call.
|
||||
const script = [...document.querySelectorAll('script')].find(
|
||||
s => s.dataset.src === 'index.jsx'
|
||||
).innerText;
|
||||
|
||||
const exports = {};
|
||||
const a = eval(script);
|
||||
|
||||
const s = await __helpers.prepTestComponent(exports.EventRSVPForm);
|
||||
const f = s.querySelector('form');
|
||||
|
||||
await React.act(async () => {
|
||||
let c = 0;
|
||||
const mockEv = {
|
||||
...new SubmitEvent('submit'),
|
||||
preventDefault: () => {
|
||||
c++;
|
||||
}
|
||||
};
|
||||
f[Object.keys(f).find(k => k.startsWith('__reactProps'))].onSubmit(
|
||||
mockEv
|
||||
);
|
||||
assert.equal(c, 1);
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
After submission, there should be an element on the page indicating the state of the name `input`.
|
||||
|
||||
```js
|
||||
async () => {
|
||||
try {
|
||||
const inp = document.querySelector(`input[type="text"]`);
|
||||
assert.exists(inp);
|
||||
const setter = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLInputElement.prototype,
|
||||
'value'
|
||||
).set;
|
||||
await React.act(async () => {
|
||||
// In order for form to be submited, all required fields are give values.
|
||||
await adjustAllRequired();
|
||||
setter.call(inp, 'John Doe');
|
||||
inp.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
|
||||
submitForm();
|
||||
});
|
||||
|
||||
const nonInputText = getInnerTextExcept('input,script');
|
||||
assert.include(nonInputText, 'John Doe');
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
After submission, there should be an element on the page indicating the state of the email `input`.
|
||||
|
||||
```js
|
||||
async () => {
|
||||
try {
|
||||
const inp = document.querySelector(`input[type="email"]`);
|
||||
assert.exists(inp);
|
||||
const setter = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLInputElement.prototype,
|
||||
'value'
|
||||
).set;
|
||||
await React.act(async () => {
|
||||
// In order for form to be submited, all required fields are give values.
|
||||
await adjustAllRequired();
|
||||
setter.call(inp, 'fcc@freecodecamp.org');
|
||||
inp.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
|
||||
submitForm();
|
||||
});
|
||||
|
||||
const nonInputText = getInnerTextExcept('input,script');
|
||||
assert.include(nonInputText, 'fcc@freecodecamp.org');
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
After submission, there should be an element on the page indicating the state of the number of attendees `input`.
|
||||
|
||||
```js
|
||||
async () => {
|
||||
try {
|
||||
const inp = document.querySelector(`input[type="number"]`);
|
||||
assert.exists(inp);
|
||||
const setter = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLInputElement.prototype,
|
||||
'value'
|
||||
).set;
|
||||
await React.act(async () => {
|
||||
// In order for form to be submited, all required fields are give values.
|
||||
await adjustAllRequired();
|
||||
setter.call(inp, 2);
|
||||
inp.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
|
||||
submitForm();
|
||||
});
|
||||
|
||||
const nonInputText = getInnerTextExcept('input,script');
|
||||
assert.include(nonInputText, '2');
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
After submission, there should be an element on the page indicating the state of the dietary preferences `input`.
|
||||
|
||||
```js
|
||||
async () => {
|
||||
try {
|
||||
const inp = document.querySelector(`input[type="text"]:not(:required)`);
|
||||
assert.exists(inp);
|
||||
const setter = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLInputElement.prototype,
|
||||
'value'
|
||||
).set;
|
||||
await React.act(async () => {
|
||||
// In order for form to be submited, all required fields are give values.
|
||||
await adjustAllRequired();
|
||||
setter.call(inp, 'diet');
|
||||
inp.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
|
||||
submitForm();
|
||||
});
|
||||
|
||||
const nonInputText = getInnerTextExcept('input,script');
|
||||
assert.include(nonInputText, 'diet');
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
After submission, there should be an element on the page indicating the state of the additional guests `input`.
|
||||
|
||||
```js
|
||||
async () => {
|
||||
try {
|
||||
let inp = document.querySelector(`input[type="checkbox"]`);
|
||||
assert.exists(inp);
|
||||
await React.act(async () => {
|
||||
// In order for form to be submited, all required fields are give values.
|
||||
await adjustAllRequired();
|
||||
const setter = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLInputElement.prototype,
|
||||
'checked'
|
||||
).set;
|
||||
setter.call(inp, false);
|
||||
inp.dispatchEvent(new Event('click', { bubbles: true }));
|
||||
|
||||
submitForm();
|
||||
});
|
||||
|
||||
// Test submission without checked input, then test submission with checked input - compare for difference.
|
||||
const stateWithFalse = getInnerTextExcept('input,script');
|
||||
|
||||
inp = document.querySelector(`input[type="checkbox"]`);
|
||||
await React.act(async () => {
|
||||
const setter = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLInputElement.prototype,
|
||||
'checked'
|
||||
).set;
|
||||
setter.call(inp, true);
|
||||
inp.dispatchEvent(new Event('click', { bubbles: true }));
|
||||
|
||||
submitForm();
|
||||
});
|
||||
|
||||
const stateWithTrue = getInnerTextExcept('input,script');
|
||||
|
||||
assert.notEqual(stateWithFalse, stateWithTrue);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
# --before-all--
|
||||
|
||||
```js
|
||||
// Submit button can either be the only button within a form, or a `button[type="submit"]`.
|
||||
function submitForm() {
|
||||
const maybeSubmit = document.querySelectorAll("form button");
|
||||
if (maybeSubmit.length === 1) {
|
||||
return maybeSubmit[0].click();
|
||||
}
|
||||
|
||||
const submitBtn = document.querySelector("button[type='submit']");
|
||||
if (submitBtn) {
|
||||
return submitBtn.click();
|
||||
}
|
||||
}
|
||||
|
||||
async function adjustAllRequired() {
|
||||
const inps = document.querySelectorAll(
|
||||
'input[required], select[required], textarea[required]'
|
||||
);
|
||||
inps.forEach(inp => {
|
||||
let setter = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLInputElement.prototype,
|
||||
'value'
|
||||
).set;
|
||||
switch (inp.type) {
|
||||
case 'text':
|
||||
setter.call(inp, 'required text');
|
||||
inp.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
break;
|
||||
case 'email':
|
||||
setter.call(inp, 'required-email@freecodecamp.org');
|
||||
inp.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
break;
|
||||
case 'number':
|
||||
setter.call(inp, 1);
|
||||
inp.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
break;
|
||||
case 'checkbox':
|
||||
setter = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLInputElement.prototype,
|
||||
'checked'
|
||||
).set;
|
||||
setter.call(inp, true);
|
||||
inp.dispatchEvent(new Event('click', { bubbles: true }));
|
||||
break;
|
||||
}
|
||||
inp.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
});
|
||||
}
|
||||
|
||||
function getInnerTextExcept(removingSelector) {
|
||||
const body = document.body.cloneNode(true);
|
||||
|
||||
const squareElements = body.querySelectorAll(removingSelector);
|
||||
squareElements.forEach(element => {
|
||||
element.parentNode.removeChild(element);
|
||||
});
|
||||
|
||||
return body.innerText;
|
||||
}
|
||||
```
|
||||
|
||||
# --seed--
|
||||
|
||||
## --seed-contents--
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Event RSVP</title>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
|
||||
<script
|
||||
data-plugins="transform-modules-umd"
|
||||
type="text/babel"
|
||||
src="index.jsx"
|
||||
></script>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script
|
||||
data-plugins="transform-modules-umd"
|
||||
type="text/babel"
|
||||
data-presets="react"
|
||||
data-type="module"
|
||||
>
|
||||
import { EventRSVPForm } from './index.jsx';
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<EventRSVPForm />
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
```css
|
||||
|
||||
```
|
||||
|
||||
```jsx
|
||||
const { useState } = React;
|
||||
|
||||
export function EventRSVPForm() {}
|
||||
```
|
||||
|
||||
# --solutions--
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Event RSVP</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
|
||||
<script
|
||||
data-plugins="transform-modules-umd"
|
||||
type="text/babel"
|
||||
src="index.jsx"
|
||||
></script>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script
|
||||
data-plugins="transform-modules-umd"
|
||||
type="text/babel"
|
||||
data-presets="react"
|
||||
data-type="module"
|
||||
>
|
||||
import { EventRSVPForm } from './index.jsx';
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<EventRSVPForm />
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
```css
|
||||
body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
font-family: 'Helvetica Neue', Arial, sans-serif;
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
background-color: #ffffff;
|
||||
padding: 40px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
|
||||
width: 100%;
|
||||
max-width: 450px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 20px;
|
||||
font-size: 28px;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
margin-right: 15px;
|
||||
font-size: 15px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.form-group input[type='text'],
|
||||
.form-group input[type='email'],
|
||||
.form-group input[type='number'] {
|
||||
flex: 2;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
width: 100%;
|
||||
font-size: 15px;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
border-color: #4caf4f4f;
|
||||
}
|
||||
|
||||
.form-group input[type='checkbox'] {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #5957e4;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 25px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-top: 15px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #413fb3;
|
||||
}
|
||||
|
||||
.submitted-message {
|
||||
margin-top: 30px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.submitted-message h3 {
|
||||
color: #5957e4;
|
||||
margin-bottom: 10px;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.form-container {
|
||||
width: 90%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
text-align: left;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.form-group input[type='text'],
|
||||
.form-group input[type='email'],
|
||||
.form-group input[type='number'] {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```jsx
|
||||
const { useState } = React;
|
||||
|
||||
export function EventRSVPForm() {
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
attendees: '',
|
||||
dietaryPreferences: '',
|
||||
bringingOthers: false
|
||||
});
|
||||
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
function handleChange(event) {
|
||||
const { name, value, type, checked } = event.target;
|
||||
setFormData({
|
||||
...formData,
|
||||
[name]: type === 'checkbox' ? checked : value
|
||||
});
|
||||
}
|
||||
|
||||
function handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
setSubmitted(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='form-container'>
|
||||
<h2>Event RSVP Form</h2>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className='form-group'>
|
||||
<label>
|
||||
Name:
|
||||
<input
|
||||
type='text'
|
||||
name='name'
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
placeholder='Your Name'
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label>
|
||||
Email:
|
||||
<input
|
||||
type='email'
|
||||
name='email'
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
placeholder='Your Email'
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label>
|
||||
Number of Attendees:
|
||||
<input
|
||||
type='number'
|
||||
name='attendees'
|
||||
value={formData.attendees || ''}
|
||||
onChange={handleChange}
|
||||
min='1'
|
||||
placeholder='Number of Attendees'
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label>
|
||||
Dietary Preferences:
|
||||
<input
|
||||
type='text'
|
||||
name='dietaryPreferences'
|
||||
value={formData.dietaryPreferences}
|
||||
onChange={handleChange}
|
||||
placeholder='Dietary Preferences (Optional)'
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label>
|
||||
Bringing additional guests?
|
||||
<input
|
||||
type='checkbox'
|
||||
name='bringingOthers'
|
||||
checked={formData.bringingOthers}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<button type='submit'>Submit RSVP</button>
|
||||
</form>
|
||||
{submitted && (
|
||||
<div className='submitted-message'>
|
||||
<h3>RSVP Submitted!</h3>
|
||||
<p>
|
||||
<strong>Name:</strong> {formData.name}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Email:</strong> {formData.email}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Number of Attendees:</strong> {formData.attendees}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Dietary Preferences:</strong>{' '}
|
||||
{formData.dietaryPreferences || 'None'}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Bringing Others:</strong>{' '}
|
||||
{formData.bringingOthers ? 'Yes' : 'No'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user