Jump to content

Leaderboard

Popular Content

Showing content with the highest reputation on 03/08/2022 in all areas

  1. Here is a little breakdown on what I've done to achieve what I needed. Basically I wanted to have the ability to change page content without browser refresh, using a simple page reference field placed on top of the page tree, allowing the selection of a page (and fields) I want to use to replace the content with. I've adopted the "kinda new" url hooks, in orded to have and end-point to make my request and have the content I wanted as response: <?php namespace ProcessWire; $wire->addHook('/emit', function ($event) { header('Content-Type: text/event-stream'); header('Cache-Control: no-cache'); $data = []; // page_selector is a page reference field $contributor = $event->pages->get('/')->page_selector->title; // get the current data for the page I have selected $data['current'] = $event->pages->getRaw("title=$contributor, field=id|title|text|image|finished, entities=1"); // other data I wanted to retrieve foreach($event->pages->findRaw("template=basic-page") as $key=>$value) { $data['contributors'][$key] = $value; } $result = json_encode($data); // This is the payload sent to the client, it has to be formatted correctly. // More on this here: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format echo "event: ping" . PHP_EOL; // The event name could be whatever, "ping" in this case. echo "data: " . $result . PHP_EOL; echo PHP_EOL; ob_end_flush(); flush(); }); Doing so we now have a url to point our SSE client. Let's see how it looks like: Vue.createApp({ data() { return { // This propertied are filled by resolveData() method below. fragmentShow: false, finished: '', id: '', title: '', text: '', image: '', imageDescription: '', contributors: '', } }, methods: { // imageUrl(), basePath() and idClass are helper functions. imageUrl() { return `http://localhost/sse_vue/site/assets/files/${this.id}/${this.image}`; }, basePath() { return `http://localhost/sse_vue/site/assets/files/`; }, idClass() { return `id-${this.id}`; }, // Retrieve data from the incoming stream getData(event) { return new Promise((resolve, reject) => { if (event.data) { let result = JSON.parse(event.data); // If the incoming page id is different than the current one, hide the container. if (this.id != result.current.id) { this.fragmentShow = false; } // This allows the <Transition> vue component to complete its animation before resolving the result. setTimeout(() => { resolve(result); }, 300) } }).then((result) => { this.resolveData(result); }); }, resolveData(result) { // Once the new values has come show the page again this.fragmentShow = true; // Set incoming values to vue reactive data() object this.finished = result.current.finished, this.id = result.current.id, this.title = result.current.title, this.text = result.current.text, this.image = result.current.image[0].data, this.imageDescription = result.current.image[0].description, this.contributors = result.contributors }, }, mounted: function() { // Init SSE and listen to php page emitter let source = new EventSource('/sse_vue/emit/'); source.addEventListener('ping', (event) => { // Get the incoming data this.getData(event); }); } }).mount('#app') On mounted vue lifecycle hook I start listening to the incoming stream and place useful informations inside the reactive data() object properties. Then I populated the html with those properties: <div id="app" class="container"> <Transition> <div :class="idClass()" class="bg-slate-200" v-show="fragmentShow"> <h1>{{title}}</h1> <p v-html="text"></p> <img :src="imageUrl()" :alt="imageDescription"> <div v-for="(contributor, index) in contributors"> <p :class="contributor.finished == 1 ? 'finished' : ''">{{contributor.title}}</p> <div v-if="contributor.document"> <a :href="basePath() + contributor.id + '/' + contributor.document[0].data">Download document</a> </div> </div> </div> </Transition> </div> Attached the video of the result (please don't look at the styling of it, it sucks). sse.mp4
    4 points
  2. $item is a Page object. The "Page" class extends the "Wire" class (which all of PW's classes do), so you can access the ProcessWire instance via $item->wire and from there you can access the "Sanitizer" class via ->sanitizer: $item->wire->sanitzer->trunc(...) <?php $list = $pages->get(1234)->children; $toJSON = $list->explode(function($item) { $summary = $item->wire->sanitizer->truncate($item->summary, [ 'type' => 'punctuation', 'maxLength' => 155, 'visible' => true, 'more' => '…', ]); return [ 'title' => $item->title, 'id' => $item->id, 'start' => $summary, ]; }); Most of the time you can even do $item->sanitizer without the ->wire but I'd consider it a best practise to use ->wire because it's slightly more efficient and less error prone.
    3 points
  3. Over the course of 2021, I re-developed the Transferware Collectors Club Database of Patterns and Sources. It is a private, members only database that contains nearly 20,000 patterns of various types of transferware, which in layman's terms is basically art on plates, bowls, cups, etc. (Wikipedia) Many archaeologists and transferware enthusiasts use this database to learn more about the historical nature of their collections or archaeological finds. The system had a long history and there were two versions prior. I believe the first iteration was made with a PHP framework of some sort, then it was re-developed around 2015 in ProcessWire. One special aspect of the previous version was that it utilized Elastic Search. Advanced search capability of the system, both in terms of free-form text entry as well as filters were the crux of the system. In 2020, I was introduced to this project as the previous developer decided to retire. While doing some various maintenance type work, I realized that there was much that could be done to improve the system, from re-imagining the user interface to utilizing all the latest features in the newer versions of ProcessWire, as it had been locked to version 3.0.135 or something at least 3-4 years old. Also, Elastic Search had become deeply outdated and unreliable since the entire site had been moved to a new server. Finally, I saw various aspects of the data model that could be improved as well as reduce several dependencies, while also using new techniques like HTMX. I redeveloped the system over the course of 2021 and the end result has greatly impacted the capabilities of the system as well as its ability to be maintained with little complication years into the future. Search: First, it became apparent that the way in which Elastic Search was being used had little benefit, became unstable and was outdated in the time since it was implemented. I don't like relying on 3rd party solutions, especially for something so critical and I thought about how I could develop a great search experience with ProcessWire alone. To do this, I utilized the WireWordTools module to handle lemmatization and such. Then, I advocated for this feature to be developed into the core, which could allow the results from multiple search queries to be easily stacked one after another and not affect order or pagination. This was a key change allowed for "ranked" search results. Finally, I integrated the BigHugeThesaurus API so that entered search terms can have their synonyms automatically generated. Filters: The previous version of the system had filtering capability, but it was too cumbersome and in my opinion, just didn't feel right. A significant amount of thought went into how to improve it while untangling some issues with the data model (and just understanding what Transferware is in the first place). The end result has has been an extensive set of filters with a special user interface so that users can easily select what they need. Frontend: I take the "classic", server-rendered approach when it comes to web-apps, and during this time, HTMX started becoming a favorite among such developers. By using HTMX, searches can be conducted without having to hit the submit button every time. It gives it that polished ajax-y feel without having to write any JavaScript whatsoever and allowing the use of ProcessWire in the expected way as opposed to it acting as some API end-point. To be able to do this is somewhat of a game-changer and I feel like I'm not "missing out" on these features which in the past were a pain to have to invent as there really wasn't a clean, straight-forward way to do it. HTMX is a gem and it couples very nicely with ProcessWire. I'm also using Alpine.js lightly for toggling things here and there. My goal was to make this project jQuery-free. UIkit 3 is being used as the frontend CSS framework. User Authentication: As mentioned, the database must be accessed after a membership has been purchased. TCC already has a 3rd party membership system, so I developed a handshake that allows a user to log in with the ProcessWire admin form using their credentials from that 3rd party system. If it's the member's first time logging in, it will then automatically create a ProcessWire account for the member so they can bookmark patterns, save searches, etc. The previous version had a different type of handshake that ultimately only logged the users into one master "member" account, so users wouldn't be able to save any information since everyone technically shared the same account. New Server: I put the site on its own dedicated droplet to squeeze out even more performance and be independent of their main marketing site (based on Drupal). We had a couple instances of configuration changes causing some clashes, so this move isolated it from any potential breaking changes that may occur on the main site. A 2-core droplet is all that is needed for now, which is cost effective. Demo Video and Article: While the site can't be visited given it requires an account, I did create an in-depth search tutorial video which shows off quite a bit which I can post here since it's publicly accessible: https://player.vimeo.com/video/666912411?h=30a94132db Feel free to read more about the TCC DB in this interview with Dr. Loren Zeller, who I worked closely with the entire time and who provided invaluable feedback and guidance along the way: https://www.transferwarecollectorsclub.org/sites/default/files/pdf/special-interest-pdf/0225_d_001_analink.pdf More details on my personal website: https://jonathanlahijani.com/projects/transferware-collectors-club/
    2 points
  4. I've created a PR for this issue https://github.com/processwire/processwire/pull/219
    2 points
  5. If anyone is still interested about this topic I've gracefully solved sse implementation both with alpine.js and vue.js (just for fun). Let me know if you want some code examples and I'll post them here.
    2 points
  6. In the RepeaterMatrix field create two repeater matrix item types: cta1 & cta2. They don't even have to have any fields, i guess. In the template file that should output the repeater items you can output them like this: <?php foreach($page->repeater_matrix_field as $item){ if ($item->type == "cta1"){ echo wireRenderFile("Call-to-Action01.php"); } if ($item->type == "cta2"){ echo wireRenderFile("Call-to-Action02.php"); } } ?> RepeaterMatrix is a pro module, though. Not Core.
    1 point
  7. Hello @fruid, the $sanitizer->text() method has a stripQuotes option: stripQuotes (bool): strip out any "quote" or 'quote' characters? Specify true, or character to replace with. (default=false) I think this should work: $sanitizer->text($yourText, ["stripQuotes" => true]); Regards, Andreas
    1 point
  8. Meanwhile i found the way to deal with my approach for translations. Possibly it helps others, so i post it here. Tl;dr: I have a function to enable translation of static template strings. This allows me to reference a _strings.php file as site translation file in the PW Backend. To apply this to the SearchEngine Module, you have to edit some configuration settings before rendering. $searchEngine = $modules->get('SearchEngine'); $config->SearchEngine = [ 'render_args' => [ 'strings' => [ 'form_label' => _t('Suche', 'search'), 'form_input_placeholder' => _t('Ihr Suchbegriff...', 'search'), 'form_submit' => _t('Suche', 'search'), 'results_heading' => _t('Suchergebnisse', 'search'), 'results_summary_one' => _t('Ein Ergebnis für "%s":', 'search'), 'results_summary_many' => _t('%2$d Ergebnisse für "%1$s":', 'search'), 'results_summary_none' => _t('Keine Ergebnisse für "%s".', 'search'), 'errors_heading' => _t('Hm, ihre Suchanfrage konnte nicht ausgeführt werden', 'search'), 'error_query_missing' => _t('Bitte tragen Sie einen Suchbegriff in das Eingabefeld ein', 'search'), 'error_query_too_short' => _t('Bitte verwenden sie mindestens %d Zeichen für den Suchbegriff.', 'search'), ] ] ] <?= $searchEngine->renderForm(); ?> <?= $searchEngine->renderResults(); ?> === Approach to translate: _func.php /** * This function enables the translation of static template strings. * All translations are done in `_strings.php`. More info within the * comments of `_strings.php`. * * @param string $text text to translate * @param string $context Context to allow doubles * @param string $textdomain Point static textdomain * @return function Function for gettext parser */ function _t($text, $context = 'Generic', $textdomain = '/site/templates/_strings.php') { return _x($text, $context, $textdomain); } _strings.php /** * Static translated template strings to use project wide * in any templates. This spares us defining page fields * for every piece of code that doesn't have to be editable * within the admin interface. The function doesn't need to * execute with PHP, so we wrap it in a comment leaving * the code only visible to the language parser. * * Relies on function `_t` (s. `_func.php`) * * Usage: * 1. Define string in markup, e.g. "_t('<string>', '<context>')" * 2. Insert string in `_strings.php`, e.g. "_x('<string>', '<context>')"; * both without the surrounding double quotes. This is just to avoid * this comment is parsed. * 3. Choose the language you like to translate the default language strings * 4. Under (possibly still empty) "Site Translation Files" click on "Translate File" to get * a list of translatable files. Choose the `_strings.php` and click send/save. You'll * see the translatable string phrases. */ /*! _x('Suche', 'search') _x('Ihr Suchbegriff...', 'search') _x('Suchergebnisse', 'search') _x('Ein Ergebnis für "%s":', 'search') _x('%2$d Ergebnisse für "%1$s":', 'search') _x('Keine Ergebnisse für "%s".', 'search') _x('Bitte tragen Sie einen Suchbegriff in das Eingabefeld ein.', 'search') _x('Bitte verwenden sie mindestens %d Zeichen für den Suchbegriff.', 'search') */
    1 point
  9. The JS is missing a call to initialise the datepicker when a field is reloaded (e.g. when an ajax-loaded Repeater item is opened). You could open a GitHub issue if you like. InputfieldDatetime.js needs something like this added: $(document).on('reloaded', '.InputfieldDatetime', function() { var $input = $(this).find('input.InputfieldDatetimeDatepicker:not(.InputfieldDatetimeDatepicker3):not(.initDatepicker)'); if($input.length) InputfieldDatetimeDatepicker($input); });
    1 point
  10. @kongondo, thanks for letting me know. Will check your module next time. In the meantime I solved my problem just doing it manually by extending some of the default navtree templates, without the help of a module.
    1 point
  11. 1. If it's your code that's putting the repeater page IDs into session then you can count on them being what you expect and you don't necessarily have to validate them against a template or a repeater field. So you could just do... $item = $pages->get($id) ...or... $item = $pages($id); 2. It's because in the first case the selector is going to the database via PageFinder and in the second case the selector is searching within a PageArray in memory. There are a number of differences between what is possible with these two kinds of selectors but unfortunately those differences aren't described in the official documentation. Some related posts: https://processwire.com/talk/topic/16067-find-repeater/?do=findComment&comment=148983 https://processwire.com/talk/topic/18343-selector-filter-for-multiple-dates/?do=findComment&comment=160451 https://processwire.com/talk/topic/20143-selector-is-case-sensitive/ 3. For your Page Reference field you can use something like this as the "Selector string" for selectable pages: id=page.your_repeater_field_name Or you could use the Custom PHP option: $wire->addHookAfter('InputfieldPage::getSelectablePages', function($event) { $page = $event->arguments('page'); if($event->object->hasField == 'your_page_reference_field_name') { $event->return = $page->your_repeater_field_name; } }); If you add new repeater items you'll need to save the page before you can select them, because PW doesn't know they exist until you do that.
    1 point
  12. I just released a new extension module AppApiPage (waits for approval), which handles the initial steps from my post above completely automatic. You install AppApi and AppApiPage. That makes the /api/page route available and you only have to add the code on top of your template php to add a custom JSON output to your pages. <?php // Check if AppApi is available: if (wire('modules')->isInstalled('AppApi')) { $module = $this->wire('modules')->get('AppApi'); // Check if page was called via AppApi if($module->isApiCall()){ // Output id & name of current page $output = [ 'id' => wire('page')->id, 'name' => wire('page')->name ]; // sendResponse will automatically convert $output to a JSON-string: AppApi::sendResponse(200, $output); } } // Here continue with your HTML-output logic... I hope that this makes it even simpler to add a full-blown JSON api to new and existing pages.
    1 point
  13. Hello all! I guess I'm a little late to the party, but here's an explanation to my AppApi approach. I basically wanted to achieve exactly the same thing with my projects as well: A universal JSON api that I make all public ProcessWire pages also queryable as JSON. For this I use the combination of my Twack module and AppApi. Twack is a component system that allows me to get a JSON output from each ProcessWire page in addition to the standard HTML output. Twack registers a route that allows to query each Page from the PageTree (via ID or Path). But for this route you don't need the Twack module, of course. As here it would work only with the AppApi module: routes.php: <?php namespace ProcessWire; require_once wire('config')->paths->AppApi . 'vendor/autoload.php'; $routes = [ 'page' => [ ['OPTIONS', '{id:\d+}', ['GET', 'POST', 'UPDATE', 'DELETE']], ['OPTIONS', '{path:.+}', ['GET', 'POST', 'UPDATE', 'DELETE']], ['OPTIONS', '', ['GET', 'POST', 'UPDATE', 'DELETE']], ['GET', '{id:\d+}', PageApiAccess::class, 'pageIDRequest'], ['GET', '{path:.+}', PageApiAccess::class, 'pagePathRequest'], ['GET', '', PageApiAccess::class, 'dashboardRequest'], ['POST', '{id:\d+}', PageApiAccess::class, 'pageIDRequest'], ['POST', '{path:.+}', PageApiAccess::class, 'pagePathRequest'], ['POST', '', PageApiAccess::class, 'dashboardRequest'], ['UPDATE', '{id:\d+}', PageApiAccess::class, 'pageIDRequest'], ['UPDATE', '{path:.+}', PageApiAccess::class, 'pagePathRequest'], ['UPDATE', '', PageApiAccess::class, 'dashboardRequest'], ['DELETE', '{id:\d+}', PageApiAccess::class, 'pageIDRequest'], ['DELETE', '{path:.+}', PageApiAccess::class, 'pagePathRequest'], ['DELETE', '', PageApiAccess::class, 'dashboardRequest'] ] ]; You can use this class to get the page-outputs: <?php namespace ProcessWire; class PageApiAccess { public static function pageIDRequest($data) { $data = AppApiHelper::checkAndSanitizeRequiredParameters($data, ['id|int']); $page = wire('pages')->get('id=' . $data->id); return self::pageRequest($page); } public static function dashboardRequest() { $page = wire('pages')->get('/'); return self::pageRequest($page); } public static function pagePathRequest($data) { $data = AppApiHelper::checkAndSanitizeRequiredParameters($data, ['path|pagePathName']); $page = wire('pages')->get('/' . $data->path); return self::pageRequest($page); } protected static function pageRequest(Page $page) { $lang = SELF::getLanguageCode(wire('input')->get->pageName('lang')); if (!empty($lang) && wire('languages')->get($lang)) { wire('user')->language = wire('languages')->get($lang); } else { wire('user')->language = wire('languages')->getDefault(); } if (!$page->viewable()) { throw new ForbiddenException(); } return $page->render(); } private static function getLanguageCode($key) { $languageCodes = [ 'de' => 'german', 'en' => 'english' ]; $code = '' . strtolower($key); if (!empty($languageCodes[$key])) { $code = $languageCodes[$key]; } return $code; } } On top of your ProcessWire-template file you have to check if an api-call is active. If so, output JSON instead of HTML: <?php // Check if AppApi is available: if (wire('modules')->isInstalled('AppApi')) { $module = $this->wire('modules')->get('AppApi'); // Check if page was called via AppApi if($module->isApiCall()){ // Output id & name of current page $output = [ 'id' => wire('page')->id, 'name' => wire('page')->name ]; // sendResponse will automatically convert $output to a JSON-string: AppApi::sendResponse(200, $output); } } // Here continue with your HTML-output logic... That should be everything necessary to enable your ProcessWire-templates to output JSON. You will have an /api/page/ endpoint, which can be called to get the JSON-outputs. /api/page/test/my-page -> ProcessWire page 'my-page' in your page-tree under test/ /api/page/42 -> ProcessWire page with id 42 /api/page/ -> root-page And a small addition: If you want to query the page files also via api, have a look at my AppApiFile module. With it you can query all images via an /api/file/ interface.
    1 point
  14. Hi @Laegnur and welcome here. Your issue come from the fact that your translated strings are on the same line (I admit it could be tricky to spot on first instance if you are not used with ProcessWire). To understand better, please read this sentence : So to get it working, write the first line on two lines lol : <?php // i18n - copyright echo _x('All rights reserver', 'copyright'); ?> <!-- html markup - separator --> | <?php // i18n - developer echo _x('Developed by', 'developer'); ?> You can use of course the syntax you want, just keep in mind that the strings need to be on two lines. For more information about: https://processwire.com/docs/multi-language-support/code-i18n/
    1 point
  15. OK, I found the problem. OPCache was enabled in the new version of MAMP Pro. Just turned it off and everything refreshes as it should. Hope that helps someone!
    1 point
  16. If you are using MAMP make sure you have disabled any cache module from the PHP Language settings on MAMP, sometimes that messes with what you are doing Also how are you setting up the cache expire time?
    1 point
  17. I played around with multi-instances and found out that we currently (PW 3.0.163) are not able to use multiple instances when more then one site has set $config->useFunctionsAPI (in site/config.php) to true! Then I saw that, (when $config->useFunctionsAPI was set to false) in all instances $config->version returned the same version, that from the master instance. So, first I was a bit confused, but then I thought that this may have to do with the early step when PW processes/build the $config. And indeed, if I set in all site/config.php files the $config->useFunctionsAPI to false, and then in all site/init.php files to true, everything is working fine. Now we can use our sites with the functions API, and we can load as many multiple instances in parallel we want. ? TL;DR site/init.php /** * FOR BETTER SUPPORT OF MULTIINSTANCES, WE ENABLE $config->useFunctionsAPI HERE, * INSTEAD OF THE site/config.php FILE: */ $wire->config->useFunctionsAPI = true; Bootstrapping three different instances, in first step each of them in a single environment: <?php namespace ProcessWire; if(!defined('PW_MASTER_PATH')) define('PW_MASTER_PATH', 'E:/laragon/www/hwm/'); if(!defined('PW_MASTER_HTTPURL')) define('PW_MASTER_HTTPURL', 'https://hwm.local/'); // bootstrap ProcessWire instance site1 (3.0.163) require_once(PW_MASTER_PATH . 'index.php'); mvd([ 'httpurl' => $wire->wire('pages')->get(1)->httpURL, 'instanceNum' => $wire->getInstanceNum(), 'config->version' => $wire->wire('config')->version, 'useFunctionsAPI' => $wire->wire('config')->useFunctionsAPI ]); When running all three in a multi instance environment, they load fine, (no compile error), all with the use for the functions API enabled: <?php namespace ProcessWire; if(!defined('PW_MASTER_PATH')) define('PW_MASTER_PATH', 'E:/laragon/www/hwm/'); if(!defined('PW_MASTER_HTTPURL')) define('PW_MASTER_HTTPURL', 'https://hwm.local/'); if(!defined('PW_SITE2_PATH')) define('PW_SITE2_PATH', 'E:/laragon/www/hwm2/'); if(!defined('PW_SITE2_HTTPURL')) define('PW_SITE2_HTTPURL', 'https://hwm2.local/'); if(!defined('PW_SITE3_PATH')) define('PW_SITE3_PATH', 'E:/laragon/www/hwm3/'); if(!defined('PW_SITE3_HTTPURL')) define('PW_SITE3_HTTPURL', 'https://hwm3.local/'); // bootstrap ProcessWire master instance (3.0.163) require_once(PW_MASTER_PATH . 'index.php'); mvd([ 'httpurl' => $wire->wire('pages')->get(1)->httpURL, 'instanceNum' => $wire->getInstanceNum(), 'config->version' => $wire->wire('config')->version, 'useFunctionsAPI' => $wire->wire('config')->useFunctionsAPI ]); // create a secondary instance from master (3.0.163) $wire = new \ProcessWire\ProcessWire(PW_MASTER_PATH); mvd([ 'httpurl' => $wire->wire('pages')->get(1)->httpURL, 'instanceNum' => $wire->getInstanceNum(), 'config->version' => $wire->wire('config')->version, 'useFunctionsAPI' => $wire->wire('config')->useFunctionsAPI ]); // create instance of a second site (3.0.162) $site2 = new ProcessWire(PW_SITE2_PATH, PW_SITE2_HTTPURL); mvd([ 'httpurl' => $site2->wire('pages')->get(1)->httpURL, 'instanceNum' => $site2->getInstanceNum(), 'config->version' => $site2->wire('config')->version, 'useFunctionsAPI' => $site2->wire('config')->useFunctionsAPI ]); // create instance of a third site (3.0.152) $site3 = new ProcessWire(PW_SITE3_PATH, PW_SITE3_HTTPURL); mvd([ 'httpurl' => $site3->wire('pages')->get(1)->httpURL, 'instanceNum' => $site3->getInstanceNum(), 'config->version' => $site3->wire('config')->version, 'useFunctionsAPI' => $site3->wire('config')->useFunctionsAPI ]);
    1 point
×
×
  • Create New...