pdoPage with Fenom

Summary

I’m trying to use pdoPage with Fenom sytnax as button ajaxMode:

<style>
    .filtered {
        font-weight: normal;
        color: gray;
        background: #B4D7CB;
        display: flex;
        justify-content: start;
        align-items: start;
        margin-bottom: 0;
        color: black;
    }
</style>

<div class="container container-xxxl min-h-section d-flex align-items-start justify-content-center"
    style="margin-top: 20rem;">
    <div class="mx-auto col-24 col-md-22 px-3 py-60 py-md-0 px-md d-flex flex-column gap-50 gap-md-70">
        <h1 class="fs-64 fw-medium mb-0">[[+longtitle]]</h1>
        <div id="pdopage">
            <div class="filters portfolio mb-50">
                <form id="filters" class="w-100 form-inline">
                    <div class="row gy-30 gx-30">
                        [[getTvOptions? &tpl=`btnFilter` &tvname=`portfolioStatus`
                        &name=`portfolioStatus` &id=`[[+id]]` &classes=`col-24 col-lg-auto`]]
                    </div>
                </form>
            </div>



            <div class="rows row gx-80 gy-60 align-items-stretch">



                {'!pdoPage' | snippet : [
    'parents' => $id,
    'depth' => 0,
    'cache' => 0,
    'limit' => 2,
    'ajax' => 1,
    'ajaxMode' => 'button',
    'where' => $_modx->runSnippet('pdoPageGetWhere', ['id' => $id]),
    'sortby' => '{"publishedon":"DESC","pagetitle":"ASC"}',
                'includeTVs' => 'portfolioGridImg,portfolioStatus',
                'processTVs' => 'portfolioGridImg',
                'includeContent' => 1,
                'tpl' => '!portfolioItem',
                'tplEmpty' => 'pdoPageNoResults',
                'frontend_js' => 'http://127.0.0.1:8800/assets/components/pdotools/js/jquery.pdopage.min.js',
                'frontend_startup_js' => 'pdoPagefrontendStartupJS',
                'frontend_init_js' => 'pdoPagefrontendInitJS'
                ]}
            </div>
            {$_modx->getChunk('pdoPageNoResults')}
            {$_modx->getPlaceholder('page.nav')}
        </div>
    </div>
</div>

Observed behavior

When i click load more button, the request is being made however when i look in the preview and getting this:

Not sure how to debug it.

On inital loading everything works fine.

thanks guys!

Maybe you can give AlpineAjax a chance instead of using the old jQuery plugin or the pdoPage script.

Why is the request being made to the “portfolio” page?
The AJAX-requests to load more data should call the file assets/components/pdotools/connector.php!


Maybe start with a simpler call.
Then if everything works, add the additional properties:

<!-- adjust the path to where the jQuery library is located on your system -->
<script type="text/javascript" src="[[++base_url]]assets/js/jquery.min.js"></script>

<!-- simple example to test the AJAX functionality -->
<div id="pdopage">
  <div class="rows">
    {'!pdoPage' | snippet : [
    'parents' => 'id' | resource,
    'limit' => 2,
    'ajaxMode' => 'button',
    'tpl' => '@INLINE <li>{{+pagetitle}} ({{+id}})</li>'
    ]}
  </div>
  {'page.nav' | placeholder}
</div>

Thanks for looking at this.

You’re right, I was misled because earlier I was using ajaxMode="default" and loading my own custom JavaScript in:
‘frontend_init_js’ => ‘pdoPagefrontendInitJS’ to support custom filters like search and some select options:

<script type="text/javascript">
    $(function() {
        var pdoPageWrapper = $('[[+wrapper]]');
        console.log("Initializing pdoPage on", pdoPageWrapper);
        if ($.fn.pdoPage) {
            // Disable native scroll after ajax load
            pdoPageWrapper.pdoPage($.extend([[+config]], {
                scrollTop: false
            }));
            pdoPageWrapper.pdoPage([[+config]]);
        } else {
            console.error("pdoPage plugin is not loaded!");
        }

        const filters = document.getElementById("filters");

        if (filters) {

            const buttons = filters.querySelectorAll(".filter .custom-btn-filter");




            // Check initial results
            const hasChildren = $('[[+wrapper]]').find('.rows').children().length > 0;
            if (!hasChildren) {
                $('.no-results').removeClass('d-none');
            }



           buttons.forEach((btn) => {
            btn.addEventListener("click", (e) => {
                // e.preventDefault();

                const params = new URLSearchParams();

                // Toggle active state
                if (btn.classList.contains("active")) {
                    btn.classList.remove("active");
                    btn.blur();
                } else {
                    // Deactivate all buttons in the same group
                    const group = btn.closest(".btn-group");
                    if (group) {
                        const groupButtons = group.querySelectorAll(".custom-btn-filter");
                        groupButtons.forEach((b) => b.classList.remove("active"));
                    }
                    btn.classList.add("active");
                    btn.blur();
                }

                // Add all active buttons to params
                params.append(btn.dataset.filterName, btn.dataset.filterValue);
                  // Update URL
                // const query = params.toString();
                // const newUrl = window.location.pathname + "?" + query;
                // window.history.pushState({}, "", newUrl);

                // Update pdoPage
                console.log(pdoPageWrapper)
                pdoPageWrapper.pdoPage('loadPage', '/?' + params.toString(), 'force');

            });
        });
})

however i will still need some custom handling when user click on the filters, do you have any idea how to implement that into this? Long story short, i will have load more button on ajax, but also i will have filters to deal with - now when im thinking about it maybe its just worth to add simple javascript script code to display block and none…

thanks!

i think i found the cause but not sure why is it happening:

for some reason in the reponse i have full code for portfolio page.. (number 1)

I should have only recieve code from number 2, the json…
The reponse element itself is correct.

I did take a closer look at this.

There are 2 “frontend_js”-scripts available in the pdoTools extra:

  • [[+assetsUrl]]js/pdopage.min.js (or pdopage.js) is used by default. This script calls the connector (assets/components/pdotools/connector.php) for AJAX-requests. This approach doesn’t seem to work for when there is a snippet call in the where-property (&where=`[[!MyGetWhereSnippet]]`) as the snippet doesn’t seem to run on AJAX-requests.
  • [[+assetsUrl]]js/jquery.pdopage.min.js' (or jquery.pdopage.js). This script calls the resource directly, but it doesn’t seem to work when Fenom is used on the page, as all the code above the pdoPage snippet call is also in the response (and not just the JSON-data from pdoPage).

My guess is that when you want to use Fenom on the page and you want to load the data with AJAX when a filter changes, you have to customize the available “frontend_js”-scripts (or maybe create a custom connector file).

One way would be to change the line here

to not expect pure JSON in the response and then extract the JSON-data from the response yourself.

When the snippet pdoPage runs on a resource, the code checks if the current request is an AJAX-request

If that’s the case, the code terminates the PHP processing by outputting the JSON data .

This seems to work OK with the normal MODX parser, but with Fenom, the part of the template that already got parsed seems to have already been written to the output and is now part of the response.

Thanks for these replies. This explains a lot!

In that case i will go with native modx syntax:

  <div id="pdopage">


            <div class="rows row gx-80 gy-60 align-items-stretch">

                [[!pdoPage?
                &parents=`[[+id]]`
                &depth=`1`
                &limit=`1`
                &ajax=`1`
                &ajaxMode=`button`
                &includeTVs=`portfolioGridImg`
                &processTVs=`portfolioGridImg`
                &includeContent=`1`
                &tpl=`portfolioGridItem`
                &tplEmpty=`pdoPageNoResults`
                &frontend_js=`[[+assetsUrl]]js/jquery.pdopage.js`
                &frontend_startup_js=`pdoPagefrontendStartupJS`
                &frontend_init_js=`pdoPagefrontendInitJS`
                ]]

            </div>
            [[!+page.nav]]
        </div>

However this still does not works as i click load more button im getting params page=2 all the time:

It seems like pagination stays the same…

ajaxMode “button” only seems to work if history is enabled (&ajaxHistory=`1`) as the code seems to read the current page number from the URL.

If you don’t want history enabled, you’ll have to adapt the code in jquery.pdopage.js (or better copy this file and change the copied file).


There is also another issue in the code, when you reload the data after the filter changes with $('[[+wrapper]]').pdoPage('loadPage', ..., 'force'); With 'force' as the mode, the “More” button will not be shown/hidden correctly.

I guess this code

should also run on line 186 if mode is 'force'.

1 Like

Hello!

I have been working on this whole morning and thanks to your comments i was able to make it work as i want. What i did is i copied jquery.pdopage.js and reshape it a bit to much my needs.

This is my code now, it might help someone:

  [[!pdoPage?
                &parents=`[[+id]]`
                &depth=`1`
                &limit=`3`
                &ajaxHistory=`1`
                &where=`[[!pdoPageGetWhere? &id=`[[+id]]`]]`
                &ajaxMode=`button`
                &includeTVs=`portfolioGridImg,portfolioStatus`
                &processTVs=`portfolioGridImg`
                &includeContent=`1`
                &tpl=`portfolioGridItem`
                &tplEmpty=`pdoPageNoResults`
                &frontend_js=`assets/components/wwd/wwd_pdopage/js/wwd.pdopage.js`
                &frontend_startup_js=`pdoPagefrontendStartupJS`
                &frontend_init_js=`pdoPagefrontendInitJS`
                ]]

below assets/components/wwd/wwd_pdopage/js/wwd.pdopage.js:

;(function ($, window, document, undefined) {

    'use strict';

    var pluginName = 'pdoPage',
        defaults = {
            wrapper: '#pdopage',
            rows: '#pdopage .row',
            pagination: '#pdopage .pagination',
            link: '#pdopage .pagination a',
            more: '#pdopage .btn-more',
            pdoTitle: '#pdopage .title',
            moreTpl: '<button class="btn btn-default btn-more">Load more</button>',
            waitAnimation: '<div class="wait-wrapper"><div class="wait"></div></div>',
            mode: 'scroll',
            pageVarKey: 'page',
            pageLimit: 12,
            assetsUrl: '/assets/components/pdotools/',
            scrollTop: true
        };

    function Plugin(element, options) {
        this.element = element;
        this.settings = $.extend({}, defaults, options);
        this._defaults = defaults;
        this._name = pluginName;
        this.key = this.settings.pageVarKey;
        this.wrapper = $(this.settings.wrapper);
        this.mode = this.settings.mode;
        this.reached = false;
        this.history = this.settings.history;
        this.oldBrowser = !(window.history && history.pushState);
        this.init();
    }

    $.extend(Plugin.prototype, {
        init: function () {
            var _this = this;
            if (this.page == undefined) {
                var params = this.hashGet();
                var page = params[this.key] == undefined ? 1 : params[this.key];
                this.page = Number(page);
            }
            switch (this.mode) {
                case 'default':
                    this.initDefault();
                    break;
                case 'scroll':
                case 'button':
                    if (this.history) {
                        if (typeof(jQuery().sticky) == 'undefined') {
                            $.getScript(this.settings.assetsUrl + 'js/lib/jquery.sticky.js', function () {
                                _this.init(_this.settings);
                            });
                            return;
                        }
                        this.stickyPagination();
                        $(this.settings.pagination).hide();
                    } else {
                        $(this.settings.pagination).hide();
                    }
                    if (this.mode == 'button') {
                        this.initButton();
                    } else {
                        this.initScroll();
                    }
                    break;
            }
        },
        initDefault: function () {
            // Default pagination
            var _this = this;
            $(document).on('click', this.settings.link, function (e) {
                e.preventDefault();
                var href = $(this).prop('href');
                var match = href.match(new RegExp(_this.key + '=(\\d+)'));
                var page = !match ? 1 : match[1];
                if (_this.page != page) {
                    if (_this.history) {
                        _this.hashAdd(_this.key, page);
                    }
                    _this.loadPage(href);
                }
            });
            if (this.history) {
                $(window).on('popstate', function (e) {
                    if (e.originalEvent.state && e.originalEvent.state.pdoPage) {
                        _this.loadPage(e.originalEvent.state.pdoPage);
                    }
                });
                history.replaceState({pdoPage: window.location.href}, '');
            }
        },
        initButton: function () {
            // More button pagination
            var _this = this;
            $(this.settings.rows).after(this.settings.moreTpl);
            var has_results = false;
            $(this.settings.link).each(function () {
                var href = $(this).prop('href');
                var match = href.match(new RegExp(_this.key + '=(\\d+)'));
                var page = !match ? 1 : match[1];
                if (page > _this.page) {
                    has_results = true;
                    return false;
                }
            });
            if (!has_results) {
                $(this.settings.more).hide();
            }
            $(document).on('click', this.settings.more, function (e) {
                e.preventDefault();
                _this.addPage()
            });
        },
        initScroll: function () {
            // Scroll pagination
            var _this = this;
            var $window = $(window);
            $window.on('scroll', function () {
                if (!_this.reached && $window.scrollTop() > _this.wrapper.height() - $window.height()) {
                    _this.reached = true;
                    _this.addPage();
                }
            });
        },
        addPage: function () {
            var _this = this;
            var params = this.hashGet();
            var current = params[this.key] || 1;
            $(this.settings.link).each(function () {
                var href = $(this).prop('href');
                var match = href.match(new RegExp(_this.key + '=(\\d+)'));
                var page = !match ? 1 : Number(match[1]);
                if (page > current) {
                    if (_this.history) {
                        _this.hashAdd(_this.key, page);
                    }
                    _this.page = current;
                    _this.loadPage(href, 'append');
                    return false;
                }
            });
        },
        loadPage: function (href, mode) {
            var _this = this;
            var rows = $(this.settings.rows);
            var pagination = $(this.settings.pagination);
            var match = href.match(new RegExp(this.key + '=(\\d+)'));
            var page = !match ? 1 : Number(match[1]);
            if (!mode) {
                mode = 'replace';
            }
            if (this.page == page && mode != 'force') {
                return;
            }
            this.wrapper.trigger('beforeLoad', [this, this.settings]);
            if (this.mode != 'scroll') {
                this.wrapper.css({opacity: .3});
            }
            this.page = page;
            var waitAnimation = $(this.settings.waitAnimation);
            if (mode == 'append') {
                this.wrapper.find(rows).append(waitAnimation);
            } else {
                this.wrapper.find(rows).empty().append(waitAnimation);
            }
            var params = this.getUrlParameters(href);
            params[this.key] = this.page;
            $.get(window.location.pathname, params, function (response) {
                if (response) {
                    _this.wrapper.find(pagination).replaceWith(response.pagination);
                    if (mode == 'append') {
                        _this.wrapper.find(rows).append(response.output);
                        if (_this.mode == 'button') {
                            $(_this.settings.pagination).hide();
                            if (response.pages == response.page) {
                                $(_this.settings.more).hide();
                            } else {
                                $(_this.settings.more).show();
                            }
                        } else if (_this.mode == 'scroll') {
                            _this.reached = false;
                        }
                        waitAnimation.remove();
                    } else {
                        _this.wrapper.find(rows).html(response.output);
                        if (mode == 'force' && _this.history) {
                            _this.hashSet(params);
                            if (_this.mode == 'button') {
                            $(_this.settings.pagination).hide();
                            if (response.pages == response.page) {
                                $(_this.settings.more).hide();
                            } else {
                                $(_this.settings.more).show();
                            }
                        }
                        }
                    }
                    _this.wrapper.trigger('afterLoad', [_this, _this.settings, response]);
                    if (_this.mode != 'scroll') {
                        _this.wrapper.css({opacity: 1});
                        if (_this.mode == 'default' && _this.settings.scrollTop) {
                            $('html, body').animate({
                                scrollTop: _this.wrapper.position().top - 50 || 0
                            }, 0);
                        }
                    }
                    _this.updateTitle(response);
                }
            }, 'json');
        },
        stickyPagination: function () {
            var pagination = $(this.settings.pagination);
            if (pagination.is(':visible')) {
                pagination.sticky({
                    wrapperClassName: 'sticky-pagination',
                    getWidthFrom: this.settings.pagination,
                    responsiveWidth: true
                });
                this.wrapper.trigger('scroll');
            }
        },
        updateTitle: function (response) {
            if (typeof(pdoTitle) == 'undefined') {
                return;
            }
            var title = $('title');
            var separator = pdoTitle.separator || ' / ';
            var tpl = pdoTitle.tpl;
            var parts = [];
            var items = title.text().split(separator);
            var pcre = new RegExp('^' + tpl.split(' ')[0] + ' ');
            for (var i = 0; i < items.length; i++) {
                if (i === 1 && response.page && response.page > 1) {
                    parts.push(tpl.replace('{page}', response.page).replace('{pageCount}', response.pages));
                }
                if (!items[i].match(pcre)) {
                    parts.push(items[i]);
                }
            }
            title.text(parts.join(separator));
        },
        getUrlParameters: function (url) {
            var result = {};
            var searchIndex = url.indexOf("?");
            if (searchIndex !== -1) {
                result = this.deparam(url.substring(searchIndex + 1));
            }
            return result;
        },
        deparam: function (params) {
            // source: https://github.com/jupiterjs/jquerymx/blob/0f917a85327a6c0196c2cf28c1d90a9095e0eae1/lang/string/deparam/deparam.js
            var digitTest = /^\d+$/,
                keyBreaker = /([^\[\]]+)|(\[\])/g,
                paramTest = /([^?#]*)(#.*)?$/,
                prep = function (str) {
                    return decodeURIComponent(str.replace(/\+/g, ' '));
                };
            var data = {}, pairs, lastPart;
            if (params && paramTest.test(params)) {
                pairs = params.split('&');
                $.each(pairs, function (index, pair) {
                    var parts = pair.split('='),
                        key = prep(parts.shift()),
                        value = prep(parts.join('=')),
                        current = data;
                    if (key) {
                        parts = key.match(keyBreaker);
                        for (var j = 0, l = parts.length - 1; j < l; j++) {
                            if (!current[parts[j]]) {
                                // If what we are pointing to looks like an `array`
                                current[parts[j]] = digitTest.test(parts[j + 1]) || parts[j + 1] === '[]' ? [] : {};
                            }
                            current = current[parts[j]];
                        }
                        lastPart = parts.pop();
                        if (lastPart === '[]') {
                            current.push(value);
                        } else {
                            current[lastPart] = value;
                        }
                    }
                });
            }
            return data;
        },
        hashGet: function () {
            var vars = {}, hash, hashes;
            if (!this.oldBrowser) {
                var pos = window.location.href.indexOf('?');
                hashes = (pos != -1) ? decodeURIComponent(window.location.href.substr(pos + 1)) : '';
                vars = this.deparam(hashes);
            } else {
                hashes = decodeURIComponent(window.location.hash.substr(1));
                if (hashes.length) {
                    hashes = hashes.split('/');
                    for (var i in hashes) {
                        if (hashes.hasOwnProperty(i)) {
                            hash = hashes[i].split('=');
                            if (typeof hash[1] == 'undefined') {
                                vars.anchor = hash[0];
                            } else {
                                vars[hash[0]] = hash[1];
                            }
                        }
                    }
                }
            }
            return vars;
        },
        hashSet: function (vars) {
            var hash = '';
            for (var i in vars) {
                if (vars.hasOwnProperty(i)) {
                    if (typeof vars[i] != 'object') {
                        hash += '&' + i + '=' + vars[i];
                    } else {
                        for (var j in vars[i]) {
                            if (vars[i].hasOwnProperty(j)) {
                                if (!isNaN(parseFloat(j)) && isFinite(parseFloat(j))) {
                                    hash += '&' + i + '[' + ']=' + vars[i][j];
                                } else {
                                    hash += '&' + i + '[' + j + ']=' + vars[i][j];
                                }
                            }
                        }
                    }
                }
            }
            if (!this.oldBrowser) {
                if (hash.length != 0) {
                    hash = '?' + hash.substr(1);
                }
                window.history.replaceState(
  { pdoPage: window.location.pathname + hash },
  '',
  window.location.pathname + hash
);
            } else {
                window.location.hash = hash.substr(1);
            }
        },
        hashAdd: function (key, val) {
            var hash = this.hashGet();
            hash[key] = val;
            this.hashSet(hash);
        },
        hashRemove: function (key) {
            var hash = this.hashGet();
            delete hash[key];
            this.hashSet(hash);
        },
        hashClear: function () {
            this.hashSet({});
        }
    });

    $.fn[pluginName] = function (options) {
        var args = arguments;
        if (options === undefined || typeof options === 'object') {
            return this.each(function () {
                if (!$.data(this, 'plugin_' + pluginName)) {
                    $.data(this, 'plugin_' + pluginName, new Plugin(this, options));
                }
            });
        } else if (typeof options === 'string' && options[0] !== '_' && options !== 'init') {
            var returns;
            this.each(function () {
                var instance = $.data(this, 'plugin_' + pluginName);
                if (instance instanceof Plugin && typeof instance[options] === 'function') {
                    returns = instance[options].apply(instance, Array.prototype.slice.call(args, 1));
                }
                if (options === 'destroy') {
                    $.data(this, 'plugin_' + pluginName, null);
                }
            });
            return returns !== undefined ? returns : this;
        }
    };

})(jQuery, window, document);

thanks!!

1 Like