BUG - MultiStateBox element `onChange` event handler executing twice

BUG - Wix editor MultiStateBox element triggering onChange event handler executing twice.

OVERVIEW
I have a multiStateBox element where I’m trying to conditionally navigate between states. The problem I’m experiencing is that the element’s onChange event handler is being triggered twice, and the targeted next state ID isn’t always equal to the one I set programmatically, but seemingly equal to the next adjacent state.

For example, if I have state IDs [1,2,3], starting at state 1 and setting my state to 3, my logs show the first transition as 13 (as desired), but then the second (undesired) transition as 3 → sometimes 2 or 3; either way, this second transition (i.e., onChange event) is a bug.

Sometimes I’ve noticed in my logs that despite all of my helper methods correctly listing state ID 3 is the next state, the onChange event handler shows state 2 as the receiving state. And the order of state transitions has been inconsistent, ranging from 13 then 32 in one playthrough, to 13 then 33 (as shown in the screenshot) in another playthrough.

CODE SNIPPET
In the following code snippet:

  • I have an onNextButtonClick event handler manually attached in the editor UI to the next button on each state; my logs show it only executing once (as desired).
  • I also have findNextPageId helper method that conditionally determines the next state from the current state; again my logs show it working correctly and only executing once.
  • Once more, I have a changePage helper method that triggers the state change; this too works correctly and only executes once.
  • But as you’ll see in the screenshot, the onChange event handler is being triggered twice.
const PAGE = {
  AGREEMENT: 'agreement',
  LOCATION: 'location',
  SERVICES: 'services',
  MOWING: 'mowing'
}

class StateManager {
  constructor() {
    log('New state manager.')
    this.currentPageId = PAGE.AGREEMENT
    this.pages = [
      { id: PAGE.AGREEMENT, active: true },
      { id: PAGE.LOCATION, active: false },
      { id: PAGE.SERVICES, active: true },
      { id: PAGE.MOWING, active: false }
    ]
    this.SERVICES = [
      {
        _id: PAGE.MOWING,
        label: 'Cutting My Grass',
        image: 'https://example.svg'
      }
    ]
  }

  changePage(newPageId) {
    log(`Changing page to ${newPageId}`)
    $w(`#statebox1`).changeState(newPageId)
  }

  findNextPageId() {
    log('findNextPageId:')
    const currentPageIndex = this.pages.findIndex(
      (page) => page.id === this.currentPageId
    )
    const nextPageIndex = this.pages.findIndex(
      (page, index) => page.active && index > currentPageIndex
    )
    const nextPageId =
      nextPageIndex !== -1 ? this.pages[nextPageIndex].id : null
    log({ nextPageIndex, nextPageId })
    return nextPageId
  }
}
const stateManager = new StateManager()

export function onNextButtonClick(_) {
  log('onNextButtonClick:')
  let nextPageId = stateManager.findNextPageId()
  if (!nextPageId) {
    throw new Error(`Next page not found from: ${stateManager.currentPageId}`)
  }
  stateManager.changePage(nextPageId)
}

$w('#statebox1').onChange((event) => {
  const previousPageId = stateManager.currentPageId
  const newPageId = event.target.currentState.id
  log({
    msg: `State changing from ${previousPageId} to ${newPageId}`,
    event,
    states: event.target.states,
    currentState: event.target.currentState
  })
  stateManager.currentPageId = newPageId
})

Note: Whether I attach the onChange event handler through the UI or programmatically, the bug remains.

SCREENSHOT:
In the following screenshot, you can see nothing is executing more than once, except the onChange event handler.

CONCLUSION
Please help me understand and mitigate the onChange event handler from executing twice.

Some things you can try:

  1. Make changePage() async and await it when it’s called. It could be that state is changing quickly and it’s being called a second time before the first changeState() resolves.
  2. Simplify the logic for findNextPageId as it’s a bit hard to reason about because StateManager is tracking the state separate from the multistatebox. Deriving the next state/page from the multistatebox would look like the below. If you need to skip certain pages then implement the logic using findIndex against the MultiStateBox.states property.
		let currentState = $w('#PreloaderStateBox').currentState;
		let nextIndex = $w('#PreloaderStateBox').states.findIndex((e) => e === currentState) + 1;
		let nextState = nextIndex < $w('#PreloaderStateBox').states.length ? $w('#PreloaderStateBox').states[nextIndex] : $w('#PreloaderStateBox').states[0];
		
		console.log("changing state to", nextState);
		await $w('#PreloaderStateBox').changeState(nextState);

I put together an example site here as well and seem unable to get onChange to fire more than once for each changeState https://anthonyl5.wixsite.com/my-site-129

Hi @anthony, thank you for the response!

Upon further investigation, it appears that the built-in form [next] button is executing its own default onClick() handler in addition to the event handler attached (either statically or programmatically).

I’ve proven this by creating a custom button with the same event handler attached as the built-in [next] button. Running it multiple times reveals that the built-in [next] button triggers onChange twice (once for its default handler, and again for my attached handler). While my custom button only logs one instance of onChange.

This would explain the race-case and why my state changes twice (in random order) using the built-in [next] button. I’ve illustrated this below.

I’ve tried various ways of removing the default onClick handler on the built-in [next] button, but to no prevail. As a workaround, I’ll simply hide all of the built-in navigation buttons (both [next] and [back]), and use custom buttons to control navigation.

I hope the Wix team recognizes this as a bug.

1 Like

Thanks for sharing this and figuring out the root cause. I’ll look at this further and escalate.

Can you confirm which editor you’re using that has the prebuilt form and which prebuilt form with a next button that it is?

It’s the regular editor (not Editor X). And it’s any of the multistate forms (the ones with a hover preview), like the green one in this screenshot.

image

I found it way easier to use a black multistate box element instead of the form (as shown here) as it also allows for changing state IDs in the editor instead of programmatically.

Are you hiring? I’ll fix this bug for you :wink:

Do you have a self contained example demonstrating this?

I seem to be unable to reproduce. For example here’s a site which has a simple onClick handler on the submit button of that green form:

$w.onReady(function () {
	$w('#button1').onClick(console.log);
});

Clicking submit results in a single call to the onClick handler and no other output:
ezgif.com-gif-maker-1

From your response, I think you misunderstand the issue. Refer back to my flow diagram.

In fact, I believe your demo perfectly demonstrates the bug. In your code example, shown below, you’re overwriting the onClick event handler on the submit button, and yet clicking it STILL changes the state to the “Thank You” page. See? What if you didn’t want to change the state immediately after clicking the button, or if you wanted to traverse states non-linearly?

Referring back to my flow diagram, the built-in [next] button on the form will always execute its default event handler in parallel with whatever additional event handler you attach to the button. In your case, you’ve attached a log, and the built-in one event handler navigates to the second state.

$w.onReady(function () {
	$w('#button1').onClick(console.log);
});

I see what you mean. While you’ve alread found a workaround that works for you another option for anyone reading would be to change the Submit buttons settings so a click on it goes to a link, and that link is set as “None” as in the screenshots below.


However I do agree this is missing functionality. We should be able to set the “After Submit” on this button to “Do nothing” on the first screen. If you’d like you can open a feature request for this improvement here and that would make sure you’re notified if it gets implemented: Product Roadmap