The Upright Man Posted 6 hours ago Share Posted 6 hours ago First, I am new to Processwire, rarely do much with PHP and find myself hitting Google for every living thing, and this is my first attempt at using HTMX! So, this is my attempt at rolling up a whole server-side UI-lib from scratch! Very much a WIP! HTMX I wanted to share this idea though because it's finally reached the "exciting" stage so as to get some feedback and suggestions on the design. Basically, I want really dynamic UI tools for an upcoming project and I can't stand having to divide code between 3rd party UI libraries in the browser and exchanging data with processwire on the backend. You end up with html files, css files, front end classes, back end classes, and maybe a bunch of jQuery spaghetti and random PHP files to tie it all together. So, I decided to put the whole thing server-side using HTMX. HTMX and Processwire work together beautifully, each complementing the other. HTMX is a small javascript library that allows any html element to make ajax calls, and directly inserts the results into the DOM as HTML rather than json. You can do some event handling to call javascript and a few other goodies as well. It's incredibly powerful! So, I first made some basic "smart tags" using HTMX markup as Hannacode elements. This looks similar to HTML but takes care of boilerplate easily. Since Processwire is page based, I set up a /forms/ page to allow urlsegments, then pass a method name as the segment! Each child page is a different form with its own subclass in php. You can layout the form in HTML with a couple of hannacode helpers and a few standard fields. The page in processwire defines all your layout and form elements. JSON Forms To allow a more natural description of the forms and allow easy nesting (such as for layouts), you can describe a form using a modified json. I say "modified" because you have to strip out the quotation marks to be legal hannacode, so it's all one big string. I changed the regex in hannacode's parser to allow newlines, which allows you to have some sanity in defining it. The resulting "big string" is run through a regexx that guesses at where the quotation marks should go, making it valid json, which is fed to php's json parser. The objects in the json are just telling the backend code what hannacode functions to call to generate the final html output. Here is the Body element for my login screen. The "formheader" is a hannacode routine that displays a standard graphic at the top of the form (you can specify an URL of a different one) and it grabs the title and prompt from fields in the form itself. You can change these at run-time! There is also an "extra_tabs" field that lists the names of other tabs to load when loading this one. I've not finished the code to add/remove tabs programmatically, but that's next. Just finished state vars! [[form size=small json="[ { new:formheader }, { new:input, id:username, type:text, label:Username }, { new:input, id:password, type: password, label: Password }, { new:layout, class:formbottom, contents: [ { new: button, action:login, text: Login }, { new: a, opentab: newuser, text: Reset Password, info: Enter your email below and we'll figure it out! } ] } ]" ]] Displaying A Form Currently, I am only doing modal forms which cover the display. I will add in-line forms soon through similar methods. To get a form to pop-up, you can either display a button that pops up the form, or you can do it directly. The direct method just outputs a blank div with the "load" trigger set to make a post request the moment the div is loaded. This is also how form initialization works - there is a hidden element in every form that calls the init method when it is loaded, and this event only fires once unless you replace that element in the DOM. The destination will be a static div creating an overlay layer on the website <div #alertbox> that normally does not display (empty divs take up no space until I put content in it). I tell HTMX to output the HTML it receives by just adding hx-target="#alertbox" to the html element that is making the POST. The form background has CSS that makes it the width and height of the screen, giving you a lightbox display for the form. A button works the same way except that the hx-trigger="click" (the default for buttons) instead of load. Closing the form is just replacing the contents of #alertbox with more empty. My login button can appear anywhere I have [[button type='loginout']] on a page! The only reason I reload the underlying page on a login is because the rest of the site isn't upgraded to HTMX yet. There is no need for redirects and other messy tricks. You click the button and the form appears without a page load delay! Your position on the site does not change. You don't have to store an URL to go back to it or anything like that. Form Tabs Part of my end goal involves dynamic tab generation/creation. So, rather than holding each tab in the DOM at once and changing which tab is visible through javascript, I completely replace the form body interior when changing tabs. This means I can reuse IDs between forms! It also means more intelligence can be offloaded to the server side, and I can share data between tabs easily (see below). Talking to Processwire Since the forms are on /form/<formname>/ I set up a convention where you append a <methodname>/ to this URL, and optionally an initializer segment (mainly to accept an encrypted verification link through email - email templates are so easy in Processwire!). You can then drop a php file in the "form" subdirectory of your templates, named the same as your form name. This subclasses a base PHP class. Every control on your form has a name and id (I keep them the same for personal sanity), and this name will be the method name called when you interact with the form. A button named "submit" on form "foo" would call /form/foo/submit/ when you click it with all the form elements included! If you edit the "summary" text field in this form, you will get a post on /form/foo/summary/ with the updated value. If there is no method defined for this, the system still quietly updates the stored copies of the variables. Define the method only if you want to have server-side validation, search, update drop-down lists, etc. HTMX can add and remove elements from your screen as it responds to the post messages. Form Logic The Form.php template (which I've duct-taped together) is called when we get a request for a form. This then auto-loads the correct sub-class file, creates an instance of this class, and calls the method named in the URL. Inside the form is a hidden div with a list of hidden input fields that provide form state plus basic utility functions to call from inside the subclasses that power the actual forms. HTMX automatically sends the values of all form elements when the request comes from a form element. I wrap them in a DIV with a unique ID just that HTMX knows where to put them on the page. I'm currently replacing the entire variable list. Soon, this will add/replace elements one at a time in the post response rather than dumping the whole list with each response. You can attach additional elements to the request by ID as well, so if you want the value of some field that isn't in the form, htmx will supply its value in the post via hx-include="#myelem". I just do it the slow way for debugging purposes. My Form.php's constructor eats up any POST variables and shoves them into my hidden DIV, storing state in the DOM elements. They'll be returned to us at the next POST. I made a simple get/set API in Form.php to manage the variables. By default, variables are loaded and stored with the name of the tab auto-prefixed to the variable, so you always get your own tab's version of the variable. You can also override this to look at variables in other forms, or just give a random name to create a namespace you can share variables under. Some basic lifecycle methods are supported. Your form is told to "unload" before you switch away to another tab, the "init" method is sent after the "open" method so your controls are on-screen. You have separate close and cancel methods so you can do a different cleanup when you close a form compared to cancelling the form, etc. Here is an example "init" method that initializes a form to some default values while also saving the contents even when you switch between tabs. public function init() { parent::init(); $email = $this->getEmailAddr($this->extdata); // validates as well if (isset($email)) { list($username,$domain) = explode("@", $email); $this->debug("Email address is $email"); $this->setField("email", $email, true); // true means to disable the element $this->setField("username", $this->getVar('username') ?? $username); $this->setField("displayname", $this->getVar('displayname') ?? ucfirst($username)); $this->setField("password", $this->getVar('password')); } else { $this->setTitle("Error"); $this->setInfo("Invalid Link!"); $this->close(5); return "Error"; // replaces button text } } Class Files So, here is the entire class file to handle logging in. The "I forgot my password" option just switches tabs to enter your email, so all that logic is in a separate form. The default close delay is 2 seconds, just so you can see the message. On error, you get to see it for 5 seconds. The "reloadpage" method inserts a <script> tag into the page that reloads using javascript. <?php namespace ProcessWire; class VRForm_login extends VRForm { // logout public function logout() { session()->logout(); $this->close(); echo "Success!"; // overwrites button text } // Catch the close, not destroy. Don't reload page on cancel public function afterclose() { $this->debug("called afterclose"); $this->reloadPage(); } // Processwire does all the magic public function login() { // Attempt to log in the user $user = session()->login($this->getVar('username'), $this->getVar('password')); // Check if login was successful if ($user) { // Login successful $this->setTitle("Login Succeeded!"); $this->setInfo("Welcome back<br>{$user->displayname}!"); $this->close(); } else { // Login failed $this->setTitle("Login Failed!"); $this->setInfo("Try again, or reset your password using the link below"); } } Conclusion As you see, it's fairly easy to set an ID on just about anything and then replace it in your webpage with data that comes from processwire. It's kind of in a glued together hack state right now with Form.php in need of some refactoring at 287 lines and another 142 lines in the form generator that handles the json bits, tab handling, etc. So, the stack is the subclass form handler -> Login.php base class -> jsonmangler.php (which may be split soon) -> Hannacode elements. Comments and advice is welcome. I'm not really planning on releasing it, but if there is interest I would certainly release it ... its just kinda in a massive state of flux at the moment if you know what I mean. 3 Link to comment Share on other sites More sharing options...
artfulrobot Posted 5 hours ago Share Posted 5 hours ago Thanks for taking the time to write this up and share, interesting read! I've been using some htmx on a processwire site recently, too, but you've gone much deeper into HTML integration than I did. To be honest I'm still not won over by htmx's "pretty much everything requires a server request" norm as it seems like excessive load and traffic in many use cases. And I did struggle a bit with multi-stage forms where pages may be used in different orders (I basically implemented a state machine flow). I'd be interested to see more of your implementation for interest/learning 🙂 as I'm into the idea of minimal no-build-step JS libs these days. It's also nice to revisit the idea that a site works without javascript, though I can't say I'm achieving this yet. So if you do have public access to view your code, please do drop a link here. 1 1 Link to comment Share on other sites More sharing options...
Recommended Posts
Create an account or sign in to comment
You need to be a member in order to leave a comment
Create an account
Sign up for a new account in our community. It's easy!
Register a new accountSign in
Already have an account? Sign in here.
Sign In Now