Code

js/views/contact.js — cross-field validation

In the code column — js/views/contact.js. It uses lesson 11's attachLiveValidation, but with cross-field rules and a full submit lifecycle.

The cross-field part is rules(values):

function rules(values) {
  const base = { /* name, email, subject, message: min 20 */ };
  if (values.subject === 'bug') {
    base.message = [required('Message'), minLength(50, 'A bug report'), maxLength(1000)];
  }
  return base;
}

Because attachLiveValidation calls getRules(values) with the current values each time, picking "Bug report" instantly makes the message rule stricter (min 50). Change the subject back — the field re-validates.

The submit lifecycle, inside submit:

submitBtn.disabled = true;
submitBtn.textContent = 'Sending…';
try {
  await fakeSend(result.values);
  // ...show the success panel...
} catch (error) {
  showToast(error.message, 'error');
} finally {
  submitBtn.disabled = false;
  submitBtn.textContent = 'Send message';
}

finally matters: whether it succeeded or failed, the button always resets. Without it, after an error the button would stay "Sending…" and disabled forever.

And fakeSend — an honest simulation (it even "rejects" @example.com so you can see the error path):

// The real version: await fetch('/api/contact', { method:'POST', ... })
// The surrounding code doesn't change — only this function.

Tip. Notice that changing subject also updates the hint under the field ("bug reports: 50+ characters"). A good form explains the rule before the user runs into it, not just shows an error after the fact.