Product design

Tally: let the last answer decide the next question

Tally is a document-style online form builder for registrations, requirements and feedback. Conditional questions adapt to answers, helping each person provide the information their path needs before submitting.

Break down the problem and try your own design and implementation first. Then explore the reasoning to check states, edge cases, trade-offs, and ways to test your approach. The guide offers a reasoning path, not a complete implementation.

01
FRONTEND EXERCISEAdvanced

How should hidden drafts affect a conditional form?

Scenario

Design a pure JS frontend function that turns a form definition and local draft into visible fields, missing answers and prepared submission data. Only answers effective on the current branch enter the submission, while the local draft may retain hidden answers for returning later. The function does not mutate the draft, manipulate the DOM or send requests. This protocol is invented for the exercise, not Tally’s actual implementation.

Input contract

  1. fields is an ordered array of at most 10,000 entries. Each is a non-null object with a unique nonempty string id, boolean required and optional when. An absent when means always visible; explicit null is invalid. Ignore other fields.
  2. when is a non-null object with mode equal to all or any and 1–20 terms. Each term has nonempty strings fieldId and equals. fieldId must reference a field strictly earlier in fields. Missing, forward and self-references are invalid. Repeated terms are allowed without changing Boolean meaning. Let m be the total number of terms.
  3. draft is a JS Map whose keys and values must be strings. Each key must identify a defined field. An empty value means unanswered. Do not trim: spaces are ordinary characters. A visible field’s nonempty draft value is an effective answer; a hidden field has no effective answer even if its value is cached.
  4. A term is true only if its referenced field is visible, has an effective answer and that answer strictly equals equals. all requires every term; any requires at least one. Unanswered and hidden fields never match. There are no nested condition trees, and conditions cannot change answers.
  5. Validate all fields, conditions and draft entries, including rules on branches that end up hidden. Any error returns a location, rejects the batch and yields no partial output. Valid input returns visibleIds, missingRequiredIds and submissionEntries, ordered [id,value] pairs containing only visible answered fields. Return these lists even when required answers are missing, but canSubmit must then be false.
  6. Empty fields with an empty draft is valid and returns empty lists with canSubmit=true. Let L be the total character length of IDs, condition constants and draft values. Do not assign arbitrary user IDs onto an ordinary object’s prototype; use entry arrays or safe containers for output.

Small example

The field order is mode (always visible and required), address (visible when mode equals online, required), invoice (visible when mode equals online, optional), and taxId (visible when invoice equals yes, required). Each condition has one term. The draft is mode=offline, address=a test address, invoice=yes, taxId=an empty string.

Give all four outputs. Then change mode to online and recalculate; finally change invoice to no and recalculate. Explain why cached invoice=yes must not show taxId while mode=offline.

Also check: (a) taxId references a future field; (b) the draft contains an unknown field; (c) a condition references __proto__, where that ID has been validly defined earlier; (d) all with one true and one false term, versus any with the same terms.

Deliverables

Describe validation, definition-order evaluation of visibility and effective answers, then output assembly. Explain why submitting the entire draft or checking only DOM visibility is insufficient. Analyze time and space in n fields, m terms and L; give at least four invariants, without complete code or line-by-line pseudocode.

Explore the reasoning
  1. Validate the whole definition and draft. Use Map to index field positions. Check duplicate IDs, types, term counts, reference order and draft keys and values. Conditions can only refer to earlier fields, so one pass in the given order is enough; repeated calculation until values stop changing is unnecessary. Current invisibility cannot excuse an unknown reference.
  2. Build effective answers in field order. Evaluate when before deciding whether to take a nonempty draft value. Conditions read computed effective answers, not raw cache: a hidden invoice=yes cannot activate taxId. all and any change combination logic, not the rules for missing answers.
  3. Keep the three lists distinct. Visible fields enter visibleIds; visible required fields without effective answers enter missingRequiredIds; visible answered fields enter submissionEntries. canSubmit depends only on whether the missing list is empty. The original draft stays unchanged.
  4. Trace the return path. Offline, only mode is visible, nothing is missing, the submission contains mode=offline, and submission is allowed. Online, all four fields are visible, taxId is missing, the submission contains mode/address/invoice, and canSubmit=false. After invoice=no, taxId is hidden, the first three fields are visible and answered, and submission is allowed. The function has never deleted cached draft values.
  5. Check boundaries and cost. Future references and unknown draft keys reject the batch. A valid __proto__ is an ordinary string ID. One true and one false term give false for all and true for any. Hidden fields neither submit nor activate later fields; inputs remain unchanged; output order is stable; errors yield no partial results. Average Map operations give O(n+m) structural operations. Account separately for hashing and comparison using L; repeated comparisons against the same long answer must include the actual repeated character work, not just unique-string length. O(n+m+L) extra space covers validation indexes and output.