I have an Easyform with two select fields. I would like the second field to be filtered based on the selected value of the first field. Both use plone.app.z3cform.widgets.select.AjaxSelectFieldWidget.
Looking at the select2 pattern, I should be able to use onSelected() somehow. But I am having trouble finding out how this works... When selecting the second field, I can see an XHR to @@getVocabulary - my objective is to change the query parameter here.
Another approach I tried, was to add a change listener in my theme's custom js, but I can't seem to find the element that is supposed to trigger this.
Update: this gets me the token in my custom js. Still would be cleaner to use onSelected()
And you get the pattern options inside the change callback like this:
const $main_select = $("#main-select-field");
$main_select.on("change", (e) => {
const pattern = e.target["pattern-select2"];
// depending if you have a multiselect field you have to split the value by separator
const vals = $(e.target).val().split(pattern.options.separator);
for(const val of vals) {
// do something with your selected values
}
});
Now it gets a little "hacky", because you have to change the option.vocabularyUrl from your child field and reinitialize the pattern with the new url:
const $main_select = $("#main-select-field");
const $child_select = $("#child-select-field");
const base_url = $("body").data("baseUrl");
$main_select.on("change", async (e) => {
// this gets the instantinated pattern from the DOM
const pattern = e.target["pattern-select2"];
// depending if you have a multiselect field you have to split the value by separator
const vals = $(e.target).val().split(pattern.options.separator);
for(const val of vals) {
// change the vocabulary query here
}
// jQuery also has an instance of the pattern code
const child_select_pattern = $child_select.data("pattern-select2")
child_select_pattern.options.vocabularyUrl = `${base_url}/@@getVocabulary?${your_new_criteria}`;
// reinitialize with new URL
await child_select_pattern.init();
});
As I just said, this is a quick and untested prototype, but may provide an approach to a solution.
I run into a traceback that involves datagridfield, even when no DGF is used in this from.
2024-10-17 12:10:29,245 ERROR [Zope.SiteErrorLog:17][waitress-3] AttributeError: http://localhost:8888/Plone/de/p-force/stapel-export/@@view/@@getVocabulary
Traceback (innermost last):
Module ZPublisher.WSGIPublisher, line 181, in transaction_pubevents
Module ZPublisher.WSGIPublisher, line 391, in publish_module
Module ZPublisher.WSGIPublisher, line 285, in publish
Module ZPublisher.mapply, line 98, in mapply
Module Products.PDBDebugMode.wsgi_runcall, line 60, in pdb_runcall
Module plone.app.content.browser.vocabulary, line 155, in __call__
Module plone.app.content.browser.vocabulary, line 316, in get_vocabulary
Module collective.z3cform.datagridfield.row, line 151, in validate
Module collective.z3cform.datagridfield.row, line 112, in validate
Module pnz.intranet.patches.plone_app_dexterity_permissions, line 58, in dx_field_permission_checker_validate
AttributeError: No such field: 'product_paths'
[9] > /usr/local/Plone6/src/pnz.intranet/src/pnz/intranet/patches/plone_app_dexterity_permissions.py(58)dx_field_permission_checker_validate()
-> raise AttributeError(f"No such field: {field_name}")
Will do some more digging...
(edit) Found:
const base_url = $("body").data("baseUrl"); does not include 'view/'
const $main_select = $('input[id^=form-widgets-brand_path]');
const $child_select = $('input[id=form-widgets-product_paths]');
const base_url = $("body").data("baseUrl");
const default_criteria = "name=pnz.intranet.ProductFilterVocabulary&field=product_paths";
const filter_criteria = "";
$main_select.on("change", async (e) => {
// this gets the instantiated pattern from the DOM
const pattern = e.target["pattern-select2"];
// depending if you have a multiselect field you have to split the value by separator
const vals = $(e.target).val().split(pattern.options.separator);
for(const val of vals) {
// change the vocabulary query here
var filter_criteria="&target_language=it&brand_path=/mdb/it/marche/pnz-produkte-it";
}
// jQuery also has an instance of the pattern code
const child_select_pattern = $child_select.data("pattern-select2")
child_select_pattern.options.vocabularyUrl = `${base_url}/view/@@getVocabulary?${default_criteria}${filter_criteria}`;
// reinitialize with new URL
await child_select_pattern.init();
});
Thank you @petschki your help was invaluable (again)
Here is what works for me:
const $main_select = $('input[id^=form-widgets-brand_path]');
const $child_select = $('input[id=form-widgets-product_paths]');
// why? $child_select.data("pattern-select2") is undefined
const child_vocabulary_url = $child_select.data("pat-select2").vocabularyUrl;
let filter_criteria = "";
$main_select.on("change", async (e) => {
// this gets the instantiated pattern from the DOM
const pattern = e.target["pattern-select2"];
// depending if you have a multiselect field you have to split the value by separator
const vals = $(e.target).val().split(pattern.options.separator);
for(const val of vals) {
// change the vocabulary query here
const lang = val.split('/')[2];
filter_criteria="&target_language="+ lang +"&brand_path="+ val;
}
// jQuery also has an instance of the pattern code
const child_select_pattern = $child_select.data("pattern-select2")
child_select_pattern.options.vocabularyUrl = `${child_vocabulary_url}${filter_criteria}`;
// reinitialize with new URL
await child_select_pattern.init();
});
and the vocabulary makes a rest call to our Plone 5.1 instance
@provider(IVocabularyFactory)
def ProductFilterVocabulary(context):
""" create a customer dependent products vocabulary
using our theme js to add extra filter parameters to the vocabularyURL
"""
terms = []
request = getRequest()
brand_path = request.form.get('brand_path','')
target_language = request.form.get('target_language','de')
title_filter = request.form.get('query', '')
if brand_path:
# do the actual REST call
log.info( 'ProductFilterVocabulary %s %s' % (brand_path, target_language))
query = {
'b_size': 500,
'metadata_fields':'getPath', # results token value
'portal_type': 'produkte',
'sort_on': 'sortable_title',
'Language': target_language,
'kundenname_verweis': brand_path,
}
rest_results = get_rest_session_results(
url=search_endpoint,
target_language=target_language,
query=query)
if rest_results:
results = rest_results.get('items', {})
terms = [
SimpleTerm(
title=result['title'],
value=result['getPath']
)
for result in results
if title_filter.lower() in result['title'].lower()
]
data = SimpleVocabulary(terms)
return data
Note: when using filtered results, make sure that the validator receives a query that includes the results you selected in the form. Otherwise wou wil get a "Wrong Contained Type" error , and might be chasing your tail for days trying to debug your already working js.
i.e. my default results include the first 500 entries out of many more, my filter allowed me to select entries outside of that range ==> Wrong Contained Type