Jump to content

Interactive updating of select inputfields


MarkE
 Share

Recommended Posts

Please note that the updated version of this script and any other associated info is now at https://github.com/MetaTunes/Form-update

I had a need to interactively update the page choices in a multi-choice page select field. I chose to do this with a general-purpose piece of jQuery. By combining it with a InputfieldPage::getSelectablePages hook, you can get the trigger field to alter the selectable pages interactively.  I have also found this to be useful in a number of other situations - e.g. updating a RuntimeMarkup field for changes on a page. There may be more elegant ways of achieving this (I'm open to suggestions), but in case it is useful to others, I'll post it here. Hopefully the comments in the script are self-explanatory and describe how to use it. Note that there are several console.log statements to help with debugging, which you can remove once happy with its operation.

Happy to answer any questions (if I can ? ). Also, if anyone can explain how to get it working fully with checbox/toggle ad radio buttons, I would be grateful.

/* Script to refresh a form content when an element gets changed
To work across all admin pages, this script needs to be loaded in admin.php
    – add the line $config->scripts->add($config->urls->templates . "scripts/form-update.js"); before the final require in templates/admin.php.
Typical use is to modify other elements based on a select drop-down change
The trigger element can have the following data attributes assigned to it (typically set these with $myInputfield->attr() in a module or hook):
* data-action="form-update" : Required to run the script.
* data-update-target="#myid1" : Required - the element to change. Note that this should not be the whole form, otherwise .find(target) will not find it.
* data-confirm="Some confirmation text": Optional - if you want to show a confirmation before the update, this holds the text to display. If absent, there will be no confirmation dialogue.
                If the user chooses ‘cancel’, the script will revert the change and terminate.
* data-alert="Some alert text": Optional – if you want to warn the user that the update cannot happen for some reason (the script will then revert the change and terminate).
* data-cache="#myid2" : Optional - if you want to cache the (changed) value, this element stores it.
* data-cache-prefix="Some prefix string" : Optional (requires data-cache) - a prefix to prepend the value stored in the cache
This currently works with the following trigger elements:
* select options
* select page (single and multiple)
* page list select (single and multiple)
* asm select
* page autocomplete (but note that data attributes must be set in the wrapper element - e.g.  $myInputfield->wrapAttr() )
* checkboxes (set attributes in wrapper as above)
but not with:
* toggle
* checkbox
* radio buttons
(These partly work - the attributes need to be in the wrapper -, but doesn't work completely as wrapper 'value' attribute is not updated by PW (always 0) )

NOTE: If you are using this with other js scripts (e.g. in a module) that listen for events in the target, you must use event delegation
(e.g. $(document).on("change","#myid", function(){}); NOT $("#myid").onchange(function(){}); ) because #myid is dynamic if it is inside the target)
 */

$(document).on('focusin', '[data-action="form-update"]', function(){
    // get the value before the element is changed
    console.log("Saving value " + $(this).val());
    $(this).data('val', $(this).val());
}).on('change','[data-action="form-update"]', function(event){
    var prev = $(this).data('val');
    var current = $(this).val();
    console.log("Prev value " + prev);
    console.log("New value " + current);
    // if trigger element has data-confirm attribute, confirm or revert and exit
    var confirmText = $(this).data('confirm');
    if (confirmText) {
        if (!confirm(confirmText)) {
            $(this).val(prev);
            return;
        }
    }
    // if trigger element has data-alert attribute, show alert and exit
    var alertText = $(this).data('alert');
    if (alertText) {
            alert(alertText);
            $(this).val(prev);
            return;
    }
    // cache the value before proceeding (if data-cache set)
    var cache = $(this).data('cache');
    var cachePrefix = ($(this).data('cache-prefix')) ? $(this).data('cache-prefix') : '';
    $(cache).val(cachePrefix + current);
    var $form = $(this).closest('form');
    var target = $(this).data('update-target');
    console.log("Target is " + target);
    var method = $form.attr('method');
    var action = $form.attr('action');
    var data = $form.serialize();
    var encodedName;
    // .serialize() will omit select elements that do not have a 'null' option (e.g. asm select, page list select)
    // or checkboxes with nothing selected
    // so find them and add empty parameters to the data string, otherwise the page field will not be updated
    $($form.find('select, input').each(function(index){
        console.log('Select element no. ' + index + ' with name ' + $(this).attr("name") + ' has serialize = ' + $(this).serialize());
        encodedName = encodeURI($(this).attr("name"))
        if (data.search(encodedName) === -1) {
            data += ('&' + encodeURI($(this).attr("name")) + '=');
        }
    }));
    console.log("Submitted data: " + data);
    if (!method)
        method = 'get';
    if (!action)
        action = window.location.href;
    // If you want to fade the affected inputfields then assign the loading class to their wrappers with method wrapClass(loading)
    $(target).find('.loading').css({
        display: 'block',
        opacity: 0.2
    }).animate({
        opacity: 1
    }, 5000);
    // then send your request
    $.ajax(action, {
        type: method,  // type used, not method, for older versions of jquery
        data: data,
        // you can also add an error handler here if required, in case the server returns an error on the request
        success: function (data) {
            // Initial ajax just returns an array with message. Need to GET the form data.
            $.ajax(window.location.href, {
                type: 'GET', cache: false, success: function (data) {
                    // then just take the target, and replace it with the target div from the returned data
                    console.log("Returned data: " + data);
                    console.log("Updating html with: " + $(data).find(target).html());
                    $(target).html($(data).find(target).html());
                }
            });
        }
    });
});

 

  • Like 1
Link to comment
Share on other sites

Updated version here - works with more field types - just make sure you have at least 3.0.148 if you want to use it with autocomplete

/* Script to refresh a form content when an element gets changed
To work across all admin pages, this script needs to be loaded in admin.php
    – add the line $config->scripts->add($config->urls->templates . "scripts/form-update.js"); before the final require in templates/admin.php.
Typical use is to modify other elements based on a select drop-down change
The trigger element can have the following data attributes assigned to it (typically set these with $myInputfield->attr() in a module or hook):
* data-action="form-update" : Required to run the script.
* data-update-target="#myid1" : Required - the element to change. Note that this should not be the whole form, otherwise .find(target) will not find it.
* data-confirm="Some confirmation text": Optional - if you want to show a confirmation before the update, this holds the text to display. If absent, there will be no confirmation dialogue.
                If the user chooses ‘cancel’, the script will revert the change and terminate.
* data-alert="Some alert text": Optional – if you want to warn the user that the update cannot happen for some reason (the script will then revert the change and terminate).
* data-cache="#myid2" : Optional - if you want to cache the (changed) value, this element stores it.
* data-cache-prefix="Some prefix string" : Optional (requires data-cache) - a prefix to prepend the value stored in the cache
This currently works with the following trigger elements:
* select options
* select page (single and multiple)
* page list select (single and multiple)
* asm select
* page autocomplete (but note that data attributes must be set in the wrapper element - e.g.  $myInputfield->wrapAttr() )
     Note also that autocomplete only works fully with the latest master 3.0.148
* checkboxes (set attributes in wrapper as above)
* checkbox (set attributes in wrapper as above; also, if you need to set or get the value (0 or 1) you may need to use getParent() )
* toggle (but only with 0,1 formatting and 'select' input type; plus see the comments for checkbox above)
but not with:
* toggle other than as described above
* radio buttons

NOTE: If you are using this with other js scripts (e.g. in a module) that listen for events in the target, you must use event delegation
(e.g. $(document).on("change","#myid", function(){}); NOT $("#myid").onchange(function(){}); ) because #myid is dynamic if it is inside the target)
 */

$(document).on('change focusin', '[data-action="form-update"]', formUpdate); // need 'change' to catch normal select fields and 'mouseenter' for others

function formUpdate(event) {
    console.log("event type = " + event.type);
    value = inputVal(this);
    if (event.type != 'change') {   // if the input has not changed, just get the value now so that we can revert if necessary when it is changed
        console.log("Saving prev value " + value);
        $(this).data('prevVal', value);
        return;
    }
    console.log("Saving current value " + value);
    $(this).data('currVal', value);
    var prev = $(this).data('prevVal');
    var current = $(this).data('currVal');
    console.log("Prev value " + prev);
    console.log("New value " + current);

    // if trigger element has data-confirm attribute, confirm or revert and exit
    var confirmText = $(this).data('confirm');
    if (confirmText) {
        if (!confirm(confirmText)) {
            $(this).val(inputVal(this, prev));
            return;
        }
    }
    // if trigger element has data-alert attribute, show alert and exit
    var alertText = $(this).data('alert');
    if (alertText) {
        alert(alertText);
        $(this).val(inputVal(this, prev));
        return;
    }
    // cache the value before proceeding (if data-cache set)
    var cache = $(this).data('cache');
    var cachePrefix = ($(this).data('cache-prefix')) ? $(this).data('cache-prefix') : '';
    $(cache).val(cachePrefix + current);
    var $form = $(this).closest('form');
    var target = $(this).data('update-target');
    console.log("Target is " + target);
    var method = $form.attr('method');
    var action = $form.attr('action');
    var data = $form.serialize();
    var encodedName;
    // .serialize() will omit select elements that do not have a 'null' option (e.g. asm select, page list select)
    // or checkboxes with nothing selected
    // so find them and add empty parameters to the data string, otherwise the page field will not be updated
    $($form.find('select, input').each(function(index){
        console.log('Select element no. ' + index + ' with name ' + $(this).attr("name") + ' has serialize = ' + $(this).serialize());
        encodedName = encodeURI($(this).attr("name"));
        if (data.search(encodedName) === -1) {
            data += ('&' + encodeURI($(this).attr("name")) + '=');
        }
    }));
    console.log("Submitted data: " + data);
    if (!method)
        method = 'get';
    if (!action)
        action = window.location.href;
    // If you want to fade the affected inputfields then assign the loading class to their wrappers with method wrapClass(loading)
    $(target).find('.loading').css({
        display: 'block',
        opacity: 0.2
    }).animate({
        opacity: 1
    }, 5000);
    // then send your request
    $.ajax(action, {
        type: method,  // type used, not method, for older versions of jquery
        data: data,
        // you can also add an error handler here if required, in case the server returns an error on the request
        success: function (data) {
            // Initial ajax just returns an array with message. Need to GET the form data.
            $.ajax(window.location.href, {
                type: 'GET', cache: false, success: function (data) {
                    // then just take the target, and replace it with the target div from the returned data
                    $(target).html($(data).find(target).html());
                    console.log("Returned data: " + data);
                    console.log("Updating html with: " + $(data).find(target).html());
                }
            });
        }
    });
}

function inputVal(el, val=null) {
    var value = $(el).val();
    var inputfield = $(el);
    if ($(el).hasClass('InputfieldCheckbox')) {
        console.log("checkbox");
        inputfield = $(el).find("input[type='checkbox'], input[type='radio']").first();
        if (val === 1) {
            $(inputfield).attr('checked', 'checked');
        } else if (val === 0) {
            $(inputfield).removeAttr('checked');
        }
        value = ($(inputfield).attr('checked') === 'checked') ? 1 : 0;
    } else if ($(el).hasClass('InputfieldToggle')) {
        console.log("toggle");
        inputfield = $(el).find("option[selected='selected']").first();
        if (val === '1') {
            $(inputfield).attr('selected', 'selected');
        } else if (val === '0') {
            $(inputfield).removeAttr('selected');
        }
        value = ($(inputfield).attr('selected') === 'selected') ? '1' : '0';
    } else if ($(el).hasClass('InputfieldPage') && $(el).find(".InputfieldPageAutocompleteData")) {
        console.log("page autocomplete");
        inputfield = $(el).find(".InputfieldPageAutocompleteData").first();
        value = $(inputfield).val();
    } else {
        console.log("other selector type");
        if (val) {
            $(el).val(val);
        }
        value = $(el).val();
    }
    console.log("returning value = " + value);
    return value;
}

 

  • Like 4
Link to comment
Share on other sites

  • 4 weeks later...

@MarkE this looks interesting. Ajax loading for runtimeMarkup or page fields sounds awesome! 

I could not get it to work so far. This is the hook called from inside a module.

 $this->addHookAfter('InputfieldPage::getSelectablePage', function(HookEvent $event) {
    $InputfieldPage = $event->object;
    $InputfieldPage->attr('data-action', 'form-update');
    $InputfieldPage->attr('data-update-target', '.InputfieldRepeaterItem');
});

I ended up adding the attributes with javascript. 

$('.InputfieldPage').attr('data-action', 'form-update');
$('.InputfieldPage').attr('data-update-target', '.InputfieldRuntimeMarkup');

This prints to the console when I select a page from the select field. It also seems like the runtimeMarkup field is updating, In the console it shows that the old content is inserted inside runtimeMarkup, so I see a quick update and it returns the same content.

Ok it's working fine when I test it on a page! 
It's not working inside repeater matrix and probably regular repeater fields for me (testet also with only one field populated). 

Link to comment
Share on other sites

I would love to use this for a page builder module I am working on.
My test has a page select field (single) and a runtimeMarkup field assigned to a repeaper matrix type.

The console JS logs seem fine, the content inside runtimeMarkup flashes and gets replaced by the same content that was the initialised value, no matter what I select. Do you have any ideas, how I could adapt your code to work inside a repeater?

Link to comment
Share on other sites

11 hours ago, jploch said:

My test has a page select field (single) and a runtimeMarkup field assigned to a repeaper matrix type.

Do you mean that the select field is outside the repeater field and that the runtimeMarkup is inside it? So the markup will be identical for each repeated item?

Link to comment
Share on other sites

4 hours ago, MarkE said:

Do you mean that the select field is outside the repeater field and that the runtimeMarkup is inside it? So the markup will be identical for each repeated item?

Hey! Thanks for responding to this! The select field and the runtimeMarkup field are inside the same repeater item.
Of course this would not work with multiple repeater items, but if I get it to work with one item I think a can change the code to work with multiple select fields and use the repeater IDs to target each select and runtimeMarkup field.

Link to comment
Share on other sites

How are you setting the data attributes for the trigger select field? AFAIK this can't be done in the usual way: $inputfield->attr(). I think it would need to be in js. Mind you, I don't have repeater matrix (pro), just the plain repeater.

Link to comment
Share on other sites

13 hours ago, MarkE said:

How are you setting the data attributes for the trigger select field? AFAIK this can't be done in the usual way: $inputfield->attr(). I think it would need to be in js. Mind you, I don't have repeater matrix (pro), just the plain repeater.

My quick test was in JS like this (works outside repeater, but not inside, testet with one repeater item in regular repeater and repeater matrix)

$(document).ready(function () {
  $('.InputfieldPage').attr('data-action', 'form-update');
  $('.InputfieldPage').attr('data-update-target', '.InputfieldRuntimeMarkup');
});

 

Link to comment
Share on other sites

@jploch: Work in progress report

Firstly, if you set the attrs in $(document).ready then the repeater needs to be permanently open. Otherwise you need to detect the opening event and then set them.

Secondly, you need to set separate attrs for each repeater item.

Thirdly, I can't get AJAX working properly in PW to update from the changed repeater inputfields. So I have done a hack to use a hidden field outside the repeater to hold a cached value. Unfortunately, because this needs to serve more than one repeater item, it needs to be prefixed so that only the relevant repeater gets updated.

I'd be grateful for any improvements on this approach!!

The code I have (in my context, with 'comment' as the cache field) is :

In the js for the runtimeMarkup field (runtime_markup_note):

$(document).ready(function () {
    $("[id^='Inputfield_rentalAdjustmentPage_repeater']").each(function (index, element) {
        var id = $(element).attr("id");
        var target = id.replace('Inputfield_rentalAdjustmentPage_repeater', '#wrap_Inputfield_runtime_markup_note_repeater');
        var cachePrefix= id.replace('Inputfield_rentalAdjustmentPage_repeater', '');
        $(element).attr('data-action', 'form-update');
        $(element).attr('data-cache', '#Inputfield_comment');
        $(element).attr("data-cache-prefix", cachePrefix + ':');
        $(element).attr('data-update-target', target);
    });
});

In the php for the same field:

$pageId = $page->id;
$adjPageId = $page->rentalAdjustmentPage;
$adjParentTitle = $page->parent->title;
$bkgPage = wire()->pages->get("title=$adjParentTitle");
$adjTypeId = $bkgPage->comment;
if (strpos($adjTypeId, $pageId) == 0) {
    $adjTypeId = str_replace($pageId . ':', '', $adjTypeId);
}
$out = wire()->pages->get("id=$adjTypeId")->summary;
if ($out) {
    echo $out;
} else {
    echo wire()->pages->get("id=$adjPageId")->summary;
}

The php is especially clunky and it would be better to get the host page via the for-page url segment, I think.

  • Like 1
Link to comment
Share on other sites

@MarkE
Thanks for your help!

It's not clear to me where to put the js and php code you posted.
Should I put the php inside the template file thats getting rendered with the runtimeMarkup field or in the runtimeMarkup Module file (FieldtypeMarkupRender.module)? 

Link to comment
Share on other sites

Also, the solution to not being able to access the attrs in the repeater items is to turn off "Repeater dynamic loading (AJAX) in editor" on the details tab of the repeater field. Then everything works whether or not the repeater items are initially shown closed.

Link to comment
Share on other sites

1 hour ago, MarkE said:

Also, the solution to not being able to access the attrs in the repeater items is to turn off "Repeater dynamic loading (AJAX) in editor" on the details tab of the repeater field. Then everything works whether or not the repeater items are initially shown closed.

A already disabled ajax loading for the repeater.

Than I added your php code to the php file that gets rendered with RuntimeMarkup (block_page.php) and added the js file (block_page.js). Both are loading fine (I testet with console log). But its still not working. I get a error with tracy:

PHP Deprecated: strpos(): Non-string needles will be interpreted as strings in the future. Use an explicit chr() call to preserve the current behavior in .../site/templates/fields/matrixgrid/block_page.php:8

Here is my complete code inside block_page.php (not sure what rentalAdjustmentPage is refering to):

<?php
namespace ProcessWire;
$pageId = $page->id;
$adjPageId = $page->rentalAdjustmentPage;
$adjParentTitle = $page->parent->title;
$bkgPage = wire()->pages->get("title=$adjParentTitle");
$adjTypeId = $bkgPage->comment;
if (strpos($adjTypeId, $pageId) == 0) {
    $adjTypeId = str_replace($pageId . ':', '', $adjTypeId);
}
$out = wire()->pages->get("id=$adjTypeId")->summary;
if ($out) {
    echo $out;
} else {
    echo wire()->pages->get("id=$adjPageId")->summary;
}
?>

<a href="<?= $page->block_page->url ?>" class="flex-container overlay-parent">
  <img data-src="<?php if($page->block_page->thumbnail) {echo $page->block_page->thumbnail->first()->url();}?>" data-sizes="auto" class="lazyload overlay-bg" />
  <h3 class="overlay absolute">
    <?= $page->block_page->headline ?>
  </h3>
</a>

JS in block_page.js

$(document).ready(function () {

  console.log("Loading is fine")

  $("[id^='Inputfield_rentalAdjustmentPage_repeater']").each(function (index, element) {
    var id = $(element).attr("id");
    var target = id.replace('Inputfield_rentalAdjustmentPage_repeater', '#wrap_Inputfield_block_page_render_repeater');
    var cachePrefix = id.replace('Inputfield_rentalAdjustmentPage_repeater', '');
    $(element).attr('data-action', 'form-update');
    $(element).attr('data-cache', '#Inputfield_comment');
    $(element).attr("data-cache-prefix", cachePrefix + ':');
    $(element).attr('data-update-target', target);
  });
});


Should the selector be like #wrap_Inputfield_block_page_render_repeater or #wrap_Inputfield_block_page_render_repeater3076 with the actual repeater id? Both don't seem to wok for me:

var target = id.replace('Inputfield_rentalAdjustmentPage_repeater', '#wrap_Inputfield_block_page_render_repeater');


Not sure where that comment field from you example is living:

The code I have (in my context, with 'comment' as the cache field) is :

 

Link to comment
Share on other sites

As I said, that code is in my context - i.e. with my field names. You will need to change those to suit your context. The field 'comment' is on the host page (outside the repeater) - just a plain text field for the cache. rentalAdjustmentPage is my trigger field inside the repeater (a page select field).

  • Like 1
Link to comment
Share on other sites

ok thanks for clarification and sorry for my slow following here!  I inserted my field names and now getting a result inside the comment field like "3076:1028", the first ID is from the repeater item, the second is for the requested page? Still the same content gets returned. This is how my select field looks in PW wich the above changes:

<select id="Inputfield_block_page_repeater3076" class="uk-select" name="block_page_repeater3076" data-action="form-update" data-cache="#Inputfield_comment" data-cache-prefix="3076:" data-update-target="#wrap_Inputfield_block_page_render_repeater3076"><option value=""> </option><option value="1269">GIGASET</option><option value="1220">JESSICA VON BREDOW</option><option value="1167">NINA HEMMER</option><option value="1230">STEFAN KNOPF</option></select>

 

Link to comment
Share on other sites

the only error I get is this with tracy:

HP Deprecated: strpos(): Non-string needles will be interpreted as strings in the future. Use an explicit chr() call to preserve the current behavior

 

Link to comment
Share on other sites

Hmm - I don't get that error. What arguments are being supplied?

BTW, a better way of finding the host page:

$bkgPageId = str_replace('for-page-', '', $page->parent->name);
$bkgPage = wire()->pages->get("id=$bkgPageId");

 

  • Like 1
Link to comment
Share on other sites

Per PHP manual

If needle is not a string, it is converted to an integer and applied as the ordinal value of a character. This behavior is deprecated as of PHP 7.3.0, and relying on it is highly discouraged. Depending on the intended behavior, the needle should either be explicitly cast to string, or an explicit call to chr() should be performed.

Link to comment
Share on other sites

@MarkE 
Its working now! Thanks for your help! It finally worked with the first code you postet. Before I used the "Updated version here - works with more field types" code example. The first code works perfectly with my page select field and your adjustments. I will send you a copy of my module once its ready, it adds drag and drop, resize support for repeater and repeater matrix fields and uses this code to ajax update page reference blocks inside repeaters and renders them with RuntimeMarkup ? . Maybe I find a way to improve this approach. It would be cool if this could be a feature of the RuntimeMarkup Module someday. I still need to learn more about PHP and PW, I am ok with javascript and frontend stuff..

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