Here, there's no HTML structure needed at all — just a Dvl'epr component, formsave, along with the insert or update SQL it needs to run.
<block> <formsave validate="Y"> <sql> <query name="newcontact" success="Contact saved." fail="Could not save contact."><![CDATA[ INSERT INTO contacts (dept_id, contact_name, email, department, phone, status, created_date) VALUES ('%deptid%', '%contact_name%', '%email%', '%department%', '%phone%', 'Active', NOW()) ]]></query> <query name="retInfo" type="data"><![CDATA[ SELECT '%newcontact_id%' AS contact_id, '%contact_name%' AS contact_name, 'CONTACTADDED' AS TYPE FROM DUAL ]]></query> </sql> </formsave> </block>
The Update form's processor follows the exact same shape — same <block> root, same <formsave> wrapper, same retInfo pattern — just an UPDATE in place of an INSERT, filtered to the one row being edited:
<block> <formsave validate="Y"> <sql> <query name="updatedcontact" success="Contact updated." fail="Could not update contact."><![CDATA[ UPDATE contacts SET contact_name = '%contact_name%', email = '%email%', department = '%department%', phone = '%phone%' WHERE contact_id = '%contact_id%' AND dept_id = '%deptid%' ]]></query> <query name="retInfo" type="data"><![CDATA[ SELECT '%contact_id%' AS contact_id, '%contact_name%' AS contact_name, 'CONTACTUPDATED' AS TYPE FROM DUAL ]]></query> </sql> </formsave> </block>
%contact_id% that post 6's hidden field carried out of the Edit form comes back in as the filter, so the update lands on exactly the row that was opened, and no other.A step in the middle doesn't have to be SQL. A query marked type="function" runs a PHP function instead, in document order, inside the same transaction as everything around it — useful for anything the database can't do on its own, like sending a welcome email once the row exists:
<formsave validate="Y"> <sql> <!-- Load the file holding the PHP function, before any query runs --> <code><include>eminc_contactfunctions.inc</include></code> <query name="newcontact" success="Contact saved." fail="Could not save contact."><![CDATA[ INSERT INTO contacts (dept_id, contact_name, email, status, created_date) VALUES ('%deptid%', '%contact_name%', '%email%', 'Active', NOW()) ]]></query> <!-- The query's own name IS the PHP function called --> <query name="sendEmail" type="function" fail="Contact saved, but the welcome email failed." /> <query name="retInfo" type="data"><![CDATA[ SELECT '%newcontact_id%' AS contact_id, 'CONTACTADDED' AS TYPE FROM DUAL ]]></query> </sql> </formsave>
And sendEmail() itself, inside eminc_contactfunctions.inc:
<?php // The & matters: it is what lets anything written back here reach // the queries that run after this one. function sendEmail(&$parmlist) { // $parmlist holds every token in scope, including newcontact_id from the query above $email = $parmlist['email']; // ... send the email ... return ''; // empty string = success // throw new Exception('Could not send welcome email'); // failure }
Return an empty string for success, throw an exception for failure — that's the entire contract. (Return a number instead and it becomes %sendEmail_id%, the same way an INSERT's new id becomes %newcontact_id%.) Everything else — the response, the message shown to the user, the transaction rolling back on failure — works exactly the same whether the step was a query or a function.
That's the full read-and-write loop the platform runs on: a list or table to browse, a detail screen to focus on one record, forms to add and update it, processors to save it. Every other screen on the platform is some combination of these same pieces. The last post in this arc puts all of them on one page together.
Putting It Together: one complete page from list to form
Post 8 is the payoff — the module file that wires the list, the table, the detail screen, and this form into a single working Contacts page.