.dev

Stop shipping one order form

I built a checkout I was proud of, in one file. Merchants could not move a field. That is the whole story.

FeeefFeeef engineering

The first custom checkout I was proud of was one file. Gallery, offers, name, phone, wilaya, city, address, quantity, and the buy button, all closed over the same useState. It matched the design. I sent it out.

Then a merchant asked me to put the phone above the name. Another wanted email, but only on one product. A third wanted the address to wait until the shopper had picked a city, because an empty address line above the wilaya box made people type a street into the state. I was in the loop for all three. That is a bad job. Those are edits to a list of blocks.

So the order form in the blank template is a shell with a content slot. Every field is its own component, and every one of them has hide.

What I mean by independent

When props.hide is true, the field renders null. The node stays in the slot, at the same index. Turn it back on and it is where they left it. Delete is a different gesture.

We had themes where "hide" removed the node. The way back was to know a library id and drop a fresh copy, which wiped the props they had already set. I watched someone do that twice. Keeping the node is the feature.

The shell owns the behavior I don't want copied into every field. It saves a draft when the phone hits the right length. It posts the order with POST /api/orders/create. It keeps the honeypot, the submission-timing check, and the floating button that scrolls to #feeef-order-buy. Fields read that state through useSlotContext("pdp"). The shell publishes it with SlotContextBridge. If I see ff.orders.send in a field, I still grep for it in review. That call belongs to an older client, and it skips the checks the shell already did.

What the blank page shows

This is the product page, in slot order. I hid the fields people rarely want, and I left them in the list so turning them on is one checkbox.

BlockShown
Offers, extra quantity, email, delivery type, note, custom fieldsHidden. hide: true. Still sitting there.
Name, phoneShown
State and cityShown, one row, two child slots
AddressThe node is there. The input waits until both state and city are set
Buy button with the totalShown. The float scrolls to this
Quantity and add to cartShown, as their own row

I split quantity off the buy button after watching someone try to park "add to cart" under the gallery and leave "buy" under the form. One component with showQty and showCart flags can only live in one place. Two nodes can live in two places.

The widget below is that list. It is a model of the editor, so you can poke it here. Toggle a field, move it, then mark state and city and watch the address row fill in.

Editor slots

Name
Phone
State
City

Address stays in this slot. It appears after state and city are both set.

Total4 900 دج
Buy it now
−1+
Add to cart

Folders, briefly

State and city are children of the geo row because they are one visual row and two inspectors. That is not a reason to wrap the whole product page in three nested shells. I use a folder when a component has slots/ or children/. Everything else is a flat name.tsx with export const meta and function App(). Siblings under pages/product/components/ compile to sections.main. The order field on meta is the sort key.

slotsLayout is easy to trust too much. It tells the editor how to present the holes. Your App() still has to render props.slots.stateSlot and props.slots.citySlot. I have declared a layout, forgotten to paint the slot, and then watched the editor show a region the shop never mounts. The merchant thinks the block is broken. It is empty.

What I left in, on purpose

Offers and the variant picker stay in the tree even though the blank theme hides them. A product with offers, and a form that cannot select one, charges the catalog price and surprises everyone at confirmation. Hidden is recoverable. Missing means the next person rebuilds offers inside the buy button, badly. I have reviewed that version.

Shipping cards render nothing until a state is selected. A card that says "choose a wilaya" before a wilaya exists is how we trained people to tap a control that does nothing. The delivery-type block is hidden by default on top of that. Unhide it and the empty-state rule still holds.

If you are porting a single-file form, start from the blank shell and move the markup into the fields that are already there. The order form doc is the checklist I use. The create guide shows the files.

On this page