-
Posts
474 -
Joined
-
Last visited
-
Days Won
31
Everything posted by FireWire
-
@robert No probs! It was a good exercise to test the quality of my code and make sure the API acted as expected 🙂
-
Hey all! I've been creating new block/widget buttons for the current project I'm working on and wanted to share them in case they may be useful to others. They're SVGs intended to complement the style of buttons that come with RockPageBuilder. This post has been updated with a link to a Github repository containing all of the buttons currently available. New buttons have been added. Existing buttons have have been tweaked for quality and consistency. Download from or fork the Builder Buttons Github repository Rather than keep this post up to date with every button that is added, visit the Github repo to see the most current example and download/clone buttons for use in your projects. Buttons include: Accordion Announcement Articles Audio Bios Call To Action Card Over Image Code/Embed Events Image Image Carousel Image Mosaic Image Roll List Lists Products Reviews Video Weather Preview (not in order of list) If you find them useful, let me know what you think!
- 8 replies
-
- 13
-
@bernhard I think you wake up every day and put on a cape before sitting down at your computer because you're a ProcessWire hero 🫡
-
@ngrmm Hey there, Fluency author here. I submitted a PR that makes ProcessTranslatePage compatible with the new major release of Fluency. It hasn't been merged yet, but you should have success using this module with these changes. https://github.com/robertweiss/ProcessTranslatePage/pull/10
-
I think we're talking about the same thing haha, I just didn't go the route of including /path/to/rockfrontend/autoload.php anywhere in my code manually. I just installed it in my root composer.json and didn't really give any further thought to it. I tinkered with something like that but didn't spend much time on it. Interested in what you find out!
-
@bernhard I had to install the package at the root level in my main composer.json file. So it's in addition to the Composer file in RockFrontend since that won't register as a namespace available globally when it's located in the RockFrontend subdirectory. "require": { "php": ">=8.2", "ext-gd": "*", "vlucas/phpdotenv": "^5.6", "roach-php/core": "^3.2", "spatie/simple-excel": "^3.6", "nesbot/carbon": "^3.7", "ramsey/uuid": "^4.7", "meyfa/php-svg": "^0.16.0", "srwiez/thumbhash": "^1.2", "latte/latte": "^3.0" } Those are all the packages I have installed for my entire project.
-
I completely forgot to mention that part. I ran into that myself and resolved it by just installing Latte via Composer. It doesn't conflict with RFE and provides the library to use via namespacing as normal. I'm not sure if that's the best way, but it got me up and running fast and with no problems. Let me know if you have any ideas for an alternate approach!
-
I have been able to get it to work. I think it was sneaky syntax that wasn't really clear and easy for me to miss! I think the challenge in our conversation was that it was hard for me to describe more robustly because it ended up being difficult to understand myself. I'll recap. Let's assume I have a file called 'buttons.latte' and inside of it I have a couple of reusable definitions, this goes along with what I was describing earlier- nothing new. {define button-default, $label} <button class="bg-blue-200 hover:bg-blue-300 transition-colors"> {$label|noescape} </button> {/define} {define button-danger, $label} <button class="bg-red-500 hover:bg-red-600 transition-colors"> {$label|noescape} </button> {/define} I can import all of those definitions using {import} and the file name in my templates and other components/partials/files etc. Then I use {include} with a specific name rather than individually from different files. Using {define} and {import} is a little like named exports in JavaScript. {import 'buttons.latte'} <h1>Welcome to the internet cafe, have a Latte</h1> {* ...A bunch of stuff on the page... *} <div class="modal"> <h2>Do you really want to do that?</h2> {* Here I can use include with button-danger instead of including a file like 'button_danger.latte' *} {include button-danger, label: 'OK'} {include button-default, label: 'Cancel'} </div> {* ...The rest of the stuff on the page... *} The problems all started when using an {embed} on the page and then attempting to use an element that was imported, in this case button-danger and button-default. Here's what happens if we create a reusable "modal" that can be embedded and populated using blocks. {import 'buttons.latte'} <h1>Welcome to the internet cafe, have a Latte</h1> {* ...A bunch of stuff on the page... *} {embed 'modal.latte'} {block content} {* The block inside the embed tag is a whole new scope and fails when you attempt to include the buttons imported outside that embed tag *} <h2>Do you really want to do that?</h2> {include button-danger, label: 'OK'} {include button-default, label: 'Cancel'} {/block} {/embed} {* ...The rest of the stuff on the page... *} This was confusing because this {embed} and the content inside the {block} were mixed in with the rest of my template markup so it wasn't easy to catch where the issue was actually occurring. Even with the improved error reporting you added, the error that was shown by Latte by this did not make it clear it was a scope issue so at the time I thought it was because the 'buttons.latte' file was not being included whatsoever. The real story here is that Latte does not provide errors with specific messages for scoping issues. It will just show an error that looks like a file can't be found. HOWEVER. Solution ahoy! The following does work because the {import} is placed inside the {embed} tag so now the elements in 'buttons.latte' are now scoped within that embed tag so they can be included by name. In this example I've left the {import} at the top of the page because that would necessary should you want to use one of the button elements anywhere outside the scope of that {embed} tag. {import 'buttons.latte'} <h1>Welcome to the internet cafe, have a Latte</h1> {* This required that import above for the parent scope *} {include button-default, label: 'Here is a random button'} {* ...A bunch of stuff on the page... *} {embed 'modal.latte'} {import 'buttons.latte'} {block content} {* These work now that the buttons have been imported in the embed scope and can be included by name *} <h2>Do you really want to do that?</h2> {include button-danger, label: 'OK'} {include button-default, label: 'Cancel'} {/block} {/embed} {* ...The rest of the stuff on the page... *} I thought that Latte's scoping rules were specific to files not tags- in this case the {embed} tag. This was really easy for me to miss in a couple of larger Latte files where noticing the {embed}{/embed} scope while surrounded by a bunch of other markup was really not easy to catch, and combine that with the not-so-clear errors, it made it seem like {import} was broken, but it isn't. This is no problem once you add that to your knowledge of Latte. That said, I think that the fact that Latte uses {include} interchangeably for files like "button.latte" and elements created using {define button}{/define} can be a point of confusion. Well, it was for me. So, Latte scope is serious business but errors leave something to be desired 🤣 I've been able to use all of the features of Latte with RFE and it's great. Hopefully my mistake will help others. Thanks as always for your help @bernhard this was a tough one 🤷♂️ This is a pretty smart solution! I didn't think about that. I'm still wrapping my head around the Latte === PHP situation. My brain hasn't connected the two yet. Slowly but surely... Since I already had it set up, I'm using the hook RFE provides to access the Latte instance and a custom extension. I posted this above, but I'll mention it here again in case it's helpful for someone to see this info all in one place. (I added a custom filter as well) <?php use Latte\Extension; final class CustomLatteExtension extends Extension { /** * Define functions available in all Latte files * These can be anything ProcessWire related or not, any functions defined here will be available everywhere */ public function getFunctions(): array { return [ 'wire' => fn (?string $property = null) => wire($property), ]; } public function getFilters(): array { return [ 'bit' => fn (mixed $value) => new Html(filter_var($value, FILTER_VALIDATE_BOOLEAN) ? 1 : 0), 'bitInverse' => fn (mixed $value) => new Html(filter_var($value, FILTER_VALIDATE_BOOLEAN) ? 0 : 1), ]; } } $wire->addHookAfter( "RockFrontend::loadLatte", fn (HookEvent $e) => $e->return->addExtension(new CustomLatteExtension), ); That hook in practice does this: // Defining the wire() function above means that you can use it in any Latte file anywhere {wire()->modules->get('SomeModule')} // The $property parameter is a little syntactic sugar that for calling wire() with arguments for a native feel {wire('modules')->get('SomeModule')} // I have a need to output booleans to the rendered markup and it was getting laborious. My use case is outputting to AlpineJS that exists in // Latte templates {var $someVariableOrPageProperty = true} function someJsInYourTemplate() { // This does nothing because you cant echo a boolean to the page and neither will Latte if ({$someVariableOrPageProperty}) { //... } // You can do this, but it gets ugly when you're working with a lot of booleans. if ({$someVariableOrPageProperty ? 1 : 0}) { //... } // With the custom filter. Clean. if ({$someVariableOrPageProperty|bit}) { //... } } Anyway, those are what I came up with and it's working out great for me.
-
@ryan Happy upcoming birthday! Great to see that you're getting some travel in! M'dude, you're going to be in the Netherlands. The core can wait IMHO 👍 We had a great time when we last visited, enjoy! @Robin S doing the lords work 🙏
-
@bernhard I think it's difficult to explain with actual code examples from my project, other than to say that in this case it is more complex than other projects. I would be simplifying my code to match the examples in the documentation. Here's an example of my workaround to use {include} vs. using Latte's {embed} tag. {* Using capture to assign to a variable *} {capture $modalContents} <h2>{=__('Welcome to my humble modal')}</h2> {$page->body|noescape} <hr> {include 'button-primary.latte'} {/capture} {include 'modal.latte', butonLabel: __('Random Details'), buttonType: 'link', size: 'lg', height: 'fit', content: $modalConents} {* Using embed *} {embed 'modal.latte', butonLabel: __('Random Details'), buttonType: 'link', size: 'lg', height: 'fit'} {block content} <h2>{=__('Welcome to my humble modal')}</h2> {$page->body|noescape} <hr> {include 'button-primary.latte'} {/block} {/embed} There are pages with multiple modals and using {embed} just makes use of the Latte features as documented. For me, it's easier to read, provides indentation, and it's useful to see the {block} feature of {embed} would come in handy- especially when there are multiple blocks. So, true- the same thing can be achieved with using {include}, but more complexity starts to feel like it's compounding. Beyond that, I can't really make a case for why embed is better or not, it's just a feature of Latte that has been helpful. Providing specific examples of where the Functions API would be used is less useful than to say that none of the ProcessWire variables or functions work in Latte files included using {embed}. That is why I mentioned the custom hook I wrote to overcome that error in my earlier post. I don't really have a strong preference whether the Function or object API (or anything else) is made available, it only matters if it's going to be implemented. I just need to know if there's any way that this would be available in RFE or if I should just keep managing it using the RockFrontend::loadLatte hook. I guess you're right about it being a preference. When I first talked about the {define} {import} style and the errors I was getting, I didn't think that had something to do with RFE. I didn't know where to start and figured I did something wrong so that's where I was troubleshooting from. I can't really make a case for this other than to say it's just a feature in Latte that works very well for me. So there's only to things I'm struggling with at the moment- Can some form of the ProcessWire API be made available through RPB to files using {embed}? Can it be possible to use {define} and {import} using files as shown in the Latte docs? I think I was too specific in my other response and didn't mean for it to get in the way of what I was trying to say. I posted before coffee this morning. I didn't mean to be poorly communicative or aggressive.
-
@bernhard I get that there are easier ways of doing things. My challenges arose when trying to use Latte according to the documentation because keeping things simple started causing problems. I started going off what the Latte docs say about these tags: {layout} {include} {import} {define} {embed} The biggest part for me is identifying what role a component of the design performs so that the code maintainable similar to the Latte docs. My example of the button follows the example use case from Latte to output simple elements. In my case I currently have 3 different kinds of buttons, probably will be more, that line up with how {define} works. Using {define} lets me organize these in one buttons.latte file and then import that file so any one or more of them can be used. {* buttons.latte *} {define button-primary, label, color: 'cerulean'} <button class="very-long tailwing-classes to-style this-button {$color === 'cerulean' ? 'bg-cerulean text-white'}{$color === 'strikemaster' ? 'bg-strikemaster text-white'} many-more-classes"> {label|noescape} </button> {/define} {define button-secondary, label, color: 'cerulean'} <button class="very-long tailwing-classes to-style this-button bg-white {$color === 'cerulean' ? 'text-cerulean'}{$color === 'strikemaster' ? 'text-strikemaster'} many-more-classes"> {label|noescape} </button> {/define} {define button-secondary, label, theme: 'light'} <button class="very-long tailwing-classes to-style this-button {$theme === 'dark' ? 'child-svg:fill-neutral-600 child-svg:hover:fill-neutral-800}{$theme ==='light' ? 'child-svg:fill-white/90 child-svg:hover:fill-white'} many-many-more-classes"> {site()->renderIcon( name: 'material-design:close', title: __('Close'), description: __('Close this modal')) } </button> {/define} {* view.latte (simplified for example) This is where importing from an external file fails. *} {import definitions('buttons.latte'); {include button-primary, __('View Tickets')} Other definitions are input elements like toggles and selects, Field outputs (like reusable containers specific from TinyMCE), and Headlines that contains multiple reusable headline styles. I have been struggling to figure out why my code doesn't work like the Latte documentation states for including everything from a file using {import}. Using {define} makes a lot of sense in this project because these are simple elements that just output markup and managing them in one file makes life a lot easier. Then there are modals, lightbox galleries, product cards, etc. I already have 32 separate components for including and embedding, and that number is growing very quickly. Some components are quite complex since they're implemented using AlpineJS (lots of JS). So for me the complexity is coming from the project requirements rather than overengineering. I think the examples below are good examples that are real world. For example, I have 3 different types of modals thus far and there will be more, these are all perfectly suited for the {embed} method that introduces the scoping considerations I mentioned. Here's an example of one of the modals that really need the usefulness that {embed} provides. {parameters $formField, $content = null, $ariaTitle, $buttonLabel, $buttonStyle = 'primary'} {import definitions('buttons.latte')} <div x-data="{ open: false }" @keydown.escape.window="open = false" x-on:toggle-modal.window="open = !open" x-on:close-modal.window="open = false" x-id="['ariaTitle']" > {if $buttonStyle === 'primary'} {include button-primary, label: $buttonLabel, click: "open = true"} {/if} {if $buttonStyle === 'text'} <button @click="open = true">{$buttonLabel}</button> {/if} <template x-teleport="body"> <div x-show="open" :aria-labelledby="$id('ariaTitle')" x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-1" x-transition:leave="transition ease-in duration-300" x-transition:leave-start="opacity-1" x-transition:leave-end="opacity-0" x-on:transitionend="event => { if (event.target !== $el) { return; } if (open) { $refs.formContainer.style.minHeight = `${ $refs.formContainer.offsetHeight }px`; {* Force focus on first field instead of close button by x-trap *} $refs.formContainer.querySelector('input').focus(); } if (!open) { const container = $refs.formContainer; container.style.minHeight = 'auto'; container.querySelector('form').reset(); [...container.querySelectorAll('.FormBuilderErrors, .input-error')].forEach(el => el.remove()); } }" class="flex fixed inset-0 z-[100] items-center justify-center w-screen h-screen bg-white/60 backdrop-blur-sm limit-max-width p-5 md:p-8" > <div class="relative w-full max-w-[40rem] bg-white shadow-xl flex flex-col overflow-y-scroll max-h-[95vh]" @click.outside="open = false" :aria-hidden="open ? 'false' : 'true'" x-trap.noscroll.inert.noautofocus="open" role="dialog" aria-modal="true" > {include button-modal-close} <div class="p-10 overflow-y-scroll after:block after:absolute after:z-10 after:bottom-0 after:left-0 after:h-10 after:w-full after:bg-gradient-to-t after:from-white after:to-transparen"> {block content} {* Headline, additional text, CTA message, etc. *} {/block} <div x-ref="formContainer" class="w-full [&>div]:w-full"> {modules('FormBuilderHtmx')->render($formField, [], ".htmx-indicator-form-processing")|noescape} </div> </div> <div class="htmx-indicator htmx-indicator-form-processing absolute inset-0 h-full w-full bg-white/70 z-10 flex items-center justify-center"> <div class="loader"></div> </div> </div> </div> </template> </div> There's not a better way to do this without {embed}. The modal embed is where the scoping is introduced that blocks out the API, so I added site(), wire(), and modules() available, but that list will grow and manually managing each API function becomes cumbersome and doesn't really fit with how ProcessWire works. I know that my current project is more complicated than the usual site, but that's where templating systems really shine! In my examples, the parameter list can already get quite long and seeing $wire passed around a lot makes Latte feel detached rather than integrated. Having Latte files get only what they need makes code and purpose a lot easier to grok. It's also just a best practice similar to OOP. When using {include} Even if they're available, I also avoid using parent variables directly because I don't know where using them from one place to another may break something. The {embed} scope ends up acting as a "safety check" to make that component safe to use anywhere. I think exposing the Functions API would be great. It makes a lot of sense to me when working with how scope is documented in Latte and also how it's documented as globally available in ProcessWire. Although my example above of modules()->get('FormBuilderHtmx')->render() is more verbose than $htmxForms->render(), the Functions API makes it clear that you are working with globally scoped modules(). I think this becomes even more useful if we consider how many non-standard global variables are made available via modules, $forms, $rockicons, $fluency, etc. I am definitely trying to keep things from getting complicated! The use cases are more complex than using {include}, but they're not overengineered. Using {include} for things like modals ended up making my code more complicated where I was able to come up with a solution, but it was very clear that I was using the wrong tool for the job. This is where my project is at and I have a ways to go before it's done, so this is going to grow a lot more. Using {define} and {import} with shared elements like above would save 8 additional files alone just right now. In my file structure above, the 'includes' are big standalone elements, 'embeds' are complex functional units of behavior independent of context, and 'definitions' are repetitious generic UI elements. My 'layouts' and 'partials' directories live in /site/templates. I was using the method you mentioned with {include} but it just got to a point where the Latte features really started making more sense. I'm fully using Page Classes so my access to $page and $pages hasn't presented any problems and that's data perfectly suited for parameters passed to a component from a template.
-
Importing definitions are also not working for me. I have this defined: When attempting to use it: I've confirmed that the path is correct, so it's seeing the file, but attempting to create simple elements using {define} and then using them with {import} fails. What am I doing wrong?
-
I came back to the thread to share some experiences and they are very similar to what @sebibu is running into. Latte introduces a lot of challenges with scoping and augments how you interact with ProcessWire. This is most pronounced for me when working with different inheritance types provided by Latte. When working with `include` there is no scope so anything that is added using {include 'file_name.latte'} will have access to the parent variables. This includes $page and $wire. Unfortunately the limitations with Latte is that you can't define blocks inside them, they only take parameters in the {include} statement. This is fine for most applications, but if you have a reusable component, such as a modal where the markup for the modal stays the same but complex contents should be contained in that markup, it would best be served by using {embed} because you can define something like {block content}{/block}. So while variables are available in primary template files, using all available features in Latte starts to lock out ProcessWire. Files included using {embed} are isolated in scope, so anything you want/need available in that file must be passed as parameters or added as rendered content within a {block}stuff here{/block}. This can get laborious for developers that are used to having ProcessWire's global variables and functions available anywhere. From my experience, there are two options. {embed 'file_to_embed.latte', page: $page, wire: $wire, foo: 'foo', bar: 'bar } {block content}waddap{/block} {/embed} You can choose to add parameters to an {embed} file that provide ProcessWire's API. I'm not entirely a fan of this because it feels laborious and if we're just going to have to pass the ProcessWire API into Latte components to overcome the limited scope, then it's just an extra step that ends up adding additional parameters passed to components all over your templates very repetitiously. I'm also used to working around the concept of only providing objects the specific data they are supposed to work with like best practices for dependency injection containers. The second option is to add custom functions to Latte using a hook at runtime. This method allows you to declare functions that will be available globally within all types of Latte reusability methods. Here's an example of a hook file on my current project. I have a dedicated latte_functions_extension.php file where I can declare these in an organized way. <?php declare(strict_types=1); namespace ProcessWire; use Latte\Extension; final class CustomLatteFunctions extends Extension { public function getFunctions(): array { return [ // Latte templating paths 'definitions' => fn (string $file) => $this->createComponentPath('definitions', $file), 'embeds' => fn (string $file) => $this->createComponentPath('embeds', $file), 'imports' => fn (string $file) => $this->createComponentPath('imports', $file), 'layout' => fn (string $file) => $this->createPath('layouts', $file), 'partial' => fn (string $file) => $this->createPath('partials', $file), // Expose ModernismWeekSite.module.php as site() 'site' => fn () => wire('modules')->get('ModernismWeekSite'), // Ensure that wire() is available in all components 'wire' => fn (?string $property = null) => wire($property), ]; } /** * Creates a path for including a component * @param string $file Dot notation subdir and filename, * @return string */ private function createComponentPath(string $componentSubdir, string $file): string { return $this->createPath("components/{$componentSubdir}", $file); } /** * Creates a component file path for a given filename does not require .latte suffix * @param string $templatesSubdir Name of directory in /site/templates * @param string $file Name of .latte file that exists in the directory */ private function createPath(string $templatesSubdir, string $file): string { !str_ends_with($file, '.latte') && $file = "{$file}.latte"; return wire('config')->paths->templates . "{$templatesSubdir}/{$file}"; } } $wire->addHookAfter( "RockFrontend::loadLatte", fn (HookEvent $e) => $e->return->addExtension(new CustomLatteFunctions), ); I've defined a 'wire' function that will be available in every component with a parameter that allows you to use it like you would expect to such as 'wire('modules')'. I have a custom site module so I've exposed that as 'site()'. If you wanted to make it easier to work with modules in your templates and included files you could define a more terse 'modules' function: <?php final class CustomLatteFunctions extends Extension { public function getFunctions(): array { return [ // ... 'modules' => fn (string $moduleName) => wire('modules')->get($moduleName), ]; } } I feel that there is a tradeoff when using Latte in ProcessWire. There are some great features in Latte, but it requires introducing abstractions and feature management to make Latte act like ProcessWire, like manually defining functions. This just means that you'll have to keep a balance of complexity/abstraction vs. using as minimal enough of a approach to keep it sane. The other challenge here is that now there can be a deviation between where the native ProcessWire API is used in Latte and other places where it isn't. So some files will use $modules, and other files will use modules(), and it's not clear whether that's referencing the ProcessWire functions API, or whether it's leveraging Latte's custom functions extension feature. Something to keep in mind when determining how other files will be included/rendered in other files. In my case I have two examples that brought this challenge out for me today. Here's one // Native behavior provided by the module // Does not work everywhere due to scoping in Latte. This caused an issue when trying to embed forms // in a modal within a {block} {$htmxForms->render($page->field_name)} // With one level of abstraction using a custom function // Because this replicates how ProcessWire provides the wire() function natively, the usage feels // natural and predictable, especially for core behavior, but this introduces a lot of verbosity // that starts to make files pretty messy {wire('modules')->get('FormBuilderHtmx')->render($page->field_name)} // With two levels of abstraction in Latte via a custom function // This looks better and still adheres to the syntax of the ProcessWire functions API // The issue is that every native ProcessWire function has to be manually replicated in our custom // functions hook class. Managing this in the long term requires extra work and cognitive load {modules('FormBuilderHtmx')->render($page->field_name)} // With 3 levels of abstraction // This has restored the feel of the variables provided by the module, but again we have to track // And implement them on an as-needed basis to manage them within the context of usage in Latte {htmxForms()->render($page->fieldName)} The level of abstraction you choose depends on how much customization you want vs. how much extra work it will take to maintain simplicity by hiding complexity. The other functions, 'embeds', 'definitions', 'imports', etc. are to overcome the relative paths all over the place in Latte. // In my home.latte file {layout './layouts/main.latte')} {var $collapseNavOnScroll = true} {import './components/definitions/headlines.latte'} {import './components/definitions/event_activity_card.latte')} {block subnav} {embed './components/embeds/event_subnav.latte', eventPage: $page->eventPage()}{/embed} {/block} // ...etc // Becomes {layout layout('main')} {var $collapseNavOnScroll = true} {import definitions('headlines')} {import definitions('event_activity_card')} {block subnav} {embed embeds('event_subnav'), eventPage: $page->eventPage()}{/embed} {/block} // etc. // In RPB blocks {embed '../../../components/embeds/example.latte', content: $block->body()}{/embed} {embed embeds('example'), content: $block->body()}{/embed} This really helps when working with Latte templates that embed components that have nested embeds and imports because the functions are generating absolute paths that Latte can handle. With these functions, I don't have to think about relative paths anywhere. As for the directory structure that I chose that requires the different paths, here's what it looks like: /templates ...etc /components /definitions /embeds /imports ...etc I chose that method because Latte has many different ways of including, embedding, and importing code from other files. It made more sense to organize my code by how Latte treats it. It wasn't my first choice, but this overcomes confusion that I was experiencing when working with all of the files sharing the same components directory. Without this type of organization it can be challenging to because of scoping and how {embed}, {include}, {define}, and {import} behave differently. Some accept parameters, export values, or use blocks, but not all of them do. So having a "modal.latte" component file that provides a {block content}{/block} next to 'button.latte' that doesn't render anything and only exports items created using {define}, next to a file that is only added to a template using {include} that doesn't take parameters or provide blocks had me jumping between files a lot and slows down development checking to see that the file is being used correctly. Just sharing some of my experiences in case it helps anyone else out. If anyone sees anything here that can be done better or if I'm missing out on features that Latte has that don't require some extra steps, let me know!
-
@bernhard This is the module I mentioned for generating placeholder images. Has worked very well.
-
@bernhard I've never been so excited to see errors in my code! Thank you!
-
@bernhard Well that's fantastic, I already forgot to do that since jumping back to work on my project 😵💫 many thanks!
-
[SOLVED] Migrations execute every time PHP/ProcessWire boots
FireWire replied to FireWire's topic in RockMigrations
@bernhard Thanks for the meeting! Going over everything was great. Problem was on my understanding and not the module itself. RM is a fantastic tool, really appreciate it! -
Thoughts on module structure for a web app
FireWire replied to MarkE's topic in Module/Plugin Development
@MarkE I know this is a late answer, but I think this is a great question and something that can feel daunting. Hopefully it's still helpful for you and possibly others. I originally wrote my translation module back in 2020 and when it came time to build upon it, add new features, and respond to feedback from the community that first version felt like a straightjacket in terms of structure. I rewrote it last year and have been able to iterate on it much more easily and confidently. This is really a decision based on how you view your project and what "feels good". When you get to writing modules as complex as the one you're describing, as always, the best way to reason about your approach is consistency and maintainability. This is more critical than trying to fit your code into someone else's structure. The most important way to think about organization is considering where you would first look to find a specific feature or behavior and if there are enough of these to warrant their own directory. My translation module probably isn't as complex as yours is, but it may not matter because we're both writing software applications that must be understood and maintained. Anyone can view the repo I shared above, but here are some notes about my reasoning. The purpose of this isn't meant to be a set of instructions or a roadmap, but more of a way to help you think about what is best for your application. [app] - This contains everything that supports what the module must do. If it has logic or supports logic, it goes here. [Caching] - Caching is a performance requirement and required abstraction and logic to tailor PW's caching feature for my use EngineLanguagesCache.php TranslationCache.php [Components] - Fieldsets used in one or more places where abstracting helped keep clean code in other files, like the main Module Traits - Shared behaviors used by Component files FluencyApiUsageTableFieldset.php FluencyStandaloneTranslatorFieldset.php [DataTransferObjects] - All DTOs that encapsulate data provided to and returned by methods in different classes Traits - Shared behaviors used by DTOs AllConfiguredLanguagesData.php ConfiguredLanguageData.php DEVDOC.md - A guide for me and others about how to use DTOs and their purpose as general data objects EngineApiUsageData.php .... [Engines] - Self contained "submodules" that interact with 3rd party services, both current and future. [DeepL] [GoogleCloudTranslation] [Traits] DEVDOC.md FluencyEngine.php FluencyEngineConfig.php FluencyEngineInfo.php [Functions] - General functions imported to support specific sub-behaviors that serve a specific purpose in more than one-two places fluencyEngineConfigNames.php typeChecking.php [Services] - Features that are added to Fluency that extend beyond the primary function of translating page content FluencyProcessWireFileTranslator.php FluencyErrors.php FluencyLocalization.php FluencyMarkup.php [assets] - Files that contain admin-facing UI files and compiled code, Gulp compiles and outputs to this directory [img] [scripts] [styles] [src] - Source JS and SASS that gets compiled and output to the assets directory [scripts] [scss] ...(editor/dotfiles) CHANGELOG.md - Semantic versioning is your friend, this is as important for you as it is for others Fluency.info.php Fluency.module.php FluencyConfig.php LICENSE README.md - This goes into high detail of all features because it also organizes my understanding of everything Fluency does ...(JS module configs and Composer file) I spent a lot of time on this decision. Probably overthought it, and since then there are things that I would do differently- however, how it exists now has been great to work with. My approach was entirely based on what this module does now, and what it may do in the future. The focal point and goals of this approach serve these purposes: - Fluency.info.php - Not required to separate, but helped keep the main module file cleaner and purpose-driven - Fluency.module.php - Aims to primarily contain methods needed to integrate with ProcessWire and any methods available via the global $fluency object - FluencyConfig.php - ProcessWire needs this to be configurable, but the amount of logic it contains certainly benefits from its own file Things I like having lived with it for over a year now. The directory structure is great and supports future expansion since it organizes by purpose. The specific way I structured my subfolders may not make sense for most others, but the message here is the logic behind the approach. Like I said, it seemed overengineered at the time, but now I know exactly where everything is and where everything should be in the future. This module has heavy UI integration and the way the JS files in src/ are broken down makes it clear what everything does. Because JS can import modules from other files, it really doesn't matter how many you have. The JS files for each Inputfield has on more than one occasion enabled me to add features or fixes that would have taken much longer without the granularity I put in place. Initially this module only supported DeepL for translation, but because of how Engines is structured as individual units, it was relatively easy to come back and add Google Cloud Translation because of how the individual units of behavior were organized and supported. What would I do differently having lived with it for over a year now? I would change the file names in app/ and remove the 'Fluency' prefix. That's why we have namespaces. Name collisions are handled by PHP so a simple file naming approach would have been best. This is something that I could change though without impacting the end user. FluencyErrors, FluencyMarkup, and FluencyLocalization might be better located in a 'Support' directory or something, but that would just be applying my approach to organization rather than needing to serve a hard purpose. It doesn't cause any confusion or problems as is. Probably some little things here or there, but not nagging enough to mention or even remember. Anyway. I chose a directory heavy structure with nesting which was a personal choice. There are a lot of great modules out there with different structures. A mark of good organization is asking the question "if I've never looked at this codebase before, would it be something I could get up to speed quickly and contribute without difficulty?". This may seem like a disconnected and impersonal approach, but inevitably you will feel like you need to relearn your own codebase in the future if you've spent enough time away from it. This is a great place to start. In a complex enough project, it may be worth considering separate modules that are installed as dependencies. This would let you group like-kind behavior where it may also be possible that some separate functionality could stand on its own. If you can organize things to separate concerns enough, it's trivial to access other installed modules from within modules using $this->wire('modules')->get('YourOtherModule'); and access all of its features and functionality. Since you can configure your module to have required dependencies, you can be sure that the modules you need will always be available. Organizing by purpose and features is really helpful. If the behavior doesn't provide anything useful as a standalone, then consider keeping it within the "wrapper" module. It depends on how portable you need individual behaviors to be separate from others. I'm not sure too much on the specifics of the implementation, but these are files that perform a specific type of work and would be candidates for their own directory. There's no rule for this, but it makes sense because it gives you a single place to look for these files. Long story short, trust yourself now and think about future you who will work on this later. You'll thank yourself for it. Hope this was relevant enough to be helpful even though it's a late-in-game response.- 1 reply
-
- 6
-
Enjoy your time away from the keyboard! Well deserved break.
-
@SIERRA I use Google Cloud services for some things but I'm not really an expert on it. I can provide some information that I am familiar with but you'll need to do research to determine if it fits your needs. I built a module that interfaces with a Google service but it doesn't provide any special insights. I'll start off with the easy answers first. Yes. Fluency translates the content that you enter into ProcessWire into other languages using the third party service, then enters that translated content into ProcessWire which is then stored in your website's database. This is stored the same way as if you had manually entered the content in other languages yourself. Even if you translated content for your website and then uninstalled Fluency altogether your translated content is stored in ProcessWire and is yours to keep. Third party translation services almost always, as far as I've seen, charge by character count. This is because words and phrases may be different lengths in different languages. It's not really possible to guess ahead of time what your character count will be according to word count, the best way to determine that is to translate some content then check your Google Cloud account to see how many characters you have used and then come up with an estimate based on that information. See my note below on using Google's tools to estimate this. Yes. ProcessWire's language support adds tabs to each multi-language field. If you manually add content to the fields under each tab and don't use Fluency to perform translation then you will have not used any translation service credits. That said, consider the following: Here you can see that Fluency is set up with the "Translate to all languages" option configured. This means that if you have entered content in any language and click "Translate to all languages", Fluency will translate that content into all of the other languages and then replace anything that has been already been entered under other language tabs. This is intentional behavior. If you use this option and have already added content that you are satisfied with in another language, then clicking "Translate to all languages" may be unnecessary because it will translate your content and overwrite it anyway. I'm not able to tell you want to do and I don't want to be responsible for any charges that came about due to bad information on my part. Google provides plenty of information about how they charge for their services and I would just be doing the same research that you would need to do to fit your use case. Google has a pricing calculator that you can use to estimate costs for usage of any Google Cloud services, including translation. Here's the link to check it out. No. Their billing is based on services usage per month so you will only pay for what you use. There isn't really a service that exists, as far as I know of, that would charge a flat fee for "a website" because that could mean a very large website or a very small website. I'll make a couple of recommendations that may help you out in this situation. If this is for a client, then the costs of using third party services are what they are. You don't have control over pricing or how much they ultimately intend to use the service because this is content that they will provide. The estimated content range of 400,000 to 500,000 words is very wide and not accurate. That range is 20% and won't be accurate, especially considering that the charges are in characters and not words which each have different character counts . To put this into context, a quick search found that the average length of a novel is 40,000 to 70,000 words long. That means the difference between the two word counts in your case is anywhere from 1-2 entire books you'd find at a book store- that isn't trivial! The best idea is to work on getting accurate pricing is to try and get a much more accurate estimate on length of content. Book publishers would not accept a 100,000 give-or-take estimate from an author in how long a novel is because there are real costs involved, and the same would best be true in this case. Here's a few things that you may be able to do: If you have all of the content ahead of time, then get an accurate character count and pad it a little to get a price you can work with. If you have some of the content, but not all, take estimate the percentage that content is relative to the entire amount of content, get the character count, and multiply it. If you estimate that you have 10% of the content right now, get the character count and multiply that by 10, then add a little to be safe, and estimate your costs. If the website will list products, or blog posts, or something that repeats, estimate the amount of content for one and then multiply that by an estimate of the number of whatever will be repeated on the site. It's much easier to estimate the content for one item of many and then estimate the number of how many there will be. If you already have a few examples, get the character count for each and calculate the average before multiplying. Hope that helps. Just trying to offer some advice that may be useful.
-
@SIERRA I'm having difficulty replicating. I just set up translation using Google Cloud Translation from scratch and it worked. Here are the steps I took: Create a new Google Cloud API key using these steps. Enabled the Cloud Translation API in Google Console at https://console.cloud.google.com using these steps. The only required steps are located under these headings: Create or select a project Enable billing Enabling the API Ensure that your API key is authorized to use Google Cloud Translation You do not need to set up OAuth to use Fluency. Only an API key that has permission to use Cloud Translation. Fluency can only show/log the errors that are provided so, unfortunately, receiving a response from the Google API with no message doesn't help with troubleshooting. The Google Translation Engine in Fluency handles errors according to their API spec and, in the event of an error, expects one of the following Google response error codes: NOT_FOUND UNAUTHENTICATED PERMISSION_DENIED INVALID_ARGUMENT RESOURCE_EXHAUSTED INTERNAL UNAVAILABLE If none of these codes is returned, Fluency falls back to unavailable which provides that vague log entry with an empty message because Google didn't actually return an error message. If you can walk through the steps above you should be able to get up and running. Also double check that your billing is properly set up. IIRC, it's necessary to have billing configured even if you are planning to stay within your free monthly allocation of API services use. I'm not sure what to do beyond that, but let me know if this helps!
-
So I thought I had fixed the issue but I'm still seeing issues with RM running on what looks like every boot of PHP. I mentioned seeing it after installing WireCache Filesystem and thought that uninstalling it fixed the issue but it's continuing. I've closed every tab and the only tab open is the logs page so it seems that the logs page refreshing is triggering migrations. I've commented out all lines in /site/migrate.php so there isn't anything to run and the only migrations I currently have are in RPB blocks. Migrations are running even when there aren't any changes to files. I've disabled migrations but files are still being watched and migrations are attempted. There aren't any changes in any files that would be watched. It happens when I stop making changes altogether. The only way I could stop the attempted migrations is by commenting out line 167 in RockMigrations.module.php I'm not sure how the file watch feature works or what's triggering the execution so not sure where to start looking to troubleshoot. Any thoughts?
-
@JayGee The module expects Method C so your markup should look something like this: <?php $htmxForm = $htmxForms->render('your_form_field'); echo $htmxForm->styles; echo $htmxForm->scripts; echo $htmxForm; The module only swaps what's inside the form markup and shouldn't have any conflicts with scripts or styles. Option C has been the best option for me because I can add the styles and scripts once on the page wherever I want them rather than directly where forms are rendered. I throw the CSS file up in the <head> element and the scripts at the bottom near the closing <body> tag with my others.
- 25 replies
-
- 1
-
- forms
- formbuilder
-
(and 2 more)
Tagged with: