Jump to content

Change Field value based on a checkbox in front-end


Val_0x42
 Share

Recommended Posts

Hi everyone, I hope this hasn't been covered anywere else but I tried to search for this topic and I couldn't seem to be able to find anything.

I'm fairly new to Processwire and I'm liking it a lot but I don't understand what could be the optimal way to do a little project I'm working on, in particular how to store user-submitted data.

Basically I'm trying to build a checklist website where users can flag a list of items as "collected" so that they can keep track of what they have so far. I'm wondering what could be the best way of storing this user-dependant data. For now I've organized the site as follow:

  • Every item is a page with specific fields (name, description ecc...)
  • Every user has a custom field that's a list of items checked, and for this I used a page reference field
  • On page load the site should check which items result as collected and tick the corrisponding checkbox 
  • The list of items is presented as a table where each row starts with a checkbox, this is not a checkbox field since if I would mark this as "checked" this would be associated with the item and not with the user resulting in the item being checked for all of them (correct me if I'm wrong).

I'm struggling on how I can collect the data from the frontend (which checkoxes are marked) and edit the user field so that I can store them and retrieve it later on page load (depending on the account who is currently watching the checklist). I was thinking of putting a javascript listener on the checkboxes who calls a php function to set the corrisponding user value but this seems a little convoluted and I'm wondering if there could be a better way to handle this, something I'm not seeing right now.

If you wanna take a look you can find the checklist here: https://valuvato.it/spells/sorceries/ (yes it's a collection of Elden Ring stuff so one can mark what he has already looted and what's missing in his gameplay).

Any idea would be appreciated, even if it changes the architecture of the project, I'm using this to learn Processwire after all!

Thanks in advance ?

  • Like 1
Link to comment
Share on other sites

Maybe this scenario can be starting point?

Create a parent page that holds all items, each as a page.
Create a parent page for visitors (users, but maybe not PW user?), that holds every visitor as a page.
Create a page reference field for the items.
Add this page reference field to the visitor template.

Show the visitor a page with a normal HTML form, showing a regular checkbox for each item. On form submission, check which ones were selected and for each of that, add its ID to the page reference field of the current visitor.

On a second visit of that visitor, you can present him/her the form with all collected items preselected (checked checkboxes). SO he/she is able to add and remove items to / from the collection. On form submission simply clear the old state of the reference field and build it up new.

That's a raw starting point that has potential for additions and modifications. But for me it looks uncomplicated and straight forward for what you try to achieve, if I have understand it correct. ?

 

  • Like 2
Link to comment
Share on other sites

Hey horst, thanks for the reply!

Ok I'm happy to see that the solution is pretty much what I thought except for the form part (I was using simple checkboxes). I'll try to manage checkboxes as a form and see how the whole submission mechanic works!

I'd also like to make it so it would update "live" and the users wouldn't be forced to submit the checks (by save button I mean) before changing page and risk losing their data. For now I'll start simpler and build up from the easiest working solution.

May I ask if there's a specific reason why you suggested to separate processwire users from the visitors? I thought that having them as standard users would have made it simpler to check who was logged in and get their selections.

Also I'm wondering if there's maybe a way to trigger form submission on page change so that the user experience would flow more easily. In the meantime I'll start searching on how to code the solutions you suggested, thanks!

  • Like 1
Link to comment
Share on other sites

Hello @Val_0x42,

16 hours ago, Val_0x42 said:

I'd also like to make it so it would update "live" and the users wouldn't be forced to submit the checks (by save button I mean) before changing page and risk losing their data. For now I'll start simpler and build up from the easiest working solution.

You could make an AJAX call on your page at the change of a checkbox and save the user data then instead of the form submission.

 

16 hours ago, Val_0x42 said:

May I ask if there's a specific reason why you suggested to separate processwire users from the visitors? I thought that having them as standard users would have made it simpler to check who was logged in and get their selections.

I would also use regular ProcessWire users to get the benefits of access control.

Regards, Andreas

  • Like 1
Link to comment
Share on other sites

1 hour ago, AndZyk said:

You could make an AJAX call on your page at the change of a checkbox and save the user data then instead of the form submission.

That's very interesting! Thanks for the resource link, I'll definetly check it out and let you know!

  • Like 2
Link to comment
Share on other sites

Hi @Val_0x42,

On 3/23/2022 at 4:42 PM, Val_0x42 said:

May I ask if there's a specific reason why you suggested to separate processwire users from the visitors? I thought that having them as standard users would have made it simpler to check who was logged in and get their selections.

Yep, please ignore my suggestion or question. It is totally fine to use PWs user, maybe a role with only guest rights, but able to register and authenticate. With guest rights, they can see nothing in the backend.

To the other points: I like to code old schooled. ? First send a complete form with checkboxes and submit button, so that it also will work without JS, or, more interesting today, when any early JS throws an error and it stops further execution. Then also a "modern page" maybe left unusable when the bound events cannot run, or even got not bound to the checkboxes, etc..

The next step for me would be to disable the submit button on DOMContentLoaded, collect all checkboxes and add a change event to them, for example: (code not tested, written in the browser)

...

<form id='myForm'>
  <label for='i1234'><input id='i1234' type='checkbox' value='1234' /> YOUR CONTENT HERE </label>
  <label for='i1235'><input id='i1235' type='checkbox' value='1235' /> YOUR CONTENT HERE </label>
  <label for='i1236'><input id='i1236' type='checkbox' value='1236' /> YOUR CONTENT HERE </label>
  <label for='i1237'><input id='i1237' type='checkbox' value='1237' /> YOUR CONTENT HERE </label>
  <label for='i1238'><input id='i1238' type='checkbox' value='1238' /> YOUR CONTENT HERE </label>
  <input type="submit" name="submit" id="submit" value="SEND" />
</form>

<script type='text/javascript'>
  document.addEventListener('DOMContentLoaded', function(event) {
	const myForm = document.getElementById('myForm');
    myForm.getElementById('submit').style.display = 'none'; // or, for better A11y support, visibility = hidden, plus shrinking the sizes, or move it out of viewport
    const checkboxes = myForm.querySelectorAll('input[type="checkbox"]');
    for(i = 0; i < checkboxes.length; i++) {
      // add on change event listener for each checkbox in the form:
      checkboxes[i].addEventListener('change', function(event) {
        // get the ID and the status (checked|unchecked) and send via AJAX to update the server. Done! :-)
      });
    }
  });
</script>
</body>
</html>

Hope that helps to get started. ?

 

  • Like 4
Link to comment
Share on other sites

On 3/24/2022 at 7:37 PM, horst said:

Hi @Val_0x42,

Yep, please ignore my suggestion or question. It is totally fine to use PWs user, maybe a role with only guest rights, but able to register and authenticate. With guest rights, they can see nothing in the backend.

To the other points: I like to code old schooled. ? First send a complete form with checkboxes and submit button, so that it also will work without JS, or, more interesting today, when any early JS throws an error and it stops further execution. Then also a "modern page" maybe left unusable when the bound events cannot run, or even got not bound to the checkboxes, etc..

The next step for me would be to disable the submit button on DOMContentLoaded, collect all checkboxes and add a change event to them, for example: (code not tested, written in the browser)

...

<form id='myForm'>
  <label for='i1234'><input id='i1234' type='checkbox' value='1234' /> YOUR CONTENT HERE </label>
  <label for='i1235'><input id='i1235' type='checkbox' value='1235' /> YOUR CONTENT HERE </label>
  <label for='i1236'><input id='i1236' type='checkbox' value='1236' /> YOUR CONTENT HERE </label>
  <label for='i1237'><input id='i1237' type='checkbox' value='1237' /> YOUR CONTENT HERE </label>
  <label for='i1238'><input id='i1238' type='checkbox' value='1238' /> YOUR CONTENT HERE </label>
  <input type="submit" name="submit" id="submit" value="SEND" />
</form>

<script type='text/javascript'>
  document.addEventListener('DOMContentLoaded', function(event) {
	const myForm = document.getElementById('myForm');
    myForm.getElementById('submit').style.display = 'none'; // or, for better A11y support, visibility = hidden, plus shrinking the sizes, or move it out of viewport
    const checkboxes = myForm.querySelectorAll('input[type="checkbox"]');
    for(i = 0; i < checkboxes.length; i++) {
      // add on change event listener for each checkbox in the form:
      checkboxes[i].addEventListener('change', function(event) {
        // get the ID and the status (checked|unchecked) and send via AJAX to update the server. Done! :-)
      });
    }
  });
</script>
</body>
</html>

Hope that helps to get started. ?

 

Very nice! That's probably what I'll try! I've literally never used AJAX so I'll have to see how to send the info to the server but I like this implementation!

I'll try to code this first and then if I manage to get it working I was thinking that the next step could be making the AJAX call only before leaving/updating/changing the page (if that's possible), maybe storing checkbox statuses in an array and then updating all of them at once so that every user will only make a call after they've finished editing and not every time they check/uncheck.

I'm only familiar with  "simple" JS listeners and I don't know how difficult it could be to implement this tho.

 

  • Like 1
Link to comment
Share on other sites

A little update: the project is going pretty well but I'm stuck on the final step of the precedure (updating the values).

I implemented the AJAX call as suggested in this other post 

And everything works fine except I can't pass the variables i need to the php code that update fields. I think there's something wrong with how I try to collect them.

To give anyone that may be interested a little context here's the JS with the AJAX call:
 

    document.addEventListener('DOMContentLoaded', function(event) {
        const checkboxes = document.getElementsByClassName("valucheckbox");
        for (i=0; i<checkboxes.length; i++){
            checkboxes[i].addEventListener('change', function(event){
                var ckVal = this.value;
                var ckStatus = this.checked;
             
                $.ajax({
                    type: "GET",
                    url: "https://valuvato.it/ajax",
                    data: {
                        pageid: ckVal,
                        status: ckStatus
                    },
                    success: function(){
                        console.log(this.url);
                    }
                })
            });
        }

As you can see I made it return the URL so that I could check the variables that are passed and it seems everything's fine, as the URL shows as follows:

https://valuvato.it/ajax?pageid=1092&status=true

And this is the php code that is executed by the call:
 

<?php
include("./index.php");

//$page_id = (int) wire('input')->get->pageid;
//$status = (text) wire('input')->get->status;

$page_id = $input->get('pageid', 'int');
$status = $input->get('status', 'text');

//$page_id = $_GET['pageid', 'int'];
//$status = $_POST['status', 'text'];

$p = wire('pages')->get($page_id);

$user->of(false);
//$user->sorceries_checked->add(1093);

if($status == 'true') {
    $user->sorceries_checked->add($p);
} else {
    $user->sorceries_checked->remove($p);
}


$user->save();

?>

As you can see I left some stuff I tried to fetch the variables as a comment.
Without the variables the code is working properly, for instance the line $user->sorceries_checked->add(1093) works, but the remaining code never executes because $status and $page_id never get initialized. 
I'm pretty sure I'm failing to retrieve them properly, as I've tried to do the sorceries_checked->add($p) without the IF part and still doesn't work.

I don't know if this can be related to the fact that I'm not properly calling the .php file but a page that has that .php file as a templeate, as was suggested in the post I linked before.

Maybe there's a simpler way to do this and I've missed it.

Any thoughts? 

As usual thanks for the help ^^
 

Link to comment
Share on other sites

Just to be on the safe sife I would try to get those input variables like this:

// On the php file whom handles your inputs (REMEMBER, IT HAS TO BE PLACED OUTISDE THE "template" folder)

$page_id = $input->get->int->pageid;
$status = $input->get->text->status;

 

Moreover, as a test I'd try to use a relative path for your ajax call:

$.ajax({
  type: "GET",
  url: "./../ajax/file.php", // or wherever your file is placed/called
    data: {
    pageid: ckVal,
    status: ckStatus
    },
  success: function(){
  console.log(this.url);
}
                

Let us know ?

Link to comment
Share on other sites

I got a lot of questions when reading the above last two posts.

A) Why using a php file OUTSIDE of /site/templates ?

B) Why using a file that needs to include PWs index.php?

C) Why using RELATIVE URLs? (Sounds the worst decision to me)

D) Then, why using jQuery? Is it used for other things on the site, or was it coming into play ONLY for the ajax action?

 

A & B) The only reason why to use a external php file, should/would be be to process something OUTSIDE from PW. But when I need PWs internal processing, why leaving it (through the front door) and directly enter it again (through the backdoor)?

I would create a template that is called, for example, "ajax", that has the family restrictions:

  • only parent is home
  • cannot have children
  • can be used for ONE page
  • the page can get the URL: /ajax/

The template file, that processes all ajax calls, is called ajax.php then, and is located under /site/templates/ajax.php.

Also, when bootstraping PW, you need to declare its namespace at first: 

<?php namespace ProcessWire;
include( PWs index.php );

Without namespace, all following PHP code cannot work properly!  <-- HINT!

 

C) The code ./ is the placeholder for the current URL a page lives under. The URL "./../somewhere/", when called on a page like https://example.com/subpage/ points to https://example.com/somewhere/.

When I call it on the homepage, it points ABOVE the homepage, what is not possible. When I call it from https://example.com/subpage/subsubpage/ it points to https://example.com/subpage/somewhere/. This looks worse to me.

Also, when reading in the code while developing, a URL ./../ doesn't tell me anything about its location, whereas a absolute URL like /site/templates/images/icon.svg does! This delivers more context that can be helpful in many cases.

 

D) @Val_0x42 Do you already use jQuery on your site for other things then ajax? If not, you should use the modern Fetch Web-API that is native available in all modern browsers, what seems to be your target group. (Also, if you want or need to support old browsers like IE11, there are polyfills available for that too ?).

My suggestion not to use jQuery ajax but the Fetch API is, because you don't know no one of them yet and need to learn one. So, why learning an old thing, that comes with the disadvantage of an additional 150k+ JS lib download, when all the things are already available in the browser engines? Fetch API is future proof!

A very detailed fetch request can look like this: 

Spoiler
        const ckVal = this.value;
        const ckStatus = this.checked;

		// FORM DATA
        const myBody = new FormData();
        myBody.append('ckVal', ckVal);
        myBody.append('ckStatus', ckStatus);
        // STRING DATA
        const myParams = new URLSearchParams(myBody);

        // REQUEST HEADERS
        const myHeaders = new Headers();
        myHeaders.append('Content-Type', 'text/html; charset=UTF-8');
        myHeaders.append('X-Requested-With', 'XMLHttpRequest');
        //myHeaders.append('X-Custom-Header', 'ProcessThisImmediately');

        // BUILD THE REQUEST
        const myRequest = new Request('/ajax/' + '?' + myParams, {
            method: 'GET',
            headers: myHeaders
        });

        // SEND THE REQUEST
        fetch(myRequest)
        .then(function(){
          // OPTIONALLY do something on success
        })
		.catch(function(){
          // OPTIONALLY do something on error
        });

 

  • Like 1
Link to comment
Share on other sites

Awesome, this worked like a charm! I'm posting the code in case someone else needs it. Thank you very much @horst for taking the time to write the detailed solution (and for letting me know about the Fetch Web-API). I realize that that I'm diving head-first in some unexplored waters (AJAX and stuff) and I'll try to read some more documentation so that I won't try again an outdated solution!

This is the ajax.php file:

Quote
<?php namespace ProcessWire;
include( './index.php' );

$page_id = $input->get('pageid', 'int');
$status = $input->get('status', 'text');

$p = wire('pages')->get($page_id);

$user->of(false);

if($status == 'true') {
    $user->sorceries_checked->add($p);
} else {
    $user->sorceries_checked->remove($p);
}


$user->save();

?>

 

 

While this is the JS file with the solution provided by @horst with some slight edits (variable names):
 

Quote

    document.addEventListener('DOMContentLoaded', function(event) {
        const checkboxes = document.getElementsByClassName("valucheckbox");
        for (i=0; i<checkboxes.length; i++){
            checkboxes[i].addEventListener('change', function(event){
                console.log("Chekbox click, value: ", this.value);
                const ckVal = this.value;
                console.log("checked status: ", this.checked);
                const ckStatus = this.checked;

                // FORM DATA
                const myBody = new FormData();
                myBody.append('pageid', ckVal);
                myBody.append('status', ckStatus);
                // STRING DATA
                const myParams = new URLSearchParams(myBody);

                // REQUEST HEADERS
                const myHeaders = new Headers();
                myHeaders.append('Content-Type', 'text/html; charset=UTF-8');
                myHeaders.append('X-Requested-With', 'XMLHttpRequest');

                // BUILD THE REQUEST
                const myRequest = new Request('/ajax/' + '?' + myParams, {
                    method: 'GET',
                    headers: myHeaders
                });

                // SEND THE REQUEST
                fetch(myRequest)
                .then(function(){
                    console.log('request success');
                })
                .catch(function(){
                    console.log('request failed');
                });
                        
            });
        }

                
    });
 

 

 

  • Like 1
Link to comment
Share on other sites

@Val_0x42 Nice that it is working for you til that point. But your ajax processing lacks a bit of security. Actually you pass any submitted ID to get a page and add it to the page reference field.

Best practice is to verify that it is a page with a correct template and parent page, etc, before you add it to the page reference field. Something like:

<?php namespace ProcessWire;
include( './index.php' );

$page_id = $input->get('pageid', 'int');
$status = $input->get('status', 'text');

$p = wire('pages')->get($page_id);

// now check if it is a real page or a NullPage
if(0 == $p->id) {
    return;  // maybe early give up on none existing page?
} else {
    // check for the right template
    if('your-sorceries-template-name' == $p->template->name) {
        // a check if it is located under the right parent page? If necessary then
        if('your-sorceries-parent-template-name' == $p->parent()->template->name) {  // or check against a hard coded page ID of the parent page ?
            $method = $status == 'true' ? 'add' : 'remove';
            $user->sorceries_checked->$method($p);
        }
    }
}

 

  • Like 3
Link to comment
Share on other sites

Ah! Right! I'll do some safety checks right away as you suggested, I'm also restructuring the site a little so my files will change, but this will still be the core method I'll use to update my fields.
Thanks for the answer!

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