Jump to content

Leaderboard

Popular Content

Showing content with the highest reputation since 03/15/2024 in Posts

  1. In the last couple of weeks I’ve been to several cities in Spain, France and Italy. I’d never been to any of those countries before (or Europe for that matter), so it was an exciting trip. Though the goal was for my kids to broaden their horizons and experience other parts of the world, as well as spend time with my parents and family. We got back this week and have been recovering from jet lag (another thing I’d not experienced before). The 6 hour difference was no problem getting there, but coming back, it’s a little harder to adjust! Next week I turn 50 years old (ugh), and then the following week I’m back in Europe again, except this time in the Netherlands on a bike trip with a client, and without my kids and parents. I’m not sure I’ll be able to do many core updates during the 10 day trip but do expect to have internet access this time, so will at least be online regularly and hope to be here in the forums. After that trip, I won’t be traveling again for a long time, and the focus will be on getting our next main/master version out. I noticed this week that @Robin S is now beating me as our most prolific module developer, with 72 modules! Great job and thanks for all the great modules Robin S.!
    39 points
  2. The traveling over the last month or so is finally finished. In late September/early October my family traveled to Spain, France, and Italy for the first time. And the last couple weeks my wife and I were in Holland on a bike trip where we lived on a boat for a week and biked all over the Netherlands (~150 miles of biking), and got to see a large portion of it. Our forum administrator @Pete was also there, as was Jan, who maintains our website on AWS, so sometimes it felt like a mini ProcessWire meetup too. The trip was one from Tripsite, a company using ProcessWire for more than 15 years, and this trip was their 25th anniversary. There were about 30 other people there as well, several whom also work ProcessWire as editors. It was an amazing trip, and now I'm completely sold on bike and boat trips being the best way to experience a country. I felt like I was a resident rather than a tourist. I’m sorry there have not been a lot of updates here lately due to all of the travel, but now that it’s done, it’s time to get back to work on our next main/master version, which I’m greatly looking forward to. While there have only been 3 commits this week, there have been 25 commits since 3.0.241, so I’m bumping the dev branch version up to 3.0.242, to get the momentum going again. Thanks for reading, and for your patience while I catch up with communications and such, and have a great weekend! Below is a photo of Pete, Jan and Ryan on the boat in Amsterdam.
    38 points
  3. I’m happy to report that today the dev branch has been merged to the main/master branch with our latest version: 3.0.244. This is after about a year on the dev branch. Relative to the previous main/master version (3.0.229) there’s a lot to cover. That’s what I’ll be working on this coming week, writing a new blog post outlining all that’s new and all that’s changed. Like with most ProcessWire versions, it should be an easy upgrade, swapping out the old /wire/ directory for the new one. Thanks for reading and stay tuned for more details next week!
    35 points
  4. I didn't know where the right place to share this was on the forums, so I'll post it here since it may be helpful to those getting started with ProcessWire hooks, or some experienced ProcessWire developers who might find them useful. Either way, dear reader, If someone already wrote it, why write it again? If you're someone with experience, feedback is welcome! If there are better or more efficient ways to do something, I would love being a student. Some of these may either address challenges that others have experienced as well or were inspired by the awesome community sharing their solutions. Kudos to all of the people out there helping all of us. If someone sees something that was solved elsewhere please share it in the comments to give credit. I have to make a disclaimer- these have worked for me and while most of them are ready to copy/paste, a few of them are going to need customization or tweaking to make them work for your use case. I don't currently have the resources (time) to provide a lot of support. Some of these were slightly rewritten or adapted for the examples. If you run into issues, the best thing to do is research the solution so that you know exactly what is happening in your application. If you adapt something, fix a bug, or address an edge case, it would be great if you can come back and share that. Be smart, if you're going to run hooks that modify or create data, run a DB backup first. This is the part where I say "I'm not responsible if your site blows up". I don't think that's possible, but do the right thing. There are dozens of hooks in the project I am sharing these from, and to manage that I created a file structure to handle this because there were far too many to put in one file and keeping the init.php and ready.php files clean really makes a huge difference in maintainability. Being able to jump between files by filename is a supremely efficient way to work as well. The filenames don't matter, they're there to identify the files and make it easy to locate/switch between them. Here's my approach to directory organization: /site - hooks -- HookUtils -- init -- lazy_cron -- ready - init.php - ready.php The ready.php file contents: <?php namespace ProcessWire; if(!defined("PROCESSWIRE")) die(); /** @var ProcessWire $wire */ // Import all ready hooks foreach (glob(__DIR__ . '/hooks/ready/*.php') as $hook) { require_once $hook; } The init.php file contents: <?php namespace ProcessWire; if(!defined("PROCESSWIRE")) die(); /** @var ProcessWire $wire */ // Import all init hooks foreach (glob(__DIR__ . '/hooks/init/*.php') as $hook) { require_once $hook; } // Import all LazyCron hooks, import after init hooks as there may be dependencies foreach (glob(__DIR__ . '/hooks/lazy_cron/*.php') as $hook) { require_once $hook; } Operational Hooks Here are some favorites. Sort items in a Repeater matrix when a page is saved This one helped sort RM items by a date subfield to help the user experience when editing pages. This implementation is configured to only fire on a specific template but can be modified to fire everywhere if modified to check that a field exists on the page being saved first. This was adapted from an answer here in the PW forum but can't find the original post, so I'm going to include it. If you're having issues getting items to sort the way you want, check out this post about natural sorting, which also works elsewhere in ProcessWire. Github Gist Automatically add a new child page when a page with a specific template is created This automatically creates a new child page and saves it when a page having a specific template is created. This also has the ability to show a message to the user in the admin when new page(s) have been created by this hook. It is also error safe by catching any potential exceptions which will show an informative error to the admin user and log the exception message. The messaging/logging operation is abstracted to a separate object to allow reuse if creating multiple pages. Github Gist Conditionally show the user a message while editing a page This one shows a message on a page with a specific template under specific conditions. May be page status, field value, type of user, etc. Visual feedback when editing complex pages can be very helpful, especially when an operation may or may not take place depending on factors like the values of multiple fields. This can reduce the amount of explanations needed on each field or training required for users to use a ProcessWire application. In my case, a message is shown if the state of a page indicates that another operation that is triggered by other hooks will or will not run, which is something that the user doesn't directly trigger or may not be aware of. Github Gist Show the user a message when viewing the page tree This is intended to display a message, warning, or error when the page tree is viewed, such as on login, but in this case executes any time the main page tree is viewed to provide consistent communication and awareness. In my case it displays if there is an activity page located under an "Uncategorized" page for an event. This is something that may be buried in the page hierarchy and not noticeable, but if an activity isn't categorized, then is isn't visible on the website, and if it's not visible on the website, people aren't seeing it or buying tickets. So having a persistent message can bring visibility to important but otherwise potentially unnoticed issues. Or you can just say hi and something nice. Github Gist Hook Enhancement - Fast user switching Hooks can run on triggers that vary widely. Some can and should be identified as those that are triggered by the current user, others may be more autonomous like executing via cron. There may be other hooks that are executed by a user that isn't logged in. Depending on the type of action and your need to identify or track it, switching from the current user to another user created specifically to handle certain tasks can be very helpful. ProcessWire tracks a number of things that are attributed to users- log entries note the user, the user that creates pages is stored, the user that last updated the page is stored, etc. You may want to know who did what when, or only take action if the last user that touched something was X and not Y. I created a separate user that has been provided only the specific permissions it needs to complete jobs that are triggered by hooks or crons. Creating a user with less permissions may also help prevent accidental behaviors, or at least help you be very intentional in determining what actions are delegated. Creating custom permissions is also useful. With a dedicated user I can see explicitly that the last update on some pages were made by an autonomous script that syncs information between the ProcessWire application and a third party platform. Github Gist - Fast user switcher Github Gist - Example of switching users in a hook Fast, powerful, and very (very) easy custom admin buttons I needed a way to add custom interactive buttons that had some specific requirements. Needs to be a button that can be clicked by the user and does something Can be conditionally shown to the user with an alternate message if that action is not available Needs to do something on the server and interact with ProcessWire Here's what that looked like for my application. The green "Refresh Activity" button in the top right. That's a custom button and you don't have to author an Inputfield module to get it. When a user clicks that button, it sends a request to the server with GET variables that are recognized in a hook, actions are taken, then a nice message indicating success or failure is shown to the user. To do this you'll need to install FieldtypeRuntimeOnly and create a new field. Following the documentation for that field, create a button with a URL to the current page with GET variables appended. Then create a hook that watches for the specific GET variable that executes if it's present. Shoutout to @Robin S for helping make short work of a potentially complex task. Note that the field code contains JS that handles the URL on page load. Since the hook is looking for a GET variable in the URL, using the back button or refreshing the page will cause the action to run twice. The JS in that example removes the entry from the browser history and also removes the GET parameter after the page loads if it's present. Github Gist - An example gist for the hook that handles the action Github Gist - An example of the FieldtypeRuntimeOnly code that is displayed and interacted with by the user. Automatically convert logged object or array data to JSON If you're using the outstanding Logs JSON Viewer (yet another great one by @Robin S module, then this hook makes for a thoroughly enjoyable logging experience. Using array or stdClass data when logging your values helps store additional information in an organized way Github Gist <?php $log->save('log_name_here', 'Regular string message'); // Remains a string $log->save('log_name_here', ['gets' => 'converted', 'to' => 'json']); $log->save('log_name_here', (object) ['is' => 'stdClass', 'object' => 'friendly']); Use a separate field to store address data for a FieldtypeMapMarker field This one is really simple, more just sharing an implementation and idea, but proved valuable for reducing data redundancy. I have a FieldtypeMapMarker field but the way that I needed to store address data was much better suited to using multiple fields for things like street, city, state, and zip code. I wanted those fields to be the "controlling" fields for the map marker field to prevent needing to edit 2 fields to keep updated, or accidental content diversion between them. On page save the value from the address fields are pulled and converted into a single string that is added to the FieldtypeMapMarker field's "address" property. I used a Custom Field (ProFields) for my address fields but this can be modified to suit your use case very easily. Github Gist You might also consider hiding the address input on the FieldtypeMapMarker field itself to reduce confusion since the values will be updated automatically anyway. You'll need to have this in a file that is appended to the Admin styles /* You can find the appropriate class for the template you are applying this to in the <body> element when editing a page You can omit that if you want to apply this everywhere */ .ProcessPageEdit-template-your_template_name .InputfieldMapMarker.Inputfield_activity_location .InputfieldMapMarkerAddress, .ProcessPageEdit-template-your_template_name .InputfieldMapMarker.Inputfield_activity_location .InputfieldMapMarkerToggle, .ProcessPageEdit-template-your_template_name .InputfieldMapMarker.Inputfield_activity_location .InputfieldMapMarkerLat, .ProcessPageEdit-template-your_template_name .InputfieldMapMarker.Inputfield_activity_location .InputfieldMapMarkerLng { display: none !important; } <?php // Add this to your ready.php file or ready-firing hook to insert the file containing that CSS to your admin. $config->styles->add("/path/to/your/custom/admin/css/file.css"); Not-A-Hook Bonus - Here's code for an interactive Google Map Renders a Google Map using a FieldtypeMapMarker field, a separate address field, Alpine.js, and Tailwind. You'll need a Google Maps API key, a styled map ID from your Google Developer account, and the aforementioned fields. I wrote it using the latest Google Maps API. Saved you some time. You'll probably need to tweak it. I adapted this so if you find a bug please let me know and I'll update the gist. Note- this makes use of the AlpineJS Intersect plugin to improve performance by only loading/initializing the map when a user scrolls close enough to it. If you don't want that, remove the x-intersect directive. If you want to see it in action, you can check it out here. Github Gist Hook Support Class - A static method class to translate a field into all languages automatically If you use the Fluency translation module, this is a class that will help out with translating a field into all languages programmatically. Sharing this here because the next hook uses this as a dependency. I keep this in the HookUtils directory noted in the file structure above. Usage is demonstrated in the next hook. Github Gist Translate all translatable fields using Fluency on page save whether from UI or API. This is useful for instances where you want a page translated automatically and especially helpful when you are creating pages programmatically. This requires the above hook support class, as well as Fluency connected to an API account. Here are things that must be kept in mind. Please read them, the code for the hook, and the code for the support class to ensure that it works to your needs. You should modify Fluency before using this, really. Change the value of CACHE_EXPIRY on line 19 in the TranslationCache file to WireCache::expireNever. Do this to prevent chewing through your API usage from month to month on repeat translations. This will become standard in the next release of Fluency. This is an expensive operation in terms of API usage, which is why you very much should modify the caching behavior. This hook does not make an effort to determine which fields have changed before translating because it doesn't really matter if the translation is already cached. First time translations of pages with a significant amount of fields/content may be slow, like noticeably slower first time page save because this operation is only as fast as the speed of the request/response loop between ProcessWire and the translation API. Later page saves will be much faster thanks to cached translations. This will not attempt to translate empty fields, so those won't cause any delays. This works with multi-language text/textarea/TinyMCE/CKEditor fields, RepeaterMatrix fields, and the newer Custom Fields (ProFields). Other fields haven't been tested, but it's definitely possible to adapt this to those needs. I prefer to target specific templates with hooks, you can add multiple but be mindful of your use case. Consider adding excluded fields to the array in the hook if it makes sense Consider adding a field to enable/disable translations from the UI, a checkbox field or something This hook is probably one of the uglier ones, sorry. If you run out of API usage on your account, you're going to see a big ugly exception error on screen. This is due to Fluency not handling an account overage error properly because the return type was not as expected. Will be fixed in the next version of the module This is one that may be tailored to my PW application, I think it's general enough to use as-is for your project, but testing is definitely required. Read all the code please. Github Gist ProcessWire Object Method & Property Hooks The following are custom methods that add functionality to native ProcessWire objects. Add a getMatrixChildren() method to RepeaterMatrixPage objects RepeaterMatrix fields represent nesting depth as an integer on each RepeaterMatrixPage item. So top level is 0, first nested level is 1, second 2, etc. When looping through RM items, determining nesting requires working with that integer. It works, but adding adding some functionality helps out. This is infinitely nestable, so accessing children, children of children, children of children of children, and so on works. Fun for the whole family. This was inspired by a forum post, another one I can't find... Github Gist <?php // Access nested RepeaterMatrix items as child PageArray objects $page->repeater_matrix_field->first()->getMatrixChildren(); // => PageArray ?> <!-- Assists with rendering nested RM items in templates Sponsors are nested under sponsorship levels in the RM field --> <div> <?php foreach ($page->sponsors as $sponsorshipLevel): ?> <h2><?=$sponsorshipLevel->title?></h2> <?php if ($sponsorshipLevel->getMatrixChildren()->count()): ?> <ul> <?php foreach ($sponsorshipLevel->getMatrixChildren() as $sponsor): ?> <li> <img src="<?=$sponsor->image->url?>" alt="<?=$sponsor->image->description?>"> <?=$sponsor->title?> </li> <?php endforeach ?> </ul> <?php endif ?> <?php endforeach ?> </div> Add a resizeAspectRatio() method to PageImage objects Adds a simple way to quickly resize an image to a specific aspect ratio. Use cases include sizing images for Google Structured Data and formatting images for consistency in image carousels. Could be improved by accepting second argument to specify an image width, but didn't fit my use case. Github Gist <?php $page->image_field->resizeAspectRatio('square')->url; // Alias for 1:1 $page->image_field->resizeAspectRatio('video')->url; // Alias for 16:9 $page->image_field->resizeAspectRatio('17:10')->url; // Arbitrary values accepted Add a responsiveAttributes() method to PageImage objects Adds a very helpful method to generate image variations and accompanying 'srcset' and 'sizes' attributes for any image. Designed to be very flexible and is Tailwind ready. Responsive sizing can be as simple or complex as your needs require. Includes an optional 'mobile' Tailwind breakpoint that matches a custom tailwind.config.js value: screens: { 'mobile': '320px'}. I added this breakpoint largely to further optimize images for small screens. The array of Tailwind breakpoints and size definitions can be edited to suit your specific setup if there are customizations When sizing for Tailwind, the last media query generated will automatically be switched to "min-width" rather than "max-width" to prevent problems arising from restricting widths. Example, you can specify values only for 'sm' and 'md' and the 'md' size will have the media query correctly adjusted so that it applies to all breakpoints above it. Github Gist <-- The responsiveAttributes() returns a renderable attribute string: srcset="{generated values}" sizes="{generated values}" --> <-- Create responsive images with arbitrary width and height at breakpoints --> <img src="<?=$page->image->url?>" <?=$page->image->responsiveAttributes([ [240, 125, '(max-width: 300px)'], [225, 125, '(max-width: 600px)'], [280, 165, '(max-width: 900px)'], [210, 125, '(max-width: 1200px)'], [260, 155, '(min-width: 1500px)'], ])?> width="240" height="125" alt="<?=$page->image->description?>" > <-- Heights can be selectively ommitted by setting the height value to null --> <img src="<?=$page->image->url?>" <?=$page->image->responsiveAttributes([ [240, 125, '(max-width: 300px)'], [225, null, '(max-width: 600px)'], [280, 165, '(max-width: 900px)'], [210, null, '(max-width: 1200px)'], [260, null, '(min-width: 1500px)'], ])?> width="240" height="125" alt="<?=$page->image->description?>" > <-- Create responsive images with only widths at breakpoints --> <img src="<?=$page->image->url?>" <?=page->image->responsiveAttributes([ [240, '(max-width: 300px)'], [225, '(max-width: 600px)'], [280, '(max-width: 900px)'], [210, '(max-width: 1200px)'], [260, '(min-width: 1500px)'], ])?> width="240" height="125" alt="<?=$page->image->description?>" > <-- Create custom sizes matched to Tailwind breakpoints --> <img src="<?=$page->image->url?>" <?=$page->image->responsiveAttributes([ 'mobile' => [240, 125], // Custom tailwind directive 'sm' => [225, 125], 'md' => [280, 165], 'lg' => [210, 125], 'xl' => [260, 155], ])?> width="240" height="125" alt="<?=$page->image->description?>" > <!-- Resizes width of image to fit Tailwind breakpoints, useful for full width images such as hero images, doesn't change height. Also accepts 'tw' as an alias for 'tailwind' --> <img src="<?=$page->image->url?>" <?=$page->image->responsiveAttributes('tailwind')?> width="240" height="125" alt="<?=$page->image->description?>" > Add PHP higher-order function methods to WireArray and WireArray derived objects WireArray objects are incredibly powerful and have tons of utility, but there are situations where I find myself needing to work with plain PHP arrays. I'm a very big fan of PHP's array functions that are efficient and make for clean readable code. I found myself often reaching for $wireArrayThing->getArray() to work with data then using functions like array_map, array_filter, and array_reduce. These return arrays, but could easily be modified to return WireArray objects if that is more helpful. Github Gist <?php // The EventPage page class has a method that determines sold out status from more than one source of data/page fields // which means that it isn't queryable using a ProcessWire selector. This returns a single integer calculated from ticket availability // of all events from non-queryable data. $totalEventsAvailable = $eventPages->reduce( fn ($total, $eventPage) => $count = $eventPage->isActive() ? $total++ : $total, 0 ); // Requires using a page class to determine status reliant on multiple data points not queryable via a selector. Knowing what the event // page is for an activity can't be determined using a selector for activity pages. $displayableActivities = $matches->filterToArray( fn ($activityPage) => $activityPage->eventPage()->isPublic() && $activityPage->isActive() ); // Iterating over each Page in a PageArray and processing data for sorting/ordering before rendering on a search results page // Executed within a page class $results = $searchResults->mapToArray(function($page) { return (object) [ 'page' => $page, 'summary' => $this->createResultSummary(page: $page, maxLength: 750), 'keywordMatchCount' => $this->getQueryMatchCount(page: $page), ]; }); Add an image orientation method/property to PageImage objects Get the portrait or landscape orientation of a PageImage. Github Gist <?php $page->image->orientation; $page->image->orientation(); Add the ability to get all related pages for Page objects at once Gets all of the related pages to a page at once by both page reference fields and links in fields. Transparently passes native arguments to Page methods for native behavior Github Gist <?php $page->allPageReferences(); $page->allPageReferences(true); // Optionally include all pages regardless of status $page->allPageReferences('your_selector=here', 'field_name'); // Use with native Page::references() and Page::links() arguments Add a saveWithoutHooks() convenience method to Page objects The number of hooks in my most recent project was... a lot. There were many that hooked into the page save event and a lot of operations that happen in the background where pages needed to be modified and saved quietly to prevent clearing ProCache files or excessive DB operations through chained hooks. Being able to use a method to do this rather than passing options felt more deliberate and clear when working across hundreds of files and in critical areas of very expensive operations. This method also accepts page save options, but in a way that hooks will always be disabled even if an option is accidentally passed enabling them. Furthermore, it also accepts a string as the first argument that, if debug mode is enabled, will dump a message to the bar via Tracy. Github Gist <?php // Adding a message can be very helpful during testing, especially when saving a page with/without hooks is conditionally based // where the result of another operation determines how a page is saved $page->saveWithoutHooks('updated event sync data hash, saved without hooks'); $page->saveWithoutHooks(['resetTrackChanges' => true]); $page->saveWithoutHooks('message and options', ['resetTrackChanges' => true]); These are a few that I've used to show some diversity in application. Hooking to ProcessWire events makes it possible to build beyond simple websites and implement truly custom behavior. Hope these may be useful to others. If you have any favorite hooks of your own, have corrections of those I've shared, or improvements, sharing them in the comments would be stellar. Cheers!
    33 points
  5. My pleasure. The hooks and inputfields APIs in ProcessWire make module development an absolute dream. It's very satisfying seeing the results you can get with just a little code, and working on modules provides a motivation to dig into the core code so I learn a lot in the process. Also, another shout out to @adrian's amazing Tracy Debugger, which has been transformative for my coding. I have several more modules in progress so watch this space. 🙂
    30 points
  6. Fridays are always the days when I'm full-time on ProcessWire. Other days I may be doing client work, or ProcessWire, just depending on the week. This week it's been mostly client work. And I just learned that I'll have to be out of the office tomorrow (Friday) for a family commitment. So I'm writing this weekly update today instead, and just sent out Teppo's great ProcessWire Weekly newsletter with ProMailer today (usually it gets sent Friday). Because of this change in schedule, I don't have much new to report just yet. Instead, I wanted to start talking a little about future plans, so here's a few ideas. I think we should get another main/master version out, perhaps by September. Following that, I thought we should focus on ProcessWire 3.1, or maybe it's time for version 4. What would be in this next major version of PW? For starters, we'll finally drop support for older PHP versions and start taking advantage of all that's been added new newer PHP versions (8+). This will involve lots of updates to the ProcessWire core code base, while remaining 100% API compatible with PW 3.x. I thought that would be a good starting point into our next major version at least. In addition, we'll likely be trimming some things out of the core and into separate non-core modules, such as FileCompiler, CKEditor, the two legacy admin themes, and a few rarely used Textformatter modules. Most likely we'll also have an overhaul of this website and some nice improvements to our primary (Uikit) admin theme to accompany the next major version as well. There will be plenty more too, but this is what I've been thinking about so far. Your feedback is welcome. Thanks for reading and have a great weekend!
    29 points
  7. Page classes are an outstanding feature of ProcessWire and probably ranks among my favorites overall (and that's really saying something with ProcessWire). I don't remember how I lived without them, well I do, but I don't like to think about it. I wrote a response to a question asking how ProcessWire can help transition from procedural code to OOP and in the process (pardon the pun) of answering, I realized how much I've come to use, if not outright rely on, page classes in every project. I wanted to compile a few more thoughts and examples here because there may be devs who are finding this feature for the first time, and besides, why not go down the rabbit hole? Page classes have been around since ProcessWire 3.0.152 and if you're not familiar this will all make a lot more sense if you do a quick read of the feature announcement by Ryan here because this post assumes familiarity. We're also going to pick up the pace between the examples below compared to my comment in the link above. Here are a few goals that I have that page classes put solutions for within reach. You may have some of your own and I'd love to hear about those as well. Keep templates clean by restricting logic to flow operations- if statements and loops. Work with data in context, accessing data and fields closer to their source. Create "universal" methods available in every template, but also have scoped methods per-template Increase the scalability of a project, growth with limited increases in complexity. Stay DRY Embrace and extend the power of OOP in ProcessWire's DNA This seems like quite a list for one feature to handle, but rest assured, this is an example of how much power comes with using page classes. I'll continue and build on the blog example from my linked response above, and you can "follow along" with the Blog module since the templates mentioned are present out of the box. Some of the examples below are taken from production code, but please excuse any errors that I may have introduced by accident and I'm happy to update this post with corrections. Some of these things can be done other ways but they're just to illustrate, replace with your ideas and think of times that this would be useful. First up, what are some features and behaviors that everyone needs on every project? What are some universal methods that would be great to have everywhere? Let's start out by creating a "base" page class called DefaultPage. Going forward, page classes will extend this class instead of Page and benefit from having access to universal methods and properties. EDIT: I initially wrote this using BasePage rather than DefaultPage for this class name as described by the custom page class writeup by Ryan in the article above. I've changed this to now use the correct DefaultPage class name. <?php namespace ProcessWire; // /site/classes/DefaultPage.php class DefaultPage extends Page { /** * Returns all top level pages for the main navigation. */ public function navigationPages(): PageArray { $pages = wire('pages'); $selector = 'parent=1'; $excludedIds = $pages->get('template=website_settings') ->nav_main_excluded_pages ->explode('id'); // Check for excluded pages from nav defined in the "Website Settings" page count($excludedIds) && $selector .= ',id!=' . implode('|', $excludedIds); $homePage = $pages->get('id=1'); $topLevelPages = $pages->find($selector); $topLevelPages->prepend($homePage); return $topLevelPages; } } All your base page are belong to us. Right off the bat we've managed to pull complex logic usually in templates and kept our markup clean. This isn't for DRY, it's to store logic out of templates. This simple example only illustrates working with a top level page nav. We can start to appreciate the simplicity when considering how much more the navigation may call for in the future. A navigationChildren method that also accounts for excluded pages is a prime example. In our markup: <!-- /site/templates/components/site_nav.php --> <nav> <ul> <?php foreach ($page->navigationPages() as $navPage): ?> <li> <a href="<?= $navPage->url; ?>"><?= $navPage->title; ?></a> </li> <?php endforeach ?> </ul> </nav> Next up, our settings page has newsletter signup fields where users can copy/paste form embed code from Mailchimp. Our website has a "settings" page where global values and fields can be edited and maintained There are multiple textarea fields on the settings page that can each contain a different mailchimp embed code There is an embed select field that can be added to templates. The values are textarea field names so an embed can be chosen by the user. An embed select field has been added to the settings page which allows for choosing a default embed code if one isn't selected when editing a page Embedded forms should be available anywhere on any template <?php namespace ProcessWire; // /site/classes/DefaultPage.php class DefaultPage extends Page { // ...Other DefaultPage methods /** * Renders either a selected form embed, fallback to default selected form */ public function renderEmailSignup(): ?string { $settingsPage = wire('pages')->get('website_settings'); $embedField = $this->signup_embed_select ?: $settingsPage->default_email_embed; return $settingsPage->$embedField; } } Excellent. We've got some solid logic that otherwise wouldn't have a great home to live in without page classes. We can also modify this one method if there are additional options or complexity added to the admin pages in the future. Wherever we use renderEmailSignup, the return value will be either the selected or default form embed code. Let's create a folder called "components" in our templates directory that will hold standalone reusable markup we can use wherever needed, here's our newsletter signup component: <!-- /site/templates/components/newsletter_signup.php --> <div class="newsletter-signup"> <?= $page->renderEmailSignup(); ?> </div> Great! We've kept logic out of our code. We can render the field, and we can account for an empty value with a fallback. Unfortunately this is pretty limited in that it only handles a specific field, and it's implementation isn't as flexible as a component should be. We can fix that, and here are some new requirements to boot: Some templates may have multiple embed select fields which may or may not have a value Each embed select field is now paired up with a text field to add some content that appears with each form embed, think "Sign up for our newsletter today!" We want to render our mailchimp form embeds using the same component file, but handle different fields Sounds like a tall order, but it's a challenge easily overcome with page classes. We're going to introduce a new method called renderComponent that will be global, reusable, and very flexible. We're also going to make use of another very great ProcessWire feature to do it. Back to our DefaultPage class: <?php namespace ProcessWire; // /site/classes/DefaultPage.php class DefaultPage extends Page { // ...Other DefaultPage methods /** * Renders a file located in /site/templates/components/ with optional variables */ final public function renderComponent(string $file, ...$variables): string { $componentsPath = wire('config')->paths->templates . 'components/'; return wire('files')->render($file, $variables, ['defaultPath' => $componentsPath]); } /** * Renders either a selected form embed, fallback to default selected form */ public function renderEmailSignup(?string $embedSelectField = null): ?string { $settingsPage = wire('pages')->get('website_settings'); $embedField = $this->$embedSelectField ?: $settingsPage->default_email_embed; return $settingsPage->$embedField; } } We've done a couple of things here. We've updated renderEmailSignup to accept a field name, so now we're flexible on exactly which field select we'd like to check for a value on before falling back to default. We've also created a renderComponent method that is going to be super useful throughout the rest of our ProcessWire application. Our renderComponent receives a file name located in the components directory and any number of named parameters. You could change the $variables parameter to an array if you'd like, but I'm a big fan of the great features we have now in PHP 8+. Here's our refactored component file: <!-- /site/templates/components/newsletter_signup.php --> <?php if ($embedField): ?> <div class="newsletter-signup"> <?php if ($text): ?> <h2><?= $text; ?></h2> <?php endif ?> <?= $page->renderEmailSignup($embedField); ?> </div> <?php endif ?> And let's hop over to our (abbreviated) home page template: <body> <!-- ...Sections full of great content --> <section class="signup-call-to-action"> <?= $page->renderComponent('newsletter_signup.php', embedField: $page->embed_select, text: $page->text_1); ?> </section> <!-- ...Your awesome template design --> <section class="page-end-call-to-action"> <?= $page->renderComponent('newsletter_signup.php', embedField: $page->embed_select_2, text: $page->text_2); ?> </section> <!-- Your footer here --> </body> I don't know about you, but this is looking really good to me. The number of things we've accomplished while having written so little code is remarkable: Because we used wire('files')->render(), the entire ProcessWire API is available within the component, so now our renderEmailSignup method is too. The variadic function parameters (or array if preferred) let us pass an arbitrary number of variables to any component file, unrestricted future flexibility Variables are scoped to each component! There's no reference to template fields in our component that could break if changes are made No more PHP includes, we don't have to juggle paths or constantly repeat them in our code, nor rely on declaring variables before including a file. ProcessWire will throw an exception if we try to render a file that does not exist which makes locating issues very easy We'll also see an exception if we try to reference a variable in our component that wasn't passed which can also help troubleshooting. Notice that the renderComponent is final. We want that behavior to remain consistent everywhere we use it and not overwritten either intentionally or by accident on our inheriting page classes. We want to eliminate any confusion between templates by knowing it will always do the same thing the same way. We can explore other uses too, perhaps a renderPartial for files in /site/templates/partials where we store files like site_header.php. As mentioned above however, if a variable is expected in the rendered file but not included in our render method, we'll see an exception. Let's use site_header.php as an example because we're sure to run into situations where variables may or may not exist: <?php namespace ProcessWire; // /site/templates/partials/site_header.php ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title><?= $page->title; ?></title> <meta name="description" content="<?= $metaDescription ?? null; ?>"> <?php if ($includeAnalytics ?? true): ?> <script> // All that Google Analytics jazz </script> <?php endif ?> </head> <body class="<?= $bodyClasses ?? null; ?>"> <header> <?= $page->renderComponent('site_nav.php', includeEmailButton: true); ?> </header> Problem: solved. By using nullsafe ?? operators, we can call $page->renderPartial('site_header', description: 'The ultimate Spice Girls fan page.'); and never get errors for variables that may not be included when calling renderComponent, such as includeAnalytics, which now also has a default value of 'true'. Nice. We haven't even gotten to our actual page classes yet... Our templates are about to receive superpowers. Let's take our blog to the next level. In my comment on the other thread, I created a specific example of adding a readTime method to our blog posts, let's go one level higher to our blog.php template. We'll populate some methods up front and then talk about what we've done: <?php namespace ProcessWire; // /site/classes/BlogPage.php class BlogPage extends DefaultPage { /** * Get latest blog posts, optionally with/without pinned post, optionally in blog category */ public function latestPosts( int $limit = 3, bool $includePinnedPost = true, ?int $categoryId = null ): PageArray { $selector = 'template=blog-post'; if ($categoryId) { $selector .= ",blog_category={$categoryId}"; } $posts = wire('pages')->get($selector); if ($includePinnedPost) { $pinnedPost = $this->getPinnedPost(); if ($pinnedPost) { $posts->remove($pinnedPost); $posts->prepend($pinnedPost); } } return $posts->slice(0, $limit); } /** * Gets an optional pinned post if set/chosen * pin_blog_post A checkbox to indicate whether a post should be pinned * pinned_post A InputfieldPage field to choose a blog post */ public function getPinnedPost(): ?Page { if (!$this->pin_blog_post) { return null; } return wire('pages')->get($this->pinned_post); } } On our main blog page a user has the ability to choose whether a blog post is "pinned". A pinned post will always remain the first post anywhere a list of posts is needed, something like a big company announcement that the client wants to keep visible. These two methods alone have given us awesome abilities. For our main blog page, when someone visits our blog page, the most recent or pinned post is presented at the top, followed by the next two most recent posts, followed by two rows of 3 posts, for a total of 9. Let's assume that we've already created the BlogPostPage.php with the readTime method from my previous example. Here's our blog.php template <?php namespace ProcessWire; echo $page->renderPartial( 'site_header.php', bodyClasses: 'blog-page', metaDescription: $page->blog_description ); $posts = $page->latestPosts(9); $firstPost = $blogPosts->first(); ?> <section class="main-post"> <article> <img src="<?= $firstPost->blog_image->url; ?>" alt="<?= $firstPost->blog_image->description; ?>"> <h1><?= $firstPost->title; ?></h1> <?= $firstPost->summary; ?> <span><?= $firstPost->readTime(); ?></span> <a href="<?= $firstPost->url; ?>">Read More</a> </article> </section> <section class="recent-Posts"> <?= $page->renderComponent('blog_preview_card.php', blogPost: $posts->get(1)); ?> <?= $page->renderComponent('blog_preview_card.php', blogPost: $posts->get(2)); ?> </section> <section class="past-posts"> <?php foreach ($posts->slice(3, 6) as $post): ?> <?= $page->renderComponent('blog-preview_card.php', blogPost: $post); ?> <?php endforeach ?> </section> <?= $page->renderPartial('site_footer.php'); ?> So first off, we've really started to use our renderComponents method and component files. We also implemented a renderPartial as speculated upon above. Each does a similar thing, but having separate methods makes everything clear, handles paths, but still has a similar interface when calling them. A big thing to notice here is that at no point have we added any markup to our page classes, and no business logic to our templates. If we need to find anything, we know where to look just by glancing at the template. Ultimate maintainability. Here's our blog_preview_card.php component: <?php namespace ProcessWire; ?> <!-- /site/templates/components/blog_preview_card.php --> <article class="blog-preview-card card"> <img src="<?= $blogPost->blog_image->url; ?>" alt="<?= $blogPost->blog_image->description; ?>"> <h2><?= $blogPost->title; ?></h2> <div class="blog-summary"> <?= $blogPost->blog_summary; ?> </div> <span class="blog-read-time"><?= $blogPost->readTime(); ?></span> <a href="<?= $blogPost->url; ?>">Read More</a> </article> I am liking how well this is working out! Page classes have done a ton of heavy lifting here: We're using our renderComponent method to it's maximum potential and it's payed off in spades Our template couldn't be cleaner or more easily maintainable BlogPostPage.php has taken care of all of our needs as far as delivering the PageArray of posts and all our template does is output the data as needed Our "card" component will render the same thing everywhere and we can update how that looks globally with changes to one file If you don't think this can get more awesome, or think this post is already too long, I have bad news for you and you should stop reading now. Still here? Let's create a BlogPostPage class and add a method: <?php namespace ProcessWire; // /site/classes/BlogPostPage.php class BlogPostPage extends DefaultPage { // ... Other page methods, like readTime() public function relatedPosts(int $limit = 3): PageArray { return wire('pages')->get('template=blog')->latestPosts( limit: $limit, includePinnedPost: false, categoryId: $this->blog_category?->id ); } } The BlogPage::latestPosts method is really flexing it's muscle here. We've used it in two different places for different purposes but requesting similar data, blog posts. If you noticed, we're also specifying a category for this blog since we have a page select field that references a blog category. That was a parameter that we included back in our BlogPage::latestPosts() method. So, blog posts have a "You might also be interested in..." section with posts in the same category as the that one that a visitor has just read. With page classes this couldn't be easier to add, so let's use the relatedPosts method we just created in BlogPostPage.php in our blog-post.php template: <?php namespace ProcessWire; // /site/templates/blog-post.php echo $page->renderPartial( 'site_header.php', bodyClasses: 'blog-post', metaDescription: $sanitizer->truncate($page->blog_summary, 160) ); ?> <!-- The page hero, blog content, stuff, etc. --> <section class="related-posts"> <?php foreach ($page->relatedPosts() as $post): ?> <?= $page->renderComponent('blog_preview_card.php', blogPost: $post); ?> <?php endforeach ?> </section> <?= $page->renderPartial('site_footer.php'); ?> We've just added a related posts feature in *checks stopwatch* seconds. This project is going so fast that you can already hear the crack of the cold beer after a job well done. One more example, and I'll let you decide if this is too much, or feels just right. We want a blog feed on our home page. So before we go to the home.php template, let's do what we do (surprise, it's a page class): <?php namespace ProcessWire; // /site/classes/HomePage.php class HomePage extends DefaultPage { // Other HomePage methods... public function blogFeed(int $limit = 3): PageArray { return wire('pages')->get('template=blog')->latestPosts(limit: $limit); } } At first glance, this might seem like a bit much. Why create a method in our page class that is so short and simple? We could just call $pages->get('template=blog')->latestPosts(3) inside our template and get away with it just fine. Here's why I think it's worth creating a dedicated page class method: This creates yet another example of predictability between templates It promotes thie philosophy I like of page classes talking to page classes It's likely that we'll have other more complex methods in the HomePage class, keeping them together feels right and helps with uniformity- we always know where to look when seeing custom methods If we need to make a change to the BlogPage::latestPosts() method that affect how other page classes call that method, we don't have to root around in our templates to make any changes. It's pretty nice to see that thus far we have only ever referenced the $page object in our templates! There aren't any calls to $pages because our template is really about doing one thing- rendering the current page without caring about other pages. That's not to say that we can't, or shouldn't, use other ProcessWire objects in our templates, but it's still impressive how much we've been able to scope the data that we're working with in each template. I truly love the page classes feature in ProcessWire and even though this is a deeper dive than my other post, this really is still just scratching the surface because we can imagine having more complex behavior and other benefits. Here are some from my experience: Handling AJAX calls to the same page. Choosing how and what type of content is returned when the page is loaded is really nice. This can be done with hooks, but I really like page classes handling things like this in-context. Working with custom sessions between one or more pages. Creating a trait that shares session behavior between page classes is a great way to extend functionality while keeping it scoped. Adding external API calls and whatever complexity that may entail Provides the ability to add significant complexity to our templates in the admin yet little to no additional complexity in template files. The basic "pinned post" and form embed features are just the start As you can imagine, this makes replicating code you use often between projects trivial Page classes provide a home for logic that otherwise would be stuck in a template, or relegated to a messy functions.php file. No more functions.php ever. We met every single goal I listed here at the beginning Every single template thus far only uses if statements and loops DefaultPage could have a "bootApplication" method if needed that does things for every page and is called in ready.php. This is also a great way to create a "bootable" method in all of your page classes where bootApplication could call a bootPage method in your page classes if it exists. Thanks for coming to my TED talk. Hope you find this useful! If you have any questions, corrections, use cases, or things to add, the comments below are open and I'd love to hear them!
    29 points
  8. With the current project that I'm working on, there's eventually going to be an enormous amount of data to edit within it (in ProcessWire), and so I've been looking for ways to optimize and facilitate the editing experience. The goal is to save the people editing as much time as possible, and reduce the number of steps necessary to make common edits. This is what motivated the recent Table field updates. The is also what motivated a new module I'm working on called PageEditChildren. The PageEditChildren module takes the existing "Children" tab in ProcessWire's page editor and replaces it with a new one that lets you edit all of the child pages inline, directly from the parent page. For specific cases, such as the one shown in this video (below), this provides a more convenient way to edit multiple pages at once. It can significantly reduce the amount of back-and-forth between the page editor and the page list, or multiple page editors, keeping it all in one page editor session. Likely you would use this module to replace the "Children" tab on just some (not all) pages. Specifically, pages where the relationship between parent and children is one where they are often edited as a group. This is a fairly common use case in ProcessWire, and one where this module can save you a lot of time. From an editing standpoint, it has a lot in common with repeaters. Other modules that have some crossover are PageTable (core) when configured to create/edit children pages, and BatchChildEditor. Both use page editors in modal windows to accomplish editing children, a different approach than PageEditChildren, which keeps it all part of the single page editor. Though BatchChildEditor can edit title and status (hidden/unpublished) inline, and supports CSV export/import options, among other useful tools. All of these modules are worth consideration when there is a need to optimize the editing experience between parent and children. This is just an early preview of PageEditChildren, so I'm going to work on the module and test it out a bit more before releasing it. But it could be ready to share as soon as next week. Supporting file/image and repeater fields in this module required some minor core improvements, which are in this week's core updates. I'm interested to hear your thoughts on whether this would be useful in your sites as well? More next week. Thanks for reading and have a great weekend!
    28 points
  9. The plan was to merge dev to the main branch today, but I’m still working through a couple of GitHub issues that I’d like to resolve or finish the conversation before finalizing the 3.0.244 main version. One example is this issue report where it was pointed out that there are some issues with UTF-8 page names. ProcessWire uses PHP’s IDN functions to manage conversion to and from the non UTF-8 version of the URL. PHP 7.4 changed the default arguments of the idn_* functions to settings that made them not work 100% for page names in a few cases, which I didn’t realize before this week. But this is not an issue that I can just fix and be done with it… There may already be page names in any given installation that are affected by the post PHP 7.4 behavior. If I were to just fix the issue, then some affected pages might no longer match when accessed directly by URL. So this had to be a carefully considered fix. What I ended up doing is fix it for any new PW installations that occur after this weekend. PW keeps track of its installation date, so can do this by way of its $config->installedAfter(“2025-01-05”) function. Existing installations will keep the imperfect behavior. Presumably it doesn’t affect that many installations since it only came to light last week and PHP 7.4 was released roughly 5 years ago. Nevertheless, existing installations that want the “fixed” behavior can get it by specifying this in /site/config.php: $config->pageNameWhitelist = 'v3' . $config->pageNameWhitelist; That essentially says to use version 3 of the page name conversion. Version 2 will also work fine, but may be slightly slower since it uses a dedicated Punycode library. And version 1 is the one that worked correctly until PHP 7.4, and still works mosts most of the time, but can produce imperfect results in some cases. Installations prior to 5 Jan 2025 use v1 by default and installations after 5 Jan 2025 use v3 by default. Chances are few (if any) will want to specify the version manually like above, but the option is there, just in case. That’s one example of why I’m waiting another few days before the dev branch merge to main. Another is that Adrian mentioned double-clicking on the “Move to Trash” button [in the page editor] makes it permanently delete the page rather than trash it. While I can’t duplicate that, despite multiple attempts, I just want to make sure there’s not something that needs fixing there. But unless any major new issues turn up, by this time next week we’ll merge to main and bump the version to 3.0.244. Thanks and Happy New Year!
    28 points
  10. This week the dev branch version has been bumped up to 3.0.243. Relative to the previous, this has 30 commits, including a lot of minor issue fixes. The plan is to release the next main/master version of ProcessWire on or before New Years day. We’re down to very few new issues being reported, and even fewer resulting in code changes on the dev branch, which is a good sign the new version is ready, or very close to it. This week while working on a site I realized that the $config->maxUrlSegments setting was not working, and I don’t think it has since the PagesRequest and PagePathFinder classes were introduced into the core. So I fixed that, while also updating some of the logic around it, and adding a new $config->longUrlResponse setting that lets you specify how it should respond when it gets too many URL segments, too long of a URL, too much path depth, etc. Next week I’ll be working on updating materials related to the new version (README file, etc.) and keeping an eye out for any newly other reported issues. Thanks for reading and Merry Christmas / Happy Holidays!
    27 points
  11. There have been a few small bug fixes made to the core this week on the dev branch. I think we’re really close on the new version and hope to have that ready around the upcoming holidays. The module I mentioned last week (InputfieldSmallSelectMultiple) also got a new version this week with support for optgroups, support for “1 of 3 selected” type labels (Adrian’s suggestion) and a couple small fixes. Some fun news: The ProcessWire site is currently going through a redesign. This time there are a couple of professional designers and long time ProcessWire users collaborating on the design. I won’t mention who’s working on this just yet, as I’ve not asked them if I could, so want to respect their privacy. But I’ve been able to see some of the work in progress, and it’s really fantastic, a major upgrade for the look of the site. Along with this will be some visual improvements to the admin theme as well, which will accompany or come after the site redesign. There’s no launch date for the site redesign just yet, I wanted to let you know it was in progress and looking great, something to look forward to for sure. Thanks and have a great weekend!
    27 points
  12. Work continues on the new processwire.com website. I’ve nearly finished developing most of the modules directory this week and next week will be working on the development side of the API reference and sites directory. Some more good news to share is that when the new site launches, the new admin look and feel will launch as well. The website and admin share a similar design language in some areas, and I’m confident you will love them both. When we use screenshots of ProcessWire in the new site design, they will be from the new admin look and feel. It is still admin AdminThemeUikit, but with a new face that is beautiful, modern and professionally designed. I’ve been using for more than a week and it’s fantastic in my opinion. If for some reason you end up wanting to keep the current look of AdminThemeUikit (perhaps a client doesn’t like change), it will remain as an option too. If you are extending AdminThemeUikit or using the admin.less feature (developed by Bernhard) to custom style the admin, all of that will continue working too. What will likely be changing is that we’ll be moving the older AdminThemeDefault and AdminThemeReno out of the core and into the modules directory. I’d rather keep the core efforts focused with AdminThemeUikit, but continue to support the older admin themes as installable options. Prior to this, most of what you’d seen in ProcessWire’s core admin and website has been designed by me (excluding AdminThemeReno). And I haven’t worked full time as a designer since 2005 or so. If I ever had any site design skills, they are long gone. So PW has always had a “designed by a developer” look. Having professional designers take over the design of both the admin and the website just feels like a major upgrade to ProcessWire all around. More than I could have guessed. I look forward to when I can share the new site design, admin look and feel, and the designers with you. Thanks for reading and have a great weekend!
    26 points
  13. Hey all! This is a module to enhance forms built using the Pro FormBuilder module by providing the ability to submit them in place using AJAX and HTMX. FormBuilderHtmx works in harmony with FormBuilder by handling front-end rendering and AJAX and lets FormBuilder manage form configuration and processing. FormBuilderHtmx provides a drop-in replacement for the $forms->render() method and provides all native features and behavior (and adds a few extra superpowers to boot). Noteworthy features: Zero configuration, install and render AJAX powered FormBuilder forms immediately Render multiple forms on the same page. Supports both multiple instances of the same form or different forms. Each form is processed independently. Non-intrusive, can be used alongside FormBuilder's native rendering methods and does not modify core module behavior Perfect for forms embedded in popups and modals Does not conflict with styling and other JavaScript already in-place, only handles the form submission/response loop Automatically disables the `Submit` button on submission to prevent duplicate requests Provides the ability to add a custom 'spinner' shown when a form is being processed Gives you the ability to add additional HTML attributes to your FormBuilder <form> element. Add additional custom functionality using HTMX attributes, hook into form actions with your JavaScript, or even add AlpineJS directly to your forms. Compatible with FieldtypeFormSelect, let users choose which forms to embed, your code determines how they are rendered Uses HTMX, a stable, powerful, and tiny (14kb gzipped) library, installation documentation available here This module is BYOH (Bring Your Own HTMX) in that the HTMX library is not included or available within this module. This ensures long-term stability by not locking FormBuilderHtmx to external asset versioning. FormBuilderHtmx uses stable core HTMX features so use the library version that works for you and confidently add this module to both new, existing, and future ProcessWire applications. In some instances CSRF protection may need to be disabled to submit forms with this module. Just test your forms and you're good to go. Using this module is truly easy. <!-- Replace the native $forms->render() method with $htmxForms->render() --> <?php $htmxForm = $htmxForms->render('your_form_name') ?> <!-- Use native ProcessWire properties and methods as usual --> <?php echo $htmxForm->styles; echo $htmxForm->scripts; echo $htmxForm; ?> Presto. You can optionally include a helpful 'spinner' or activity animation that will be showed to users while their form request is being processed. Check out these ready-to-go examples you can use in your projects. <style> /* Add these styles to your CSS, the `.htmx-request` must be present as shown here. Be sure to include any CSS your 'spinner' may need, and style everything as desired */ .activity-indicator { display: none; } .htmx-request .activity-indicator, .htmx-request.activity-indicator { display: block; } </style> <!-- Optional second argument matches that of the $forms->render() method for pre-populated values The third argument is the CSS selector matching your 'spinner' element --> <?= $htmxForms->render('your_form_name', [], '#indicator-for-the-form') ?> <div id="indicator-for-the-form" class="activity-indicator"> <span class="spinner"></span> </div> Presto (again) Check out the documentation for detailed usage and other available features. Pull requests and issues filed on Github are welcome, or drop by here to get some help! Install as a ProcessWire module Install using Composer Download from the FormBuilderHtmx Github repository . Cheers!
    26 points
  14. This week I've bumped the dev branch version to 3.0.241. Relative to the previous version, this one has 29 commits with a mixture of issue resolutions, new features and improvements, and other minor updates. A couple of PRs were also added today as well. This week I've also continued work on the FieldtypeCustom module that I mentioned last week. There were some questions about its storage model and whether you could query its properties from $pages->find() selectors (the answer is yes). Since the properties in a custom field are not fixed, and can change according to your own code and runtime logic, it doesn't map to a traditional DB table-and-column structure. That's not ideal when it comes to query-ability. But thankfully MySQL (5.7.8 and newer) supports a JSON column type and has the ability to match properties in JSON columns in a manner similar to how it can match them in traditional DB columns. Though the actual MySQL syntax to do it is a little cryptic, but thankfully we have ProcessWire selectors to make it simple. (It works the same as querying any other kind of field with subfields). MySQL can also support this with JSON encoded in a more traditional TEXT column with some reduced efficiency, though with the added benefit of supporting a FULLTEXT index. (Whereas the JSON column type does not support that type of index). For this reason, FieldtypeCustom supports both JSON and TEXT/MEDIUMTEXT/LONGTEXT column types. So you can choose whether you want to maximize the efficiency of column-level queries, or add the ability to perform text matching on all columns at once with a fulltext index. While I'm certain it's not as efficient as having separate columns in a table, I have been successfully using the same solution in the last few versions of FormBuilder (entries), and have found it works quite well. More soon. Thanks for reading and have a great weekend!
    26 points
  15. This week a need came up in a client project. The client's team wanted to be able to navigate to their tours (pages) in the admin page-list by different criteria (operator, brand, boat, etc.). You can do this in Lister already (filtering by page references), but the client was looking for some predefined navigation paths in the main page list, as they thought this would be a helpful and time saving optimization, as they spend a lot of time in ProcessWire. They don't always know the exact tour at first, so starting from an operator, brand or boat helps them get to where they want to go more quickly. Once implemented, I thought it was actually quite useful for a lot of situations so decided to develop it into a module on my own time, and that's now available for download in the modules directory. I've published a new blog post that describes it more and covers all the details— https://processwire.com/blog/posts/page-list-custom-children-module/
    26 points
  16. This week we've bumped the ProcessWire dev branch version to 3.0.239. Relative to the previous version, this includes 15 commits. This version is required if you decide to test out the new PageEditChildren module, which has also been released in the ProFields board today. I'm not sure it'll remain in the ProFields set of modules, as it's not even a field. But after spending multiple days updating ProFields modules to be compatible with it (also posted today), I thought it was best to keep them together for the short term. It's easier for me to support that way at least. Longer term, it may even become a core module. There's also a couple other modules in ProFields right now that I'm planning to move into the modules directory and GitHub. These most likely include TextBlocks, AutoLinks, and potentially FunctionalFields. These modules have not required much in terms of support resources in awhile, so may not need to be part of ProFields any longer. Thanks for reading and have a great weekend!
    26 points
  17. Welcome to the ProcessWire forums! This is a great question and I think ProcessWire is a great platform to begin transitioning into OOP because ProcessWire itself is object oriented and is built using OOP. It includes powerful tools and features that can help make your code cleaner, more efficient, and reusable. I recommend starting with custom Page classes. Custom page classes lets you use OOP principles to extend ProcessWire and add additional custom behaviors by thinking with objects. There are a couple of examples in that link, but I'll provide one here that specifically contrasts different methods of doing the same thing. This example is a real-world case that I use on many projects, and because of how it's written I can replicate this feature easily when I start new projects. This is just a simple example of code I use that hopefully opens the door to thinking in OOP when working with ProcessWire. On blog posts I like to add something that shows how long it will take to read it, a la "5 minute read" like articles on Medium do. I wrote code that implements Medium's own method of calculating read time and use it often. The code that calculates the reading time is real, but I threw this together for illustration so please excuse any errors. Lets assume that you have a template called blog-post.php where you calculate reading time and output that value to the page. Here is what that looks like using procedural code: <?php namespace ProcessWire; // site/templates/blog-post.php // Calculate the read time for this article by using the words contained in the title, summary, and // body fields $text = "{$page->title} {$page->summary} {$page->blog_body}"; $blogText = explode( ' ', $sanitizer->chars($text, '[alpha][digit] ')); $wordCount = count($blogText); $dom = new \DOMDocument; @$dom->loadHTML( "<?xml encoding=\"UTF-8\"><div>{$blogText}</div>", LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ); // Account for images in text and add to read time // Starts at 12 seconds for first img, 11, 10, 9, etc. until floor of 3 secs $secondsPerImage = 12; $imageReadTimeInSeconds = 0; $imageCount = $dom->getElementsByTagName('img')->length; while ($imageCount > 0) { $imageReadTimeInSeconds += $secondsPerImage; $imageCount--; $secondsPerImage > 3 && $secondsPerImage--; } // Word count divided by average adult reading speed per minute with image read time added $readTime = (int) (ceil($wordCount / 275) + ceil($imageReadTimeInSeconds / 60)); ?> <!DOCTYPE html> <html lang="en"> <head> <title><?= $page->title; ?></title> </head> <body> <h1><?= $page->headline; ?></h1> <span class="read-time"><?= $readTime; ?> minute read</span> <div class="summary"> <?= $page->summary; ?> </div> <div class="blog-content"> <?= $page->blog_body; ?> </div> </body> </html> So, there's nothing wrong with that- gets the job done! But it could be better... It adds a lot of logic to our template and makes it harder to read, imagine if we had to add more logic for other features Mixing raw PHP and HTML works, but can be confusing when it comes to managing and maintaining our code We can't reuse the the code that calculates reading time, if we wrote this in another place then we have to make sure both are bug free and accurate Of course, we could create a function called readTime() that does the same thing and cleans up the template. But now we are writing functions that do a specific thing but exist without context and are harder to organize and maintain flexibility. Luckily, there's a better way. I'll let the notes by Ryan in that link I shared above explain how to start using custom Page classes, so I'll assume you have that set up. So now lets think in objects and use OOP to improve our code. Now we have our template, blog-post.php, and a custom Page class called BlogPostPage.php. Lets refactor. Here's our BlogPostPage.php file: <?php namespace ProcessWire; // site/classes/BlogPostPage.php class BlogPostPage extends Page { private const INITIAL_SECONDS_PER_IMAGE = 12; private const WORDS_READ_PER_MINUTE = 275; private const ALLOWED_CHARACTERS = '[alpha][digit] '; public function readTime(): int { $text = "{$this->title} {$this->summary} {$this->blog_body}"; $blogText = explode( ' ', wire('sanitizer')->chars($text, self::ALLOWED_CHARACTERS)); $wordCount = count($blogText); $dom = new \DOMDocument; @$dom->loadHTML( "<?xml encoding=\"UTF-8\"><div>{$blogText}</div>", LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ); $secondsPerImage = self::INITIAL_SECONDS_PER_IMAGE; $imageReadTimeInSeconds = 0; $imageCount = $dom->getElementsByTagName('img')->length; while ($imageCount > 0) { $imageReadTimeInSeconds += $secondsPerImage; $imageCount--; $secondsPerImage > 3 && $secondsPerImage--; } $time = ceil($wordCount / self::WORDS_READ_PER_MINUTE) + ceil($imageReadTimeInSeconds / 60); return (int) $time; } } Now that our code is in the context of a class method, we've made a couple of extra changes: $page->{name of field} is now $this->{name of field} because we're now working within BlogPostPage which extends the Page class itself. We've added constants to define values that would otherwise be a little difficult to understand at first glance. Seeing self::WORDS_READ_PER_MINUTE is clear and self-documenting where only using the integer 275 in place doesn't really state what that number means We switched $sanitizer to wire('sanitizer') because the $sanitizer variable does not exist in the method scope and the wire() function makes the entire ProcessWire API available to us both inside and outside classes ProcessWire will use BlogPostPage class to create the $page object we use in our templates when it boots, executes our code, and renders content via our templates. Thanks to OOP inheritance, BlogPostPage has all of the methods and properties available in the Page class and can be used in our templates with the $page object. And now let's go back to our blog-post.php template: <?php namespace ProcessWire; // site/templates/blog-post.php ?> <!DOCTYPE html> <html lang="en"> <head> <title><?= $page->title; ?></title> </head> <body> <h1><?= $page->headline; ?></h1> <span class="read-time"><?= $page->readTime(); ?> minute read</span> <div class="summary"> <?= $page->summary; ?> </div> <div class="blog-content"> <?= $page->blog_body; ?> </div> </body> </html> Now we're talking. With a little extra code and some OOP we've created a method on the Page object. Some benefits: Our template is cleaner and easier to maintain We've made the BlogPostPage class extend Page, so it inherits all of the methods and properties you access via $page The $page object is used to output the reading time to the page, just like $page outputs our field content, so our custom behavior is predictable and and feels at home with the core ProcessWire API It's easier to find where your programming logic is and keep a separation of concerns What's even better is that because we have used OOP to extend the Page class and add new functionality, we can use this a lot more places in our templates (so now it's reusable too). Let's say that you want to add a blog feed to the home page that shows the latest 3 blog posts and displays them with their title, read time, summary, and a link to read the post. <?php namespace ProcessWire; // site/templates/home.php ?> <!DOCTYPE html> <html lang="en"> <head> <title><?= $page->title; ?></title> </head> <body> <header> <h1><?= $page->headline; ?></h1> </header> <section class="blog-feed"> <!-- Create an <article> preview card for each blog post --> <?php foreach ($pages->get('template=blog-post')->slice(0, 3) as $blogPost): ?> <article> <h2><?= $blogPost->title; ?></h2> <span class="read-time"> <?= $blogPost->readTime(); ?> minute read </span> <div class="post-summary"> <?= $blogPost->summary; ?> </div> <a href="<?= $blogPost->url; ?>">Read More</a> </article> <?php endforeach ?> </section> </body> </html> That would be a lot harder to do if you had to write more procedural code to calculate the read time for each blog post. Thanks to that method in BlogPostPage, we can use it anywhere we reference a blog post. Think we can make this better? Let's improve it using PHP traits. Using a Trait will allow us to reuse our code that calculates reading time in many places thanks to OOP. We'll create another file called CalculatesReadingTime.php and put it in a new folder at /site/classes/traits. Time to refactor, here's our new trait file: <?php namespace ProcessWire; // site/classes/traits/CalculatesReadingTime.php trait CalculatesReadingTime { private const INITIAL_SECONDS_PER_IMAGE = 12; private const WORDS_READ_PER_MINUTE = 275; private const ALLOWED_CHARACTERS = '[alpha][digit] '; /** * Takes an arbitrary number of field values and calculates the total reading time * @param string $fieldValues Contents of fields to calculate reading time for * @return int Total read time, in minutes */ public function calculateReadingTime(string ...$fieldValues): int { $text = array_reduce( $fieldValues, fn ($content, $fieldValue) => $content = trim("{$content} {$fieldValue}"), '' ); $text = explode( ' ', wire('sanitizer')->chars($text, self::ALLOWED_CHARACTERS)); $wordCount = count($text); $dom = new \DOMDocument; @$dom->loadHTML( "<?xml encoding=\"UTF-8\"><div>{$text}</div>", LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ); $secondsPerImage = self::INITIAL_SECONDS_PER_IMAGE; $imageReadTimeInSeconds = 0; $imageCount = $dom->getElementsByTagName('img')->length; while ($imageCount > 0) { $imageReadTimeInSeconds += $secondsPerImage; $imageCount--; $secondsPerImage > 3 && $secondsPerImage--; } $time = ceil($wordCount / self::WORDS_READ_PER_MINUTE) + ceil($imageReadTimeInSeconds / 60); return (int) $time; } } Let's take a look at that before we go back to our other files. Our new trait has a name that is an "action" because now, as we'll see, we can add this ability to other classes so they can calculate reading times We've abstracted our code. Now instead of referring to content as $blogText, we are calling it $text because it can be used in many places and many contexts but provide the same behavior The readTime() method is now called calculateReadingTime() and has been converted to a variadic function so you can pass as many field values as needed We've taken an extra step of type hinting our parameters as strings to make sure the method is always getting the proper type of data to work with. This will help a lot as this method becomes used more in different places Our docblock is more robust to help understand what this method does, what parameters it takes, and what it will return, another extra step to help us as we use this method in more places Now back to our BlogPostPage.php file <?php namespace ProcessWire; // site/classes/BlogPostPage.php require_once __DIR__ . '/traits/CalculatesReadingTime.php'; class BlogPostPage extends Page { use CalculatesReadingTime; public function readTime(): int { return $this->calculateReadingTime($this->title, $this->summary, $this->blog_body); } } Now we're giving our BlogPostPage class the ability to calculate reading time and all of the functionality has been kept the same. We were able to abstract the logic for calculating reading time to a reusable trait that can be included in any Page class Our readTime() method still exists and is available to all of the templates where we were already using $page->readTime() Now our readTime() method calls the calculateReadingTime() method and passes the fields we know we need as they exist in our blog-post template This code is clean, concise, and easy to maintain. Right now it looks like OOP has made things look nice but caused some extra work... but then the phone rings. Your client wants to add Press Releases to the blog section of the site and it's going to need a new layout and different fields, but they love your reading time calculator so much that they want it on the new Press Release pages too. So we create our new files- a press-release.php template, and a PressReleasePage.php file. We'll skip writing out the press-release.php HTML, but while creating the new template in ProcessWire you've created new fields. The new fields are 'pr_abstract', 'pr_body', 'company_information', and 'pr_contact_info'. Here's our new PressReleasePage.php file: <?php namespace ProcessWire; // site/classes/PressReleasePage.php require_once __DIR__ . '/traits/CalculatesReadingTime.php'; class PressReleasePage extends Page { use CalculatesReadingTime; public function readTime(): int { return $this->calculateReadingTime( $this->pr_abstract, $this->pr_body, $this->company_information, $this->pr_contact_info ); } } Since we've created this simple class that uses the CalculatesReadingTime trait, we can use $page->readTime() in all of our Press Release pages, and anywhere that a Press Release $page object is present. Very nice. Now OOP has really shown how useful it is and we can appreciate how ProcessWire uses objects and provides tools for us extend that power with our own code. There's also some other OOP things happening here: Our BlogPostPage and PressReleasePage classes do one thing and one thing only: handle logic and features for their respective pages. So, they have a single responsibility The calculateReadingTime() and readTime() methods do one thing and one thing only, calculate reading time based on content. They only care about one thing and have no side effects. Our BlogPostPage and PressReleasePage clases can both calculate reading time, but other hypothetical page classes, like "HomePage" and "AboutUsPage" aren't required to have a readTime() method that isn't used, thanks to making use of traits to share behavior only where it's needed. So, that's composition over inheritance Our readTime() method does not expose how it calculates reading time and it provides an interface to only expose information we want our object to make available. So, readTime() is read-only and can safely be used knowing that the value will never be overwritten or modified except when content is changed by editing the page. This is a great tool that shows the difference between setting a value to $page->title and getting a value from readTime(), each have their purposes and roles. Our code is modular and easy to maintain. If we had to adjust how reading time was calculated- we could for example adjust the value of WORDS_READ_PER_MINUTE in our CalculatesReadingTime trait. Then all of our Press Releases and Blog Posts would have their reading time correctly calculated with one change. We can also add CalculatesReadingTime to any future page classes that need it. ProcessWire's strong OOP foundation and the way that it uses objects that are created from classes for everything (like $page, $config, $input, etc.) is the reason that the API is easy to work with, enjoyable, and powerful. If you get the hang of working with OOP in ProcessWire you can build even more powerful websites and applications, better understand the ProcessWire core code, and write your own modules (which is actually pretty fun). Wasn't sure of your overall exposure to OOP but hopefully this helps and inspires!
    26 points
  18. This week I got wrapped up in unexpected client work, but it did lead to development of a new Inputfield module, which I think I’ll be using a lot, and hoping some others might find it useful too. It’s called Small Select Multiple, and the purpose of it is to provide a multi-selection input that is as simple as a single-selection input, and that didn't introduce new UI elements, sticking just to native browser controls. It came about because a client has a large front-end form that has a lot of multi-selection inputs, which we handled with checkboxes. It started to become too much for users, so we tried changing to AsmSelect. That helped a lot, but it still became overwhelming once a lot of options were selected. We needed something that looked simple from the start, and stayed simple even after a lot of selections had been made. The Small Select Multiple input seems to do the trick. You can use this input type with Options fields or Page fields, or anywhere else you might us ProcessWire’s SelectMultiple, Checkboxes, or AsmSelect input types. Like the other Inputfield types, it’s not always going to be the right fit, and has it’s own unique set of benefits and drawbacks, but for the times where it suits the need, I hope you find it useful. Rather than writing more about it, I’ll link you to the module page for it. There’s more details and screenshots there. While you are in the modules directory, see the 3 other new modules posted this week too, they look great. Core updates coming next week. Thanks for reading and have a great weekend.
    25 points
  19. It’s been a fairly quiet week here at the ProcessWire HQ as it’s a Thanksgiving week where the kids don’t have school, which means less time in the office. This is the time of year when we think about what we’re thankful for, and I’m especially thankful for all of you and the ProcessWire community. Even with the holiday week, work continues on our next main/master version of ProcessWire. This week Bernhard and Adrian identified that ProcessWire doesn’t work well with PHP 8.4 due to some new deprecations introduced by this version of PHP. It’s a technical detail, but PHP 8.4+ wants a question mark before function/method arguments of named types that have a default value of null. So if the function arguments were (PageArray $items = null) PHP 8.4 wants it to be (?PageArray $items = null). Why? Who knows, perhaps not even PHP does, as “?” points directly to it being a question, one with no obvious answer. Perhaps it wants to make sure we really meant what we wrote, so the “?” is an “are you sure?”. Without question, the “= null” part is a pretty clear statement about what the intention is, no? I question the question mark, doesn’t this turn arguments into questions? Questionable arguments? Being so close to a new master/main version, there’s no question that we want to make sure it works with the latest available PHP version, questionable or not. So that meant adding new questions to 72 different core files, which you’ll find on the dev branch this week. There may be more questions yet to answer, but those are the instances I was able to find so far. There’s a tradeoff in that the questions were introduced by PHP 7.1, so it’ll produce a parse error on any prior versions of PHP. Meaning, our minimum required PHP version is now 7.1. Prior to today, it was 7.0 (actually, PW still even worked even on PHP 5.6, not that it matters). Similar questions will have to be added to modules before they are PHP 8.4 compatible to not throw deprecation notices, so I’ll be making some questionable updates to Pro and public modules in the coming weeks as well. Thanks for putting up with all my questions and have a great weekend! Please reply with your questions below.
    25 points
  20. ProcessWire 3.0.244 is our newest main/master/stable version. It’s been more than a year in the making and is packed with tons of new features, issue fixes, optimizations and more. This post covers all the details— https://processwire.com/blog/posts/pw-3.0.244/
    24 points
  21. This week on the dev branch, we have a new $page->preload() method that enables you to preload multiple fields in one query (in this GitHub commit). This is kind of like what the autojoin option does (when configured with a field), or what the $pages->findJoin() method does, but with one big difference. Those options happen as part of the page loading process. Whereas $page->preload() can be applied to a page that has already loaded. Here’s one example where you might find this useful. Say you have a page living at /products/product/ and it has a hundred fields. At the top of your template file that renders the page, you could have a $page->preload(‘field1’, ‘field2’, ‘field3’); to preload all those fields before outputting them. This enables you to load field1, field2 and field3 in 1 query rather than 3. On your first call to $page->field1 it won’t trigger a load from the database and instead will return the value that has already been preloaded. You can also call $page->preload(); without any arguments, and it will preload ALL the supported fields for the page. In reality, ProcessWire is already pretty quick with loading fields, so you probably won’t benefit from preloading until the scale is quite large. While developing this, I was testing by iterating 500 pages and accessing 50 different fields from each. Without preload this took 12 seconds. With preload it took 6 seconds. So for this particular case, preloading cut the time in half. I’m not a query counter, as very often lots of simple DB queries are faster than a single big query, but I’ll mention that it also reduced the quantity of database queries by more than a factor of 10. For this large scale case, that meant more than 20000 queries down to well under 2000. Like with autojoin, there are some limitations with preloading. It supports primarily Fieldtypes that use the core loading mechanism. Some Fieldtypes (especially non-core) override this, and preload() skips over those fields. It also skips over most FieldtypeMulti (multi-row fields), but FieldtypePage is supported when used with Page fields that carry one value. Multi-value can be enabled for testing with an option you’ll find in the function $options, but like autojoin, is limited by MySQL’s group_concat limit. By default that limit is 1024, which supports 140-170 page reference values in a given page field. That's quite a lot, but I don't want to assume folks won't go over that, so it's disabled by default. I’m guessing that most won’t need the preload() function, but a few might really benefit from it, especially at larger scale. So I think it’s a worthwhile addition to the core, and another method that answers a need that may arise as an installation grows, further supporting ProcessWire’s ability to scale up as needs do. Though consider it experimental and "work in progress" at the moment, as we’ll need to do more testing to make sure it is fully stable in a broader range of situations, and further improvements are likely. Special thanks to @tuomassalo at Avoine who came up with the idea for preload() and helped me to get started developing it. Last week I told you how Pete, Jan and I met up in Holland. I also wanted to mention, a couple weeks ago, right before I left for Amsterdam, Oliver from @update AG (update.ch) in Zürich, Switzerland sent me a DM saying that he was in my neighborhood, so we got together for coffee near my house. They’ve been using ProcessWire at Update AG almost as long as PW has been open source, so it was good to meet another long time ProcessWire user, and a nice coincidence that he was in the neighborhood. It’s always great to meet ProcessWire users in person and I hope to meet more of you in the future as well. Thank you for reading and have a great weekend!
    24 points
  22. This week there’s new $pages->saveFields() and $page->saveFields() methods on the core dev branch. You might already be familiar with the $pages->saveField($page, $field); method which lets you save one field from a page, or $page->save($field); which does the same. This is useful when you only need to save one field from a page, rather than the entire page. Now we have a plural version of that method, which lets you specify multiple fields on a page to save: $pages->saveFields($page, [ 'title', 'body', 'summary' ]); Below is the same thing, but on a $page object, so you don't need to specify a $page argument: $page->saveFields([ 'title', 'body', 'summary' ]); You can also use a string if you prefer: $page->saveFields('title,body,summary'); In my case, I needed this method for a project I'm working on, and I also needed it to save without updating the 'modified' time or user, which you can achieve by specifying the 'quiet' argument. Though note, the 'quiet' argument is available for all the page saving methods and has been around a long time. But I'm not sure how widely used it is, so I'll mention it. $page->saveFields('title,body,summary', [ 'quiet' => true ]); This week the API methods for Select Options fields have also been updated to add more convenience for getting, adding and removing options to an existing selection. Let's say we have an Options field of checkboxes named "colors". Each selectable option has an ID, optional value, and title. Now you can get, add, or remove by any of those properties. Previously you had to work directly with SelectableOption instances, which wasn't as convenient. // add by title of option 'Blue' $page->colors->addByTitle('Blue'); // remove the option with value 'orange' from colors field $page->colors->removeByValue('orange'); // get SelectableOption instance of option with title 'Red' $red = $page->colors->getByTitle('Red'); echo "ID, value, title: $red->id, $red->value, $red->title"; // check if colors has an option with value 'purple' if($page->colors->hasValue('purple')) { // it has purple } The methods added to SelectableOptionArray (not yet reflected in linked docs page) include: getByID($id) getByValue($value) getByTitle($title) addByID($id) addByValue($value) addByTitle($title) removeByID($id) removeByValue($value) removeByTitle($title) That's all for this week. There likely won't be any core updates next week, as I'll be out of town again. Thanks for reading and I hope that you all have a great weekend and great week ahead.
    24 points
  23. As a default? Big NOPE from me. The super easy way and low barrier to enter the world of ProcessWire was one of the main reasons I gave it more than a quick look, did all the tutorials, read the docs, read the forum, ... and stayed. I tried a lot of CMSs back then and because ProcessWire had this "direct output"-way of achieving a lot without needing to know much about PHP or programming in general was the biggest PLUS ever. if/else/echo/foreach is almost everything you need to know to get up and running - and this quite far. Easier than anything. Easier than the Wordpress-loop, Silverstripe's way of doing things, or Drupal and Typo3. Such a delight and super fun to learn something new, a new CMS. Sorry for interrupting this great discussion.
    24 points
  24. I was able to speak with someone at TinyMCE on a Zoom call this week and we had a good meeting. They are going to make it possible for us to continue using TinyMCE 7.x+ in the core, even though it is using a GPL license, while we use the MPL 2.x license. They will make a custom license available for ProcessWire and I hope to have the details of that potentially next week. We’ll have to review the conditions and everything to make all is good, but it sounds like it will very likely solve the issue for us. I’m really happy about this and look forward to working with TinyMCE 7.x They also expressed interest in us potentially collaborating on a separate module that would make some of the commercial and advanced TinyMCE features available to ProcessWire users that wanted them via a paid service, like a Pro module. This option hasn't been available to us before, so I thought it sounded interesting. I'll definitely be communicating with them more about that to see what's possible. That’s all I know so far, but will keep you up-to-date as I learn more.
    23 points
  25. I hope you all have had a great week. Last week was the blog post for our newest main/master version 3.0.244. This week I've been catching up with some other projects, so no new core updates to report. But one thing I've been working on (and am still working on) is a module that lets you provide filters in the admin page list. In my case, a client wants to be able to filter by the first letter of page titles, so they can quickly jump to all pages that start with the letter "C", for example. It figures out all the starting first-characters for page titles and builds a kind of pagination-style list for it, like seen in the screenshot below. Clicking any of the single character filters to just those pages by sending an Ajax request to the server, grabbing just the relevant pages and listing them. I think it's pretty useful in many cases. And I think there's potential for predefined filters to go beyond just letters. There's more to work out with this, but I hope to release it in the near future. Thanks for reading and have a great weekend!
    22 points
  26. This week the most useful core update is likely the refactored column width slider in the template editor, located Setup > Templates > [your template] > Basics > Fields. You may or may not already know that clicking, holding and dragging the percent indicator on the right side of each field adjusts the column width. With the term “column width”, I mean the width of the field in the page editor, for when you want to have multiple fields in different columns on the same row. It’s a convenient and time saving shortcut. But it was also a little tricky to use, as it allowed anything between 10% and 100% in 1% increments, and it was a little finicky trying to get the percentages just right sometimes. It’s something that’s been bugging me for awhile, and @Pete messaged me on Slack this week and mentioned it. He suggested making it operate in 5% increments rather than 1% increments. He also suggested making a double click of the percent indicator open up the dedicated column width range slider that allows for more precise adjustments. I thought those were good suggestions, so I went ahead and implemented them this week. In addition to now using 5% increments, it also supports the commonly used 33%, 34% and 66% width values as well. But if you happen to already have some field that is using a less common width, like 27% or 72%, etc., then it reverts back to 1% increments for the same behavior as before. Of course, you can also use the 1% increments by double clicking the percent indicator to open the dedicated column width range slider. Thanks Pete for the suggestions, I think it all works better now. I’ll be applying the same changes to FormBuilder’s equivalent of this feature as well. This week I’ve also been working on the new CustomFields modules (FieldtypeCustom and InputfieldCustom). Most recently I’ve been working on adding support for multi-language fields, as well as adding more examples and tools to make it really easy to use and configure. I may have it ready as soon as next week or the following week. The PageAutosave module is also getting a new version soon. I’ve been focused on the LivePreview feature of it and making a version of it that doesn’t depend on auto-save. The alternative LivePreview option (which we’ll just call “Preview”) will work anywhere because it has no field limitations. It simply updates the preview window whenever you save the page. While that’s not as fancy as live preview as-you-type, it’s still very helpful, while being reliable in any situation. It’s reliable and portable enough that I may end up putting the feature in the core, but will be testing it out in the next version of the PageAutosave module first. Have a great weekend!
    22 points
  27. This week I'm releasing the ProcessWire Fieldtype/Inputfield module "Functional Fields" as open source in the modules directory. This was originally built in 2017 for ProFields, but hasn't required much in terms of support resources, so I thought I should release this newest version (v4) publicly. While this module is completely different from other Fieldtypes, I think it's quite useful and fulfills some needs better than other Field options. Give it a try and please let me know what you think. For an introduction to Functional Fields, please revisit this blog post which covers all the details: https://processwire.com/blog/posts/functional-fields/ Functional Fields is available for download in the modules directory here: https://processwire.com/modules/fieldtype-functional/ Thanks and have a great weekend!
    22 points
  28. This week the core dev branch version remains at 3.0.239 but relative to last week, it contains several optimizations and improvements. Details can be found in the dev branch commit log. I've also moved two Textformatter modules out of ProFields and into GitHub and the modules directory. These include: Auto Links This Textformatter module automatically links your specified phrases/words to your specified URLs. This is a potential SEO and accessibility tool for creating automatic contextual links with little effort. If there are pages that you commonly link to in your site from your textarea/rich text fields, then this Textformatter can save you some effort, automatically linking to those URLs. Text Blocks This Textformatter module enables you to assign a name to any block/region of text in a Textarea field. They are defined by typing start_name where you want the block to start, and stop_name where you want the block to stop. The block(s) of text can then be shown in any other Textarea field at runtime (site-wide) simply by typing the block name on its own line in the format show_name. Note that the word "name" in all of these examples would be whatever you've decided to name the block. Both modules have been updated just for public release with a new version. Both are considered Stable, as they have both been running without incident for several years. These modules were moved from ProFields to the public modules directory for three reasons. First is that they don't consume hardly any support resources, so they don't need to be commercially supported modules anymore. Second is that I'd like to keep ProFields focused more on field related modules (Fieldtype, Inputfield, and related) rather than Textformatter modules. Though admittedly the TextBlocks module does blur the line a bit, as it promotes potential greater reusability with existing Textarea/TinyMCE/CKEditor fields. Third is that there's already a lot in ProFields and I wanted to make room for new modules, such as the recently added PageEditChildren, which is part of ProFields at least in the short term. The FunctionalFields module may come out of ProFields well, as it hasn't required much in terms of support resources recently, though it is a useful Fieldtype/Inputfield module that is very much in keeping with the theme of ProFields, so I'm still debating on that one. Thanks for reading and have a great weekend!
    22 points
  29. This week there is a new version of the Site Profile Exporter module released (ProcessExportProfile). This is the module that was used to export last week's Invoices Application Site Profile. While this module has been around for a decade now, this latest version makes some nice improvements, which I'll cover below. @bernhard pointed out to me that he's using the module to make a new site profile, but he found it cumbersome to enter all the profile information every time he wanted to export the profile. So this new version now lets you update an existing profile with 1-click, making it much easier to re-export a profile. This version also adds a preview of what your profile will look like in the ProcessWire installer (the part where the user would select it for installation). Lastly, this version has several other small improvements and fixes as well. If you've ever thought of building a site profile, this module now makes it that much easier. This week I've also been starting to focus on the next long term client project, which is kind of a different and unique one that I look forward to. That's in part because I expect it will also involve a lot of improvements to the ProcessWire core and ProFields modules as part of it. Some of my favorite ProcessWire improvements have accompanied projects like this. There just isn't any substitute for real-life, large-scale projects when it comes to improving and optimizing the core and modules. Next week will be a shortened week here, so I'll likely post the usual weekly update Thursday rather than Friday. Thanks for reading and have a great weekend!
    22 points
  30. We’ve been building web site with ProcessWire since 2013. ProcessWire serves to us as a secure, reliable platform, and honestly, I don’t remember any significant issue after 11 years of daily use (for us, PW is also a CRM). The magic of ProcessWire is that it is always growing with our needs and serves any unimaginable demand that our platform has had. All these years, our business has grown around ProcessWire, and as it usually happens, popularity brings the other side of the coin. In our case, it was online fraud and scammers. Our first approach was to use existing fraud prevention systems, but as our business is not pure e-commerce and fraud was indirect, there was no solution that matched our needs, so we started developing our system from scratch. This is where I exactly understood how ProcessWire and the community really hooked me. (-: First of all, taking ProcessWire as an example, we decided not to heavily rely on frameworks. It was a hard decision, because new school engineers like to bring as many dependencies as possible, but I continuously pointed to ProcessWire, and as a result, we created a fraud prevention/user behaviour analytics platform with ~4 PHP dependencies. The second decision that we took, looking at ProcessWire, was even harder than the first. The fraud prevention market, in contrast with CRM, is not widely targeted for open source software, but taking ProcessWire as an example, we decided to open source our system after ~8,000 engineer-hours under the AGPL license. For sure, after being open-sourced for one week, it's premature to give any feedback, and it is highly possible that open sourcing was a mistake. However, it brings me to the understanding that the real measure of software is not downloads or stars, but its influence over other developers, and from them onto other developers, like ripples on the water. From this perspective, I am infinitely grateful to @ryan, @Ivan Gretsky, @Soma, and every person behind the ProcessWire community for all the inspiration through these years. I'm so grateful that 11 years ago I met this community and had the chance to work with some of you. As I'm more reader than writer, I would like to use this rare opportunity to wish the ProcessWire community and your families a Merry Christmas and all the best in the New Year! Best Alex P.S. While this post is not intended to be an advertisement, if someone from community is facing challenges related to online fraud, user spam, or security, please feel free to contact me directly.
    21 points
  31. I think the most talked about feature is an asset manager. The drilling down of file and imagefields so that you can choose assets from a global library that have already been uploaded somewhere, instead of having to upload assets multiple times. Actually, the approach using references is the best I've come across so far. The data remains where it was originally uploaded, but is only referenced in a file/image field on another page.
    21 points
  32. This week we have ProcessWire 3.0.238 on the dev branch. This version contains 17 commits containing new features (some via feature requests) as well as issue fixes. Recent updates have been nicely covered in ProcessWire weekly issues #517 and #518. Also added this week are the following: Improvements to ProcessPageClone, which now supports some new options such as the ability to configure it to always use the full clone form (a requested feature), and an option to specify whether cloned children should be unpublished or not. New $datetime->strtodate($date, $format) method that accepts any PHP recognized date string and reformats it using the given format. It also supports date strings that PHP doesn't recognize if you give it a 3rd argument ($options array) with an `inputFormat`. The existing $datetime->strtotime() method now supports an `inputFormat` option as well. The Inputfield class now has new methods: $inputfield->setLanguageValue($language, $value) and $inputfield->getLanguageValue($language), added by the LanguageSupport module. Previously there were no dedicated methods for this, so you had to manage them with custom keys containing language IDs if you wanted to programmatically use an Inputfield in multi-language mode. The ProcessWire.alert() JS method has been updated with an auto-close option (`expire` argument) that automatically closes the alert box after a specified number of seconds. InputfieldDatetime has been updated with 7 new interactively configurable settings for the jQuery UI date picker. See the screenshot below for an example of a few. Plus, its API has been updated with the ability for you to specify or override any jQuery UI datepicker option, either from PHP or JS, as requested by Toutouwai/Robin S. See the new datepickerOptions() method and phpdoc for details. Next week we've also got some more updates to InputfieldTable that take the newly added actions even further and make them easier to use. Thanks for reading and have a great weekend!
    21 points
  33. Big thanks to everyone that shared your favorite ProcessWire features last week, it was very helpful for the new ‘features’ section of the website. Speaking of the website, I’ve been continuing to work on that this week, and was primarily focused on the modules directory. I’ve got plenty more to do there, but making good progress. The website is going to be the focus of the next few weeks, with some core updates along the way. This week the core updates were a few issue fixes on the dev branch, with more on the way next week. Thanks for reading and have a great weekend!
    20 points
  34. Just a quick note to say that the new version blog post is coming tomorrow rather than today. Here in Atlanta, tomorrow it is supposed to rain, while today the weather is perfect. So I spent the day outside and am moving my work day to tomorrow instead.
    20 points
  35. The invoices application site profile that I uploaded last week now has a companion blog post: https://processwire.com/blog/posts/invoices-site-profile/ Thanks for reading and have a great weekend!
    20 points
  36. This week I received some client specifications for project that indicated it was going to need a lot of fields, and fields within fields. There are a lot of tools for this kind of stuff in ProFields, but none that I thought would make this particular project "easy" per se. The closest fit was the Combo field, but with 40-50 or so subfields/columns within each of the "groups", it was still going to be kind of a pain to put together. Not to mention, just a lot of fields and subfields to manage. To add more challenge to it, there was another group of 30 or so fields that needed to be repeatable over any number of times (in a repeater field). I wasn't looking forward to building out all these fields. That scale of fields/subfields is at the point where I don't really want to build and manage it interactively. Instead, I want to just edit it in a code editor or IDE, if such a thing is possible. With any large set of fields, there's often a lot of redundant fields where maybe only the name changes, so I wanted to be able to just copy and paste my way through these dozens of fields. That would save me a lot of time, relative to creating and configuring them interactively in the admin. Rather than spending a bunch of time trying to answer the specifications with existing field types, I ended up building a new Fieldtype and Inputfield to handle the task. Right now it's called the "Custom" field (for lack of a better term). But the term kind of fits because it's a field that has no structure on its own and instead is defined completely by a custom JSON file that you place on your file system. (It will also accept a PHP file that returns a PHP array or InputfieldWrapper). Below is an example of defining a custom field named "contact" with JSON. The array keys are the field names. The "type" can be the name of any Inputfield module (minus the "Inputfield" prefix). And the rest of the properties are whatever settings are supported by the chosen Inputfield module, or Inputfield properties in general. /site/templates/custom-fields/contact.json { "first_name": { "type": "text", "label": "First name", "required": true, "columnWidth": 50 }, "last_name": { "type": "text", "label": "Last name", "required": true, "columnWidth": 50 }, "email": { "type": "email", "label": "Email", "placeholder": "person@company.com", "required": true }, "colors": { "type": "checkboxes", "label": "What are your favorite colors?", "options": { "r": "Red", "g": "Green", "b": "Blue" } }, "address": { "type": "fieldset", "label": "Address", "children": { "address_street": { "type": "text", "label": "Street" }, "address_city": { "type": "text", "label": "City", "columnWidth": 50 }, "address_state": { "type": "text", "label": "State/province", "columnWidth": 25 }, "address_zip": { "type": "text", "label": "Zip/post code", "columnWidth": 25 } } } } The result in the page editor looks like this: The actual value from the API is a WireData object populated with the names mentioned in the JSON definition above. If my Custom field is named "contact", I can output the email name like this: echo $page->contact->email; Or if I want to output everything in a loop: foreach($page->contact as $key => $value) { echo "<li>$key: $value</li>"; } After defining a couple large fields with JSON, I decided that using PHP would sometimes be preferable because I could inject some logic into it, such as loading a list of 200+ selectable countries from another file, or putting reusable groups of fieldsets in a variable to reduce duplication. The other benefits of a PHP array were that I could make the field labels __('translatable'); and PHP isn't quite as strict as JSON about extra commas. So the module will accept either JSON or PHP array. Here's the equivalent PHP to the above JSON: /site/templates/custom-fields/contact.php return [ 'first_name' => [ 'type' => 'text', 'label' => 'First name', 'required' => true, 'columnWidth' => 50 ], 'last_name' => [ 'type' => 'text', 'label' => 'Last name', 'required' => true, 'columnWidth' => 50 ], 'email' => [ 'type' => 'email', 'label' => 'Email address', 'placeholder' => 'person@company.com' ], 'colors' => [ 'type' => 'checkboxes', 'label' => 'Colors', 'description' => 'Select your favorite colors', 'options' => [ 'r' => 'Red', 'g' => 'Green', 'b' => 'Blue' ], ], 'address' => [ 'type' => 'fieldset', 'label' => 'Mailing address', 'children' => [ 'address_street' => [ 'type' => 'text', 'label' => 'Street' ], 'address_city' => [ 'type' => 'text', 'label' => 'City', 'columnWidth' => 50 ], 'address_state' => [ 'type' => 'text', 'label' => 'State/province', 'columnWidth' => 25 ], 'address_zip' => [ 'type' => 'text', 'label' => 'Zip/post code', 'columnWidth' => 25 ] ] ] ]; The downside of configuring fields this way is that you kind of have to know the names of each Inputfield's configuration properties to take full advantage of all its features. Interactively, they are all shown to you, which makes things easier, and we don't have that here. There is yet another alternative though. If you define the fields like you would in a module configuration, your IDE (like PhpStorm) will be able to provide type hinting specific to each Inputfield type. I think I still prefer to use JSON or a PHP array, and consult the docs on the settings; but just to demonstrate, you can also have your custom field file return a PHP InputfieldWrapper like this: $form = new InputfieldWrapper(); $f = $form->InputfieldText; $f->name = 'first_name'; $f->label = 'First name'; $f->required = true; $f->columnWidth = 50; $form->add($f); $f = $form->InputfieldText; $f->name = 'last_name'; $f->label = 'Last name'; $f->required = true; $f->columnWidth = 50; $form->add($f); $f = $form->InputfieldEmail; $f->name = 'email'; $f->label = 'Email address'; $f->required = true; $f->placeholder = 'person@company.com'; $form->add($f); // ... and so on Now that this new Fieldtype/Inputfield is developed, I was able to answer the clients specs for lots of fields in a manner of minutes. Here's just some of it (thumbnails): There's more I'd like to do with this Fieldtype/Inputfield combination before releasing it, but since I've found it quite handy (and actually fun) to build fields this way, I wanted to let you know I was working on it and hope to have it ready to share soon, hopefully this month. Thanks for reading and have a great weekend!
    20 points
  37. The core dev branch version has been bumped up to 3.0.240 this week. As always, see @teppo's excellent ProcessWire Weekly for details on what has been added recently. More can also be found in the commit log. This week adds the ability to hook into ProcessWire's admin live search to add your own custom results. This is useful for modules or for your own hooks placed in /site/templates/admin.php. Previously, only modules implementing the SearchableModule interface could manipulate the admin search engine results. Now there's another even simpler way to do it. We'll focus on examples placed in /site/templates/admin.php, but they could also go in /site/ready.php, /site/init.php or in an autoload module's init() or ready() method. The following is a simple example for when one types "today" into the search engine, it returns search results of pages that were modified today. $wire->addHook('ProcessPageSearchLive::findCustom', function(HookEvent $event) { $data = $event->arguments(0); // array $search = $event->object; // ProcesPageSearchLive $group = 'Pages modified today'; // description of this type of search if($data['q'] === 'today') { $items = $event->wire()->pages->find("modified>=today, include=unpublished"); foreach($items as $item) { $search->addResult($group, $item->title, $item->editUrl); } } }); The point of that example is just to demonstrate something simple. In reality, that example isn't that useful because you can already type "modified>=today" to get the same results in the search engine. So let's look at a potentially more useful example. There have recently been requests for better "id" search support in the search engine. In this next example, we add support for "id=123" searches, where it will match templates, fields or pages having id "123" (or whatever ID you specify). Further, it will also support it if you just type "123" on its own. Here's how we might accomplish that below. We'll limit this particular type of search to the superuser since this hook doesn't have any access control checking. if($user->isSuperuser()) { $wire->addHook('ProcessPageSearchLive::findCustom', function(HookEvent $e) { $search = $e->object; /** @var ProcessPageSearchLive $search */ $data = $e->arguments(0); /** @var array $data Search data */ $type = $data['type']; // what to search $q = $data['q']; // search query text // support search of "id=123" or just "123" // skip search if not an "id" search, or query is not a number if(($type != 'id' && !empty($type)) || !ctype_digit($q)) return; // reduce default search operator "%=" to just "=" (if used) $operator = trim($data['operator'], '%'); // search for id in templates, fields and pages foreach(['templates', 'fields', 'pages' ] as $apiVarName) { $apiVar = $e->wire($apiVarName); // get API var $selector = "id$operator$q"; // selector i.e. id=123 or id<10, etc. // some additional considerations for page ID searches if($apiVarName === 'pages') { // PW already handles "id=123" for pages, so skip that kind if($type === 'id' && $operator === '=') continue; // add more to selector for page searches $selector .= ", include=all, limit=$data[limit], start=$data[start]"; } // find by selector $items = $apiVar->find($selector); // tell ProcessPageSearch which results we found foreach($items as $item) { $title = $item->get('title|label|name'); $url = $item->editUrl(); $search->addResult($apiVarName, $title, $url); } // optionally return false to tell it to stop searching after this hook // which would prevent the default behavior of the search engine // $e->return = false; } }); } As you may (or not) know, the search engine displays help if you type the word "help". Help for the search engine usually consists of example searches and descriptions of what those examples do. If we want to add some help for our example above, we could add this to to the top of our hook above, perhaps right after the $data = $e->arguments(0); line: if(!empty($data['help'])) { return $search->addHelp('ID Search Help', [ 'id=1234' => 'Find templates, fields, pages with id 1234', '1234' => 'Same as above but “id=” is optional', ]); } That's the gist of it. These are fairly basic examples but hopefully communicate enough to show that you can add any kind of results you want into the search engine. Right now it's pretty simple and enables anyone with a ProcessWire site to add custom results into the admin live search. But if you find your needs go beyond this, the SearchableModule interface does support more options too. It's also worth using TracyDebugger to examine the $data array that the hook receives, as there are several other things in there you may find useful as well. Thanks for reading and have a great weekend!
    20 points
  38. File Mover Allows the selection of files or images for moving or copying to a different field. The destination field can be on the same page as the source field or on a different page. Screencast For convenience in the screencast this demonstration shows moving and copying images and files between fields on the same page, but you can also move/copy between pages by following the same process. Usage In any Images or Files field, hover on the field label to show the File Mover icon. Clicking on the icon will reveal the File Mover buttons. If no items are yet selected you'll see a button labelled "Select items to later move or copy". Click the button to enter selection mode. While in selection mode, click one or more images/files to select them. When you have finished selecting items click the "Done" button. Note: you can only select from one Images and one Files field at a time before completing the move/copy step. In the Images or Files field that you want to move/copy the items to, hover the label to show the File Mover icon and click it to reveal the File Mover buttons. When you click the Move or Copy button the page will automatically be saved and the selected items will be moved or copied accordingly. There is also a button to clear the current selection if needed. If you hover on any of the buttons a tooltip shows the filenames of the currently selected items, in case you need a reminder. Configuration There is a field in the module config that defines which roles are allowed to use the File Mover module. If the field is left empty then all roles are allowed. https://github.com/Toutouwai/FileMover https://processwire.com/modules/file-mover/
    20 points
  39. This week on the dev branch are 8 issue resolutions. The most significant one #2035 (found by @Jonathan Lahijani) is that when you trash a page with children, and then later restore it, the page gets restored correctly, but the children/descendants only partially get restored. By that I mean that the children would get moved out of the trash, but they'd continue to have trash status. So those pages could silently behave like they were in the trash, making them visible in the PageList but otherwise inaccessible. There wasn't really any way to fix it interactively, since you can't manually assign or un-assign trash status, at least not without using the API. While I suspect this bug has been around for a very long time, I couldn't find any installations where I had trashed and restored a nested structure of pages like this, but was able to reproduce it manually. Just in case any of your installations have pages like that, the PageList (as of this week's commits) now highlights them with both trash and warning icons. Fixing the issue is just a matter of editing the page and clicking Save. If you want to quickly check if you have any pages affected by the issue (in almost any PW version), you can paste in the following URL, assuming /processwire/ is the path to your admin: /processwire/page/search/for?status=trash&has_parent!=7&include=all While it might be rare to trash and restore a structure of pages, I want to make sure nobody is affected by it so I'll likely update the current main/master version within the next week, merging the current dev branch. Coming in ProcessWire 3.0.246 is support for another type of conditional hook which enables you to match the return value, or properties of the return value, for any hooked method. Currently conditional hooks let you match things from the object being hooked, or the arguments of the method being hooked, but it's not previously been possible to match the return value of of methods with conditional hooks. Next week it will be, which I think as a useful improvement to our hooks system. More on that next week. Thanks for reading and have a great weekend!
    19 points
  40. I've got a lot of travel coming up in the weeks ahead, so there may be a few quieter-than-usual weeks in terms of core updates. I'll be in and out of town a few times, and I'm not much of a traveler, so will see how it goes. I'm not yet sure whether I can do work remotely, or what will be available in terms of internet access. There's a lot of client work to wrap up before hitting the road, so I've been focused on that this week and will have to next week as well. It's all ProcessWire related work though, so still having fun. There have been a few core updates this week, and there will likely continue to be in the coming weeks, but just not major core updates. By November hopefully all will be back to normal in terms of schedule. I'll be focused on getting a new main/master version out then. I also expect to have a new version of the CustomFields module (from last week's blog post) ready in the next week or so as well. Thanks for reading and have a great weekend!
    19 points
  41. Making ProcessWire stronger for full-stack web application development, allowing it to become an unassuming alternative to Laravel and Rails but from the origins as a CMS. ProcessWire is the perfect CMS (there's no doubt in my mind about that), and it's actually already quite good for web application development (both natively and with 3rd party modules), but with some enhancements to make it more "batteries included", enhancing page classes and some tooling, ProcessWire can have its feet in both the CMS and full-stack framework buckets in a way that's perhaps unique. I can elaborate on this further as that sounds a little too generic, but I've been developing a web application with PW for over 9 months (it's a very complicated project and it's replacing an existing, in-production system which makes it even more tricky and high-stakes) and when it's done I can share some ideas. This one enhancement alone moved the needle quite a bit in making ProcessWire more web application friendly.
    19 points
  42. I've been continuing to work on the PageEditChildren module that I wrote about here last week. This week a "Clone" feature was added. You can click the Clone action and it'll create a copy of the page below the one you cloned, and then make it immediately editable before you even have to save. This Clone action is an example of an Inputfield header action, and it accompanies the Move, Pub/Unpub and Trash actions for each page in PageEditChildren. I thought this ability to add Inputfield actions seemed useful beyond just this module, so the ability has been added directly into ProcessWire's core Inputfields JS API. If you grab the current dev branch of ProcessWire, you can test it out if you'd like. You can paste these examples in your web browser's Javascript console. But in actual web development, you'd likely put these in a separate JS file that is added to your page editor with a hook. Open the page editor to any page, then open your Javascript console in your web browser, and then paste this in: Inputfields.addHeaderAction('title', { icon: 'fa-hand-stop-o', callback: function() { ProcessWire.alert('Hello World!'); } }); That should add a hand icon to your "Title" field header, and if you click it, it'll pop-up a dialog box that says "Hello World". Let's say you wanted it to show a different icon when you hover it, and also to show tooltip text via a title attribute. You can add the 'overIcon' to specify what icon to show on mouseover, and 'tooltip' to specify the tooltip text: Inputfields.addHeaderAction('title', { icon: 'fa-hand-stop-o', overIcon: 'fa-hand-peace-o', tooltip: 'Hello world', callback: function() { ProcessWire.alert('Hello World!'); } }); Above are examples of 'click' actions. We also support toggle or "on/off" actions: Inputfields.addHeaderAction('title', { onIcon: 'fa-toggle-on', onTooltip: 'Click to toggle off', onCallback: function() { ProcessWire.alert('You toggled on'); }, offIcon: 'fa-toggle-off', offTooltip: 'Click to toggle on', offCallback: function() { ProcessWire.alert('You toggled off'); } }); Toggle actions like this are primarily what is used by the PageEditChildren module to handle the Published vs Unpublished state, and Trash on/off state. Whereas "Clone" is an example of a Click action. Our callback functions in the examples above are very simple alerts, but of course you could make it do anything you want in those callbacks. Also added this week to the Inputfield JS API was an update to the Inputfields.label() function that now allows you to set the label for an Inputfield (previously it could only get the label). This is a bit of a contrived example, but since we were talking about header actions above, let's demonstrate the label() function within a header action. This example adds an action that lets you edit the Inputfield label: Inputfields.addHeaderAction('title', { icon: 'smile-o', callback: function($f) { ProcessWire.prompt('Enter new label', 'Label', function(val) { if(val) Inputfields.label($f, val); }); } }); By the way, there's a lot more to the Inputfields JS API, and it's all documented directly in ProcessWire's inputfields.js file. I'll be bumping the core dev version next week to hopefully accompany the first version of the PageEditChildren module. Thanks for reading and have a great weekend!
    19 points
  43. Ever felt like your ProcessWire emails look like they're stuck in 1999? You know the drill - sending emails is super easy with WireMail: $m = new WireMail(); $m->from('foo@bar.com'); $m->to('xxx@yyy.com'); $m->subject('Hello there!'); $m->bodyHTML('<h1>This is great!</h1><p>I am an ugly mail...</p>'); $m->send(); But let's be honest - they look about as pretty as a website built with Microsoft FrontPage! 😅 🪄 Enter the Mail Pimp Hook! Drop this magical hook into your /site/ready.php (or even better Site.module.php), and watch your emails transform from ugly ducklings into beautiful swans: <?php $wire->addHookBefore('WireMail::send', function(HookEvent $event) { // double check that we got a wiremail instance // this also tells the IDE what $mail is (to get IntelliSense) $mail = $event->object; if (!$mail instanceof WireMail) return; // get current mail body $html = $mail->get('bodyHTML'); if (!$html) return; // get email layout markup $layoutFile = wire()->config->paths->templates . 'mails/default.html'; if (!is_file($layoutFile)) return; // replace ##content## with actual mail content $html = str_replace( '##content##', $html, wire()->files->render($layoutFile) ); // write new body to mail $mail->bodyHTML($html); }); The HTML Just create a beautiful MJML template at /site/templates/mails/default.mjml, put ##content## where your email content should go, convert it to HTML and BOOM! 💥 Every email gets automatically wrapped in your gorgeous template. No more CSS wrestling matches, no more "Why does this look different in Outlook?" headaches. Just pure email beauty, automagically! ✨ Now your clients will think you spent days crafting those emails, when in reality, you're sipping coffee while your hook does all the heavy lifting. Work smarter, not harder! 🚀 #ProcessWire #EmailMagic #NoMoreUglyEmails PS: This is the MJML template that I used: <mjml> <mj-head> <mj-attributes> <mj-all font-family="Tahoma" /> <mj-text line-height="140%" /> </mj-attributes> </mj-head> <mj-body background-color="#efefef"> <mj-section background-color="#ffffff" background-repeat="repeat" padding-bottom="30px" padding-top="30px" text-align="center" > <mj-column> <mj-image align="center" padding="25px" src="xxx" target="_blank" width="200px" alt="Logo" ></mj-image> <mj-text>##content##</mj-text> </mj-column> </mj-section> <mj-section> <mj-column> <mj-text font-size="10px" color="#a0a0a0" align="center" > powered by <a href="https://www.baumrock.com/" style="color: #158f66" >baumrock.com</a > </mj-text> </mj-column> </mj-section> </mj-body> </mjml> VSCode has an extension to get a live preview and export MJML to HTML: And here are some other free templates: https://mjml.io/templates I use https://www.base64-image.de/ to add the logo to my mail template as src="data:image/jpeg;base64,/9j/4QAWRXhpZgAATU0AKgAAAA..." to avoid headaches with image paths, remote assets blocking etc.
    18 points
  44. This week most of the core dev branch commits are related to minor fixes and improvements. While last week we added a new $page->preload() method, I’m going to avoid more major additions or features so that we’re not creating more things that need lots of testing. For that reason, the commits over the next weeks or month will be similar to those from this week, so that we can get a new main/master version out as soon as possible. I was just looking at the date of our last master version (3.0.229) and see that it’s been more than a year! It feels like it’s been 3 months to me — time sure does fly! Seeing how long it’s been definitely motivates me to not wait too much longer on this next main version. The current dev branch fixes and adds quite a few things relative to 3.0.229 as well, so I think of it as being the more stable version at this point… a good sign it’s about time for a new release version. Thanks for reading and have a great weekend!
    18 points
  45. Hello, I have created a simple module to preview theater seat reservations. This is my very first module, so be gentle as I'm not a coder. What is it about? The module creates 5 fields that must be added to the template of your choice. (e.g. event-post) In the template, you can then set the number of rows and the number of seats in each row. After save your preview is created. You can then book or cancel seats by clicking on the seats boxes or trash icon to cancel them. We have a small theater and sometimes we remove some seats, so I also added the option to remove them. Seat-booking.mp4 You can the render this on your frontend with: <?php // Assuming $page is the current page object $rows = $page->rows ?: 9; // Default to 9 rows if not set $seatsPerRow = $page->seats_per_row ?: 8; // Default to 8 seats per row if not set // Load the existing CSS for styling $cssFile = $this->wire()->config->urls->siteModules . 'TheaterSeating/styles.css'; echo '<link rel="stylesheet" href="' . $cssFile . '">'; // Start the seating chart output echo '<div class="theater-seating">'; // Loop through rows for ($i = $rows; $i > 0; $i--) { echo '<div class="row">'; // Start a new row echo '<div class="row-label">Vrsta ' . $i . '</div>'; // Row label // Loop through seats for ($j = 1; $j <= $seatsPerRow; $j++) { $seatId = "$i-$j"; $occupiedClass = in_array($seatId, explode(',', $page->booked_seats ?: '')) ? 'selected' : ''; $disabledClass = in_array($seatId, explode(',', $page->disabled_seats ?: '')) ? 'disabled' : ''; // Output the seat div echo '<div class="seat ' . $occupiedClass . ' ' . $disabledClass . '" data-seat-id="' . $seatId . '">'; // Add the cross overlay for disabled seats if ($disabledClass) { echo '<div class="cross">✖</div>'; // X overlay } echo '</div>'; // Close seat div } echo '</div>'; // Close row div } echo '<div class="stage">Oder</div>'; echo '</div>'; // Close theater seating div ?> and maybe style with: .seat { width: 50px; height: 50px; margin: 0 5px; /* Horizontal margin between seats */ background-color: #ccc; cursor: default; /* Change cursor to indicate no interaction */ display: flex; align-items: center; justify-content: center; position: relative; } .seat.occupied { background-color: #f00; /* Red for occupied seats */ } .seat.selected { background-color: #0f0; /* Green for selected (booked) seats */ } .seat.disabled { background-color: rgba(255, 0, 0, 0.5); /* Semi-transparent red for disabled seats */ } .cross { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255, 0, 0, 0.5); /* Semi-transparent overlay */ display: flex; align-items: center; justify-content: center; font-size: 24px; color: white; } .row-label { font-size: 16px; margin-right: 10px; /* Space between the label and seats */ font-weight: 600; width: 100px; /* Set a fixed width to align labels */ } I hope someone will find it usefull. Fell free to make it better. 😉 You can download it here: TheaterSeating.zip Cheers 😉 Roych
    18 points
  46. Hey @ryan thank you very much for the invitation to write a blog post. I'll probably ping you about that when my client projects that I'm working on are done! Regarding selling modules: I want to add that it can be very complicated to sell stuff online in the EU due to tax regulations. In very short as soon as you hit 10.000€ revenue you need to charge tax in the country of the buyer, which can be 7% CH, 19% DE, 20% AT etc... There are companies that offer the service to sell stuff for you, so you only have one customer instead of hundreds. But they charge for it, of course. So if you ask me it would be ideal if PW had a shop for commercial modules where all you have to do is add a github link to a private github repo and PW does the rest. Which is invoicing, taxes, generating license keys, offering download links etc. I know it's a huge task to do, but if I'm just sharing my point of view from selling modules. It would also be great if the customers were able to update modules with one click, just like they can do with regular modules. In case of commercial modules the'd need to have a valid license key in their settings, of course. And of course, that should only be done on development environments. I'd be more than happy to pay a reasonable share for that and support the project with my work. ------------------------ But what I actually wanted to say: @ryan could you please make it more obvious how great PW is and how actively it is developed? I think @teppo does a great job with his weekly newsletter and that also deserves a lot more attention! I just checked the website and the github repo again. I know we've "lost" people just because they looked at the github repo and saw the last commit on the main branch was over a year ago and they thought the project is dead. We all know better, but newcomers don't and the amount of work put into PW is unbelievable and people should instantly see this! I also checked the download page on the website: These two buttons probably don't mean anything to a newcomer. And the sentence below is really hiding itself. What about something like this? The breaking change thing might get a footnote that we are talking about breaking changes to the API here. English is not my first language so there might be better wordings, but you hopefully get the idea. Or maybe something like this: I think "we" already have an absolutely great product and we can be very happy to be able to use it. @ryan I hope you are very proud ? But I think this is not obvious to someone evaluating ProcessWire and maybe we could do more in that context. I don't know if growing the userbase of PW is a goal of @ryan at all or should be. But at least for selling modules it would be a benefit. Also when looking for a job it's a lot easier to find an employment as a WordPress dev compared to a PW dev, I guess. An interesting retrospective: I remembered we had a discussion about this topic some time ago. Turns out "some time ago" was 2017 ? https://processwire.com/talk/topic/17348-is-pw-becoming-more-popular/ It's an interesting read and shows that the same still applies 7 years later. wow. Ok so I googled a bit and found that the current website of processwire.com seems to be from 2019: https://processwire.com/blog/posts/pw-3.0.124-and-new-site/#new-website-launched And while I can remember that a lot has improved it's still not a great website at first sight, to be honest. @ryan please don't get me wrong, I know it's a lot of work and it's a great website and I know it has a lot of great stuff under the hood (like multiple instances talking to each other via API and bootstrapping, etc). Or take the blog post: That's great, but no website visitor will ever realise that! All a first time visitor will see is an more or less default UIkit theme which looks outdated to be honest. Compare that to some agencies from the showcase section (eg https://typneun.de/) and you'll notice a difference ? Maybe one of them would be happy to sponsor a facelift for processwire.com ? At least @heldercervantes showed his interest in that in 2017. Ok, so I explored a bit more... And compared... What do the folks at Nette do? From the Design imho not a lot better if at all, but I love the catchy statement at the top and they instantly show some big brands that sponsor the project which instantly builds trust. I'm not a marketing or communication specialist, but this is some basics I realise. I'm quite sure we have experts that find a lot more room for improvement ? I'd also love to see sponsorship packages for processwire where agencies can support the project and also SHOW the support. Something like this maybe: Gold sponsor Logo on frontpage 999$ / year Silver sponsor Name on frontpage 499$ / year Bronze sponsor Fund 1 hour of bug-fixing 99$ / year Maybe github issues of gold sponsors could be addressed with priority or such. I don't know. Don't get me wrong again. I like that PW is not dependent on someone elses money, but it will definitely not hurt to add the opportunity for PW users to give something back without asking for any return (which is different to buying pro modules). I know that many buy pro modules just to support ryan, which is nice but again is not obvious and does not build trust for anybody viewing the pw.com frontpage. Ok so I jumped back to pw.com ... For the record, here is a current screenshot: So I clicked on "demo" - I haven't been there for some years... Ok... Frontend... Ufff. 2024? Doesn't look like. And again it hides all the beauty that lies under the hood. Ok, so over to the backend demo: A lot has been said about the pw backend already, this shows that it might be a worthwhile area to work on. What it shows is a big orange warning about an .htaccess issue. This is probably not a good invitation for anybody new. Maybe it even draws people off. Instead it should show a huge warning like this: What if we dropped the skyscraper profile and instead built a new modules directory which can serve as a new showcase? Keeping a profile up to date that is not used (and probably not of any use other than showcasing pw) is maybe unnecessary work if the same thing could be done with something that creates additional value. Also I think it could be good to showcase the PW can be an absolutely great choice for modern frontend tools like TailwindCSS, AlpineJS or HTMX. I think we could really do a lot on that front just by thinking less about the product (pw is great and I think that is not going to change, whatever we do or don't do) and more about how to communicate this. What about a section where we showcase "ProcessWire as XXX Alternative in 2024" Many people are unhappy with existing CMSs. If I was unhappy with WordPress I'd probably google "wordpress alternative 2024". Many products do that and have comparison pages on their website where they show strengths and weaknesses of both tools and by that make it easier for people to understand what PW is, what it can do, where it is good at and where other tools might be better. This post has again got far too long ? But one final note: I think it would be great if PW not only was a great product but also helped people using the tool make money. What do I mean by that? IMHO ProcessWire does not sell itself. It can be hard to compete with other tools that are more popular. At the moment basically anybody doing business with PW has to "sell" processwire as the correct tool on his own. Not everybody is good at marketing though, so it would be great if that was done by experts. For example we could have professional guides why pw is the right tool for job x or y on the official website. Like @dotnetic's post for example: https://dotnetic.de/blog/processwire-the-best-cms ; Edit: Note that I don't think that this is the responsibility of PW, but I try to say that PW could make it easier for us on the marketing front and everything that helps people that are using PW make money will help the project as they will have more money to spend or more resources to contribute or have more spare time to share modules with the community. The idea is to establish a strong win-win situation. I don't know how exactly but I'd love to see processwire being a tool that, when chosen, let's the dev assured that he/she can on one hand trust the product and rely on it and on the other hand also know that there is a market for it so that he/she can make a living and that it's worth learning and exploring the system. PS: If anybody has not yet starred the pw repo and is still reading, please do it now ? https://github.com/processwire/processwire
    18 points
  47. This week on the dev branch are a few updates to the core including the following: Support for Inputfield header icon actions defined in PHP. If you recall, a couple weeks ago we released header actions for Inputfields via the JS Inputfield API. Robin S requested that the same be available from the PHP Inputfield API, and now it is. More details can be found in the phpdoc for the Inputfield::addHeaderAction() method here. New link action was added to Inputfield header actions. This was also added by request and it's another type of header action that simply links to a URL. It can optionally be opened in a modal window. The $database API variable has been updated with a new $database->reset() method which resets the database connection. This is potentially useful after a "MySQL server has gone away" type error, to re-establish the connection. The $database->execute() method also now uses it to attempt to reestablish the connection (for a few tries) when appropriate. Though I've not yet been able to test this one with an actual lost connection. There were various other minor updates, optimizations and fixes this week as well. In addition to the above core updates, we've got a new version of the UserActivity module (v8) posted in the ProDevTools support board. This version adds support for the PageEditChildren module and can properly identify which child pages are being edited alongside the parent. Previous versions only identify the page open in the page editor, and not the children open in PageEditChildren. Thanks for reading, have a great weekend!
    18 points
  48. Here's a copy of my blog with some reflections on building my first site with ProcessWire as someone coming from Drupal: peopleandplanet.org ProcessWire is an open source CMS Content Management System or Framework (CMS / CMF respectively) using PHP and MariaDB/MySQL. It's been around a while, humbly gathering users and sites. I spent a good while reviewing lots of open source CMSes recently as I have previously relied on Drupal 7 (excellent) and didn't like Drupal 8+ nor BackDrop (a fork of Drupal 7). WordPress? not with Gutenberg/React and all those plugin ads, thanks. Turns out I'm quite picky about CMSes! This is because my role is developer, trainer, implementer, themer, discusser of problems and solutions and dreams. I have personal relationships with my clients and am here to help. So I need a system that makes it simple for them to do as much as possible without me, while also being flexible enough for me to rapidly develop new features. So I was shopping for me and my clients, not just one of those parties. ProcessWire seemed a good balance, and once I started developing with it I was hooked. I've now launched my first site with it: peopleandplanet.org and my clients are pretty pleased with it, and I have another job in the pipeline. Templates and pages In ProcessWire, everything (even users!) is a Page, and every Page has a Template. So in Drupal-speak, it's kinda like Page = Content/Entity and Template = Content/Entity Type. A Template is configured with lots of options and describes what fields apply. Templates don't all need to be renderable, but typically are, so generally have an accompanying Template File. Key implementation decisions I made There are many ways to generate and theme page HTML in ProcessWire and I went through them all! Here's what I settled on: Use Page classes: these are PHP classes which add/bend functionality for a particular page/template. Doing pre-processing of data this way seemed the cleanest way to organise things, using inheritance to share code between similar templates. I used the excellent Latte templating engine instead of plain PHP or other options like Blade/Smarty/... Latte is context-aware which makes templates cleaner and clearer to look at and safer because it knows to escape content in one way as HTML text and another as an attribute, for example. The final clincher is that it uses the same syntax as PHP itself, so as a developer hopping between PHP and Latte, there's much less brain strain. Use PageTableNext. This is similar to Drupal's Paragraphs or Gutenberg's Blocks (ish). It allows a page to be built from slices/sections of various different templates, e.g. I have things like "text" and "text-over-image" and "animated stats" etc. These let authors make interesting pages, applying different preset styles to things, giving a good mix of creative control and theme adherence. What I liked Beyond the above features, I liked these things: Fairly unopinionated: the core is quite small and everything is a module, so when you write your own module, you have similar level of access. e.g. I was able to make a core module behave differently by using hooks rather than having to maintain a patch of a core code file. The selector API is a domain-specific query language for fetching page data that makes a lot of common things you need to do very easy and clear to read. I like readable code a lot. A lot of basic CMS features are implemented really nicely; thought has gone into them. e.g. Drupal has a redirect module that can add redirects from old URLs when you update the 'path alias' for a page - great - but ProcessWire's implementation (a) prevents you making circular redirects which is a quick way to kill a Drupal site by accident that's happened more than once, and (b) has some useful rules like let's only do this if the page has been in existence for a little while - because typically while first composing a page you may change the title quite a few times before you publish. e.g. when you save a page that has links to other pages in it, it stores the page IDs of those other pages too, and when the page is fetched it will check that the URLs exist and still match the ID, fixing them if needed. Images - have 'focus' built in as well as resizing etc. so if a crop is needed you can ensure the important content of the image is still present. Booting is flexible; it enabled me to boot ProcessWire from within Drupal7 and thereby write a migration script for a lot of content. There's a good community on the forum. A forum feels a bit old fashioned, but it's good because you can have long form discussions; it sort of doubles as a blog, and a lot of new features are announced in forum posts by Ryan and others. The Tracy debugger is mind-blowingly good, just wow. Modules - third party or core - are typically very focussed and often tiny. This is a testament to what can be achieved throught the flexible and well-designed APIs. Weekly updates on core development from both the lead developer on the blog and the community, both with RSS feeds so it's easy to keep updated. What I don't like Logging is weird and non-standard. I prefer one chronological log of well categorised events, but instead we have lots of separate files. This is a bit weird. One thing it is opinionated on is that there should be a strict hierarchy between pages and URLs. I like that level of order, but in real life, it's not what I needed. e.g. People & Planet has three main campaigns and it wanted those at /campaign-name not /campaigns/campaign-name. And we wanted news at /news/2024-03-25/title-of-news-article, but we don't want a page at /news/2024-03-25/ and we want the news index to be under an Info page in the menu. This meant I had to write a custom menu implementation to implement a different hierarchy to display what we wanted it to look (3 campaigns under a Campaigns menu, the news index under an Info menu). There was a page hook for having news articles describe their URLs with the publish date, but this took quite a bit of figuring out. Ryan Cramer leads/owns the project and other contributors are sparse. He seems like a lovely person, and I'm super grateful for his work, but there's only one of him, so this is a bit of a risk. Also, the code has no phpunit tests. Gulp. There have been various initiatives, but this sort of thing needs to come from a core team, and it's not a priority for Ryan. I love tests, they help avoid regressions, help document designed behaviour, etc. Likewise, there's a styleguide, but it's not adhered to, so... Right decision? I'm very happy with it, and it seems a great fit for my clients' needs, and they're very happy. All software has risks and I got burned twice with Drupal 8/9 - once backing out after months of development; another project that went to completion I regret and dislike administering/maintaining now. But so far, so good, and I intend to use ProcessWire for other projects (including replacing this website) very soon. Contributions I have two ProcessWire modules that are there for anyone who needs them. TagHerder provides a page at Setup » Tag Herder that lists every tag and every page tagged with that tag, and provides three options: Rename the tag; Trash the tag; Replace the tag with another. Useful for cleaning up tags that have gotten out of control! EditablePublishedDate lets you edit the published date of a page. Useful for entering historical page information or such.
    18 points
  49. On the dev branch this week are a few issue fixes, but also some new features. The "Parent" field in the page editor (Settings tab) now is contextual to the page's template family settings. Now it will just show you the allowed parents, if you've configured them in the template. Previously it would show you a page tree to select from anywhere in the site. This saves you time, as well as the hassle of getting an error message after save, should you select a parent that isn't allowed by the page's template family settings. Next, the page tree "Move" actions (that you see when hovering a page in the tree) are now a little smarter. Previously, these Move actions would appear for any page that was either sortable or moveable. But now it also checks how many other allowed parents there are, per template family settings. If there aren't yet any other potential parent pages existing in the site, the page is no longer considered moveable, and thus won't show a Move action. This useful addition was added per Bernhard's request, as was the addition to a couple new classes used in the page tree to better differentiate between public vs non-public pages... something that I understand Bernhard's admin style may soon demonstrate. This week a question came up through email about how to make multi-language "required" fields require a value in all languages the page is active in. Currently multi-language required fields only require a value to be present in the default language. If you want to make them require all active languages, you can do so with a hook in /site/ready.php. I thought it was pretty useful so thought I'd share it here. Though maybe we'll have to add it as a feature. if($page->process == 'ProcessPageEdit') { $wire->addHookAfter('Inputfield(required=1,useLanguages=1)::processInput', function($event) { $inputfield = $event->object; $page = $inputfield->hasPage; if(!$page) return; foreach($event->wire()->languages as $language) { if($language->isDefault()) continue; if(!$page->get("status$language")) continue; // skip languages not active on page $value = $inputfield->get("value$language"); if(empty($value)) { $inputfield->error("Value required for language " . $language->get('title|name')); } } }); }
    18 points
  50. This week we have a new core version on the dev branch. Relative to the previous dev branch version, the new 3.0.237 version has 33 commits containing 20 issue fixes, 6 feature requests, and more. See the core updates section of ProcessWire Weekly 511, 512, 514 and 515 for more details. It's been about a month and a half since 3.0.236, which is a little longer than usual between version numbers, but that's largely because if the new Invoices site profile (see blog post). I'm off work tomorrow (Friday), so writing the usual weekly post a day early. As always, thanks for reading and I hope you have a great weekend!
    18 points
×
×
  • Create New...