Jump to content

Leaderboard

Popular Content

Showing content with the highest reputation on 05/13/2020 in all areas

  1. Developing custom fields for ProcessWire is great! And it's easier than many might think once you get the basic concepts. But it's hard to learn those concepts by reading the code and doing some trial and error... That's why I think we need a good tutorial about that topic. It took me quite long, but now I feel knowledgeable enough to write such a tutorial. I also have the idea (or the need) for some new fields that might be helpful to the community. What I don't have is time ? So I thought to share the workload and share my knowledge while the development of the module (and testing, writing docs, etc) could be done by someone else under my supervision (hope that does not sound scary ? ). What do you think? I'm happy to hear your opinions - we are in the PUB ? Have a great week and happy coding!
    8 points
  2. I've seen a couple of questions regarding namespaces and autoloading floating around the forum recently, so I decided to write a little tutorial. In general, I often see people getting confused when they try to wrap their head around namespaces, autoloading, Composer and the mapping of namespaces to directory structures all at once. In fact, those are very much independent, distinct concepts, and it is much easier to explain and understand them separately. So this guide is structured as follows: How namespaces work in PHP. How autoloading works in PHP. Conventions for mapping namespaces to directory structures: PSR-4. How autoloading works in Composer and ProcessWire's class loader. How to use the class loader in a ProcessWire module. Feel free to skip the sections you're already familiar with. Namespaces in PHP The purpose of namespaces in PHP is to avoid naming conflicts between classes, functions and constants, especially when you're using external libraries and frameworks. Nothing more. It's important to understand that this has nothing at all to do with autoloading, directory structures or file names. You can put namespaced stuff everywhere you want. You can even have multiple namespaces inside a single file (don't try this at home). Namespaces only exist to be able to use a generic name – for example, ProcessWire's Config class – multiple times in different contexts without getting a naming conflict. Without namespaces, I couldn't use any library that includes a Config class of it's own, because that name is already taken. With namespaces, you can have a distinction between the classes ProcessWire\Config and MoritzLost\Config. You can also use sub-namespaces to further segregate your code into logical groups. For example, I can have two classes MoritzLost\Frontend\Config and MoritzLost\Backend\Config– a class name only needs to be unique within it's namespace. You can declare the namespace for a PHP file using the namespace statement at the top: // file-one.php <?php namespace ProcessWire; // file-two.php <?php namespace MoritzLost\Frontend; This way, all classes, methods and constants defined inside this file are placed in that namespace. All ProcessWire classes live in the ProcessWire namespace. Now to use one of those classes – for example, to instantiate it – you have a couple of options. You can either use it's fully qualified class name or import it into the current namespace. Also, if you are inside a namespaced file, any reference to a class is relative to that namespace. Unless it starts with a backward slash, in this case it's relative to the global namespace. So all of those examples are equivalent: // example-one.php <?php namespace ProcessWire; $page = new Page(); // example-two.php <?php use ProcessWire\Page; $page = new Page(); // example-three.php <?php $page = new ProcessWire\Page(); // example-four.php <?php namespace MoritzLost\Somewhere\Over\The\Rainbow; $page = new \ProcessWire\Page(); The use statement in the second example can be read like this: “Inside this file, all references to Page refer to the class \ProcessWire\Page” How autoloading works Every PHP program starts with one entry file – for ProcessWire, that's usually it's index.php. But you don't want to keep all your code in one file, that would get out of hand quickly. Once you start to split your code into several individual files however, you have to take care of manually including them with require or include calls. That becomes very tedious as well. The purpose of autoloading is to be able to add new code in new files without having to import them manually. This, again, has nothing to do with namespaces, not even something with file locations. Autoloading is a pretty simple concept: If you try to use a class that hasn't been loaded yet, PHP calls upon it's registered autoloaders as a last-ditch attempt to load them before throwing an exception. Let's look at a simple example: // classes.php <?php class A { /** class stuff */ } class B { /** class stuff */ } // index.php <?php spl_autoload_register(function ($class) { include_once 'classes.php'; }); new A(); new B(); This is a complete and functional autoloader. If you don't believe me, go ahead and save those two files (classes.php and index.php) and run the index.php with php -f index.php. Then comment out the include_once call and run it again, then you'll get an error that class A was not found. Now here's what happens when index.php is executed (with the autoloader active): Our anonymous function is added to the autoload queue through spl_autoload_register. PHP tries to instantiate class A, but can't because it's not loaded yet. If there was no autoloader registered, the program would die with a fatal error at this point. But since there is an autoloader ... The autoloader is called. Our autoloader includes classes.php with the class definition. That was a close one! Since the class has been loaded, execution goes back to the index.php which can now proceed to instantiate A and B. If the class was still not loaded at this point, PHP would go back to the original plan and die. One thing to note is that the autoloader will only be called once in this example. That's because both A and B are in the same file and that file is included during the first call to the autoloader. Autoloading works on files, not on classes! The important takeaway is that PHP doesn't know if the autoloader knows where to find the class it asks for or, if there are multiple autoloader, which one can load it. PHP just calls each registered autoloader in turn and checks if the class has been loaded after each one. If the class still isn't loaded after the last autoloader is done, it's error time. What the autoloader actually does is pretty much wild wild west as well. It takes the name of the class PHP is trying to load as an argument, but it doesn't have to do anything with it. Our autoloader ignores it entirely. Instead, it just includes classes.php and says to itself “My job here is done”. If class A was in another file, it wouldn't have worked. This process has two main advantages: Since autoloaders are only called on-demand to load classes just in time, we only include the files we actually need. If in the example above class A and B are not used in some scenarios, the classes.php will not be included, which will result in better performance for larger projects (though this isn't as cut and dry, since autoloading has it's own overhead, so if you load most classes anyway during a single request, it will actually be less efficient). If the autoloader is smart enough to somehow map class names to the files they're located in, we can just let the autoloader handle including the classes we need, without having to worry about jamming include statements everywhere. That brings us to ... PSR-4, namespaces and directory structures As you see, namespaces and autoloading are both pretty limited concepts. And they aren't inherently linked to each other. You can namespace your classes without ever adding an autoloader, and you can autoload classes that are all in the same namespace. But they become useful when you put them together. At the core of all that autoloading talk is a simple idea: By putting classes in files named after their class names, and putting those files in directory hierarchies based on the namespace hierarchy, the autoloader can efficiently find and load those files based on the namespace. All it needs is a list of root namespaces with their corresponding directories. The exact way class names and namespaces are mapped to directory structures and file names is purely conventional. The accepted convention for this is PSR-4. This is a super simple standard which basically just sums up the ideas above: A base namespace is mapped to a specific directory in the file system. When the autoloader is asked to load a class in that namespace (or a sub-namespace of it), it starts looking in that folder. This "base" namespace may include multiple parts – for example, I could use MoritzLost\MyAwesomeLibrary as a base and map that to my source directory. PSR-4 calls this a "namespace prefix". Each sub-namespace corresponds to a sub-directory. So by looking at the namespace, you can follow subdirectories to the location where you expect to find the class file. Finally, the class name is mapped directly to the file name. So MyCoolClass needs to be put inside MyCoolClass.php. This all sounds simple and straightforward - and it absolutely is! It's only once you mash everything together, mix up language features, accepted conventions and proprietary implementations like Composer on top that it becomes hard to grasp in one go. Composer and ProcessWire's class loader Now all that's left is to talk about how Composer and ProcessWire provide autoloading. Composer, of course, is primarily a tool for dependency management. But because most libraries use namespaces and most developers want to have the libraries they're using autoloaded, those topics become a prerequisite to understanding what Composer does in this regard. Composer can use different autoloading mechanisms; for example, you can just give it a static list of files to include for every request, or use the older PSR-0 standard. But most modern libraries use PSR-4 to autoload classes. So all Composer needs to function is a mapping of namespace prefixes to directories. Each library maintains this mapping for it's PSR-4-structured classes through the autoload information in their composer.json. You can do this for your own site to: Just include the autoload information as shown in the documentation and point it to the directory of your class files. Composer collects all that information and uses it to generate a custom file at vendor/autoload.php — that's the one you need to include somewhere whenever you set up Composer in one of your projects. Bells and whistles aside, this file just registers an autoloader function that will use all the information collected from your own and your included libraries' composer.json to locate and include class files on demand. You can read more about how to optimize Composer's autoloader for production usage here. If you want to read up on how to set up Composer for your own sites, read my ProcessWire + Composer integration guide instead. And finally, what does ProcessWire do to handle all this? Turns out, ProcessWire has it's own autoloader implementation that is more or less PSR-4 compliant. You can access it as an API variable ($classLoader or wire('classLoader'), depending on context). Instead of using a static configuration file like Composer, the namespace -> directory mapping is added during the runtime by calling $classLoader->addNamespace. As you would expect, this function accepts a namespace and a directory path. You can use this to register your own custom namespaces. Alternatively, if you have site-specific classes within the ProcessWire namespace, you can just add their location to the class loader using the same method: $classLoader->addNamespace('ProcessWire', '/path/to/your/classes/'). Utilizing custom namespaces and autoloading in ProcessWire modules Now as a final remark, I wanted to give an example of how to use custom namespaces and the class loader in your own modules. I'll use my TrelloWire module as an example: Decide what namespace you're going to use. The main module file should live in the ProcessWire namespace, but if you have other classes in your module, they can and should use a custom namespace to avoid collisions with other modules. TrelloWire uses ProcessWire\TrelloWire, but you can also use something outside the ProcessWire namespace. You need to make sure to add the namespace to the class loader as early as possible. If either you or a user of your module tries to instantiate one of your custom classes before that, it will fail. Good places to start are the constructor of your main module file, or their init or ready methods. Here's a complete example. The module uses only one custom namespaced class: ProcessWire\TrelloWire\TrelloWireApi, located in the src/ directory of the module. But with this setup, I can add more classes whenever I need without having to modify anything else. /** * The constructor registers the TrelloWire namespace used by this module. */ public function __construct() { $namespace = 'ProcessWire\\TrelloWire'; $classLoader = $this->wire('classLoader'); if (!$classLoader->hasNamespace($namespace)) { $srcPath = $this->wire('config')->paths->get($this) . 'src/'; $classLoader->addNamespace($namespace, $srcPath); } } Source Thanks for making it through to the very end! I gotta learn to keep those things short. Anyway, I hope this clears up some questions about namespaces and autoloading. Let me know if I got something wrong, and feel free to add your own tips and tricks!
    4 points
  3. @bernhard I am up for it if you can share the knowledge, I can write an article around it as a tutorial since I tend to write tutorials. Ironically this was part of a series I am creating for Processwire, just video editing skills delaying but writing this as a tutorial shouldn't be a problem. Just let me know what you need me to do, I will create time for this. Take care
    2 points
  4. RockAwesome ProcessWire Fieldtype to easily choose FontAwesome Icons Usage Install the module. Set paths in the Inputfield's settings page. Add a RockAwesome field to any template (or change an existing TEXT field). Preview https://modules.processwire.com/modules/fieldtype-rock-awesome/ https://github.com/BernhardBaumrock/RockAwesome
    1 point
  5. The idea is that the link to the css-file will change every second, and the browser will be tricked into thinking this is a new stylesheet and loading it fresh every time (and not cached copy).
    1 point
  6. @adrian I just came across this module for the first time and it works like a charm! Thank you!
    1 point
  7. ☝️ I'd love to help while learning! In my personal experience Fieldtype/Inputfield development is like one of the few things of the modular approach of ProcessWire that isn't completely clear to me, so I think a tutorial would be super useful for a lot of people. I'd like to stop abusing RuntimeMarkup some day haha (which is and awesome module of course!)
    1 point
  8. I always use the Markup Regions, until now I haven't needed more complicated system in my projects. Roughly this is my standard configuration: _main.php <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Title</title> <link rel="stylesheet" href="..3rd_party_styles_1.css"> <link rel="stylesheet" href="..3rd_party_styles_2.css"> <pw-region id="styles_area"></pw-region><!--#styles_area--> <link rel="stylesheet" href="..my_main_style.css"> <pw-region id="head_area"></pw-region><!--#head_area--> </head> <body> <header> Header Content Here (regularly a file included) </header> <div class="container"> <pw-region id="content_area"></pw-region><!--#content_area--> </div> <footer> Footer Content Here (regularly a file included) </footer> <pw-region id="append_area"></pw-region><!--#append_area--> <script src="..3rd_party_scripts_1.js"></script> <script src="..3rd_party_scripts_2.js"></script> <pw-region id="scripts_area"></pw-region><!--#scripts_area--> <script src="..my_main_scripts.js"></script> </body> </html> template_file.php <?php namespace ProcessWire; $vars_or_arrays = "fill_some_vars_or_arrays_here"; ?> <pw-region id="styles_area"> <link rel="stylesheet" href="..3rd_party_styles_3.css"> <style> body { background: #F2F2F2 !important; } h2 { text-transform: uppercase !important; } .classA { background: red; } .classB { font-size: 2rem; } </style> </pw-region> <pw-region id="head_area"> <link rel="dns-prefetch" href="//example.com"> <link rel="preconnect" href="//example.com"> <link rel="prefetch" href="image.png"> </pw-region> <pw-region id="content_area"> Here render your page content from $page or using $pages, $vars_or_arrays, etc. </pw-region> <pw-region id="append_area"> <div class="modal"> <a href="#">close</a> <div class="modal_body"> Content of modal window </div> </div> </pw-region> <pw-region id="scripts_area"> <script src="..3rd_party_scripts_3.js"></script> <script> window.onload = function(event) { alert('Hello World!'); }; </script> </pw-region> Use the regions from the main layout as needed on each page, to add a modal window, run a javascript, add styles, etc. I always use <pw-region> because this element is removed from the final markup if it's not used in the template file.
    1 point
  9. Thanks @kongondo. I found a fix for it although I don't understand the issue root cause. Discovered that editing the page url slug fixes this. Even if I just add an extra letter. Once the page saves, I can view it. Not wanting to have to do this manually to 50+ pages, I wondered what would happen if I changed the name (Settings > Name) of the parent for the 50 posts. That worked too and seems to have 'fixed' the 50 pages. It must be something related to the Page Path History module or something similar.
    1 point
  10. Could post a brief code snippet of what you are trying to do? I am assuming you should have something like this: On _main.php: <footer id="footer-region"> ... </footer> On other_template.php <div id="footer-region" pw-append> <script>...</script> </div> @huseyin Just to give my personal opinion, I just really like that with markup regions you can code the markup pretty much like if you were doing normal template output, no string concatenation to fill up a variable to output content, instead of doing that you wrap your markup with the Markup Regions conventions. That would probably be the downside, that you have to understand that maybe not all markup visible in your code, will actually end up in the output markup (depending on how you use it!), and this has sometimes caused me confusion while coding, but nowadays I practically code all my templates with markup regions strategy. So in conclusion, I see it more as a "style" rather than something better or worse.
    1 point
  11. There is not only the possibility to use ProcessWire in a subdirectory and for one page only. You can also use ProcessWire content in other pages, that are not being served by ProcessWire. This process is called bootstrapping and others here already wrote about this. You can find more information in "Including and bootstrapping ProcessWire"
    1 point
  12. Why "upgrading" today to 2.8?? The current stable Version is 3.0.148, and 2.8 is years old and a dead end branch. It is not further maintained. Please use 3.0.148
    1 point
  13. In case you missed it, GitHub bought npm https://blog.npmjs.org/post/612764866888007680/next-phase-montage https://github.blog/2020-03-16-npm-is-joining-github/
    1 point
  14. We could possibly be talking about different things here, but I don't see a difference between WordPress and ProcessWire in this regard: both can be installed into a subdirectory. There are a number of topics here regarding subdirectory installs ? If you're looking into something more specific or custom, some sort of .htaccess trick (or including ProcessWire's index.php, essentially bootstrapping ProcessWire within plain PHP script) might be the way to go — but again, if we're just talking about running a ProcessWire site from a subdirectory, that shouldn't be necessary at all.
    1 point
  15. Thx for the report - always nice to hear that it does not only work for me ? You should definitely take a look at the quite new migrate() method that lets you create fields, templates and pages via simple array syntax ? I'm using this in every module now and it saves me so much time ? /** * Setup this module */ public function setup() { $this->rm->migrate([ 'fields' => [ 'field_done' => [ 'type' => 'textarea', ], ], 'templates' => [ 'template_triggers' => [ 'fields' => ['title'], 'icon' => 'database', 'noParents' => -1, // only one 'childTemplates' => ['template_trigger'], 'noChildren' => 1, // we don't allow pages to be created via backend 'sortfield' => '-created', 'tags' => 'RockTrigger', ], 'template_trigger' => [ 'fields' => [ 'title', 'field_done', ], 'icon' => 'bolt', 'noChildren' => 1, 'parentTemplates' => ['template_triggers'], 'pageClass' => '\RockTrigger\TriggerPage', 'noSettings' => 1, 'tags' => 'RockTrigger', ], ], 'pages' => [ 'rocktriggers' => [ 'title' => "Triggers", 'template' => 'template_triggers', 'parent' => 1, 'status' => ['hidden', 'locked'], ], ], ]); } PS: In my project this setup() method is triggerd on every modules refresh. Maybe a little overhead but for now the easiest option...
    1 point
  16. We use RockMigrations regularly for adding custom functionality, pages, templates and fields to new projects and update the existing sites modules. Works a treat.
    1 point
  17. Our effort seems to be prized.. I see many new members on the forum with their first posts....
    1 point
  18. ... indeed ... For some other page reference inputfields like radio buttons or checkboxes the following hook example could be a first approach. I use it for a field to select a system user. Each label is preceded by a user icon. It should work for checkboxes as well. You need to hook in InputfieldCheckboxes then. $this->addHookBefore('InputfieldRadios::render', function($e) { $input = $e->object; // quick exit if ($input->name != 'clients') return; $options = $input->getOptions(); // $key = page ID foreach ($options as $key => &$label) $label = "<i class='fa fa-user'></i> $label"; $input->setOptions($options); }); $this->addHookAfter('InputfieldRadios::render', function($e) { // quick exit if ($e->object->name != 'clients') return; $e->return = wire('sanitizer')->unentities($e->return); });
    1 point
  19. Welcome to the forums @Pip! Technically you might be able to use hooks and custom CSS to modify a core Page Reference inputfield such as AsmSelect to include a thumbnail image but it would be quite advanced/challenging. Instead I suggest you use a third-party inputfield that supports images - see @Macrura's InputfieldSelectize: https://modules.processwire.com/modules/inputfield-selectize/
    1 point
  20. Hello @bernhard Thank you for your module. I decided to give it a try for an existing project that would like to switch to v5. So I uploaded the /css and /media folders of the free version and set the path to be correctly pointing to the corresponding files. After creating a field and assigning it to a template, I was able to search for the new icons version, but they did not appear correctly. After a bit of testing, I was able to restore the proper functionality by copying the /webfonts folder besides the css and media. Just mentioning it in case someone else stumbles upon the same issue. Not sure if it is because the profile already is loading some libraries of FA4 but I just made it work so am quite happy to use it ?
    1 point
  21. Hey @Mikie, thank you for this hint. Unfortunately this isn't an option for the upcoming project cause this service is not hosted in the EU. In any case I will have a closer look at it, maybe I can use it in one or the other project. I'm not only concerned with the costs that would arise from an upgrade. Of course I'm willing to pay for good software and also support Ryan with the use of some ProModules. I just wanted to clarify beforehand if I can still use the old version of Padloper, which I have already had good experience with, even if it has been difficult at times. Or if the new version of Padloper will be available in the very near future and if it is worth waiting for.
    1 point
  22. In this tutorial I want to write about handling special cases and change requests by clients gracefully without introducing code bloat or degrading code quality and maintainability. I'll use a site's navigation menu as an example, as it's relatable and pretty much every site has one. I'll give some examples of real situations and change requests I encountered during projects, and describe multiple approaches to handling them. However, this post is also about the general mindset I find useful for ProcessWire development, which is more about how to handle special cases and still keep your code clean by making the special case a normal one. The problem: Special cases everywhere Since ProcessWire has a hierarchical page tree by default, as a developer you'll usually write a function or loop that iterates over all children of the homepage and displays a list of titles with links. If the site is a bit more complex, maybe you additionally loop over all grandchildren and display those in drop-down menus as well, or you even use a recursive function to iterate over an arbitrary amount of nested child pages. Something like this: function buildRecursiveMenu(Page $root): string { $markup = ['<ul class="navigation">']; foreach ($root->children() as $child) { $link = '<a class="navigation__link" href="' . $child->url() . '">' . $child->title . '</a>'; $children = $child->hasChildren() ? buildRecursiveMenu($child) : ''; $markup[] = "<li class="navigation__item">{$link}{$children}</li>"; } $markup[] = '</ul>'; return implode(PHP_EOL, $markup); } But then the requests for special cases come rolling in. For example, those are some of the requests I've gotten from clients on my projects (by the way, I'm not saying the clients were wrong or unreasonable in any of those cases - it's simply something I needed to handle in a sensible way): The homepage has the company's name as it's title, but the menu link in the navigation should just say "Home". The first page in a drop-down menu should be the top-level page containing the drop-down menu. This was requested because the first click on the top-level item opens the sub-navigation instead of navigating to that page (espcially on touch devices, such as iPads, where you don't have a hover state!), so some visitors might not realize it's a page itself. Some top-level pages should be displayed in a drop-down menu of another top-level page, but the position in the page tree can't change because of the template family settings. The menu needs to contain some special links to external URLs. For one especially long drop-down menu, the items should be sorted into categories with subheadings based on a taxonomy field. In general, my solutions to those requests fall into three categories, which I'll try to elaborate on, including their respective benefits and downsides: Checking for the special case / condition in the code and changing the output accordingly (usually with hard-coded values). Separating the navigation menu from the page tree completely and building a custom solution. Utilizing the Content Management Framework by adding fields, templates and pages that represent special states or settings. Handling it in the code This is the simplest solution, and often the first thing that comes to mind. For example, the first request (listing the homepage as "Home" instead of it's title in the navigation) can be solved by simply checking the template or ID of the current page inside the menu builder function, and changing the output accordingly: // ... $title = $child->template->name === 'home' ? 'Home' : $child->title; $link = '<a class="navigation__link" href="' . $child->url() . '">' . $title . '</a>'; // ... This is definitely the fastest solution. However, there are multiple downsides. Most notably, it's harder to maintain, as each of those special cases increases the complexity of the menu builder function, and makes it harder to change. As you add more special conditions, it becomes exponentially harder to keep changing it. This is the breeding ground for bugs. And it's much harder to read, so it takes longer for another developer to pick up where you left (or, as is often cited, for yourself in six months). Also, now we have a hard-coded value inside the template, that only someone with access to and knowledge of the template files can change. If the client want's the link to say "Homepage" instead of "Home" at some point, they won't be able to change it without the developer. Also, each special case that is hidden in the code makes it harder for the client to understand what's going on in terms of template logic - thus increasing your workload in editorial support. That said, there are definitely some times where I would go with this approach. Specifically: For smaller projects that you know won't need to scale or be maintained long-term. If you are the only developer, and/or only developers will edit the site, with no "non-technical" folk involved. For rapid prototyping ("We'll change it later") Building a custom solution My initial assumption was that the main navigation is generated based on the page tree inside ProcessWire. But of course this isn't set in stone. You can just as easily forgo using the page tree hierarchy at all, and instead build a custom menu system. For example, you could add a nested repeater where you can add pages or links on a general settings page, and generate the menu based on that. There are also modules for this approach, such as the Menu Builder by @kongondo. This approach is not the quickest, but gives the most power to the editors of your site. They have full control over which pages to show and where. However, with great power comes great responsibility, as now each change to the menu must be performed manually. For example, when a new page is added, it won't be visible in the menu automatically. This is very likely to create a disconnect between the page tree and the menu (which may be what you want, after all). You may get ghost pages that are not accessible from the homepage at all, or the client may forgot to unpublish pages they don't want to have any more after they've removed them from the menu. I would only go with this approach if there are so many special cases that there hardly is a "normal case". However, even then it might not be the best solution. The direct relationship between the page tree, the menu structure and page paths are one of the strongest features of ProcessWire in my opinion. If many pages need to be placed in special locations without much structure in regards to what templates go where, maybe you only need to loosen up the template family settings. I have built one site without any template family restrictions at all - any page of any template can go anywhere. It's definitely a different mindset, but in this case it worked well, because it allowed the client to build custom sections with different page types grouped together. It's a trade-off, as it is so often, between flexibility and workload. Weigh those options carefully before you choose this solution! Utilizing the CMF This is the middle ground between the two options above. Instead of building a completely custom solution, you keep with the basic idea of generating a hierarchical menu based on the page tree, but add fields and templates that allow the editor to adjust how and where individual pages are displayed, or to add custom content to the menu. of course, you will still write some additional code, but instead of having hard-coded values or conditions in the template, you expose those to the client, thereby making the special case one of the normal cases. The resulting code is often more resilient to changing requirements, as it can not one handle that specific case that the client requested, but also every future change request of the same type. The key is to add fields that enable the client to overwrite the default behaviour, while still having sensible defaults that don't require special attention from the editor in most cases. I'll give some more examples for this one, as I think it's usually the best option. Example 1: Menu display options This is probably the first thing you thought of for the very first change request I mentioned (displaying the homepage with a different title). Instead of hard-coding the title "Home" in the template, you add a field menu_title that will overwrite the normal title, if set. This is definitely cleaner than the hard-coded value, since it allows the client to overwrite the title of any page in the menu. I'll only say this much in terms of downsides: Maybe the menu title isn't really what the client wanted - instead, perhaps they feel limited because the title is also displayed as the headline (h1) of the page. In this case, the sensible solution would be an additional headline field that will overwrite the h1, instead of the menu_title field. Which fields are really needed is an important consideration, because you don't want to end up with too many. If each page has fields for the title, a headline, a menu title and an SEO-title, it's much more complicated than it needs to be, and you will have a hard time explaining to the client what each field is used for. Another example in this category would be an option to "Hide this page in the menu". This could be accomplished by hiding the page using the inbuilt "hidden" status as well, but if it's hidden it won't show up in other listings as well, so separating the menu display from the hidden status might be a good idea if your site has lots of page listings. Example 2: "Menu link" template One solution that is quite flexible in allowing for custom links to pages or external URLs is creating a menu-link template that can be placed anywhere in the page tree. This templates can have fields for the menu title, target page and/or external target URL. This way, you can link to another top-level page or an external service inside a drop-down menu, by placing a Menu Link page at the appropriate position. This is also a clean solution, because the navigation menu will still reflect the page tree, making the custom links visible and easily editable by the editors. A minor downside is that those templates are non-semantical in the sense that they aren't pages with content of their own. You'll need to make sure not to display them in listings or in other places, as they aren't viewable. It may also require loosening up strict family rules - for example, allowing for Menu Link pages to be placed below the news index page, which normally can only hold news pages. Example 3: Drop-down menu override This one is a more radical solution to override drop-down menus. You add a repeater field to top-level pages, similar to the one mentioned as a custom solution, where you can add multiple links to internal pages or URLs. If the repeater is empty, the drop-down menu is generated normally, based on the sub-pages in the page tree. But if the repeater contains some links, it completely overrides the drop-down menu. It's similar to the fully custom solution in that as soon as you override a sub-menu for a top-level page, you have to manually manage it in case the page structure changes. But you can make that decision for each top-level page individually, so you can leave some of them as is and only have to worry about the ones that you have overwritten. Again, this offers sensible defaults with good customizability. A downside is that the mixed approach may confuse the client, if some changes to the page tree are reflected in the drop-down menu directly, while others don't seem to have any effect (especially if you have multiple editors working on a site). Finding the right solution So how do you choose between the approaches? It depends on the client, the requirements, and on what special cases you expect and want to handle. Sometimes, a special request can be turned down by explaining how it would complicate editorial workflows or have a negative impact on SEO (for example, if you risk having some pages not accessible from the homepage at all). Also, make sure you understand the actual reason behind a change request, instead of just blindly implementing the suggestion by the client. Often, clients will suggest solutions without telling you what the actual problem is they're trying to solve. For example: In one case, I implemented the drop-down override mentioned in example three. However, what the client really wanted was to have the top-level page as the first item in the drop-down menu (see the example requests mentioned above). So they ended up overwriting every single drop-down menu, making the menu harder to maintain. In this case, it would have been better to go with a more specialized solution, such as adding a checkbox option, or even handling it in the code, since it would have been consistent throughout the menu. Another example was mentioned above: If the client requests an additional "Menu title" field, maybe what they really need is a "Headline" field. I recommend reading Articulating Design Decisions by Tom Greever; it includes some chapters on listening to the client, finding out the real reason behind a change request, and responding appropriately. It's written from a design perspective, but is applicable to development as well, and since UX becomes more important by the day, the lines between the disciplines are blurred anyway. Conclusion I realize now this reads more like a podcast (or worse, a rant) than an actual tutorial, but hopefully I got my point across. ProcessWire is at is greatest if you utilize it as a Content Management Framework, creating options and interfaces that allow for customizability while retaining usability for the client / editor. I usually try to hit a sweet spot where the editors have maximum control over the relevant aspects of their site, while requiring minimal work on their part by providing sensible defaults. Above, I listed some examples of requests I've gotten and different solutions I came up with to handle those with custom fields or templates. Though in some cases the requirements call for a custom solution or a quick hack in the template code as well! What are some of the special requests you got? How did you solve them? I'd love to get some insights and examples from you. Thanks for reading!
    1 point
  23. Once again, you can do that by hooking processProfileForm(). Lets say you added a field 'images' to the system user template and you added the 'images' field to the module configuration, e.g. : then in ready.php : wire()->addHookAfter('LoginRegister::processProfileForm', function($e) { $form = $e->arguments[0]; // get the form foreach ($form->children as $field) { if($field instanceof InputfieldImage) { foreach ($field->getAttribute('value') as $value) { wire('user')->images->add($value); } wire('user')->save(); } } }); To show the image on the frontend, you can hook renderProfileForm().
    1 point
  24. Thanks Flydev! I red your link, don't need install any CKE Addon!! For new entry, the "CKEditor Toolbar" is in Setup -> Fields -> Body -> Imput. Here, add only words not plugin: Save, NewPage, Preview, Templates Cut, Copy, Paste, PasteText, PasteFromWord, -, Undo, Redo Find, Replace, -, SelectAll, -, Scayt, -, Sourcedialog Form, Checkbox, Radio, TextField, Textarea, Select, Button, ImageButton, HiddenField Bold, Italic, Underline, Strike, Subscript, Superscript, -, RemoveFormat NumberedList, BulletedList, -, Outdent, Indent, -, Blockquote, CreateDiv, -, JustifyLeft, JustifyCenter, JustifyRight, JustifyBlock, -, BidiLtr, BidiRtl, Language PWLink, Unlink, Anchor PWImage, Flash, Table, HorizontalRule, Smiley, SpecialChar, PageBreak, Iframe Styles, Format, Font, FontSize TextColor, BGColor Maximize, ShowBlocks For each word PW install one CKE Plugin. Remove the words for to remove the plugin!
    1 point
×
×
  • Create New...