Skip to content
Grav 2.0 is officially stable. Read the announcement →

Community guidelines

Please keep discussions civil and on-topic. Repeated violations may lead to a temporary ban.

Plugins

Plugin generating modular page

Solved by Birger Krägelin View solution

Started by Birger Krägelin 5 years ago · 15 replies · 846 views
5 years ago

I write a plugin which generates a login profile page the same way as the login plugin does. But it should work with a modular page.

It registers the page using the same code

PHP
$route = $this->config->get('plugins.login.route_profile');
$pages = $this->grav['pages'];

$page = new Page();
// $page->init(new \SplFileInfo(__DIR__ . '/pages/profile.md'));
$page->init(new \SplFileInfo(__DIR__ . '/pages/modular.md'));
$page->slug(basename($route));

$pages->addPage($page, $route);

// Profile page may not have the correct Cache-Control header set,
// force no-store for the proxies.
$page->expires(0);

If it is a normal page, it works perfect. If it is a modular page, it does not render the modular subpages. The same page (including subpages) renders, when I save it as a normal page (like 04.profile).

It looks like

YAML
content:
    items: '@self.modular'

does not find the modular pages. My Twig template using page.collection() does not evaluate to an array, the {% for ... loop does not render the loop body.

How do I register the modular subpages?
Is there a way to do that in the frontmatter as fully named items?
Do I have to do it programmatically in my plugin?
Is there any example code?

Thanks,
Birger

5 years ago

As far as I know there is no need to register the other pages. Your plugin creates the primary modular page which actually is just a regular page. The only difference with a normal Grav page is that a primary modular page does not have any content. Instead it specifies which other modular pages to include as it were by creating a collection in the primary page frontmatter (or header).

This is what you already do. So it must be something very simple which is causing troubles.

Since you specify the collection as the set of all child pages below the modular primary page, I wonder how do these pages get created? If they are somewhere else in the site structure you need to create a different collection.

Also make sure the other modular pages (the so called Module-folders) all start with an underscore character.

last edited 03/11/21 by Ron Wardenier
5 years ago

Thanks for the feedback. But I'm afraid, this does not work as expected.

If you create a normal page in /user/pages, this does work. That's the normal way to create pages. But if you add a plugin dependent page and put it into your plugin folder like the login-plugin does, this does not work out of the box.

I examined the files in /system/src/Grav/Common/Page and found the protected function recurse(), which does a filesystem traversal to register all pages, subpages and modulars.

Unfortunately there is no public function to add/register a page, which does the same just for one page. Something like addModularPage() or addPageWithChildren(). At least I didn't find one so far.

As I didn't find any plugin, which registers it's own modular page, I try to learn from GRAV source and pick the necessary parts only. But I'm not already there...

5 years ago

@bkraegelin:
But if you add a plugin dependent page and put it into your plugin folder like the login-plugin does, this does not work out of the box.

Are you talking about page templates, that are in plugin's structure? If so, do you have templates passed to Grav in your plugin? Eg.:

PHP
    public function onPluginsInitialized(): void
    {
        if ($this->isAdmin()) {
            $this->enable(
                [
                    'onGetPageTemplates'  => ['onGetPageTemplates', 0],
                ]
            );

            return;
        }

        $this->enable(
            [
                'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0],
            ]
        );
    }

    public function onTwigTemplatePaths()
    {
        $this->grav['twig']->twig_paths[] = __DIR__ . '/templates';
    }

    public function onGetPageTemplates(Event $event)
    {
        $locator = Grav::instance()['locator'];
        $event->types->scanTemplates($locator->findResource('plugin://' . $this->name . '/templates'));
    }
5 years ago

I do not talk about templates, I talk about the page itself. I use standard templates only.

My page (it's a replacement for /login/profile) does not get written to /user/pages, it's beneath /user/plugin/my-plugin/pages.

Like login-plugin I'm able to allocate any standard page including forms. I want to replace that with a modular page which consists of several elements and multiple forms within one browser page. This does work perfectly when I allocate this modular page beneath /user/pages, as the standard filesystem traversal finds all the subpages and modulars.

If I add the page programmatically, there is no such function. addPage() registers one page only. I have to implement something like file traversal, registering all found subpages and modulars and linking them together.

As I know all components in advance, I don't need any full fledged filesystem traversal. I just look for hints to get the right sequence of addPage() and how to link together the children.

👍 1
last edited 03/12/21 by Birger Krägelin
5 years ago

I see Login plugin creates page dynamically and adds it to $pages. You said you know all your modules. Can you create all module pages using same logic and set parent to each of them?

Something like

PHP
    public function addModularPage()
    {
        /** @var Pages $pages */
        $pages = $this->grav['pages'];
        $page = $pages->dispatch($this->route);

        if (!$page) {
            // Only add login page if it hasn't already been defined.
            $page = new Page();
            $page->init(new \SplFileInfo(__DIR__ . '/pages/my_modular.md'));
            $page->slug(basename($this->route));

            $pages->addPage($page, $this->route);
        }

        // Login page may not have the correct Cache-Control header set, force no-store for the proxies.
        $cacheControl = $page->cacheControl();
        if (!$cacheControl) {
            $page->cacheControl('private, no-cache, must-revalidate');
        }

        foreach (['my', 'modules'] as $module) {
            $this->addModulePage($module, $page)
        }
    }

    protected function addModulePage($module, $parent)
    {
        /** @var Pages $pages */
        $pages = $this->grav['pages'];
        $page = $pages->dispatch($this->route . '/_' . $module); // << Not sure about this part how module routes should be if it's needed at all

        if (!$page) {
            // Only add login page if it hasn't already been defined.
            $page = new Page();
            $page->init(new \SplFileInfo(__DIR__ . '/pages/modules/_' . $module . '.md'));
            $page->slug(basename($this->route));

            $page->parent($parent); // << here goes your main modular page as a parent

            $pages->addPage($page, $this->route);
        }

        // Login page may not have the correct Cache-Control header set, force no-store for the proxies.
        $cacheControl = $page->cacheControl();
        if (!$cacheControl) {
            $page->cacheControl('private, no-cache, must-revalidate');
        }
    }

Keep in mind I didn't test it, so it might need some adjustments if it works at all

last edited 03/12/21 by Karmalakas
5 years ago

Thanks for this snippet. Unfortunately it throws an error in /system/src/Grav/Common/Page/Page.php

"Call to a member function root() on null" on line 2508

PHP
2499    $uri = Grav::instance()['uri'];
2500    $pages = Grav::instance()['pages'];
2501    $uri_path = rtrim(urldecode($uri->path()), '/');
2502    $routes = Grav::instance()['pages']->routes();
2503
2504    if (isset($routes[$uri_path])) {
2505        /** @var PageInterface|null $child_page */
2506        $child_page = $pages->find($uri->route())->parent();
2507        if ($child_page) {
2508            while (!$child_page->root()) {
2509                if ($this->path() === $child_page->path()) {
2510                    return true;
2511                }
2512                $child_page = $child_page->parent();
2513            }
2514        }
2515    }

There is still something missing for setting up the parent/child relationship.

5 years ago

Looks like some paths are wrong. Notice in my provided snippet you need to change all paths and routes to the ones you have.

5 years ago

I did that. Instead of having my-plugin/pages/_module.md I have my-plugin/pages/_module/text.md according to the documentation of modular pages.

If I change that to pages _module.md, I do not get the error. But instead I get several times the parent (modular) page instead of the module page. And I get this page alternating twig processed and raw.

Interlinking pages and access to child items do still not work correctly. Everything I try is like digging in the dark, still havo no idea, how it should work.

5 years ago

Just discoverd a minor glitch (or bug?) in /system/src/Grav/Common/Page/Page.php (see code from above line 2508). You find the same function activeChild() in /system/src/Grav/Common/Flex/Types/Pages/Traits/PageRoutableTrait.php with code

PHP
109  $child_page = $page ? $page->parent() : null;
110  while ($child_page && !$child_page->root()) {
111      if ($this->path() === $child_page->path()) {
112         return true;
113      }
114      $child_page = $child_page->parent();
115  }

After changing that in Page.php, it does not throw errors anymore. But still no success rendering the modules.

5 years ago

Could you maybe share your plugin with info how to use it and what's the expected result? I might give it a try

5 years ago

Sorry, this would not work. I'll explain:

I run a (privately organised) cloud based service for email, webhosting and some predefined webservices with some hundreds of users of a closed community. Part of that is a portal, where people can apply for services and manage their accounts.

The first portal was self programmed in Perl (back in 1999), the current portal is based on Drupal 6. I try to give GRAV a chance to become the next portal.

My module handles managing your own account and some services, the user database is a backend LDAP server. For that I combine Login-Plugin with LDAP-Login-Plugin and some glue in my own plugin.

Already working I have

  • registering/applying for a new account (sorry, not open. closed community)
  • removing an account
  • changing account parameters (at least programmatically)

My intent is to have a profile page with three forms, one for managing account data, one for service parameters, one for changing the password. Organized as one page.

I have a normal modular page beneath /user/pages with three forms. But this one does not get filled with account data, and it does not work together with Login-Plugin for changing the user profile.

My plugin should be the glue to have my own profile page based on three modular forms. I want to implement minimally invasive instead of building a full clone of Login-Plugin. Just to be save on GRAV enhancements.

My hope was that programmatically creating modular pages was already solved by others.

5 years ago

I see..
Do you have to have modular page there or maybe three AJAX forms would do?

5 years ago Solution

I just got the right inspiration - have it working.

Solution:
I have my own pages directory /user/plugin/my-plugin/pages with all pages and modules like written in the documentation. Every module is a subdirectory _module-xx with its .md-file.

The suggested code of Karmalakas works perfectly.

In my modular page (parent page, modular.md within /user/plugin/my-plugin/pages) I have to use @self.all for the page collection. Neither @self.modular nor @self.children nor @self works.

Thanks for your help.

5 years ago

As I suspected, my snippet just needed a bit of touch :)

Plugin class:

PHP
    // @modular.php

    protected $route = '/modular';

    public function onPluginsInitialized(): void
    {
        /** @var Uri $uri */
        $uri = $this->grav['uri'];

        if ($uri->path() === $this->route) {
            $this->enable(
                [
                    'onPagesInitialized' => ['addModularPage', 0],
                ]
            );

            return;
        }
    }

    public function addModularPage()
    {
        /** @var Pages $pages */
        $pages = $this->grav->get('pages');
        $page  = $pages->dispatch($this->route);

        if (!$page) {
            $page = new Page();
            $page->init(new \SplFileInfo(__DIR__ . '/pages/modular.md'));
            $page->slug(basename($this->route));

            $pages->addPage($page, $this->route);
        }

        $cacheControl = $page->cacheControl();
        if (!$cacheControl) {
            $page->cacheControl('private, no-cache, must-revalidate');
        }

        foreach (['module_1', 'module_2'] as $module) {
            $this->addModulePage($module, $page);
        }
    }

    protected function addModulePage($module, $parent)
    {
        $route = $this->route . '/_' . $module;

        /** @var Pages $pages */
        $pages = $this->grav->get('pages');
        $page  = $pages->dispatch($route);

        if (!$page) {
            $page = new Page();
            $page->init(new \SplFileInfo(__DIR__ . '/pages/modules/_' . $module . '/text.md'));
            $page->slug($module);
            $page->parent($parent);
            $page->modularTwig(true);

            $pages->addPage($page, $route);
        }

        $cacheControl = $page->cacheControl();
        if (!$cacheControl) {
            $page->cacheControl('private, no-cache, must-revalidate');
        }
    }

Pages structure:

TXT
pages
 |- modules
 |   |- _module_1
 |   |   '- text.md
 |   '- _module_2
 |       '- text.md
 '- modular.md

text.md

YAML
---
title: Module 1/2
---

Module 1/2 content

modular.md

YAML
---
title: Modular title
content:
    items: '@self.modular'
---

If you navigate to /modular, both modules are outputted
Notice $page->modularTwig(true) on each module

image|690x306

last edited 03/17/21 by Karmalakas
5 years ago

@bkraegelin:
In my modular page (parent page, modular.md within /user/plugin/my-plugin/pages ) I have to use @self.all for the page collection. Neither @self.modular nor @self.children nor @self works.

You have to add $modulePage->modularTwig(true) and then you can use @self.modular

Suggested topics

Topic Participants Replies Views Activity
Plugins · by Rene, 1 week ago
2 44 1 week ago
Plugins · by Xavier, 4 weeks ago
2 54 4 weeks ago
Plugins · by Luka Prinčič, 7 years ago
3 1181 1 month ago
Plugins · by Sebastian van de Meer, 1 month ago
1 48 1 month ago
Plugins · by PIERROT Alain, 2 months ago
3 73 2 months ago