Back to Freecodecamp

Build an Event RSVP

curriculum/challenges/english/blocks/lab-event-rsvp/67d936de7055982b02baf186.md

latest22.7 KB
Original Source

--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: [email protected]
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;
}

The input[type="number"] element should not accept values less than 1.

js
try {
  const input = document.querySelector('form input[type="number"]');
  assert.exists(input);
  assert.equal(input.min, '1');
} catch (e) {
  console.log(e);
  throw e;
}

Changing the value of the input[type="text"] elements should update component state.

js
  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
  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 = '[email protected]';
      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
  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
  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
  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
  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
  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, '[email protected]');
      inp.dispatchEvent(new Event('input', { bubbles: true }));

      submitForm();
    });

    const nonInputText = getInnerTextExcept('input,script');
    assert.include(nonInputText, '[email protected]');
  } 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
  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
  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
  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, '[email protected]');
        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>
  );
}