Robin S Posted August 29, 2016 Share Posted August 29, 2016 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. 2 Link to comment Share on other sites More sharing options...
LostKobrakai Posted August 29, 2016 Share Posted August 29, 2016 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). 2 Link to comment Share on other sites More sharing options...
adrian Posted August 30, 2016 Share Posted August 30, 2016 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. 2 Link to comment Share on other sites More sharing options...
Robin S Posted August 30, 2016 Author Share Posted August 30, 2016 Thanks @adrian, glad to have those modules as a proof-of-concept. Link to comment Share on other sites More sharing options...
Robin S Posted August 31, 2016 Author Share Posted August 31, 2016 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 More sharing options...
BitPoet Posted August 31, 2016 Share Posted August 31, 2016 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. 1 Link to comment Share on other sites More sharing options...
Robin S Posted August 31, 2016 Author Share Posted August 31, 2016 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 More sharing options...
BitPoet Posted August 31, 2016 Share Posted August 31, 2016 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; }); 1 Link to comment Share on other sites More sharing options...
Robin S Posted September 1, 2016 Author Share Posted September 1, 2016 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 More sharing options...
adrian Posted November 30, 2016 Share Posted November 30, 2016 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. 3 Link to comment Share on other sites More sharing options...
elabx Posted February 17, 2023 Share Posted February 17, 2023 Hi! Wondering if there was an example of this scenario (multiple/dynamic config inputfields) with the "new" module configuration through a separate ModuleConfig derived class? Link to comment Share on other sites More sharing options...
Jan Romero Posted July 14 Share Posted July 14 (edited) On 11/30/2016 at 7:44 AM, adrian said: 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. Agreed. FWIW, this is how you might do the same thing for Field configs. For example, to have users enter CSV formatted data but store it as JSON, so it’ll automatically be available in a structured form: public function ___getConfigInputfields(Field $field) { $inputfields = parent::___getConfigInputfields($field); $myTabularData = $field->get('myTabularData') ?? []; $myTabularDataCSV = implode("\n", array_map(fn($r) => "{$r['surname']}, {$r['givenname']}, {$r['shoesize']}", $myTabularData)); /** @var InputfieldTextarea $f */ $f = $this->wire()->modules->get('InputfieldTextarea'); $f->attr('name', 'myTabularData'); $f->label = $this->_('Important config data'); $f->description = $this->_('Enter some important data in CSV form with the following column order: Surname, Given Name, Shoesize'); $f->attr('value', $myTabularDataCSV); $inputfields->add($f); $this->addHookAfter('Fields::saveReady', null, function($event) use ($field) { if ($event->arguments(0) !== $field) return; $csv = trim($field->myTabularData); if (!$csv) { $field->myTabularData = []; return; } $myTabularData = []; foreach (explode("\n", $csv) as $r) { $arr = explode(',', $r, 3); $myTabularData[] = [ 'surname' => sanitizer()->text($arr[0] ?? '—'), 'givenname' => sanitizer()->text($arr[1] ?? '—'), 'shoesize' => (int)($arr[2] ?? 0), ]; } $field->myTabularData = $myTabularData; }); return $inputfields; } So… yikes. Is there a better way? Of course it would be optimal to just have a bespoke structured Inputfield. Edited July 14 by Jan Romero Link to comment Share on other sites More sharing options...
Recommended Posts
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 accountSign in
Already have an account? Sign in here.
Sign In Now