Leaderboard
Popular Content
Showing content with the highest reputation on 05/02/2019 in all areas
-
This is part two of my tutorial on integrating Twig in ProcessWire sites. As a reminder, here's the table of contents: 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 Make sure to check out part one if you haven't already! This part will be less talk, more examples, so I hope you like reading some code ? Adding functionality This is more generic Twig stuff, so I'll keep it short, just to show why Twig is awesome and you should use it! Twig makes it super easy too add functions, filters, tags et c. and customize what the language can do in this way. I'll show a couple of quick examples I built for my projects. As a side note, I had some trouble with functions defined inside a namespace that I couldn't figure out yet. For the moment, it sufficed to define the functions I wanted to use in twig inside a separate file in the root namespace (or, as shown further below, put all of it in a Twig Extension). If you want a more extensible, systematic approach, check out the next section (going further). Link template with external target detection This is a simple template that builds an anchor-tag (<a>) and adds the necessary parameters. What's special about this is that it will automatically check the target URL and include a target="_blank" attribute if it's external. The external URL check is contained in a function: // _functions.php /** * Finds out whether a url leads to an external domain. * * @param string $url * @return bool */ function urlIsExternal(string $url): bool { $parser = new \League\Uri\Parser(); [ 'host' => $host ] = $parser->parse($url); return $host !== null && $host !== $_SERVER['HTTP_HOST']; } // _init.php require_once($config->paths->templates . '_functions.php'); // don't forget to add the function to the twig environment $twig_env->addFunction(new \Twig\TwigFunction('url_is_external', 'urlIsExternal')); This function uses the excellent League URI parser, by the way. Now that the function is available to the Twig environment, the link template is straightforward: {# # Renders a single anchor (link) tag. Link will automatically # have target="_blank" if the link leads to an external domain. # # @var string url The target (href). # @var string text The link text. Will default to display the URL. # @var array classes Optional classes for the anchor. #} {%- set link_text = text is not empty ? text : url -%} <a href="{{ url }}" {%- if classes is not empty %} class="{{ classes|join(' ') }}"{% endif %} {%- if url_is_external(url) %} target="_blank"{% endif %}> {{- link_text -}} </a> String manipulation A couple of functions I wrote to generate clean meta tags for SEO, as well as valid, readable IDs based on the headline field for my sections. /** * Truncate a string if it is longer than the specified limit. Will append the * $ellipsis string if the input is longer than the limit. Pass true as $strip_tags * to strip all markup before measuring the length. * * @param string $text The text to truncate. * @param integer $limit The maximum length. * @param string|null $ellipsis A string to append if the text is truncated. Pass an empty string to disable. * @param boolean $strip_tags Strip markup from the text? * @return string */ function str_truncate( string $text, int $limit, ?string $ellipsis = ' …', bool $strip_tags = false ): string { if ($strip_tags) { $text = strip_tags($text); } if (strlen($text) > $limit) { $ell_length = $ellipsis ? strlen($ellipsis) : 0; $append = $ellipsis ?? ''; $text = substr($text, 0, $limit - ($ell_length + 1)) . $append; } return $text; } /** * Convert all consecutive newlines into a single space character. * * @param string $text The text to convert. */ function str_nl2singlespace( string $text ): string { return preg_replace( '/[\r\n]+/', ' ', $text ); } /** * Build a valid html ID based on the passed text. * * @param string $title * @return string */ function textToId(string $title): string { return strtolower(preg_replace( [ '/[Ää]/u', '/[Öö]/u', '/[Üü]/u', '/ß/u', '/[\s._-]+/', '/[^\w\d-]/', ], [ 'ae', 'oe', 'ue', 'ss', '-', '-', ], $title )); } // again, add those functions to the twig environment $twig_env->addFilter(new \Twig\TwigFilter('truncate', 'str_truncate')); $twig_env->addFilter(new \Twig\TwigFilter('nl2ss', 'str_nl2singlespace')); $twig_env->addFilter(new \Twig\TwigFilter('text_to_id', 'textToId')); Example usage for SEO meta tags: {% if seo.description %} {% set description = seo.description|truncate(150, ' …', true)|nl2ss %} <meta name="description" content="{{ description }}"> <meta property="og:description" content="{{ description }}"> {% endif %} instanceof for Twig By default, Twig doesn't have an equivalent of PHP's instanceof keyword. The function is super simple, but vital to me: // instanceof test for twig // class must be passed as a FQCN with escaped backslashed $twig_env->addTest(new \Twig\TwigTest('instanceof', function ($var, $class) { return $var instanceof $class; })); In this case, I'm adding a TwigTest instead of a function. Read up on the different type of extensions you can add in the documentation for Extending Twig. Note that you have to use double backslashes to use this in a Twig template: {% if og_img is instanceof('\\Processwire\\Pageimages') %} Going further: custom functionality as a portable Twig extension Most of the examples above are very general, so you'll want to have them available in every project you start. It makes sense then to put them into a single library that you can simply pull into your projects with git or Composer. It's really easy to wrap functions like those demonstrated above in a custom Twig extension. In the following example, I have wired the namespace "moritzlost\" to the "src" folder (see my Composer + ProcessWire tutorial if you need help with that): // src/MoritzFuncsTwigExtension.php <?php namespace moritzlost; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; use Twig\TwigFilter; use Twig\TwigTest; class MoritzFuncsTwigExtension extends AbstractExtension { // import responsive image functions use LinkHelpers; public function getFunctions() { return [ new TwigFunction('url_is_external', [$this, 'urlIsExternal']), ]; } public function getFilters() { return [ new TwigFilter('text_to_id', [$this, 'textToId']), ]; } public function getTests() { return [ new TwigTest('instanceof', function ($variable, string $namespace) { return $variable instanceof $namespace; }), ]; } } // src/LinkHelpers.php <?php namespace moritzlost; trait LinkHelpers { // this trait contains the textToId and urlIsExternal methods // see the section above for the full code } Here I'm building my own class that extends the AbstractExtension class from Twig. This way, I can keep boilerplate code to a minimum. All I need are public methods that return an array of all functions, filters, tests et c. that I want to add with this extension. As is my custom, I've further split the larger functions into their own wrapper file. In this case, I'm using a trait to group the link-related functions (it's easier this way, since classes can only extend one other class, but use as many traits as they want to). Now all that's left is to add an instance of the extension to our Twig environment: // custom extension to add functionality $twig_env->addExtension(new MoritzFuncsTwigExtension()); Just like that we have a separate folder that can be easily put under version control and released as a micro-package that can then be installed and extended in other projects. Translations If you are building a multi-language site, you will need to handle internationalization of your code. ProcessWire can't natively handle translations in Twig files, so I wanted to briefly touch on how to handle this. For a recent project I considered three approaches: Build a module to add twig support to ProcessWire's multi-language system. Use an existing module to do that. Build a custom solution that bypasses ProcessWire's translation system. For this project, I went with the latter approach; I only needed a handful of phrases to be translated, as I tend to make labels and headlines into editable page fields or use the field labels themselves, so there are only few translatable phrases inside my ProcessWire templates. But the beauty of ProcessWire is that you can build your site whatever way you want. As an example, here's the system I came up with. I used a single Table field (part of the ProFields module) with two columns: msgid (a regular text field which functions as a key for the translations) and trans (a multi-language text field that holds the translations in each language). I added this field to my central settings page and wrote a simple function to access individual translations by their msgid: /** * Main function for the translation API. Gets a translation for the msgid in * the current language. If the msgid doesn't exist, it will create the * corresponding entry in the settings field (site settings -> translations). * In this case, the optional second parameter will be used as the default * translation for this msgid in the default language. * * @param string $msgid * @param ?string $default * @return string */ function trans_api( string $msgid, ?string $default = null ): string { // this is a reference to my settings page with the translations field $settings = \Processwire\wire('config')->settings; $translations = $settings->translations; $row = $settings->translations->findOne("msgid={$msgid}"); if ($row) { if ($row->trans) { return $row->trans; } else { return $msgid; } } else { $of = $settings->of(); $settings->of(false); $new = $translations->makeBlankItem(); $new->msgid = $msgid; if ($default) { $default_lang = \Processwire\wire('languages')->get('default'); $new->trans->setLanguageValue($default_lang, $default); } $settings->translations->add($new); $settings->save('translations'); $settings->of($of); return $default ?? $msgid; } } // _init.php // add the function with the key "trans" to the twig environment $twig_env->addFunction(new \Twig\TwigFunction('trans', 'trans_api')); // some_template.twig // example usage with a msgid and a default translation {{ trans('detail_link_label', 'Read More') }} This function checks if a translation with the passed msgid exists in the table and if so, returns the translation in the current language. If not, it automatically creates the corresponding row. This way, if you want to add a translatable phrase inside a template, you simply add the function call with a new msgid, reload the page once, and the new entry will be available in the backend. For this purpose, you can also add a second parameter, which will be automatically set as the translation in the default language. Sweet. While this works, it will certainly break (in terms of performance and user-friendliness) if you have a site that required more than a couple dozen translations. So consider all three approaches and decide what will work best for you! Bonus: responsive image template & functions I converted my responsive image function to a Twig template, I'm including the full code here as a bonus and thanks for making it all the way through! I created a gist with the extension & and template that you can drop into your projects to create responsive images quickly (minor warning: I had to adjust the code a bit to make it universal, so this exact version isn't properly tested, let me know if you get any errors and I'll try to fix it!). Here's the gist. There's a usage example as well. If you don't understand what's going on there, make sure to read my tutorial on responsive images with ProcessWire. Conclusion Including the first part, this has been the longest tutorial I have written so far. Again, most of this is opinionated and influenced by my own limited experience (especially the part about translations), so I'd like to hear how you all are using Twig in your projects, how you would improve the examples above and what other tips and tricks you have!4 points
-
Thanks to @Macrura for the idea behind this module. Page Field Info Adds information about options in Page Reference fields. Supports InputfieldSelect and inputfields that extend InputfieldSelect: InputfieldSelect InputfieldRadios InputfieldSelectMultiple InputfieldCheckboxes InputfieldAsmSelect Requires ProcessWire >= 3.0.61 and AdminThemeUikit. Screenshots Field config Example of changes to inputfield Example of info field filled out in Page Edit Installation Install the Page Field Info module. Configuration In the Input tab of the settings for a Page Reference field... Tick the "Add info tooltips to options" checkbox to enable tooltips for the options in the field. Tooltips are not possible for Select or AsmSelect inputfield types so for those types you would want to tick the next option. Tick the "Append info about selected options" checkbox to append information about the selected options to the bottom of the inputfield. If the Page Reference field is a "multiple pages" field then the info for each selected option will be prefixed with the option label (so the user will know what option each line of info relates to). In the "Info field" dropdown select a text field that will contain information about the page, to be used in the tooltips and appended info. Of course this field should be in the template(s) of the selectable pages for the Page Reference field. Hook In most cases the "Info field" will supply the text for the tooltips and appended info, but for advanced usages you can hookPageFieldInfo::getPageInfo() to return the text. For example: $wire->addHookAfter('PageFieldInfo::getPageInfo', function(HookEvent $event) { $page = $event->arguments(0); // The page $inputfield = $event->arguments(1); // InputfieldPage $field = $event->arguments(2); // The Page Reference field $info = $event->return; // Text from the info field, if any // Set some custom text as the $event->return... }); https://github.com/Toutouwai/PageFieldInfo https://modules.processwire.com/modules/page-field-info/3 points
-
It's working great, i have it on about 20 fields now on 1 current/major project and this going to really make a world of difference in reducing support (calls/emails/trello cards)... I've noticed the more of these sorts of things i put in the admin (including stuff like Field Descriptions Extended, Context help for templates, and ProcessDocumentation), the more careful clients are to check before wasting my support resources...2 points
-
Hi @bernhard Here is my solution which uses the server name instead of a directory structure. So you are safe if you are changing paths or the OS where paths are different. $base_url = $_SERVER['SERVER_NAME']; $config->base_url = $base_url; switch ($base_url) { case "p-jentschura.localhost": case "p-jentschura.acemanpc": case "p-info.localhost": case "p-info.acemanpc": $config->dbHost = 'localhost'; $config->dbName = 'dbname'; $config->dbUser = 'root'; $config->dbPass = 'dbpass'; $config->dbPort = '3306'; $config->debug = false; $config->httpHosts = array( 'p-jentschura.localhost', 'p-jentschura.acemanpc', '192.168.178.16', '192.168.178.23', 'localhost.p-jentschura' ); break; default: // LIVE CONFIG $config->dbHost = '127.0.0.1'; $config->dbName = 'dbname'; $config->dbUser = 'dbuser'; $config->dbPass = 'dbpass'; $config->dbPort = '3306'; $config->debug = false; $config->httpHosts = array( 'p-jentschura.com', 'www.p-jentschura.com', 'p-jentschura.de', 'www.p-jentschura.de', 'p-jentschura.nl', 'www.p-jentschura.nl', 'p-jentschura.shop' ); }2 points
-
@Noboru I now have changed it to work with all regular use cases in PW. But this includes to have a regular file and optionally a webp file. But this is/was the behave before the core webp addition too. Please try if it now works for you, or report back. If not, we can define a custom hook or a custom function to suite your setup.2 points
-
Sorry, I didn't understand the original issue properly before. You shouldn't have to activate the tooltip option for the append option to work for selects - I've fixed the logic in v0.1.2 so it should work as expected now.2 points
-
ok got it, i didn't get that for single select you have to check the tooltips option for this to work.. it's working now once i checked that... Maybe the description of that field could be modified to say that it is required to check that box for single select fields... thanks again!2 points
-
Excellent tutorial, thanks! Just wondering: why do you write your own str_truncate and textToId functions instead of using core $sanitizer methods like truncate() and name()? (for the latter there are several different sanitizer methods that could be used to get an ID string)2 points
-
2 points
-
This won't work, you need to use the require command to add dependencies to your project. This will add the dependency to your composer.json and download it. The install command is used to read the composer.lock file and download all the dependencies listed in there, as well as update the autoloader. This is why install doesn't take a library as an argument. @bramwolf Pretty sure that BitPoet is right. That error indicates that the class ProcessWire\PaymentModule doesn't exist. If you installed the PaymentModule through the backend or downloaded it from the module page, you would get the master branch, which doesn't have the correct namespace. So this version of the module would have the \PaymentModule class inside the root namespace, but not inside the Processwire namespace (which is what the PaymentMollie module tries to extend). Install the PW3 branch and you should be fine.2 points
-
You can detect whether the current page was loaded from ajax by checking the value of $config->ajax from your template file: <?php if($config->ajax) { // page was requested from ajax } Following that, you will likely want to render the page differently to accommodate whatever you are doing from the javascript side. For instance, you might want do one of these: 1. Deliver alternate or reduced markup when loaded from ajax 2. Deliver a JSON or XML string for parsing from javascript Below are examples of each of these scenarios. 1. Deliver alternate or reduced markup when loaded from ajax You might find checking for ajax helpful when you want portions of pages to load in your site without re-rendering the entire page for each request. As a simple example, we'll use the default ProcessWire site and make it repopulate it's #bodycopy area when you click a page in the top navigation. (To use this example, you'll need the default ProcessWire site templates, though you can easily adapt the example to another situation.) To accomplish this, we'll update our main page template to only include the header and footer markup if the page is NOT being loaded from ajax: /site/templates/page.php <?php if(!$config->ajax) include("./head.inc"); echo $page->body; if(!$config->ajax) include("./foot.inc"); Next we'll update the top navigation to do ajax loads of the pages when the client has javascript (and leave as-is when they don't). Paste this javascript snippet before the closing </head> tag in the header markup file: /site/templates/head.inc: <script type="text/javascript"> $(document).ready(function() { $("#topnav a").click(function() { $("#topnav a.on").removeClass('on'); // unhighlight selected nav item... $(this).addClass('on'); // ...and highlight new nav item $("#bodycopy").html("<p>Loading...</p>"); $.get($(this).attr('href'), function(data) { $("#bodycopy").html(data); }); return false; }); }); </script> Now when you click on any page in the top navigation, it pops into the bodycopy area without a page load visible from your browser. And all pages remain accessible from their URL as well. Note that this is just a test scenario, and I probably wouldn't use this approach for the entire bodycopy area on a production site (it would make bookmarking difficult). But this approach can be very useful in the right places. 2. Deliver a JSON or XML string for parsing from javascript Lets say that you want pages in your site to return a JSON string with the page's id, title, and number of children when it is requested from ajax. When not requested from ajax, they will return their content as normal. To handle the ajax requests, you'd want to add something like this at the top of your template file before any other output. <?php if($config->ajax) { // this is an ajax request, return basic page information in a JSON string $json = array( 'id' => $page->id, 'title' => $page->title, 'numChildren' => $page->numChildren ); echo json_encode($json); return; } // not ajax, continue with regular page output And here is some markup and inline javascript you might use to test the ajax call on some other page (or the same one if you prefer). You would paste this snippet right in your site's markup where you want that info to appear. <ul id='info'></ul> <script type='text/javascript'> var url = '/'; // this is homepage, so replace '/' with page URL you want to load JSON from $(document).ready(function() { $.getJSON(url, function(data) { $.each(data, function(key, value) { $("#info").append("<li>" + key + ": " + value + "</li>"); }); }); }); </script> The above snippet would output something like this: • id: 1 • title: Home • numChildren: 5 To take this example further, you could build an ajax-driven sitemap or any number of web services. Conclusion Hope this helps you to see how simple it is to use ProcessWire to deliver output for ajax. These are just contrived examples, but hopefully examples that might lead to more ideas. In addition, much of what you see in these examples is also applicable to building web services in ProcessWire.1 point
-
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
-
Oh, @Robin S my bad. I don't know how but I've missed to check the module configuration page and was looking at the tag options again. It works perfectly now and I can increase the size from there as most of my tags would require bigger window. Thank you again for the help.1 point
-
1 point
-
1 point
-
No special reason, to be honest. I just don't use the sanitizer that much, so it's easier for me to write some regex than using the sanitizer. Though I did want some special replacements (for German umlauts ä, ü and ö for example) that I'm not sure the sanitizer can handle.1 point
-
Yeah, but html5.validator.nu is down and validator.w3.org/nu doesn't work with the panel for some reason, so there is really only the checker.html5.org option anyways ?1 point
-
1 point
-
If you have mod_pagespeed on your server it can convert jpg to webp on the fly. Can do lots of other amazing stuff also.1 point
-
@szabesz Thanks, I have opened an issue: https://github.com/processwire/processwire-issues/issues/8691 point
-
"Anti-snobbery" resulted in my boss recently sending me an email with a link to javascript:void(0);1 point
-
I have and it's the closest thing to MAMP available on Linux when I looked around last year. For myself, I'm not too bothered about the "bloat" and I'd rather maximise time spent solving real issues. So I was happy to use it when I had to do some Processwire work.1 point
-
1 point
-
Maybe I'm being obvious, but have you tried running the shell command "composer require" on the root the module (site/modules/PaymentMollie)? Like this: composer require mollie/mollie-api-php EDIT: Fixed for correctness, thanks @MoritzLost1 point
-
Boom! Finally I've got Intelephense working as expected ? Before: After: Turns out that it is already available as setting in settings.json:1 point
-
Astounding! I'm installing now on several sites! Working great so far, had to change line 195 to support textarea, as that's what i use for the info $text_fields = $this->wire('fields')->find('type=FieldtypeText|FieldtypeTextarea'); Testing the single select now, but noticing that the options don't have the data-info attribute, so there is no way to update the selected option's description by javascript – do you think it is possible to add the data-info to the options on plain selects?1 point
-
Any change if you use the version from the PW3 branch in the github repo for PaymentModule (which is basically the same with the namespace added)?1 point
-
1 point
-
This is provided by Page Edit Soft Lock. Sure you have it installed?1 point
-
First of all... ProCache. It compiles my SCSS and JS files to small and handy assets. It combines multiple CSS/JS files if necessary. Awesome if you use Premium Templates and don't want to mess around with all of its assets. And it minifies HTML, creates static copies of all your pages and therefore produces all in all really fast sites even on cheaper hosting solutions. BUT... you have to be careful if you handle custom-made forms or any real-data-interaction. ProCache bypasses almost every PHP request so handling user input and things like won't work on ProCache-enabled pages. Second... FormBuilder. It handles 90% of all my necessary use cases of contact forms or any other kind of form my clients or I need. It works pretty well with ProCache - but you have to use the iFrame option then but that's another story. In summary: this form-builder tool can make things quite easy but customizations are sometimes a good amount of work. Replacing one of the includes frameworks, like Bootstrap and UIKIT, can be replaced and you can even use vanilla CSS for it but you have to invest some time here as well. Third... RepeaterMatrix (ProFields). You can create almost every modern (aka lapa.ninja featured) design with it and create your very own page builder tool-kit with it BUT... you have to dig deep into how everything works. It's a tool to built bigger tools. I use it for a very long time now and I guess I used only about 20% of its full potential. Last but not least... every other module from the ProFields collection. There is so much in it and you really should take a closer look at the provided screencasts to imagine what they can do. So... feel free to ask if you need details on one or the other modules.1 point
-
Hi @horst, My ImageSizerEngineVips is based on your ImageSizerEngineIMagickCLI with code from other sizers. The module works fine, but at the moment only for my needs, because I optimized for speed and left some options like sharpening. But if I have the time and interest, I can polish it. Another thing: I only use WebP in pages with lazy loading and srcset. Because of that I have only variations as WebP, not the uploaded pageimage. Your change here checks not only variations, right? For me, it changes many variations that already exist.1 point
-
Haven't used this in backends yet but sometimes those skeletons are pretty nice. https://codepen.io/oslego/pen/XdvWmd1 point
-
@Mikie, you will probably need to open separate issues for each concern in the Issues or Request repos in order for Ryan to notice - he doesn't tend to participate much in the general forums these days. In terms of which repo, I'd say that it belongs in the Issues repo if something is broken or not working as expected, and in the Request repo if it's about adding a new feature or improving something that already works. In each issue please describe exactly what the problem is and what a solution would look like. And if you are able to contribute code for a suggested fix that's great (but not essential). I have some possible fixes for some of the things you mentioned that you'd be welcome to include in any issue report if you want... The footer sits under whatever content makes up the page, so if new content is loaded into the page by AJAX or page elements change in height then it's expected that the footer will move. Are you suggesting that the footer should be sticky to the bottom of the viewport when the height of the page content is less than the viewport height? Personally I would prefer that, but it's perhaps a matter of opinion. Here is some CSS that could be used for a sticky footer: /* Sticky footer */ html { height:100%; } body:not(.modal) { display:flex; flex-direction:column; height:100%; } body:not(.modal) #main { flex:1 0 auto; width:100%; box-sizing:border-box; } body:not(.modal) footer { flex-shrink:0; } This FOUC with WireTabs was much worse with the older admin themes - I hardly notice it in AdminThemeUikit but it is there. A CSS fix: /* Prevent FOUC for WireTabs content */ .WireTabs + .Inputfields > .InputfieldWrapper { display:none; } The main FOUC relating to fields that bugs me is the delay in hiding inputfields with show-if conditions. I'd rather have show-if fields hidden by default and then shown when necessary. There's an open request about that, along with a roll-your-own fix for the meantime: https://github.com/processwire/processwire-requests/issues/179 I'm not sure what sort of change you want to see with regard to CKEditor fields or image fields. Some inputfields take a moment to initialise - I'd rather see the interface as soon as possible even if parts of it need to adjust slightly after loading than have the whole interface hidden until loading is complete (not sure if that's what you're suggesting). Generally speaking the behaviour of non-core modules is up to the author of those modules. But in the case of the Lister Pro button you mentioned this is due to the way the admin theme wraps and clones "head" buttons. A CSS fix: /* Hide Lister button links at bottom when results are empty */ #ProcessListerResults:empty + .InputfieldButtonLink { display:none; }1 point
-
Hi @eelkenet, Thanks for your fix! The module still worked on our website, bit in the newest version I used your code. I updated the Github repository. https://github.com/Typografics/PaymentMollie-PW31 point
-
@itsberni I just ran into this on my development server (using PHP7.2) The clues to a fix were found here. NB, this module doesn't need ImageMagick/Ghostscript to have write permissions for PDFs to generate the thumbnails, read permission worked just fine for me. <policy domain="coder" rights="read" pattern="PDF" />1 point
-
Hani, ProcessWire will scale pretty infinitely in terms of pages, but not in terms of fields or templates. The same can be said of databases in general–you can have millions of rows in a table, but probably don't want millions of tables. I would say you've gone beyond what may be practical for long term use and maintenance of the site. At least, I have never tested ProcessWire with any more than 200 fields (and I thought that was an insane amount). I would be curious why so many fields are necessary? It might be good for us to get a list of all the fields, which may lead to some good suggestions on alternatives. But this is a situation where I think you might really benefit from making your own Fieldtype and Inputfield combinations. It is surprisingly simple to do, and can represent any DB table, no matter how many columns it needs. There is far less overhead in having a custom Fieldtype that represents a table with 100 columns, than there is in having 100 separate fields. Have a look at Adrian's new FieldtypePhone/InputfieldPhone module for a good example of how to represent a DB table with a Fieldtype. Some other options I can think of relate to ProFields, a pack of modules I've been working on to release soon (probably as commercially supported like FormBuilder and ProCache). It includes FieldtypeMultipler, FieldtypeTable, FieldtypeTextArray and FieldtypeTextareas, among others. These enable you to have a single field that can to represent any quantity of inputs. Though they all do it in a little bit different ways. FieldtypeMultiplier lets you take any other Fieldtype (that isn't already multi-value) and turn it into a multi-value field. FieldtypeTable is a multi-value compound type that lets you keep a user-defined combination of fields contained in a group… not quite as powerful as a repeater, but it's only 1 field and has far less overhead. FieldtypeTextArray lets you have a defined quantity of inputs for any text field… like say you needed a field to represent 3 email addresses, for instance. Lastly, FieldtypeTextareas lets you have 1 field that can represent any number of named textareas while having them all be searchable from the same field. For example, you could have a single field called "content" that contains separate inputs for body, sidebar, summary and about_the_author or something like that, but they only consume 1 field, accessed via $page->content->body, $page->content->sidebar, etc. All of these Fieldtypes/Inputfields are aimed at really reducing the overall quantity of fields necessary to represent lots of data. I developed this set because I had a site with lots of similar fields, and this group of modules helped me to reduce the quantity of fields by more than 70%. However, these aren't ready for production use, so it may be a little while before I have them ready. But if you'd be interested in helping to beta test them, your use case might be about perfect.1 point
-
Dave, You can add whatever fields you want to the user template. I'm building a fairly complex user profile setup as well. Go to: Setup > Templates, at the top of the page open the "filters" area and enable "show system templates". Now you can add any of your fields to the user template.1 point