Jump to content

Checkbox Values Not Saving in Custom Module


froot
 Share

Recommended Posts

Hello everyone,

I'm working on a custom ProcessWire module, and I've encountered an issue with checkbox values. The checkboxes render correctly in the page edit form, but when I tick them, save the page, and then reload it, the selected values are not saved to the page.

I've tried various approaches, including hooks, to ensure that the selected values persist in the database, but so far, I haven't been successful. I'm seeking advice and solutions to resolve this issue and ensure that the checkbox values are properly saved when a page is edited.

Any help or suggestions would be greatly appreciated! Thanks in advance!

I wish you a nice Sunday otherwise.
 

class MenuAllocator extends WireData implements Module, ConfigurableModule {
    public static function getModuleInfo() {
        return array(
            'title' => 'Menu Allocator',
            'version' => 1,
            'summary' => 'A module to add a custom field to page edit forms.',
            'autoload' => 'template=admin',
        );
    }

    
    public function __construct() {
    
        $menuAllocatorSettings = wire('modules')->getConfig($this);
        foreach ($menuAllocatorSettings as $key => $value) {
            $this->$key = $value;
        }
    
    }


    public function init() {
        // Add a hook to build the page edit form
        $this->addHookAfter('ProcessPageEdit::buildForm', $this, 'addCustomFieldToPageEditForm');
        
        // Add a hook to save the field value when the page is saved
        $this->addHookBefore('Pages::executeSave', $this, 'saveCustomField');
    }

    
    public function addCustomFieldToPageEditForm(HookEvent $event) {

        $this->message('this is addCustomFieldToPageEditForm');

        $page = $event->object->getPage();
    
        // Check if this is a frontend page (you can define your criteria here)
        if (strpos($page->path, '/admin/') !== 0) {
            // Get the selected menu names from the module's settings

            $menuNames = $this->menus;

            if ($menuNames == '') return;

            $menuNames = explode(' ', $menuNames);
    
            // Create the custom field as an InputfieldAsmSelect
            $field = $this->modules->get('InputfieldCheckboxes');
            $field->name = 'menus';
            $field->label = 'Menus';
            $field->description = 'Select menu names for the page.';
    
            // Add menu options based on module configuration
            foreach ($menuNames as $menuName) {
                $field->addOption($menuName, $menuName);
            }
    
            // Add the field to the page edit form
            $form = $event->return;
            $form->insertBefore($field, $form->getChildByName('title')); // Adjust the position as needed
        }
    }


    public function saveCustomField(HookEvent $event) {
        $page = $event->arguments[0];
    
        // Check if this is a frontend page (you can define your criteria here)
        if (strpos($page->path, '/admin/') !== 0) {
            // Get the field value from the input
            $fieldValue = $this->input->post->menus; // Use the correct field name

            $this->message($fieldValue);
    
            // Save the field value to the page
            $page->set('menus', $fieldValue); // Use the correct field name
        }
    }
    
                  
}

 

Link to comment
Share on other sites

Hi fruid,

I'm not the most indicated to help, but in the meanwhile some other much more skilled then me, I try to give mi 50c:

  1. Maybe in your custom field creation foreach, is missing something like:
    $field->attr('checked', empty($data[$menuName]) ? '' : 'checked');

And a guess: Surely it's a typo error but before calling the InputfieldCheckboxes you have a commented note for an ASMSelect.
Maybe there are other copy/paste mistakes?

P.S. Dumping the checkboxes values, are they null/0/empty?

  • Like 1
Link to comment
Share on other sites

Thanks @Mike-it for your suggestion!

Before I continue I would like to understand one thing.

Is it even possible to render a field on the page-edit page and save its value to the page? As to my understanding it should, because a page is an object and I can add properties to a page object with code as well, so why not with the UI?

Or do I need to, upon installing the module, create a field, add it to a fieldgroup and a template and, optionally, remove it accordingly when uninstalling?

Link to comment
Share on other sites

public function saveCustomField(HookEvent $event) { 
	$fieldValue = $this->input->post->menus;
	bd($fieldValue);
	...
	$page->set('menus', $fieldValue);
}
    

the fieldValue is null when no checkbox is ticked, if first checkbox is ticked it stores an array like so:

array (0 => 'top')

which is what I expect. My guess is one of the following

- I cannot pass just pass an array to a $page->set() method
- I misconfigured the hooks
- Since I hook a function and edit the saved value I need to return the edited object
- I cannot save the value to the page cause the field does not actually live on the page
- something about the InputFieldCheckboxes API that works entirely different

Link to comment
Share on other sites

1 hour ago, fruid said:

Is it even possible to render a field on the page-edit page and save its value to the page? As to my understanding it should, because a page is an object and I can add properties to a page object with code as well, so why not with the UI?

From what I am understanding there is no "menus" field on the pages your are running this module on, so this won't get saved anywhere when you set it on the $page object. So yes you can add properties to the page, but that doesn't mean that property represents a Field/Fieldtype within the Page, which holds the actual persistence mechanism of the fields.

Why not create the "menus" field from the start? Maybe explain a bit further what are you trying to solve with the module? 

Link to comment
Share on other sites

1 hour ago, fruid said:

Thanks @Mike-it for your suggestion!

Before I continue I would like to understand one thing.

Is it even possible to render a field on the page-edit page and save its value to the page? As to my understanding it should, because a page is an object and I can add properties to a page object with code as well, so why not with the UI?

Or do I need to, upon installing the module, create a field, add it to a fieldgroup and a template and, optionally, remove it accordingly when uninstalling?

I’m not sure but I think that a field in a page must live first of all in the db, so if there isn’t a link in a table that joins the field value and the page, that field doesn’t exists for that page…

I think you’re right: You should build your module in a way that it adds the field into the fieldgroup associated to the template on installing, and removing it on module unsintall.
 

If you need that field isn’t visible, set it as hidden and the reveal it by API/hook

Link to comment
Share on other sites

19 minutes ago, elabx said:

Maybe explain a bit further what are you trying to solve with the module? 

in the module or module config/settings, I want to define an array of "menus".
Then, on every page in the page tree, except on pages inside "Admin", I want to render an array of checkboxes (InputFieldCheckboxes), one for each entry in the predefined "menus"-array.
When the content manager edits a page they will see these checkboxes, and one or more before saving the page will naturally be saved somewhere, ideally as a property of the page.
When creating the menus in the markup (e.g. top menu, footer, sidebar, you name it), one should be able to select the pages according to the values set for said field on the pages. 

19 minutes ago, elabx said:

Why not create the "menus" field from the start?

I'm not sure if creating adding a field and adding it to a fieldgroup and template is necessary (also this approach is quite prone to errors, I have to consider __install() and __uninstall() etc.), I managed to accomplish something very simliar with a different module (https://github.com/dtjngl/simpleSearch/blob/main/SimpleSearch.module.php), the difference was, that one renders a text field on each template and when editing a template, the entered value gets saved to the template object. Works like a charm.

Thanks for you input!

  • Like 1
Link to comment
Share on other sites

5 hours ago, elabx said:

What if you use the meta() function? Then on adding the field, checking for the state of the data and set the field's checked state accordingly. On the save, instead of saving to the page, use the meta() API.

I can try that, I'm just trying to understand why my approach doesn't work. Why does a similar logic work for editing templates but not for editing pages?

Link to comment
Share on other sites

13 hours ago, fruid said:

I'm not sure if creating adding a field and adding it to a fieldgroup and template is necessary (also this approach is quite prone to errors, I have to consider __install() and __uninstall() etc.),

If you want to save the data using the Page object I see no other way than this or the meta object. I do understand it can get troublesome to manage all the custom fields, but I've seen a few module authors handle it like this!

I'm not sure if my explanation will be completely correct but I'll give it a shot haha

So if you take a look at the Templates class, you'll see that if actually extends WireSaveableItems, which is where persistence to the database is implemented. See how $some_template->save() (Template, no "s") actually calls the $templates->save method (Templates = global $templates). If you take a look at the actual database through phpmyadminer or Adminer and see the templates table, you'll find one row per template and a column named data that holds the template config. This is all thanks to WireSaveableItems!

The Page object is different, it doesn't have a data column and it doesn't implement persistence of fields like this. What happens is that each field's Fieldtype implements its own persistence mechanism. So, when you are adding that InputfieldCheckboxes, the Page doesn't know anything about it and later when you assign the Inputfield's value as a property of the Page object it also doesn't know anything about how it should be saved either, this is all delegated to different classes decoupled from the Page class, the Fieldtypes. Check how the PagesEditor class ends up calling savePageField, a method from the Fieldtype abstract class.

So when you are adding these properties dynamically these are just set as regular properties, the further process of saving the pages doesn't really know that this property is a field since it's not in the template/fieldgroup assigned to the page and there is no default 'save to the data column on the page table' behaviour. 

Link to comment
Share on other sites

doesn't work 😞 
should be so easy though, right? 
 

class MenuAllocator extends WireData implements Module {

    public static function getModuleInfo() {
        return array(
            'title' => 'Menu Allocator',
            'version' => 1,
            'summary' => 'A module to allocate menus to pages.',
            'autoload' => 'template=admin',
        );
    }


    public function ready() {
        // Add a hook to build the page edit form content
        $this->addHookAfter('ProcessPageEdit::buildFormContent', $this, 'addCustomFieldToPageEditForm');
        
        // Add a hook to process the form input when the page is saved
        $this->addHookBefore('Pages::saveReady', $this, 'saveCustomField');

    }


    public function init() {
        bd($this->meta);
    }


    public function addCustomFieldToPageEditForm(HookEvent $event) {
        $form = $event->return;

        // Check if this is a frontend page (you can define your criteria here)
        if (strpos($this->input->url, '/admin/') !== false) {
            // Create the custom field as an InputfieldText
            $field = $this->modules->get('InputfieldText');
            $field->name = 'custom_text_field'; // Use a different name for rendering
            $field->label = 'Custom Field for Display'; // Different label for rendering
            $field->description = 'Enter a custom value for this page (display only).';
        
            // Add the field to the page edit form
            $form->add($field);
        }
    }


    public function saveCustomField(HookEvent $event) {
        // Get the page object from ProcessWire's API
        $page = wire('page');
    
        // Check if the page object exists
        if ($page instanceof Page) {
            // Get the custom_text_field value from the form input
            $fieldValue = $this->input->post->custom_text_field; // Use the correct field name
    
            // Set the custom field value to the page's meta data
            $page->meta->set('custom_text_field', $fieldValue); // Use the correct field name for meta data
        }
    }
                                    
}
Link to comment
Share on other sites

47 minutes ago, fruid said:
    public function saveCustomField(HookEvent $event) {
        // Get the page object from ProcessWire's API
        $page = wire('page');

Ah and I'm not sure if that is a good idea. wire('page') is the current PW page. Did you check if that is really the correct page and not the page with id 10 that is responsible for rendering the page edit form??

I'd better get the edited page from the HookEvent:

$page = $event->arguments(0);

 

  • Like 2
Link to comment
Share on other sites

4 hours ago, bernhard said:

I'd better get the edited page from the HookEvent:

 

public function ready() {
	$this->addHookAfter('ProcessPageEdit::buildFormContent', $this, 'addCustomFieldToPageEditForm');
	// ...
}

public function addCustomFieldToPageEditForm(HookEvent $event) {
	$page = $event->arguments('page'); // null
	// ...
}

;(

 

EDIT:

$id = $this->input->get('id'); // frontend page object id
$page = wire('pages')->get($id); // frontend page object

😄

  • Like 1
Link to comment
Share on other sites

You have to read carefully. What I showed was the syntax for saveCustomField() method. You now posted the one for addCustomFieldToPageEditForm() - that are two different things.

saveCustomField hooks into Pages::saveReady, therefore the $page is available as the very first argument of the HookEvent: $event->arguments(0) or $event->arguments('page')

addCustomFieldToPageEditForm on the other hand hooks into ProcessPageEdit::buildForm, therefore the edited $page is available as $event->object->getPage()

Your solution is fine as well, but it will only work for situations where the page id is available as a get parameter. So I'd recommend you read again my post/video about hooks here: https://processwire.com/talk/topic/18037-2-date-fields-how-to-ensure-the-second-date-is-higher/#comment-158164

 

  • Like 2
Link to comment
Share on other sites

thanks @bernhard, yes I noticed that later on.

I managed to get the module to serve my needs, it's here: https://github.com/dtjngl/MenuAllocator

I (re)watched your hook-tutorial, it's interesting but I doubt it can help me in this very use-case since I do not really populate fields on the page and the fields in question are rendered at runtime (by a another hook), so I had a hard time saving the values to the page (luckily there's ->meta). I'm not sure if this is the best approach but it works.

@everyone, please give it a try, I'd love to get feedback.

  • Like 1
Link to comment
Share on other sites

13 hours ago, fruid said:

I managed to get the module to serve my needs, it's here: https://github.com/dtjngl/MenuAllocator

[...snip...]

@everyone, please give it a try, I'd love to get feedback.

Hi @fruid

At the moment it looks like you are grabbing all pages (with parent id = 1) and iterating through them individually to find the ones matching the required menu.

With a small number of pages this may well be perfectly fine, but if there are hundreds/thousands of pages it's not very efficient.

It would be better to just grab only the pages in the required menu - unfortunately you can't (currently?) do that with a PW selector, but you can use a MySQL query directly combined with a PW find selector -- check out the following thread:

 

  • Like 1
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

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...