Leaderboard
Popular Content
Showing content with the highest reputation on 11/27/2024 in all areas
-
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 points
-
I add to Git site and wire. On wire I have some Git patches that I may apply again after updating to a new version, and each wire version may solve or add bugs, so I add it to version control. So .gitignore files exclude vendor directory and : I need some files in assets, so I exclude everything that is not needed and keep the rest. I also add the database.sql to Git, a clean version ready to be deployed on a new site. Everytime I do changes in admin I export the DB.3 points
-
I add Git in the root folder and can recommend GitKraken as app. 😀 I have a develop and a master branch and an origin remote. Our remote repositories are hosted on Bitbucket. This is my current .gitignore file: Regards, Andreas2 points
-
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.2 points
-
This is an example from PHP documentation. Just saying that I discovered right now that constants are now allowed in traits since 8.2. ^^2 points
-
This module allows you to add media metadata information to FieldtypeFile fields. Under the hood it uses the PHP library by James Heinrich and stores the raw data in the fields' filedata property. Download & Install Github: https://github.com/neuerituale/PagefileMetadata Modules directory: https://processwire.com/modules/pagefile-metadata/ Composer: coming soon1 point
-
Hey @bernhard, I usually use a setup where a production website and a staging website are on the same file system. I love the filesOnDemand feature because it saves a lot of disk space for the local development system and also for the staging system, but it is also very slow on sites with a lot of assets. I think it would be great to be able to specify a local path instead of a URL to a website (or in addition). Ideally, if a local path is specified, then if a file or variation (for images) is not present, filesOnDemand would first search via the specified alternate path to see if the assets are present and deliver them (without downloading or copying) directly. The assets folder of the staging system would therefore only grow when a user creates new pages or makes changes to existing ones, but not when content that exists in the production system (on the same file system) is to be accessed. I assume that filesOnDemand has so far been developed primarily for downloading assets that are only available remotely. But perhaps it could also cover this application purpose - or a new feature with a different name? Cheers, Flo1 point
-
Marked as solved. It might have been an issue of correctly updating RockMigrations. I completely removed the submodule, made a commit, added RockMigrations as a submodule again (main branch, 6.0.1), commited and pushed to Github and now everything is working as expected. At least this might be helpful for somebody running into similar issues.1 point
-
We were experiencing a similar issues because the POST request now goes to / root instead of the current page loaded like before with ./ In our case the console result frame would load the homepage of the frontend instead of the actual results. I solved it for our case (a hook to PageView::execute on /) by returning if it is a tracy request $wire->addHookBefore("ProcessPageView::execute", function (HookEvent $event) { // do not redirect if tracy request if(isset($_POST['tracyConsole']) && $_POST['tracyConsole'] == 1) return; // alternative // if (isset($_SERVER['HTTP_X_TRACY_AJAX']) && $_SERVER['HTTP_X_TRACY_AJAX']) return; if ($_SERVER['REQUEST_URI'] === '/') { ... session()->redirect("{$lang}{$marketName}"); } We need that redirect to determine language and market based on a request to GeoApify API. I think that others might potentially run into this as well. So if the change of xmlhttp.open("POST", "./", true); to root / is not strictly necessary for Tracy to work, it might be better to revert that? @adrian1 point
-
Just use RockMigrations for everything that you add or remove. That's it. Once you push to production RockMigrations will kick in and do everything that you did locally also on the remote system. If you have to add lots of content for whatever reason then just add that on the production site (eg on a new unpublished page) and do a db:pull instead of pushing your local database to the remote.1 point
-
Not sure what you are trying to say, but that's exactly how the new config traits work. I've added a note about PHP8.2 to the docs: https://github.com/baumrock/RockMigrations/commit/4e776f5185ce19866fd853c87a9acec47f4e74851 point
-
Similar here. I init my PW Git repos inside the site folder and to exclude some stuff via .gitignore.1 point
-
Might be easier to tell what I exclude which is: /site/assets /vendor multiple things like "site/modules/ComposerModule" , example of any module that I manage through composer rather than git. .env - I do this to be able to include config.php in version control. On my todo is to try: https://github.com/uiii/processwire to also handle PW core as a composer dependency.1 point
-
I solved it myself by the following code. 1. Create a custom RepeaterPage class for my tracks and add a specific method to create a session-based URL for streaming. <?php namespace ProcessWire; /** * @property Pagefile $audio */ class TracksRepeaterPage extends RepeaterPage { /** * @param string $streamUrl Default stream URL when audio file does not exist * * @return string */ public function getStreamUrl(string $streamUrl = ''): string { // Check whether audio file exists and if it does create a session-based stream URL if ($this->audio) { // Create the file hash and add it to the session as key for the audio file path $audioPath = $this->audio->filename(); $fileHash = hash('sha1', $audioPath . session_id()); session()->setFor('stream', $fileHash, $audioPath); // Create the stream URL with the file hash $streamUrl = '/stream/' . $fileHash . '.mp3'; } return $streamUrl; } } 2. Create a custom hook to watch for the stream URL in the ready.php: <?php namespace ProcessWire; if (!defined("PROCESSWIRE")) { die(); } wire()->addHook('/stream/([\w\d]+).mp3', function ($event) { // Get the audio file path by file hash $fileHash = $event->arguments(1); $audioPath = session()->getFor('stream', $fileHash); // Check whether audio file exists and stream it when it does or throw 404 if (file_exists($audioPath)) { wireSendFile($audioPath, [], [ 'content-transfer-encoding' => 'chunked', 'accept-ranges' => 'bytes', 'cache-control' => 'no-cache' ]); } throw new Wire404Exception(); }); Ta da! 🙂 When the session expires, the session-based file hashes are destroyed and the stream URL no longer work. So every time the session is renewed with a new session ID, a new unique session-based stream URL is generated for each tracks. Have I missed anything or are there any security issues?1 point
-
I'm reading Head First Design Patterns, because a recommendation of @da² in this beautiful post.1 point
-
RockMigrations v5.3.0 Added an automatic Modules::refresh() before executing $rm->installModule() Added GET_PHP_COMMAND for deployments as requested by @snck Added docs for the great filesOnDemand feature1 point