Jump to content

Leaderboard

Popular Content

Showing content with the highest reputation on 12/14/2023 in all areas

  1. If you haven't yet noticed it, @teppo has hit the 500th issue of PW Weekly! That is a ridiculously massive milestone and an amazing achievement, and after some quick maths, also tells me that he's held strong for almost 10 years now! I'm not sure what's more surprising, that he's managed to keep it going continuously for this long, or that I remember when he started it... That's a long darn time. I may not be a hugely active member of the community, but I'm darn proud to be a part of it regardless. Thank you so much for your devotion to the PW Weekly project, @teppo!!!!
    5 points
  2. In this tutorial I want to show you how to set up a simple ajax-driven auto-complete search within ProcessWire. Something that looks like this: Requirements: 1. Use the Fieldtype Cache to create a search index For fast and easy search queries we will create a search index field. You can read more on this here: https://processwire.recipes/recipes/set-up-search-index-with-fieldtypecache/ First of all, go to Modules and install a Core module called FieldtypeCache. Add a new field “search_cache” and select “Cache” as it’s type. Save the field. On the Details tab choose the fields you want to make searchable. Save again. Add “search_cache” to all templates you want to include in your search results. Optional but recommended: use the “Regenerate Cache” option found from the Details tab of the field to make existing content instantly searchable. In my case i only want to search inside body and title fields, so I included those fields in the settings of the search cache field: 2. Install the Pages2JSON module We want to make ajax requests to the search template and those results should be returned in JSON format so that our java script can process the results inside the auto-complete dropdown. Therefore we make use of this module here: https://processwire.com/modules/pages2-json/ In the module settings we define what data will be included in the JSON object that is returned. Remember that this is the data that we want to display in our auto-complete dropdown. So for my case I only want the title and the url field. Now let's start: 1. Setting up the search template Set up a template file called „search“. Then create a page using this template. On most ProcessWire installations this is already the case and this template exits in the file system. This template will handle the search queries and list all the results for us and that in two ways: - you can send a search query and the search result page will list all the results (as you would expect) - you can send an ajax search query and the result page will return the results as a json object to be processed within javascript and displayed in the frontend in real-time search.php: <?php if($config->ajax) { // Return search results in JSON format $q = $sanitizer->selectorValue($input->get->q); $results = $pages->find('search_cache%=' . $q);; // Find all pages and save as $results header("Content-type: application/json"); // Set header to JSON echo $results->toJSON(); // Output the results as JSON via the toJSON function return $this->halt(); } ?> <main> <div class="uk-container"> <?php // look for a GET variable named 'q' and sanitize it $q = $sanitizer->selectorValue($input->get->q); // did $q have anything in it? if($q) { // Find pages that match the selector $matches = $pages->find('search_cache%=' . $q); // did we find any matches? ... if($matches->count) { echo "<h2>We found $matches->count results:</h2>"; echo "<ul class='uk-list uk-list-square'>"; foreach($matches as $match) { echo "<li><a href='$match->url'>$match->title</a>"; echo "<div class='summary'>$match->summary</div></li>"; } echo "</ul>"; } else { ?> <h2>No results found.</h2> <div uk-grid class="uk-flex uk-flex-center"> <div class="uk-width-1-1 uk-width-1-2@m"> <?= $files->render('elements/_searchbox'); ?> </div> </div> <? } } else { ?> <h2>Search:</h2> <div uk-grid class="uk-flex uk-flex-center"> <div class="uk-width-1-1 uk-width-1-2@m"> <?= $files->render('elements/_searchbox'); ?> </div> </div> <? } ?> </div> </main> Explanation: This part here at the top of the template handles the requests that are send via ajax. This is the important part for later on. <?php if($config->ajax) { // Return search results in JSON format $q = $sanitizer->selectorValue($input->get->q); $results = $pages->find('search_cache%=' . $q);; // Find all pages and save as $results header("Content-type: application/json"); // Set header to JSON echo $results->toJSON(); // Output the results as JSON via the toJSON function return $this->halt(); } ?> What this does: check if the current request is an ajax request if so, search inside the search cache field return the results in JSON format then quit processing the rest of the template (we don’t want to render any markup in that case!) Everything below this part is the normal search template logic. If you send a search request via a form somewhere on the website you want to be redirected to the result page and all the results will be listed on the page just like you would expect. 2. Make the search „work“ Create a template file called "_searchbox.php" (this file is also included in the search template code above, so adjust your paths/names accordingly). To make search requests you want to include this search form anywhere on your page. _searchbox.php: <div class="search-wrapper uk-padding uk-text-center"> <h4>Searchbox</h4> <form class="searchform uk-position-relative uk-flex" method="get" action="<?= $pages->get("template=search")->url ?>"> <input class="uk-input" id="searchInput" name="q" type="search" aria-label="Suchen" autocomplete="off" placeholder="z.B. Bagger"> <button class="uk-button uk-button-primary search-button" type="submit"> <span class="uk-visible@s">Search</span> </button> <div id="suggestions" class="uk-box-shadow-medium"> </div> </form> </div> With this simple search form you should now be able to do a basic search that leads you to a search result page. 3. Make the ajax-search „work“ Now comes the interesting part. We will add in a java script snippet into our just created _searchbox.php that sends ajax requests to the search template page while we are typing into the search intput field and it will display the results in a nice little dropdown. Feel free to adjust the code to your needs! <script> document.addEventListener("DOMContentLoaded", () => { const searchForm = document.querySelector('.searchform'); const searchUrl = searchForm.getAttribute('action'); const searchInput = document.getElementById('searchInput'); const suggestionsDiv = document.getElementById('suggestions'); let selectedSuggestionIndex = -1; // close the auto-complete container when clicked outside the element document.addEventListener('click', function(event) { if (!suggestionsDiv.contains(event.target)) { suggestionsDiv.style.display = 'none'; } }); searchInput.addEventListener('input', () => { const searchText = searchInput.value.toLowerCase(); // Immediately Invoked Function Expression (IIFE) (async () => { try { const response = await fetch(searchUrl+"?q="+searchText, { headers: { 'X-Requested-With': 'XMLHttpRequest' } }); if (response.status === 200) { const data = await response.json(); showResults(data); } else { console.log(response.status); console.log(response.statusText); } } catch (error) { console.error(error); } })(); function showResults(data) { // Show suggestions only if the input has at least two characters if (searchText.length >= 2) { const suggestionHTML = data.map(item => { // Highlight the matching characters using a <span> element with a CSS class const highlightedTitle = item.title.replace( new RegExp(searchText, 'gi'), match => `<span class="highlight">${match}</span>` ); return `<li class="suggestion"><a href="${item.url}">${highlightedTitle}</a></li>`; }).join(''); // Array to string conversion suggestionsDiv.innerHTML = ''; // Clear the suggestions if input length is less than two characters // create list and append search results const suggestionList = document.createElement("ul"); suggestionList.classList.add('suggestion-list', 'uk-list'); suggestionList.innerHTML = suggestionHTML; suggestionsDiv.appendChild(suggestionList); selectedSuggestionIndex = -1; // show the results suggestionsDiv.style.display = "block"; } else { suggestionsDiv.innerHTML = ''; // Clear the suggestions if input length is less than two characters } } }); // Event listener for arrow key presses searchInput.addEventListener("keydown", function (event) { const suggestions = document.querySelectorAll(".suggestion"); if (event.key === "ArrowDown") { event.preventDefault(); selectedSuggestionIndex = Math.min( selectedSuggestionIndex + 1, suggestions.length - 1 ); } else if (event.key === "ArrowUp") { event.preventDefault(); selectedSuggestionIndex = Math.max(selectedSuggestionIndex - 1, -1); } else if (event.key === "Enter") { event.preventDefault(); if (selectedSuggestionIndex === -1) { searchForm.submit(); } else { window.location = suggestions[selectedSuggestionIndex].querySelector('a').href; } } // Highlight the selected suggestion suggestions.forEach((suggestion, index) => { suggestion.classList.toggle( "selected", index === selectedSuggestionIndex ); }); }); }); </script> Keep in mind that you need some CSS styes to make it look good and actually work. These are my styles(in LESS format) but feel free to write you own stylesheet for this. search.less: .search-wrapper { position: relative; background: @secondary-blue; h4 { color: @primary-blue; } .highlight { color: @primary-red; } } #suggestions { display: none; position: absolute; top: 100%; left: 0; right: 0; z-index: 10; background: @white; .suggestion-list { margin: 0; li { transition: background-color 150ms ease-in-out; a { padding: 10px; display: block; text-align: left; &:hover, &:focus, &:active { text-decoration: none; } } &:hover, &:focus, &:active, &.selected { background: @secondary-blue; } } li + li { border-top: 1px solid @secondary-blue; } } } That's it! Again feel free to adjust all of the code to your needs. If you have any suggestions how to achieve things a bit easier just let me know.
    4 points
  3. Hear, hear! I love the PW Weekly - a highlight of the week! Putting out such a well-written newsletter week after week for 10 years - what an incredible contribution to the community! Hats off to @teppo! I like to read each new issue when it's hot off the press, which is normally on a Sunday (NZ time), but the official email from newsletter@processwire.com doesn't arrive until the following Saturday. A long while ago I asked Ryan why this is and there was a sensible reason that I've forgotten now, but I can't wait that long for my PW news ?. So my solution has been to set up an email alert for the RSS feed using Blogtrottr - that way I can dive into each issue when it's at maximum freshness ✨?
    4 points
  4. Working on the installer (to handle the new features). I'm hoping to finish by next week and release 009 beta. Hopefully we are in for a Christmas treat ?.
    1 point
  5. @kongondo no worries at all and thank you as always for the explanation. Yeah, was straight forward enough to be honest!! All gravy. Keep up the good work boss! Which reminds one! When are we all due a renewal fee to support the development time?!
    1 point
  6. So it seems this is field assignation that triggers change detection, it works like that: $pageSessions = $page->raceSessions; $pageSessions->removeAll(); foreach ($sessionsTemplate->raceSessions as $raceSession) $page->raceSessions->add($raceSession); $page->raceSessions = $pageSessions; // Assignation triggers change detection
    1 point
  7. Yes this is what I was doing... but turns out I had a typo and it was loading my 404 page in the panel I think. This is why I was getting the nav bar showing ?. The panels now work exactly as expected (and as you describe) to load my module functionality! ?
    1 point
  8. I've written before about how to use Twig with ProcessWire (see my tutorials on integrating Twig into ProcessWire and extending Twig with custom functionality for ProcessWire). But those posts don't really talk about why I like to use Twig instead of plain PHP templates. For me, this comes down to one killer feature that I'm going to talk about below. But first, let's look at some of the more commonly mentioned advantages of Twig and why I don't actually think they're all that important in the context of ProcessWire: The syntax is nicer. While I personally agree with this, it's entirely subjective (and familiarity is comforting while trying something new is scary). Autoescaping provides security by default. This is true to a degree, but most ProcessWire projects (at least for me) aren't the type of expansive community-driven sites with lots of user-generated content where this would be most relevant. Most of my ProcessWire projects so far have featured a few trusted editors managing content, where you don't really need autoescaping for every template to make sure nobody slips in some malicious code. Twig forces separation of concerns between logic and presentation. Again, this is true, but not relevant to most ProcessWire projects. Most of my ProcessWire projects (and, judging by the showcase, most ProcessWire projects period) are mostly classic brochure sites without a lot of interactivity or app-like behaviour. Those projects are 99% presentation with only some small snippets of logic in between, so separating the two isn't really an issue. With that out of the way, let's talk about the killer feature that makes Twig essential to my work: Inheritance and block-based overwrites. To explain why this is important, I'll start out with a basic template for a header component in PHP and see how it can handle additional content being added to it. Then I'll write the same component in Twig for comparison. If you need a general guide on template inheritance in Twig, read this first: https://twig.symfony.com/doc/3.x/tags/extends.html The reusable header template Here's our basic reusable header template written in PHP: <header class="header"> <h1 class="header__headline"><?= $page->title ?></h1> <?php if ($page->subline): ?> <p class="header__subline"><?= $page->subline ?></p> <?php endif; ?> <?php if ($page->image) echo wireRenderFile('inc/responsive-image.php', ['image' => $page->image]) ?> </header> Sidenote, I'll use wireRenderFile to keep the examples brief, you could also write the image tag inline here. The header component may be included in a page template like this: <?= wireRenderFile('inc/header.php') ?> You have two options for where to do this. Option one is to include this template in every template that needs it (templates/home.php, templates/project.php, templates/news.php). Option two is to use the appendTemplateFile setting to keep the basic page layout in a shared template file that's always included at the end of the request (_main.php). Option one allows you to pass the template different variables depending on context, but it also means you've already started with the code duplication. Option two is probably the more common approach, but with this option you can only pass it one set of variables – those variables might be overwritten by the page template, but this will also lead to some problems as you'll see shortly. Let's introduce our first change request, one particular page needs to display a video instead of an image. No problem, we can just check if the page has a video field and display it conditionally: if ($page->video) { wireRenderFile('inc/video.php', ['video' => $page->video]); } elseif ($page->image) { wireRenderFile('inc/responsive-image.php', ['image' => $page->image]); } This still works fine. But crucially, the logic for the video header is now part of the header template, not part of the template for the page with video headers. This means that every time I want to edit the header template, this little piece of conditional logic is something I have to deal with. But that's fine, multiple pages might need a video header, so having this switch in the header template is acceptable. But then another change request come in: In the page template for some kind of project page, instead of the image, we want to display a list of project data coming from a separate project_data field. Again, we can adjust the template: if ($page->project_data) { wireRenderFile('inc/project-data.php', ['data' => $page->project_data]); } elseif ($page->video) { wireRenderFile('inc/video.php', ['video' => $page->video]); } elseif ($page->image) { wireRenderFile('inc/responsive-image.php', ['image' => $page->image]); } But now some display logic that's specific to one template is part of the global header template, not part of the project.php. This trend will continue: every custom feature required for the header of any page template will inflate the header.php file, and every adjustment requires reading all of it and making sure my change doesn't break any of the other features. This is unsustainable and inherently unscalable. Another example, what if a specific page has both the video and the image fields, but I want to display the image instead of the video? Currently, this is not possible. Now I have to build in some kind of switch: $preferImage = $preferImage ?? false; if ($page->project_data) { wireRenderFile('inc/project-data.php', ['data' => $page->project_data]); } elseif ($page->video && !$preferImage) { wireRenderFile('inc/video.php', ['video' => $page->video]); } elseif ($page->image) { wireRenderFile('inc/responsive-image.php', ['image' => $page->image]); } Again, this solution doesn't scale. Did you notice the subtle bug in there? The noise to signal ratio is becoming worse with every feature. Now you're probably thinking that you would approach those change requests in a different way. Let's look at some of the possible solutions to those problems. Lots of variables You can solve this to a degree by using lots of variables to control what you're template is doing. If we're using a shared _main.php template file that includes the inc/header.php template, the project-specific template (e.g. templates/project.php) is loaded first. So those templates can set some variables that change the content and behaviour of the header component. For example, say you want to do keep the template for the project data block in your project.php so it's easy to find. Let's go back to the original header template and introduce an optional variable that can be used to replace the image with something else: <?= $headerImageContent ?? wireRenderFile('inc/responsive-image.php', ['image' => $page->image]); Now you can set the $headerImageContent variable in your project.php and it will replace the image. But what if I want both the normal image (without duplicating code) AND some custom content? No problem, add even more variables: <?= $headerImageBeforeContent ?? '' ?> <?= $headerImageContent ?? wireRenderFile('inc/responsive-image.php', ['image' => $page->image]); <?= $headerImageAfterContent ?? '' ?> Now repeat that for every part of the header template which might need to be adjusted for some of the page templates (hint: it's all of them). You end up with a template that uses tons of variables, the signal to noise ratio becomes abhorrent. Throw in the fact that those variables are all unscoped, so there's no way to tell where they are being set or overwritten, and variable names have be very specific to avoid collisions. All of this might make sense to you the day you've written it, but what about your colleague that hasn't touched this project yet? What about yourself in six months? Make templates more granular Another solution is to make the templates more granular. I've started this trend above by using wireRenderFile to put little isolated template parts into their own dedicated template – for example, to display a single responsive image or an HTML5 video player. In the same grain, you can split up the header.php into multiple smaller template to mix and match and include include those you want to in each specific context. But this has downsides as well: You end up with a fractal nightmare, a deluge of templates with increasing granularity and decreasing utility, just to be able to include those smaller templates separately from each other. Cohesion and readability is reduced, and there's no way from directory structure alone to tell which templates go together in what ways. Splitting an existing template into two smaller templates is not backwards compatible – you have to make an adjustment in every place the original template was included. Or you keep the original template but change it to just include the two new templates. I said fractal nightmare already, didn't I? Duplicate code You can, of course, just keep separate header templates for each page type. But then you're duplicating the common parts of those templates all over again, and changing those means you have to touch a lot of separate files – definitely not DRY. Most real-life solutions will include a mix of the three approaches. I tried to be fair and write the templates in the leanest and cleanest way possible, but things still got out of hand quickly. Now let's look at the same component written in Twig: Resuable components in Twig Here's the basic header template but written in Twig: {# components/header.twig #} <header class="header"> <h1 class="header__headline">{{ page.title }}</h1> {% if page.subline %} <p class="header__subline">{{ page.subline }}</p> {% endif %} {% block header_image %} {% if page.image %} {{ include('components/responsive-image', { image: page.image, }) }} {% endif %} {% endblock %} </header> One important difference is the block tag defining the header_image block. So far we don't need that, but it will become important in a second. For the page templates, it's common to have a base template that all other templates inherit from: {# html.twig #} <!doctype html> <html lang="en" dir="ltr"> <head> <title>{%- block title -%}{%- endblock -%}</title> {% block seo %} {{ include('components/seo', with_context = false) }} {% endblock %} </head> <body> {% block header %} {{ include('components/header') }} {% endblock %} {% block content %}{% endblock %} {% block footer %} {{ include('components/footer') }} {% endblock %} </body> The base template defines some blocks and includes some default components (seo, header, footer). Now the template for the project page just inherits this: {# project.twig #} {% extends 'html' %} With the PHP template, things got difficult once we wanted to overwrite part of the header template with some content specific to one page template. This is where the header_image block comes in handy: {# project.twig #} {% extends 'html' %} {% block header %} {% embed "components/header" %} {% block header_image %} {# project data template … #} {% endblock %} {% endembed %} {% endblock %} Now the project.twig extends the base html.twig template and overwrites the header block. Then it includes the components/header template and overwrites only the header_image block while keeping the rest. This approach has some major advantages over the plain PHP template: All the code for the project template is in one place – to see what's special about this particular page in comparison to the base template, I just have to look at one template. I didn't have to repeat any of the header template code, so I can still change the header in a central place. The components/header template stays small and manageable, it doesn't know or care what other templates extend it and which parts get overwritten where. As a sidenote, some people may not like the embed syntax. Another approach would be to once again create a custom header template for the project template. But this time, we don't need to repeat any code because we can use inheritance: {# components/project-header.twig #} {% extends "components/header" %} {% block header_image %} {# project data template … #} {% endblock %} I prefer the embed approach because it keeps all the related code together. But both approaches allow for full flexibility with no code duplication. Now what if you want to change other parts of the components/header.twig template in an extending template? In this case, you can always add more blocks: {# components/header.twig #} {% block header_headline %} <h1 class="header__headline">{{ page.title }}</h1> {% endblock %} Adding blocks doesn't change anything about the base template, so it's 100% backwards-compatible. You can always add more blocks without ever having to worry about breaking any existing templates or introducing bugs. Another challenge for the PHP template was to add some additional content to a part of the header template while still keeping the default content. Let's say we want to display a publication date above the headline in a news template, but keep the headline as is. No problem: {# project.twig #} {% block header_headline %} <time>{{ entry.published_date }}<time> {{ parent() }} {% endblock %} The parent() function returns the content of the block in the base template, so you can extend a block without overwriting it completely. Conclusion You can solve all the challenges I posed here in PHP. Most solutions will include a combination of the three approaches mentioned above (making templates more granular, using lots of variables and duplicating code). And a well thought-out mix of those approaches can work reasonably well. The problem is that while those solutions improve reusability and scalability, they usually require lots of boilerplate code and unscoped variables. This reduces the readability and makes the system harder to modify, while making it easier for bugs to creep in. Again, there are solutions for those problems that introduce other problems until the solutions cancel each other out in trade-offs. To me, Twig is a great alternative that requires fewer trade-offs. It allows you to achieve complete freedom and flexibility in your templates all while keeping your templates DRY and keeping code that belongs together in a single file. On top of that, Twig uses a nice, readable syntax (warning: personal opinion) and provides a lot of utility methods and other features to improve your template structure. Some notable caveats to all of this: All of the discussed problems are about scaling a project to a larger scope or team size. For small projects that will never need to scale in this way, this doesn't really matter. ProcessWire's built-in markup regions seem to tackle a lot of the same problems I mentioned in this post. Can't really speak for it as I haven't tried it yet. If this all sounds interesting to you and you want to learn more, you can check out my tutorials on integrating Twig into ProcessWire and extending Twig with custom functionality for ProcessWire.
    1 point
  9. I have recently started to integrate Twig in my ProcessWire projects to have a better separation between logic and views as well as have cleaner, smaller template files as opposed to large multi-purpose PHP-templates. Though there is a Twig module, I have opted to initialize twig manually to have more control over the structure and settings of the twig environment. This is certainly not required to get started, but I find that having no "secret sauce" gives the me as a developer more agency over my code structure, and better insights into how the internals of the libraries I'm using work. Framework like Drupal or Craft have their own Twig integration, which comes with some opinionated standards on how to organize your templates. Since Twig is not native to ProcessWire, integrating Twig requires one to build a solid template structure to be able to keep adding pages and partials without repeating oneself or having templates grow to unwieldy proportions. This will be an (opinionated) guide on how to organize your own flexible, extensible template system with ProcessWire and Twig, based on the system I developed for some projects at work. Somehow this post got way too long, so I'm splitting it in two parts. I will cover the following topics: Part 1: Extendible template structures How to initialize a custom twig environment and integrate it into ProcessWire How to build an extendible base template for pages, and overwrite it for different ProcessWire templates with custom layouts and logic How to build custom section templates based on layout regions and Repeater Matrix content sections Part 2: Custom functionality and integrations How to customize and add functionality to the twig environment How to bundle your custom functionality into a reusable library Thoughts on handling translations A drop-in template & functions for responsive images as a bonus However, I will not include a general introduction to the Twig language. If you are unfamiliar with Twig, read the Twig guide for Template Designers and Twig for Developers and then come back to this tutorial. That's a lot of stuff, so let's get started ? Initializing the Twig environment First, we need to install Twig. If you set up your site as described in my tutorial on setting up Composer, you can simply install it as a dependency: composer require "twig/twig:^2.0" I'll initialize the twig environment inside a prependTemplateFile and call the main render function inside the appendTemplateFile. You can use this in your config.php: $config->prependTemplateFile = '_init.php'; $config->appendTemplateFile = '_main.php'; Twig needs two things: A FilesystemLoader to load the templates and an Environment to render them. The FilesystemLoader needs the path to the twig template folder. I'll put my templates inside site/twig: $twig_main_dir = $config->paths->site . 'twig'; $twig_loader = new \Twig\Loader\FilesystemLoader($twig_main_dir); As for the environment, there are a few options to consider: $twig_env = new \Twig\Environment( $twig_loader, [ 'cache' => $config->paths->cache . 'twig', 'debug' => $config->debug, 'auto_reload' => $config->debug, 'strict_variables' => false, 'autoescape' => true, ] ); if ($config->debug) { $twig_env->addExtension(new \Twig\Extension\DebugExtension()); } Make sure to include a cache directory, or twig can't cache the compiled templates. The development settings (debug, auto_reload) will be dependent on the ProcessWire debug mode. I turned strict_variables off, since it's easier to check for non-existing and non-empty fields with some parts of the ProcessWire API. You need to decide on an escaping strategy. You can either use the autoescape function of twig, or use textformatters to filter out HTML tags (you can't use both, as it will double escape entities, which will result in a broken frontend). I'm using twig's inbuilt autoescaping, as it's more secure and I don't have to add the HTML entities filter for every single field. This means pretty much no field should use any entity encoding textformatter. I also added the Debug Extension when $config->debug is active. This way, you can use the dump function, which makes debugging templates much easier. Now, all that's left is to add a few handy global variables that all templates will have access to, and initialize an empty array to hold additional variables defined by the individual ProcessWire templates. // add the most important fuel variables to the environment foreach (['page', 'pages', 'config', 'user', 'languages', 'sanitizer'] as $variable) { $twig_env->addGlobal($variable, wire($variable)); }; $twig_env->addGlobal('homepage', $pages->get('/')); $twig_env->addGlobal('settings', $pages->get('/site-settings/')); // each template can add custom variables to this, it // will be passed down to the page template $variables = []; This includes most common fuel variables from ProcessWire, but not all of them. You could also just iterate over all fuel variables and add them all, but I prefer to include only those I will actually need in my templates. The "settings" variable I'm including is simply one page that holds a couple of common settings specific to the site, such as the site logo and site name. Page templates By default, ProcessWire loads a PHP file in the site/templates folder with the same name of the template of the current page; so for a project template/page, that would be site/templates/project.php. In this setup, those files will only include logic and preprocessing that is required for the current page, while the Twig templates will be responsible for actually rendering the markup. We'll get back to the PHP template file later, but for the moment, it can be empty (it just needs to exist, otherwise ProcessWire won't load the page at all). For our main template, we want to have a base html skeleton that all sites inherit, as well as multiple default regions (header, navigation, content, footer, ...) that each template can overwrite as needed. I'll make heavy use of block inheritance for this, so make sure you understand how that works in twig. For example, here's a simplified version of the html skeleton I used for a recent project: {# site/twig/pages/page.twig #} <!doctype html> <html lang="en" dir="ltr"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{% block title %}{{ '%s | %s'|format(page.get('title'), homepage.title) }}{% endblock %}</title> <link rel="stylesheet" type="text/css" href="{{ config.urls.site }}css/main.css"> </head> <body class="{{ page.template }}"> {% block navigation %}{{ include("components/navigation.twig") }}{% endblock %} {% block header %}{{ include("components/header.twig") }}{% endblock %} {% block before_content %}{% endblock %} {% block content %} {# Default content #} {% endblock %} {% block after_content %}{% endblock %} {% block footer %}{{ include("components/footer.twig") }}{% endblock %} </body> </html> All layout regions are defined as twig blocks, so each page template can override them individually, without having to touch those it doesn't need. I'll fill the content block with default content soon, but for now this will do. Now, templates for our content types can just extend this base. This is the template for the homepage: {# site/twig/templates/pages/page--home.twig #} {% extends "pages/page.twig" %} {# No pipe-seperated site name on the homepage #} {% block title page.get('title') %} {# The default header isn't used on the homepage #} {% block header %}{% endblock %} {# The homepage has a custom slider instead of the normal header #} {% block before_content %} {{ include('sections/section--homepage-slider.twig', { classes: ['section--homepage-slider'] }) }} {% endblock %} Note that I don't do most of the actual html markup in the page templates, but in individual section templates (e.g. section--homepage-slider.twig) that can be reused across content types. More on this in the next section. We still need to actually render the template. The page template (which will be the entry point for twig) will be loaded in our _main.php, which we defined as the appendTemplateFile earlier, so it will always be included after the template specific PHP file. $template_file = 'pages/page--' . $page->template->name . '.twig'; $twig_template = file_exists($twig_main_dir . '/' . $template_file) ? $template_file : 'pages/page.twig'; echo $twig_env->render($twig_template, $variables); This function checks if a specific template for the current content type exists (e.g. pages/page--home.twig) and falls back to the default page template if it doesn't (e.g. pages/page.twig). This way, if you want a blank slate for a specific content type, you can just write a twig template that doesn't extend page.twig, and you will get a blank page ready to be filled with whatever you want. Note that it passes the $variables we initialized in the _init.php. This way, if you need to do any preprocessing or data crunching for this request, you can do it inside the PHP template and include the results in the $variables array. At this point, you can start building your default components (header, footer, navigation, et c.) and they will be included on every site that extends the base page template. Custom sections Now we've done a great deal of setup but haven't actually written much markup yet. But now that we have a solid foundation, we can add layout components very easily. For most of my projects, I use the brilliant Repeater Matrix module to set up dynamic content sections / blocks (this is not the focus of this tutorial, here's a detailed explanation). The module does have it's own built-in template file structure to render it's blocks, but since it won't work with my Twig setup, I'll create some custom twig templates for this. The approach will be the same as with the page template itself: create a "base" section template that includes some boilerplate HTML for recurring markup (such as a container element to wrap the section content in) and defines sections that can be overwritted by section-specific templates. First, let's create a template that will iterate over our the Repeater Matrix field (here it's called sections) and include the relevant template for each repeater matrix type: {# components/sections.twig #} {% for section in page.sections %} {% set template = 'sections/section--' ~ section.type ~ '.twig' %} {{ include( [template, 'sections/section.twig'], { section: section }, with_context = false ) }} {% endfor %} Note that the array syntax in the include function tells Twig to render the first template that exists. So for a section called downloads, it will look look for the template sections/section--downloads.twig and fallback to the generic sections/section.twig. The generic section template will only include the fields that are common to all sections. In my case, each section will have a headline (section_headline) and a select field to choose a background colour (section_background) : {# sections/section.twig #} {% set section_classes = [ 'section', section.type ? 'section--' ~ section.type, section.section_background.first.value ? 'section--' ~ section.section_background.first.value ] %} <div class="{{ section_classes|join(' ')|trim }}"> <section class="container"> {% block section_headline %} {% if section.section_headline %} <h2 class="section__headline">{{ section.section_headline }}</h2> {% endif %} {% endblock %} {% block section_content %} {{ section.type }} {% endblock %} </section> </div> This section template generates classes based on the section type and background colour (for example: section section--downloads section--green) so that I can add corresponding styling with CSS / SASS. The specific templates for each section will extend this base template and fill the block section_content with their custom markup. For example, for our downloads section (assuming it contains a multivalue files field download_files): {# sections/section--download.twig #} {% extends "sections/section.twig" %} {% block section_content %} <ul class="downloads"> {% for download in section.download_files %} <li class="downloads__row"> <a href="{{ download.file.url }}" download class="downloads__link"> {{ download.description ?: download.basename }} </a> </li> {% endfor %} </ul> {% endblock %} It took some setup, but now every section needs only care about their own unique fields and markup without having to include repetitive boilerplate markup. And it's still completely extensible: If you need, for example, a full-width block, you can just not extend the base section template and get a clean slate. Also, you can always go back to the base template and add more blocks as needed, without having to touch all the other sections. By the way, you can also extend the base section template from everywhere you want, not only from repeater matrix types. Now we only need to include the sections component inside the page template {# pages/page.twig #} {% block content %} {% if page.hasField('sections') and page.sections.count %} {{ include('components/sections.twig' }} {% endif %} {% endblock %} Conclusion This first part was mostly about a clean environment setup and template structure. By now you've probably got a good grasp on how I organize my twig templates into folders, depending on their role and importance: blocks: This contains reusable blocks that will be included many times, such as a responsive image block or a link block. components: This contains special regions such as the header and footer that will probably be only used once per page. sections: This will contain reusable, self-contained sections mostly based on the Repeater Matrix types. They may be used multiple times on one page. pages: High-level templates corresponding to ProcessWire templates. Those contain very little markup, but mostly include the appropriate components and sections. Depending on the size of the project, you could increase or decrease the granularity of this as needed, for example by grouping the sections into different subfolders. But it's a solid start for most medium-sized projects I tackle. Anyway, thanks for reading! I'll post the next part in a couple of days, where I will go over how to add more functionality into your environment in a scalable and reusable way. In the meantime, let me know how you would improve this setup!
    1 point
×
×
  • Create New...