Jump to content

kongondo

PW-Moderators
  • Posts

    7,473
  • Joined

  • Last visited

  • Days Won

    144

Everything posted by kongondo

  1. Apologies for the delay folks. Here you go: Demo code for Using htmx to Refresh ProcessWire Frontend Content.
  2. Sorry folks, I've been tied up. Will do so later today. If you mean the whole page (i.e. including, e.g. the site menu, etc), then what @netcarver said. die() will also work. Yes, if in a module halt() will not work. In some contexts, another process might be returning content as well. If exit() does not work for you, please show us your whole code and/or tell us more about your context.
  3. Is there a reason why you cannot follow this same pattern in the present project? I can see how this will be painful in the long run. It is also prone to mistakes; posts being added in the wrong sub-sub-category. What comes to mind is a custom module. Could be a process module or a runtime inputfield coupled with a Pages Save Hook. You probably won't even need to save they dynamic field as it is only there to help you drill down to the final parent of the post you are creating. An inputfield may not help with the challenge of going via the tree. It may even confuse authors as they will click on a parent page in the tree to 'Add' a post but the post will then be re-parented. So, maybe a process module is more friendly in this regard. At the top, you would have a dynamic select like field to drill down to sub-sub-category you want. It would then reveal a post add form. On create page, post author is redirected to the edit page of that post. The challenge here is that you will be working away from the usual page edit form. You will need to embed ProcessPageEdit::execute() output in your form. It is not difficult to do. There are posts in the forum that show you how. Just some preliminary thoughts...
  4. Erm, sorry, no. I am even more confused now. But maybe it's just me ?. I don't really understand what you mean by 'placed/published'. It seems to me that you want the editing actions on the right image (editing of a page with a dynamic field in it) to affect the ProcessWire tree in the left image. Is this correct? If this is your goal, then I think you have somehow misunderstood how this module works. It does not affect your publishing or ProcessWire tree. Am I missing something here? In your right image, I see you are already publishing the new post (My new post) and the parent (category) you want, i.e. 'Math'. I don't understand what you need dynamic selects to do in this case. My understanding was that you wanted one of the below: Select a POST in a dynamic select and it will show you all of that post's CATEGORIES. E.g. Selecting My Post 1 will show you Math. This is possible using the group relationship. Select a CATEGORY in a dynamic select and it will show you all of that category's POSTS. E.g. Selecting Math will show you My Post 1 and My Post 2. This you already do as shown in the left image, when you select Math. Am I on track with these assumptions? Sorry if I am taking you round in circles!
  5. Thanks, this is clearer. One thing still remains unclear. I don't understand what you mean by tree hierarchy in admin. Are you referring to the ProcessWire admin pages tree? If yes, then, no, dynamic selects cannot change your admin tree structure. I have a feeling this is not what you meant. If not, what tree are you referring to and how and where are you building it? Or, do you mean creating a dynamic selects in the backend/admin that will subsequently be used in a frontend template to display a 'tree' in a page in the frontend? Meaning, is the 'Subjects' tree as show in your example an illustration of how it should look like in the frontend (as opposed to the ProcessWire admin tree)? Hope my question is clear :-).
  6. Thanks for the purchase! ?. I don't fully understand this use case...Are you trying to build a menu? When I get a minute, I'll respond to your other post in MenuBuilder ?. Yes, this should work since Dynamic Selects will return the current value of a given field (any supported field as per its documentation). I may have misunderstood your use case/question though, so please clarify if this is the case.
  7. Here you are getting a single page ($pages->get). You are looping through it and the value in each loop is a value of a property in that page. These will mainly be strings (e.g. 'My page title') or integers, but there could be a Pageimages in there as well, if your page template has an images field. Either way, 'My page title'->url does not exist since this is a string (a non-object), hence the notice. This selector returns a collection, a PageArray. When you loop through it, each value in the loop is a Page object. Hence, $pageInLoop->url works. Although it starts with a $pages->get, the chaining of ->children ends up with getting the children of "/", i.e. a PageArray.
  8. Sorry to hear this. A little bit more information would help, i.e.: ProcessWire version. Multi or single language site. PHP version. Steps to reproduce, unless you mean you got that error right after the module installation finished. Thanks.
  9. Sure... If you haven't read deeply into htmx, the main premise is that the server is the single source of truth regarding both data and markup, i.e. whole application state. If we need to update either data or markup, the server handles that. We just need to tell it what action to take. Validation has to pass, of course, before the server will oblige ?. Below is a quick example that demonstrates updating the markup using htmx based on user actions. Only 'remove' locations is demonstrated in this example. I wasn't sure whether your app shows the user both their removed and added locations and whether they can reset the session to have all locations reloaded afresh. It doesn't matter much as it wouldn't change much of the logic in the example. Secondly, note that this example makes use of alpine.js and tailwind css just for the pizzaz. These are not required by htmx. To let ProcessWire recognise htmx requests, please refer to this thread. Depending on the approach you take from there, you might not even need a JavaScript file! In the example below, we do have a JavaScript file just because we want to use alpine.js (for notifications) and we need htmx to talk to alpine.js. We also need the JavaScript file to tell htmx to add XMLHttpRequest to its request headers so that ProcessWire's $config->ajax will understand the request. First, let's see a demo then we'll see how easy our work is using htmx. Just for this demo, I have included htmx (and alpine.js and tailwind css via their respective CDNs [in production, you want to purge your tailwind css ?]) in my _main.php. Inside the template I am using for this demo, I have the following code. Here, I have removed the tailwind classes used in the demo so we can focus on htmx. Note that you don't need a dedicated template file for this to work. It will work with any template. As long as htmx is loaded in the page view and your template (in this case, the current page's template) is listening to ajax requests. In the template file, the main htmx magic happens here: <?php namespace ProcessWire; $out = "<a hx-post='./' hx-target='#locations' hx-vals='{\"location_add_id\": \"{$page->id}\"}' hx-include='._post_token' hx-indicator='#locations_spinner_indicator'>Add</a>" . "<a hx-post='./' hx-target='#locations' hx-vals='{\"location_remove_id\": \"{$page->id}\"}' hx-include='._post_token' hx-indicator='#locations_spinner_indicator'>Remove</a>"; echo $out; Let's go through the htmx attributes: hx-post This tells htmx where to send its ajax request. In this case, we are sending it to the same page we are viewing (./) and are using POST. We could have used hx-get if we wanted to (GET). hx-target This tells htmx which markup to replace/swap. The default is to replace the markup of the element from which htmx was called. However, hx-target can be used to specify the element to replace. In this example, we are replacing the whole listing so we target its wrapper element which has the id locations. hx-vals This is optional but we need it in our example. We want to tell the server which location (ID) has been added/removed. If we had a form element, we could have used it for this instead. Since we don't have one, we are telling htmx to process the (JSON) value of hx-vals and send that together with its request. Note: the escape slashes are so we can have raw, valid JSON in the attribute as required by htmx. hx-include This is also optional but important in our case. It tells htmx to include input elements found via the selector in this attribute in its ajax request. In our example, we are telling htmx to include the CSRF token we set on the server together with its request. hx-indicator Also optional. Tells htmx to show/hide this element to show the user that something happened. In this case we use a spinner. We could have used a progress indicator as well, .e.g., if we were uploading a file. That's it really! No event listeners, no handlers! We can add (and I did add one), event listeners on htmx events in order to do something after the event. In this example, htmx fires a custom event which alpine.js is listening to in order to show notifications after the DOM has settled. The notification type (success, error, etc) and the message are all coming back from the server but not as JSON. The markup to update the page is also coming back from the server. htmx receives it and plugs it into the DOM per the hx-target (also see hx-swap) attribute value. In this example, we are updating the whole listing. If we wanted, we could update just the location that was removed, e.g. add it back but with some removed 'indicator', e.g. greyed-out. In this example we use anchor tags as htmx triggers. For htmx, it doesn't matter; button, div, p, li, whatever valid HTML element would work. You already have a backend logic that's working for you but I show an excerpt of mine here, for completeness. Inside my-template-file.php (the template file for the template for the current page, in this example), I have the following code: <?php namespace ProcessWire; if ($config->ajax) { // check CSRF if (!$session->CSRF->hasValidToken()) { // form submission is NOT valid throw new WireException('CSRF check failed!'); } // ................ more code // e.g. check removed locations in the session, etc // get previously removed locations (keeping things in sync) $removedLocations = $session->get('removedLocations'); // REMOVING LOCATION if ((int) $input->post->location_remove_id) { $mode = 'remove'; $notice = "Removed"; $id = (int) $input->post->location_remove_id; } else if ((int) $input->post->location_add_id) { // ADDING LOCATION $mode = 'add'; $notice = 'Added'; $id = (int) $input->post->location_add_id; } // ................ more code // e.g. check if we really have a page by that id, SET $noticeType, $options and update session 'removedLocations' etc // build final content //----------- // @note: buildLocationsCards() is a function that does what it says on the tin. We use it in both the ajax response here and also below, in non-ajax content, when the page loads // the $options array contains a key 'skip_pages_ids' with an array of IDs of locations (pages) that have been removed in this session. We skip these in buildLocationCards(). $out = buildLocationCards($page, $options); // @note - here we always return one input only // we use the values in this input in JS to pass event details to alpine.js to show the correct notification and the notification message. $out .= "<input type='hidden' id='location_notice' name='location_notice' data-notice-type='{$noticeType}' value='{$notice}'>"; echo $out; $this->halt(); } // NON-AJAX CONTENT BELOW.... That's all there is to it ?. I am happy to share the full code if anyone wants to play with this further.
  10. Just throwing in htmx as an alternative here. If you are interested, I could put together a rough example.
  11. You have several possibilities depending on whether you are looping through items or whether you just needed a single item from your items. Here are examples of both scenarios. Inside a loop <div> <!-- getGridItems here is a function that returns the current grid items --> <template x-for="(item, index) in getGridItems"> <!-- bind id to item id and its text to item state --> <span :id='item.id' x-text='item.state' :key="index"></span> </template> </div> Get One Item Several possibilities here: // use filter and since expecting only one item, get it at the first array index const myItem = items.filter((item) => item.id === itemId)[0]; // loop let myItem; for (const item of items) { if (item.id === itemId) { // you have found your item myItem = item; // break out of your loop break; } } // find const myItem = items.find((item) => item.id === itemId); // do some sanity checks here before using myItem For more on loops and iteration, have a look at the Mozilla docs. Depending on the app you are building, you might want to keep a caniuse tab open ?. I see that you are storing tagName (div, etc), meaning you are creating elements on the fly? You can use vanilla JS for this or depending on the number of element possibilities (div, span, ...n), you could use a number of x-ifs. Note, though, that there is no x-else in Alpine. The flatter an object, usually the easier your work. However, sometimes this is not always possible. To access nested data, again, you have several options. You could create a function that returns the object property you want. You can also use either of brackets or dot notation (myItem.breakpoints.base.css.color). However these can get messy if the nesting is very deep. Maybe you could flatten the breakpoints object since really, it is all mainly CSS? It would make it easier to bind styles to an element.
  12. Have you tried this? <?php $_SERVER['X-Shopify-Topic'];
  13. Let's start from here: No. you don't have to use Alpine.store(). However, store comes with some advantages as explained here and here. I prefer to use the store, maybe due to vuex influence. Not sure. When do you want to release it? Soon? Then I'd release it and refactor it later. A potential challenge here is that you may not find the time 'later' to refactor it ?. Do you have to use Alpine JS? No you don't. Personally though, I will be removing jQuery from everything I've built. It will take time though. This has nothing to do with the new tech such as Alpine really, but I now prefer to use vanilla JS and/or Alpine/Vue, etc. Modern vanilla JS syntax is just clearer to me. Anyway, I digress. Using reactive frameworks, you would have at least two options: #1. model the value: E.g. v-model or x-model in vue and alpine respectively. This two-way data bind means you don't have to listen to the on change event. That will be taken care for you, unless you also want to listen to the event and do something based on that. This approach will also trigger changes in other areas of the app that depend on the changed value. Your data will be changed (instead of the input value) and behind the scenes vue/alpine will change the value of the input for you. Everything stays in sync. By the way, if you want one-way bind, have a look at x-bind:value #2. on:handler: You handle the event yourself. E.g. with Alpine, @click='doSomethingWithClick' or x-on:click='doSomethingWithClick'. In this case as well, the idea is generally to modify your data based on some event INSTEAD OF modifying the input value directly based on the event. If you modify the data, you are back to #1, where everything is synced for you. Using Alpine, you won't need to work with the JSON object. You will be working with JavaScript objects, arrays, bools, etc. You won't have to do this. If modelling your data (x-model) to inputs, Alpine will update your data for you automatically. Your work will be to validate values and check other app states if required. This is very easy to do with Alpine/Vue. Just use an @click handler and handle that. I could give you a basic example but I would prefer to see your use case/code example as you might not need the click handler at all. As mentioned above, you don't have to do this yourself. Let x-model take care of it for you. When the form is saved, it will save with the modelled values. E.g. <input type='text' name='your_name' x-model='person_name'/> In the above example, when you type into the input box, Alpine will update the value of person_name to what you input. If you want to see it live as you type, you could do: <input type='text' name='your_name' x-model='person_name'/> <p x-text='person_name'></p> The text inside the paragraph will be synced to and output the current value of 'person_name'. I would need to see this in action to get my head around how you would model it in Alpine ?. Data You have a number of choices to pass your data from ProcessWire to your Alpine app. $config->js If your data was static, you could use ProcessWire's config->js to get it populated in the browser. E.g, send the current page ID or the URL to some backend API, etc. You could then access these in your JS code/Alpine as, e.g. // configs object const configs = ProcessWire.config.InputfieldPageBuilder Inline Script You can have your inputfield, in render(), output inline script that has your data. This is useful if you data is dynamic. You can see this usage in ProcessWire, e.g. in InputfieldRepeater. E.g. <?php // e.g. inside ___render() $data = ['age' => 24, 'gender' => 'female', 'department' => 'design']; $out = "<p>Some Inputfield Render Output</p>"; $script = "<script>ProcessWire.config.InputfieldPageBuilder = " . json_encode($data) . ';</script>'; $out .= $script; return $out; You can then access access the data as in the configs object in JS above. Inline x-data Attach your data directly to the x-data value in ProcessWire markup. JSON or 'object' (e.g. a string that resembles a JavaScript object) both work. <?php $json = json_encode(['age' => 27, 'gender' => 'female', 'department' => 'ai']); $out = "<div x-data='{$json}'><p x-text='age'></p></div>"; return $out; In all of the above, you don't need this: Or this: Is there a reason you are using Alpine as a module here? I'd probably send this data to the browser using an inline <script> instead of as a value of '#Inputfield_pgrid_settings'. Just makes life easier. I would then set the data as a property in my Alpine.data() or in Alpine.store(). Quick Tips If using $store inside a ProcessWire module, I create a class property and set my store value to it. Then, I only have to call it like this where I need it: $this->xstore. Saves with typing plus change it once, change it everywhere. In JS, I use getters and setters, either separately or combined into once function to handle data and store values. Saves from typing this.$store.mystore.settingsData.someValue = 1234, all the time. E.g. // below could be converted to single getter/setter methods // could also be adapted to handle nested properties or create separate methods for such setStoreValue(property, value){ this.$store.mystore[property] = value; } getStoreValue(property){ return this.$store.mystore[property]; } Hope this helps.
  14. Glad you got it sorted...? Until you didn't...? Please open a separate thread, maybe under dev talk as this is about Alpine. We'll then continue this conversation there, thanks.
  15. In the beginning it was too fast for me! I couldn't notice that a change had taken place ?.
  16. Do you have a reference or is it from personal experience? Have you seen this discussion and/or x-ignore? I don't want to derail the topic in this thread though, so we might have to hive off this talk. ?. Edit: A reference to a similar issue to @dotnetic's
  17. If you go the Vue route, the easier way is to let it create the inputs but also assign usual ProcessWire classes to them, which then, would be picked up by ProcessWire styling when rendered. However, you will need the full house of markup including the wrappers somewhere wrapping the Vue-generated inputs. Otherwise, some ProcessWire styles might not be applied. Some label inputs might not appear, etc. Better to use Alpine if you can.
  18. Then Alpine JS it is ?. Learn Vue, say thanks to the creators then say hello to Alpine JS. You will write its code right inside your ProcessWire inputfields, markup, whatever. Everything will be reactive. Make use of Inputfields->attr to assign Alpine properties. You might need the full attribute, e.g. 'x-on:change' instead of @change as ProcessWire might strip the latter out. Below is a quick example. I haven't checked it for errors. Inside your ProcessWire code. <?php // get a ProcessWire inputfield wrapper $wrapper = new InputfieldWrapper(); // get an inputfield text to store a name $field = $this->modules->get('InputfieldText'); // add some usual field values $field->name = 'your_name'; // OR // $field->attr('name', 'your_name') // etc... // let's alpinejs-it! $field->attr([ // MODEL THE VALUE DIRECTLY IN THE STORE // 'x-model' => "\$store.mystore.person.name", // OR HANDLE THE CHANGE YOURSELF // @note: ProcessWire will strip out the '@' - so we use x-on:event instead 'x-on:change' => "handleNameChange",// this is a method inside your Alpine data code ]); // IF YOU NEED TO.... // set a wrapper-level class, e.g., as a show-if // @note we escape some things here, e.g. the $ // could also use JavaScript template literals for the 'some_text', e.g. `some_text` // here we add a hidden class to the wrapper depending on some store value // we could have used x-show or x-if instead $class = "{ hidden: \$store.mystore.some_text==\"some_text\"}"; $field->wrapAttr( 'x-bind:class', $class, ); $wrapper->add($field); // initialise alpine js data // 'persons' could also be an object here, but for more complex objects, better to assign to a function in your code like this $out = "<div x-data='persons'>" . $wrapper->render() . "</div>"; return $out; You JavaScript // ALPINE (version 3+) document.addEventListener("alpine:init", () => { Alpine.store("mystore", { // YOUR STORE FIELDS // for some results results: [], some_text: "some_text", is_modal_open: false, person: {}, }) Alpine.data("persons", () => ({ handleNameChange(event) { this.changeCustomerName(event.target.value) }, changeCustomerName(name) { this.$store.mystore.person.name = name } })) }) Hope this helps.
  19. One is in the works....still in my head though...as I clear my in-tray first! (you know what I'm talking about ?). Yes and yes. If you are using Vue 2, there is a post somewhere in the forums (@elabx, help please, thanks) with a copy-amend-paste code from WP. That code will enable live reload in ProcessWire backend but view will still need to be running on some other port, e.g. 3000. However, you won't have to keep an eye on that as you will be viewing the changes in the ProcessWire backend. I have since adopted Vue 3 and Vite and the live reload in ProcessWire is as simple as this: <?php // e.g., in execute() if developing a Process Module $out = " // 3000 is the port vite is running at $out .= ' <script type="module" src="http://localhost:3000/@vite/client"></script> <script type="module" src="http://localhost:3000/src/main.js"></script>'; //return $out; if in a method/function Vite does not use Webpack. So, no (crazy) Webpack configs needed! The live reload just works! Having said that, I highly recommend Alpine JS (as a Vue alternative, if you can get away with it) and HTMX (no Axios, no JSON!). With either, no build is required! You keep your DOM and work right inside ProcessWire. Future tutorials in the works, as well ?. If you understand Vue then you already understand Alpine JS. If not, I still suggest you get to know the basics of Vue anyway. Hope this helps.
  20. True, but not applicable to usage of ProcessWire itself unless something's changed ?.
  21. Works here just fine. I don't even have to manually load its assets. All of the below work. <?php $field = $this->modules->get('InputfieldDatetime'); $field->inputType = 'text'; $field->datepicker = 3; // $field->datepicker = \ProcessWire\InputfieldDatetime::datepickerFocus; //$field->datepicker = InputfieldDatetime::datepickerFocus;
  22. @howdytom, Glad you like the module. There is a defaultOptions but it doesn't cover your rel and anchor tag title needs. I highly suggest you use the getMenuItems() method to build your menu and customise it however you wish. There are a number of examples at that link. Please let us know if you need further assistance.
  23. @Jonathan Lahijani's strategy is quite good. Here are a few more HTMX + $config->ajax options: 1. hx-headers (docs). Quick and dirty addition of X-Requested-With: XMLHttpRequest to the headers. As per the docs, make sure to use the valid JSON option AND NOT the JavaScript eval(). Example: <?php namespace ProcessWire; // define variables $someURL = '/some-url/'; $XMLHttpRequestJSON = json_encode(["X-Requested-With" => "XMLHttpRequest"]); $out = "<div hx-get='{$someURL}' hx-headers='{$XMLHttpRequestJSON}'>Get Some HTML</div>"; // use $out... The disadvantage here is having to add the headers manually to all the markup that need it. I think it could be added to body but from what I recall, there are disadvantages to adding attributes to the body tag? Maybe someone could confirm/disapprove this. Yes. hx-headers can be added to the body tag. Edit: From the docs... hx-headers is inherited and can be placed on a parent element. A child declaration of a header overrides a parent declaration. 2. The ajax-header Extension (docs) Here you will need to include the tiny (7 lines of code) extension ajax-header.js script to your HTML. You can then issue ajax requests as usual like shown below using the hx-ext attribute. <?php namespace ProcessWire; // define variables $someURL = '/some-url/'; $out = "<div hx-get='{$someURL}' hx-ext='ajax-header'>Get Some HTML</div>"; // use $out... 3. htmx:configRequest (docs) Use JavaScript to configure the request. This one is a bit more involved but gives you greater control than #2. In this example, we assume you have a main.js file that you include in your template(s) file(s). Inside main.js, add the code as shown below. // main.js // DOM ready document.addEventListener("DOMContentLoaded", function (event) { // init htmx with X-Requested-With initHTMXXRequestedWithXMLHttpRequest(); }); function initHTMXXRequestedWithXMLHttpRequest() { document.body.addEventListener("htmx:configRequest", (event) => { // if you wish to add CSRF checks. here token added to headers // but you can also add to request using hx-vals, for example. // const csrf_token = getCSRFToken(); // event.detail.headers[csrf_token.name] = csrf_token.value; // add XMLHttpRequest to header to work with $config->ajax event.detail.headers["X-Requested-With"] = "XMLHttpRequest"; }); } function getCSRFToken() { // find element, e.g. hidden input, with id '#_post_token' // it holds our token name and value const tokenInput = htmx.find("#_post_token"); return tokenInput; } Then in your template file: <?php namespace ProcessWire; // define variables $someURL = '/some-url/'; $out = "<div hx-get='{$someURL}'>Get Some HTML</div>"; // use $out...
  24. Are any of the methods here helpful? https://processwire.com/api/ref/markup-pager-nav/ Especially, setBaseUrl(), setQueryString(), setGetVars() Not sure if it is applicable here but in the past I've had to append ?pageNum=1 to the URL of page 1. $input->queryString() might also be helpful to handle your channel strings?
  25. @horst This is a Pageimage object. $photo->mediaField; This is a Pageimages object. https://mediamanager.kongondo.com/documentation/frontend-output-of-media-manager-fields/media-manager-objects/
×
×
  • Create New...