Jump to content

Page field with context-aware parent


abdus
 Share

Recommended Posts

Hey,

I'm building a website with following page hierarchy

home/
    /blog (template: listing)
        /tags (template: tags)
            tag1 (template: tag)
            tag2
            tag3
        post1 (template: post)
        post2
        post3
    /development (template: listing)
        /tags (template: tags)
            tag1 (template: tag)
            tag2
        post1 (template: post)
        post2


Ok, so I created a Page field to reference tag pages for categorization of posts in different listings. I set up the field to use InputfieldPageAutocomplete module for quickly referencing and creating any number of tags within a post. However, during the setup, Processwire allows me to pick only one parent. I want to change this behaviour such that when searching for a tag, it should only search under its own listing. Specifically, posts under /development should only be able to reference tags under /development/tags and those under /blog should reference tags under /blog/tags etc. Also, when I create a new tag, it should create under its own tags page.

I don't want to use a tags page under the homepage, because I want the URL structure to be /blog/tags/journal or /dev/tags/processwire etc, not /tags/processwire. I know I can use input()->urlSegments to achieve the url structure I want, and I did it like that in the past, but it felt too hacky to change the paths for pages from their own path in page tree. I tried hooking into InputfieldPage:processInputAddPages method to make the field accept the correct tags page, but I couldnt make it work.

How can I achieve this?

Link to comment
Share on other sites

Regarding the listing, if you are willing to switch from Autocomplete to, say, ASM Select, you can easily allow tags to be contextually selected based on their grandparents (i.e. development and blog, respectively as rootParent) by making use of the setting "Custom PHP code to find selectable pages" (Input Tab when editing your page field). @note: The 'create under its own tag page' would NOT work in this case, though since that needs a named parent + template.

Edited by kongondo
Link to comment
Share on other sites

  On 1/9/2017 at 11:18 PM, kongondo said:

If you are willing to switch from Autocomplete to, say, ASM Select, you can easily allow tags to be contextually selected based on their grandparents (i.e. development and blog, respectively as rootParent) by making use of the setting "Custom PHP code to find selectable pages" (Input Tab when editing your page field).

Expand  

Exactly. By hooking into InputfieldPage:getSelectablePages method, I'm able to reference correct tag pages depending on the rootParent, however it does not allow me to quickly create tag pages under correct rootParent, as you can only select one parent. When editing a post under different rootParent than the one that Page field is set up with, creating new tag pages gives error "Page $page does not have required parent $parent_id"

Link to comment
Share on other sites

For listing/searching only, you don't need to hook into anything, mind. For creating new tag pages, what you will need to do, I think, is to hook into the page creation process. So, you could say, select the one parent in that setting, but in your Hook, you change that to the parent you want.

Edited by kongondo
page creation
Link to comment
Share on other sites

  On 1/9/2017 at 11:41 PM, kongondo said:

Aah, of course. I get you, my bad. Did you see my updates as well? About Hooking into page save in respect of contextual parent...

Expand  

I tried that as well. I can intercept and hook into InputfieldPage::processInputAddPages events, and change parent id to correct one, but PW ignores the change. 

 

// choose correct tags parent depending on the listing under which page the post lives
wire()->addHookAfter('InputfieldPage::processInputAddPages', function (HookEvent $event) {
    // The parent page the field is set up with  
    // $parentId = $event->object->parent_id;
    // $parent = $event->pages->get("id=$parentId");
    
    // the page that is currently being edited
    $currentPage = $event->pages->get($event->arguments(0)->id);
    $rootParent = $currentPage->rootParent;
    // correct tags page
    $tagsPage = $event->pages->get("template=tags, has_parent=$rootParent");

    $event->object->parent_id = $tagsPage->id;
});

// PW ignores the change in parent_id

Changing addHookAfter to addHookBefore does not help either.

Link to comment
Share on other sites

  On 1/10/2017 at 12:05 AM, abdus said:

I tried that as well. I can intercept and hook into InputfieldPage::processInputAddPages events, and change parent id to correct one, but PW ignores the change. 

Expand  

As you found, you can't change the $parent_id variable inside the InputfieldPage::processInputAddPages method using this type of hook because $parent_id is neither an argument to the method nor a return value of the method.

I think you would have to use a replace hook - copy the whole InputfieldPage::processInputAddPages into your hook function and change as needed. Bear in mind that doing this means that any future core changes in InputfieldPage::processInputAddPages will not take effect while your hook is in place.

  • Like 2
Link to comment
Share on other sites

  On 1/10/2017 at 12:54 AM, Robin S said:

As you found, you can't change the $parent_id variable inside the InputfieldPage::processInputAddPages method using this type of hook because $parent_id is neither an argument to the method nor a return value of the method.

I think you would have to use a replace hook - copy the whole InputfieldPage::processInputAddPages into your hook function and change as needed. Bear in mind that doing this means that any future core changes in InputfieldPage::processInputAddPages will not take effect while your hook is in place.

Expand  

Hmm, that method looks interesting, but as you said it's not future-proof. Also, the method utilizes other methods and fields from InputfieldPage class itself, so replacing it won't do any help either.

Link to comment
Share on other sites

  On 1/10/2017 at 12:59 AM, abdus said:

Also, the method utilizes other methods and fields from InputfieldPage class itself, so replacing it won't do any help either.

Expand  

InputfieldPage is available in the hook function as $event->object - so in your hook code copied from InputfieldPage::processInputAddPages you can replace class methods called like $this->someClassMethod() with $event->object->someClassMethod(). But you will need to distinguish between class methods and other PW API variables accessed with $this. E.g. $this->getSetting() vs $this->pages()

Edit: actually, that will only work for public methods and properties so it's probably not sufficient.

Another alternative is to copy the entire InputfieldPage module to /site/modules/ and make changes there.

Link to comment
Share on other sites

For page creation, I meant that you should hook into the actual page creation process, or just after it. That means hooking into 'added'. I have tested (code below) and it works. Once the new tag has been created, we get the root parent of the 'post' being edited. If it is /blog/, we do nothing. Otherwise, we change its parent to the child of its root parent. Meaning, if editing /development/post1/, the root parent of the new tag will be /development/tags/new-tag/. But there's a catch....

Although the new tag(s) will be created fine, ProcessWire will not allow them to be listed as selectable pages since when setting up your page field, in order to be able to create 'new tags from field', as you know, you have to select both its parent and its template. The former is our problem. In my setup, the parent specified under 'Parent of selectable pages' is /blog/tags/. Since our new tag's parent in the case of development is /development/tags/ ProcessWire will throw an error 'Page 1234 is not valid for name_of_the_page_field'.

So, it turns out our problem is not the actual page creation and contextual parent assignment, but the listing! That, from what I can tell, can't be circumvented via a Hook. Is there any reason you cannot use different templates and fields for /blog/post/ and /development/post/?

Code to re-assign parent on new tag creation. Posted here in case anyone needs it. This was tested in a module. @note: the code could be optimised to only hook into the 'relevant pages'.

public function init() {
	$this->pages->addHookAfter("added", $this, "changeTagParent");
}

public function changeTagParent(HookEvent $event) {
	// get 'post' page being edited
	$process = $this->wire('process'); 
	if($process && $process->className() != 'ProcessPageEdit') return;
	$pageEdit = $process->getPage();

	// get the root parent of the 'post' being edited. we use it to determine whether to re-parent the 'new tag'
	$rootParent = $pageEdit->rootParent;
	// check root parent by path but can also check by ID, title, etc.
	if($rootParent->path == '/hook-tests-development/') {
		// newly created 'tag' page
		$page = $event->arguments[0];
		// change the parent of the 'new tag' to /development/tags/
		$page->parent = $rootParent->child();
		$page->save();
	}	

}

 

  • Like 3
Link to comment
Share on other sites

  On 1/10/2017 at 9:36 AM, kongondo said:

Is there any reason you cannot use different templates and fields for /blog/post/ and /development/post/?

Expand  

Actually, no.
I ended up doing that, since it was the least cumbersome way of doing it. Other methods requires going through hoops to modify core behaviour, which I wanted to avoid doing too much in the first place. 

I created created another field "devTags" and renamed the original to "blogTags" and created a new template for posts under /dev named "work"

/blog (template: listing)
    /tags (template: tags)
        tag1 (template: tag)
        tag2
    post1 (template: post, field: blogTags)
    post2
    post3
/dev (template: listing)
    /tags (template: tags)
        tag1 (template: tag)
        tag2
    work1 (template: work, field: devTags)
    work2

then without changing the templates, I created a new property hook that redirects $page->tags to correct tags field (blogTags or devTags) depending on the name of the rootParent of the post/work.

// return pages referenced with tags field depending on the rootParent
wire()->addHookProperty('Page::tags', function (HookEvent $event) {
    $page = $event->object;
    $fieldName = $page->rootParent->name . 'Tags';

    // check if field actually exists
    if (!$page->fields->get($fieldName) instanceof Field) {
        throw new WireException("{$page->template->name} template does not have $fieldName field.");
    }
    $event->return = page("$fieldName");
});

I also created another hook that lets me get the pages tagged with a specific tag, for when listing posts/works under the url /blog/tags/tagName

// return posts tagged with the current tag page
wire()->addHookProperty('Page::tagged', function (HookEvent $event) {
    $page = $event->object;
    $fieldName = $page->rootParent->name . 'Tags';

    if ($page->template->name === 'tag') {
        $event->return = pages("$fieldName=$page");
    } else throw new WireException('Only pages with tag templates can use tagged property');
});

# EXTRA: A bit of overengineering: change field name when rootParent's name changes

// rename tags field depending on its rootParents name
wire()->addHookBefore('Pages::renamed', function (HookEvent $event) {
    $page = $event->arguments(0);

    // check if page cant have tags template under it
    if ($page->template->name !== 'listing') return;
    // check if actually has tags template under it
    // if(! $page->hasChildren('template=tags')->count) return;

    $oldFieldName = $page->namePrevious . 'Tags';
    $tagsField = fields()->get($oldFieldName);
    if (!$tagsField instanceof Field) return;

    /* @var $tagsField Field */
    $newFieldName = $page->name . 'Tags';
    $tagsField->setName($newFieldName);
    $tagsField->save();

    wire()->message("Renamed $oldFieldName field to $newFieldName", true);
});

Thanks a lot for the insight!
 

Edited by abdus
added extra hook
  • Like 3
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
×
  • Create New...