Jump to content

Strategies for repeatable fields in module config


Robin S
 Share

Recommended Posts

Suppose you have a module where you want users to be able to fill out some fields for a configuration, but then the user should be able to add several of these configurations. This is something I've come up against a few times when working on modules. Any suggestions of strategies for this?

I'm thinking of something similar to a Repeater - is this possible in a module configuration?

I know my module could add a page to the tree and literally use a Repeater field within it but I'm not keen on that because (a) the Repeater module isn't installed by default and it seems a bit mad to require it just for a module configuration, and (b) I'd prefer to have the module configuration done in the module settings rather than on a separate page

The only alternative I can think of is to use a textarea field in the module config and require users to carefully format/separate the values for multiple settings on a single line, and with each subsequent line being a different configuration. This seems a bit primitive and error-prone, although it is what I have been using thus far.

Is there some middle ground between these two options? Do you think some sort of repeatable block for module config would be a good feature to add to PW? Not something that actually adds pages like a Repeater, but rather something that still allows the whole module config to be stored as a string in the DB.

  • Like 2
Link to comment
Share on other sites

As the module config is stored as json string anyways you could use any array structure to model those config data, but there's no ready made inputfield to support that structure by now. All multi instance/multi value fields do depend on actual pages be present (or at least a fieldtype db table for fieldtypetable).

  • Like 2
Link to comment
Share on other sites

Here are a couple of example modules that do this already: 

http://modules.processwire.com/modules/process-email-to-page/

http://modules.processwire.com/modules/process-custom-upload-names/

The approach is the same in both, although I don't honestly think you should steal code directly as it's not particularly pretty (a big understatement) - I'd like to redo the approach for both at some point), but it will hopefully give you a starting point.

 

  • Like 2
Link to comment
Share on other sites

I see that the approach of ProcessEmailToPage and ProcessCustomUploadNames is to save the repeatable fields as a combined JSON string to a single hidden inputfield.

I'd like to try something different and use the core inputfields (as per a normal module config) inside a fieldset, with the number of fieldsets being variable. I've nearly got this working but have struck a problem with config fields being retained in the DB data after they have been removed from the config. This isn't just an issue for this repeatable config I'm trying to achieve but would also be an issue for a more typical module config if the number of config fields changed between versions.

To illustrate, say my getInputfields() function consists of two text fields: text_1 and text_2. In the DB this is stored as:

{"text_1":"foo","text_2":"bar"}

Now I remove text_2 from getInputfields() and the inputfield is gone, all good. But the DB is unchanged and the value for text_2 is still stored. If the normal behaviour is not to remove config data when the config inputfield is removed, can I use a hook in my module to change this? I'm having trouble tracing where the ModuleConfig class actually saves to the DB. Does it do this via saveConfig() in Modules.php and would that be the right method to hook?

Edit: found the source of this issue here in ProcessModule.module - the existing config array is loaded first and then data from the config inputfields is merged in. The net effect of this is that the inputfields specified in ModuleConfig can only ever add to the array of saved fields, never remove fields from it. The method isn't hookable so I don't see a way to override this behaviour.

Link to comment
Share on other sites

I think using an after or replacement hook on Modules::saveConfig should be possible, even if it creates a bit of repetition overhead. Retrieve the fields list (again) through getModuleConfigInputfields(), then iterate over all configuration values, unset() the respective entry if $fields->get($key) returns null and finally save (again). It's actually a tiny bit more complex since language support also needs to be taken into account, but not that much.

  • Like 1
Link to comment
Share on other sites

Thanks @BitPoet. Module config fields don't exist in $fields so I can't use that to check which config values should be weeded out in a hook to saveConfig(). But it did give me another idea: save my own JSON string of current config fieldnames in a hidden config field, then check the keys in the config array against that and unset any that are not included. But now I'm banging my head against another issue which is that it's annoyingly hard to set the value of a config inputfield using the ModuleConfig class.

Link to comment
Share on other sites

I was thinking of Inputfields return from getModuleConfigInputfields. Here's a snippet to illustrate my thought:

wire()->addHookAfter("Modules::saveConfig", function(HookEvent $event) {
	wire('log')->message("Hook after Modules::saveConfig");
	$class = $event->arguments(0);
	if(is_object($class)) $class = $class->className();
	$moduleName = wireClassName($class, false);
	
	$id = wire('modules')->getModuleID($class);
	
	$data =  $event->arguments(1);
	
	$fields = wire('modules')->getModuleConfigInputfields($moduleName);
	
	foreach(array_keys($data) as $key) {
		if(! $fields->get($key)) {
			unset($data[$key]);
			wire('log')->message("Removed property $key from module {$moduleName} config");
		}
	}

	// Code shamelessly stolen from Modules::saveConfig	
	$json = count($data) ? wireEncodeJSON($data, true) : '';
	wire('log')->message("Data = $json");
	$database = $this->wire('database'); 	
	$query = $database->prepare("UPDATE modules SET data=:data WHERE id=:id", "modules.saveConfig($moduleName)"); // QA
	$query->bindValue(":data", $json, \PDO::PARAM_STR);
	$query->bindValue(":id", (int) $id, \PDO::PARAM_INT); 
	$result = $query->execute();
	$this->log("Stripped module '$moduleName' config data");
	
	$event->return = $result;
});

 

  • Like 1
Link to comment
Share on other sites

Thanks, that's a good idea. Unfortunately it doesn't work for my situation because getModuleConfigInputfields() still reports the old set of configuration fields at the time that the saveConfig() hook runs. Not sure exactly when the inputfields are updated following a change - perhaps on the following page load of the module config screen?

I have come up with something that works for now. I have an integer field in the module config that defines the number of repeated fieldsets, whose names contain an integer according to the iteration. In the saveConfig() hook I unset any fieldnames containing an integer higher than the fieldset count.

Link to comment
Share on other sites

  • 2 months later...

I wanted to store multidimensional data in the config settings of my new AdminActions module and I ended up taking the approach of hooking before saveModuleConfigData so that I could change the content of $event->arguments(1), which is the data array, before it is saved.

https://github.com/adrianbj/ProcessAdminActions/blob/b3a82c308377f03c86ae4a2b8fc78e4cdc73b94e/ProcessAdminActions.module#L383-L390

I am not sure if that helps you or not, but thought I'd post just in case.

PS - I feel like PW could benefit from a more standard way of storing multidimensional config data.

  • Like 2
Link to comment
Share on other sites

  • 6 years later...

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...