You are viewing limited content. For full access, please sign in.

Question

Question

Clickable custom button to show/hide fields in Modern Forms designer

asked on August 1, 2023 Show version history

I'm trying to create a clickable custom button in Laserfiche Cloud, using the Modern Forms Designer to show/hide fields or activate field rules to show/hide - rather than using "when xxxxx field changes..." (I have how to style it worked out). I created the button using an HTML field. What I want to do is rather than having a wrong answer INSTANTLY show that it is wrong, I would rather the user be able to click a button like this one to display a hint as to where to find the answer, before the form is even submitted.

Here is the Button I have:

and the Basic, Advanced Panels, and CSS for the Custom HTML button:


 

 

And this is what I need the button to do:
Activate the show/Hide rules just as clicking on a wrong answer does right now, with a link to the video to watch to find the answer:

RIGHT ANSWER:

WRONG ANSWER:

And here is the current field rule that activates showing the Red text and link if wrong:

Answer

APPROVED ANSWER
replied on September 19, 2023 Show version history

This is not an intended behavior within the Layout Designer and will be deprecated in cloud’s October 2023.10 release and self-hosted's Forms 11 Update 5 release. At that time, we will add a supported mechanism to perform your intended behavior. The below can be used in place of your current setup:

Original Custom HTML Field

<button onclick="document.getElementById('LFForm_sandbox').contentWindow.postMessage('click-start-timer-button', '*');">Click to Start the Test</button>

New Custom HTML Field

<button onclick="clickStartTimerButton()">Click to Start the Test</button>
  • Note this will call a function that is defined within the LFForm Sandbox.
  • All functions will be run in the LFForm Sandbox frame, so you have access to the LFForm object as well.
    • E.g., you may call LFForm.setFieldValues from custom HTML fields.

 

Old LFForm Sandbox Code

window.addEventListener("message", function (e) {
  //Verify message source is the parent page.
  if (e.origin != document.referrer.replace(/\/$/, "")) {
    return;
  } else {
    //Handle the specific messages.
    if (e.data.toString() == "click-start-timer-button") {
      LFForm.setFieldValues({ fieldId: timerFieldID }, timerLength);
      LFForm.setFieldValues({ fieldId: startedFlagID }, "Started");
      LFForm.changeFieldSettings(
        { fieldId: startButtonFieldID },
        { HTMLContent: "" }
      );
    }
  }
});

New LFForm Sandbox Code

window.clickStartTimerButton = function () {
  LFForm.setFieldValues({ fieldId: timerFieldID }, timerLength);
  LFForm.setFieldValues({ fieldId: startedFlagID }, "Started");
  LFForm.changeFieldSettings(
    { fieldId: startButtonFieldID },
    { HTMLContent: "" }
  );
};

Note:

  • You no longer need to listen to the “message” event on the window.
  • You only need to register the function on the window object within the sandbox and you can invoke anything defined there.
  • Static parameters can be passed as arguments to the functions being invoked. Dynamic parameters cannot be passed at this time

 

Ref #477261

replied on September 19, 2023

What about forms that have multiple custom buttons, how do we know what one was clicked? 

 

Replies

replied on August 1, 2023

Oh!  I just did something like this on a form I submitted to the Solution Marketplace (but it's still in the review phase). 

I set-up the button to send a message that the custom Javascript could listen for.  Then the custom Javascript can populate a value into a hidden field or something that triggers the lookup.

Give me a minute and I'll get some info posted.

replied on August 1, 2023 Show version history

EDIT:  Anyone reviewing this in the future should refer to the selected answer reply on this post by Zachary St. Louis.

 

Here's the code for the button, when clicked it will send a message that the main Javascript iFrame can listen for: 

<button onclick="document.getElementById('LFForm_sandbox').contentWindow.postMessage('click-start-timer-button', '*');">Click to Start the Test</button>

 

The actual message is where it says "click-start-timer-button" and you can list whatever you want there, just make sure you use the same message in the listener's code.

Note that the '*' is discouraged in instruction guides for the messaging functionality (except when testing) because of security concerns as it's not limiting the source/destination URL.  But in thise case, we really can't limit it because the built-in Javascript runs within a sandboxed iFrame, so you'll get cross-origin errors if you try to limit it.  I figured this was a minor issue for 3 reasons: 1) because I'm only coding the listener for that one specific message value, 2) it's checking at the time of the message event to compare the two page URLs (the form and the Javascript iFrame), and 3) the particular form in my case is only for internal use (it's not public).  So, I proceeding with it, but you should be aware of it at least.

Here's the Javascript code that is listening for that message, and making a couple changes to the form when the event is received and confirmed as coming from the same URL.  It's populating two fields (a timer field and a text field) which both have Field Rules set-up on them, and it's also clearing the button out of the Custom HTML field so it cannot be clicked again. 

//Set adjustable parameters for the form.
var timerLength = 75;         //Length is seconds given to complete the test.
var timerFieldID = 3;         //This is the q-id value for the (hidden) field that will track the remaining time.
var startedFlagID = 4;        //This is the q-id value for the (hidden) field that tracks whether or not the test has been started.
var startButtonFieldID = 2;   //This is the q-id value for the custom HTML element that contains the button to start the test.

//Listen for messages.  This allows this sandboxed
//iframe to receive messages from the form,
//so that custom buttons can be used to
//trigger this code.
window.addEventListener('message', function (e) {
  //Verify message source is the parent page.
  if(e.origin != document.referrer.replace(/\/$/, "")) {
    return;
  }
  else {
    //Handle the specific messages.
    if(e.data.toString() == 'click-start-timer-button') {
      LFForm.setFieldValues({fieldId: timerFieldID}, timerLength);
      LFForm.setFieldValues({fieldId: startedFlagID}, 'Started');
      LFForm.changeFieldSettings({fieldId: startButtonFieldID},{HTMLContent: ''});
    }
  }
});

 

The version that I submitted for the Solution Marketplace includes several other things, including a visual timer image that reduces over time, but I removed that from this code to simplify it.

replied on August 7, 2023

Thank You Matt - I tried implementing it as a first step in getting it to do what I want --- I figure once I get the button to put the word 'Ready' in the single line field, a field rule that tested for the word 'Ready' would fire and allow the user to proceed.

I'm missing something though, because right now I'm just trying to use the button to put the word 'Ready' into that single line field (field 130 - Hidden Review Status). When I click... rather than the word Ready showing up in the field... nothing happens. Any ideas?

 

replied on August 7, 2023

I'm not immediately seeing any issues.

Why don't you add some console.log lines to the Javascript to see if you can test which parts of the code are being called and which are not.

Something like this:

//Set adjustable parameters for the form.
var timerLength = 75;         //Length is seconds given to complete the test.
var timerFieldID = 3;         //This is the q-id value for the (hidden) field that will track the remaining time.
var startedFlagID = 130;        //This is the q-id value for the (hidden) field that tracks whether or not the test has been started.
var startButtonFieldID = 129;   //This is the q-id value for the custom HTML element that contains the button to start the test.

//Listen for messages.  This allows this sandboxed
//iframe to receive messages from the form,
//so that custom buttons can be used to
//trigger this code.
console.log('test 1');
window.addEventListener('message', function (e) {
  //Verify message source is the parent page.
  console.log('test 2');
  if(e.origin != document.referrer.replace(/\/$/, "")) {
    console.log('test 3');
    return;
  }
  else {
    //Handle the specific messages.
    console.log('test 4');
    if(e.data.toString() == 'click-start-timer-button') {
      console.log('test 5');
      LFForm.setFieldValues({fieldId: timerFieldID}, timerLength);
      LFForm.setFieldValues({fieldId: startedFlagID}, 'Ready');
      LFForm.changeFieldSettings({fieldId: startButtonFieldID},{HTMLContent: ''});
    }
  }
});

 

That should at least give us a feel for which parts of the code are running and which are not.  I would expect to see "test 1" when the form loads, and "test 2", "test 4", and "test 5" when the button is clicked.  If we see something different than that expectation, we can try to narrow down what's going wrong.

replied on August 8, 2023

This is what I got:

replied on August 8, 2023

Okay, so it's having an issue with this part: 

if(e.origin != document.referrer.replace(/\/$/, "")) {

 

Where it is attempting to confirm the button and the iFrame are on the same source.  This is just there to try to boost security.

If this is entirely an internal form, that won't be public - you would probably be okay removing that particular if/else statement, and just running the code that is currently inside the else part of the statement.

But if it is public, we want to confirm what the issue is.

Can you add these two lines before that if statement? 

console.log(e.origin);
console.log(document.referrer.replace(/\/$/, ""));

 

So we can see why that isn't working?

replied on August 9, 2023 Show version history

Here's what I got:

replied on August 9, 2023

Also, I dont know why it goes through the tests over and over - I only clicked the button twice.

replied on August 9, 2023 Show version history

In my environment, which is on prem, both of these return the same value: 

console.log(e.origin);
console.log(document.referrer.replace(/\/$/, ""));

 

But in your case, probably a difference with LFCloud, the first one is returning the URL and the second one is returning nothing.  So it's not catching that they match, and therefore isn't progressing into the "else" part of the code.

As I mentioned before, that if statement is trying to confirm the message from the button press and the listener in the iFrame are both on the same page - trying to prevent a malicious message from outside being injected.  It might be okay to remove that check - especially if this won't be available publically.  Even if it is public, you are still limiting it to just that one particular message via the remaining if statement, and the actions it is taking to populate the word ready into the form are pretty limited.  So that could probably be removed - but that's a decision you have to make.  If you are okay with that, you could comment out the other sections, and it would look something like this: 

//Set adjustable parameters for the form.
var timerLength = 75;         //Length is seconds given to complete the test.
var timerFieldID = 3;         //This is the q-id value for the (hidden) field that will track the remaining time.
var startedFlagID = 130;        //This is the q-id value for the (hidden) field that tracks whether or not the test has been started.
var startButtonFieldID = 129;   //This is the q-id value for the custom HTML element that contains the button to start the test.

//Listen for messages.  This allows this sandboxed
//iframe to receive messages from the form,
//so that custom buttons can be used to
//trigger this code.
console.log('test 1');
window.addEventListener('message', function (e) {
  //Verify message source is the parent page.
  //console.log('test 2');
  //console.log(e.origin);
  //console.log(document.referrer.replace(/\/$/, ""));
  //if(e.origin != document.referrer.replace(/\/$/, "")) {
    //console.log('test 3');
    //return;
  //}
  //else {
    //Handle the specific messages.
    console.log('test 4');
    if(e.data.toString() == 'click-start-timer-button') {
      console.log('test 5');
      LFForm.setFieldValues({fieldId: timerFieldID}, timerLength);
      LFForm.setFieldValues({fieldId: startedFlagID}, 'Ready');
      LFForm.changeFieldSettings({fieldId: startButtonFieldID},{HTMLContent: ''});
    }
  //}
});

 

Because the if block is commented out, and the start and end of the else block are commented out, the code that was previously not running within the else block, will just run automatically as the only code.  So you should see it say test 1 when the form loads, and then when the button is clicked, it should say test 4, test 5, populate the word Ready into that field, and hide the button.

Sadly, I have no idea why you were seeing it run those code blocks multiple times like that - it's like it is sending that message over and over again, or creating the listener multiple times, and it should not be doing either of those.  If it is still doing that after the code is changed (if you see it saying test 4 and test 5 over and over again), we might need to investigate that.  It is not a behavior I saw in my environment.  Though, in my environment, the else block ran and the button was consequently hidden, but in your environment that was not happening, so maybe that was the reason, and you'll no longer see that happen with this code change...

EDITED TO STRIKE OUT THAT LAST PARAGRAPH - SEE FOLLOW-UP REPLY FOR EXPLANATION.

replied on August 9, 2023 Show version history

EDITED TO STRIKE OUT THIS TEXT - SEE THE NEXT REPLY.

If you want to try to narrow down what is being triggered multiple times, the button click or the listener - you could try this...

On the button itself, where it starts with this:

<button onclick="document.getElementById('LFForm_sandbox')...

Insert a console.log command into the onclick event, which should post a message every time the button is clicked:

<button onclick="console.log('clicked!'); document.getElementById('LFForm_sandbox')...

 

Basically, if you only see that "clicked!" message once, but you see the "test..." messages too many times, we know that the click is only happening once, but the listener is happening lots of times.  But if you see that "clicked!" message a lot of times, we know the click event on the button is happening too many times.  That doesn't solve the issue, but at least helps us know what to research.

replied on August 9, 2023

Ugh - this should have been obvious to me in the beginning...

Forms is likely also using its own message events, so of course the listener is triggering more times, it's not just your button click, but all the other events happening.  I didn't dive as deep with the console.log tests as you did, so I didn't see it.  But I just tested now, and I see at least 5 other message events being triggered when I load the form, that have nothing to do with the button click.

That's not a problem, and that's why our code is not only triggering at the message event, but checking for the specific message ("click-start-timer-button").  The listener will trigger from any message, we only care about ours.

Sorry - I feel sheepish about that.

replied on August 9, 2023

Here's cleaned up version of the code from the earlier reply.

This doesn't have any of the console.log testing, and doesn't do the origin versus referred check.  It only verify that the message is the expected one and then proceeds if it is.  

//Set adjustable parameters for the form.
var timerLength = 75;         //Length is seconds given to complete the test.
var timerFieldID = 3;         //This is the q-id value for the (hidden) field that will track the remaining time.
var startedFlagID = 130;        //This is the q-id value for the (hidden) field that tracks whether or not the test has been started.
var startButtonFieldID = 129;   //This is the q-id value for the custom HTML element that contains the button to start the test.

//Listen for messages.  This allows this sandboxed
//iframe to receive messages from the form,
//so that custom buttons can be used to
//trigger this code.
window.addEventListener('message', function (e) {
  //Handle the specific messages.
  if(e.data.toString() == 'click-start-timer-button') {
    LFForm.setFieldValues({fieldId: timerFieldID}, timerLength);
    LFForm.setFieldValues({fieldId: startedFlagID}, 'Ready');
    LFForm.changeFieldSettings({fieldId: startButtonFieldID},{HTMLContent: ''});
  }
});

 

replied on August 9, 2023

Ok - Its not getting to writing test 4 to the console. Does it have something to do with the 'message' parameter? or if its firing a bunch of messages - maybe not getting the right message first?

replied on August 9, 2023

The curly bracket after the else block wasn't commented out.

replied on August 9, 2023

Doh!

replied on August 9, 2023

Ok, Im still getting that HTML error. I'm including the code for the button itself too, in case I may have made a mistake in there. I'm wondering if since writing e to the console doesnt work, somewhere either e is a null or what it's compared to is a null.

replied on August 9, 2023 Show version history

That looks like it's not finding the iFrame where the Javascript is running.  :-(

If you go into the Inspect Elements function of the browser on your form, we're looking for an iFrame element that looks something like this (this is the beginning and end of it, there's a lot of other stuff in the middle).

Notice at the end there, it includes the id of LFForm_sandbox?

What the button is doing is trying to send a message to that element, the iFrame with the LFForm_sandbox ID.  The error you showed in your last reply makes it look like it isn't finding that LFForm_sandbox element.

I'm wondering if this is different in LFCloud - either that iFrame is structured completely differently, or it has a different ID or something... sad

Unfortunately, I'm just speculating here - as I don't have access to a LFCloud environment to test/compare.

replied on August 9, 2023 Show version history

This is what's going on in the page. It does look like it's making that LFForm_sandbox ID. Though it is including html quotes:

replied on August 9, 2023

haha.

Looks like mine is LFForm_sandbox and yours is LFForm-sandbox.  Underscore versus hyphen.  I have no idea why that would be different, but whatever...

On the button in the custom HTML element, please try to change it from LFForm_sandbox to LFForm-sandbox and see if that makes a difference.

replied on August 10, 2023

Thanks Matt - I did that and it still comes up "null". I was looking up general javascript examples about how to access an iframe in an iframe.
I dont suppose there's a way to put a "setfield" in the HTML block as inline javascript? It seems to turn to gobbledy-gook if you put too much javascript in there. Or to call a function just to set the field from the button? 
Everything seems to hang on that listener looking inside the iframe.
I tried it this way - I mean... I knew there's no way it could be that simple, but I wanted to see if it could access the iframe from inside.

<!--<button onclick="document.getElementById('LFForm-sandbox').contentWindow.postMessage('click-check-answers-button', '*');">Click to Check Answers</button>-->
<button type="button" id="checkAnswersButton" onclick="LFForm.setFieldValues({fieldId: 130}, 'Ready');">Check Answers</button>

replied on August 10, 2023

Ugh - I didn't realize LFCloud was going to be so different than On Prem.

Unfortunately, you wouldn't be able to do that with the LFForm object in the button, as the LFForm object is only accessible and authenticated through the iFrame.  And because the iFrame is sandboxed, this messaging functionality is the only way (and I have spent a lot of time looking) that I have found to get the button in the Custom HTML element to impact anything within the iFrame.  sad

Other than the hyphen instead of the underscore - I don't see anything that would explain why it is not working for you.  The iFrame structure is pretty similar to what I see other than that slight difference with the ID, so it seems reasonable that it would be working, having adjusted for that ID difference.

Maybe try making a test button like this - it should at least help confirm that it is finding the iFrame properly.  If it returns null or undefined, we know it isn't, if it returns an object, we know it is finding the iFrame: 

<button onclick="console.log(document.getElementById('LFForm-sandbox'));">Click to Test the iFrame Object</button>

 

You are not allowed to follow up in this post.