Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inconsistent update with template x-if #2803

Closed
cjloong opened this issue Mar 31, 2022 Discussed in #2802 · 9 comments
Closed

Inconsistent update with template x-if #2803

cjloong opened this issue Mar 31, 2022 Discussed in #2802 · 9 comments

Comments

@cjloong
Copy link

cjloong commented Mar 31, 2022

Discussed in #2802

Originally posted by cjloong April 1, 2022
Hi there,
I have a page structured as follows:
section1 (x-if="bar")

  • input x-model="foo"

section2

  • part 1 (x-if="!bar")
  • part 2 (appears in section 1 and 2)
  • part 3 (x-if="!bar")

The code to control the above is quite complicated. We discovered an intermittent issue where section 2, part 1 sometimes does not appear. Same as part 3.

I have manage to condense the issue into a codepen as follows:
https://codepen.io/cjloong/pen/zYpzQGr

Click on the "Simulation Error" button to reproduce the error.

@cjloong
Copy link
Author

cjloong commented Apr 12, 2022

Hi, anyone have any idea on this. Its reproducible.

@Foxbud
Copy link

Foxbud commented May 14, 2022

I believe I'm experiencing a related issue with x-show. It almost seems like x-show breaks when its dependent value toggles too quickly.

In this self-contained example, clicking Enable under the Unexpected Behavior header results in both the Enable and Disable buttons being displayed despite them having mutually exclusive display conditions. Then the Expected Behavior header demonstrates how adding a slight delay seems to fix the issue.

<html>
	<head>
		<script src="https://cdnjs.cloudflare.com/ajax/libs/alpinejs/3.10.2/cdn.min.js" defer></script>
	</head>
	<body>
		<h1>Unexpected Behavior</h1>
		<div x-data="{state: false}"
			 x-init="$watch('state', val => {
						if (val) {state = false}
					})">
			<button type="button"
					x-show="state"
					@click="state = false">
				Disable
			</button>
			<button type="button"
					x-show="!state"
					@click="state = true">
				Enable
			</button>
		</div>
		
		<h1>Expected Behavior</h1>
		<div x-data="{state: false}"
			 x-init="$watch('state', val => {
						setTimeout(() => {if (val) {state = false}}, 100)
					})">
			<button type="button"
					x-show="state"
					@click="state = false">
				Disable
			</button>
			<button type="button"
					x-show="!state"
					@click="state = true">
				Enable
			</button>
		</div>
	</body>
</html>

Codepen: https://codepen.io/foxbud/pen/xxYRjQv

@vlad-iliescu-tallarium
Copy link

I've had a similar issue with x-if, which I think is the cause for the behavior seen in the codepen. When x-if hides the elements under it, it also removes the effects of its children (ref), the issue is that this happens mid loop, while the job queue is being processed (loop in question).
On button toggle (this.foo = ""; this.bar = null;) the jobs in the queue should be 0: "Update input foo due to x-model" 1: "Hide Screen1 due to x-if", 2: "Show Screen2:PART1 due to x-if" 3: "Show Screen2:PART2 due to x-if".
What will happen here is that when we "Hide Screen1", we will remove item 0 from the queue, but the index stays the same in the loop. So after processing queue[1], i will become 2, but since the queue was mutated, it will jump directly to "Show Screen2:PART2".
In the coderpen example, changing the toggle loop to:

         if (this.toggle) {
            this.bar = null;
            this.foo = "";
          } else {
            this.foo = "some text for input field";
            this.bar = { hello: "world" };
          }

will solve the issue, as the queue will become 0: "Hide Screen1 due to x-if", 1: "Show Screen2:PART1 due to x-if" 2: "Show Screen2:PART2 due to x-if" 3: "Update input foo due to x-model" and removing the item from the end won't make a difference.

This probably requires a proper bugfix, though.

@mgschoen
Copy link
Contributor

Adding a real-world example here to illustrate practical implications of this bug. I'm building an audio player with Alpine.js. I'm using a store that represents playback state of an HTML5 audio element. The store listens to a couple of HTMLMediaElement events such as loadstart and playing to determine if the audio is currently loading, playing or paused. It looks something like this:

const audio = document.createElement('audio');

export const WebAudioStore = {
    isPlaying: false,
    isLoading: false,

    init() {
        audio.addEventListener('loadstart', () => this.onStateChanged());
        audio.addEventListener('playing', () => this.onStateChanged());
        ...
    },

    onStateChanged() {
        // set isPlaying and isLoading depending on audio element state
    }
}

The bug occurs when a user jumps to another position in the audio that is already buffered. In that case, the audio element fires loadstart, directly followed by playing, causing state to switch from isLoading=true to isPlaying=true within milliseconds. Due to the bug described above, the UI shows both loading and playing state afterwards.

To anyone interested, I solved the issue by adding a timeout. This cannot be a permanent solution though since it slows down responsivity of the UI. My solution looks something like this:

let isStateUpdatePrevented = false;
let isStateUpdatePending = false;

export const WebAudioStore = {
    ...
    onStateChanged() {
        if (isStateUpdatePrevented) {
            isStateUpdatePending = true;
            return;
        }

        // set isPlaying and isLoading depending on audio element state

        isStateUpdatePending = false;
        isStateUpdatePrevented = true;
        setTimeout(() => this.onStateUpdateTimeout(), 25);
    },

    onStateUpdateTimeout() {
        isStateUpdatePrevented = false;
        if (isStateUpdatePending) {
            this.onStateChanged();
        }
    }
}

@josh-tt
Copy link

josh-tt commented Aug 15, 2022

Has this issue also been addressed by the x-show fix?

@mgschoen
Copy link
Contributor

@josh-tt Unfortunately not, the x-if issue still exists :(

@josh-tt
Copy link

josh-tt commented Aug 16, 2022

Bummer, thanks. I've been on an older version of Alpine and just ran into issues with templates and stores. 3.8.1 working ok for me (some memory issues when fast clicking, but working ok), but anything above that is behaving inconsistently.

@dfallman
Copy link

We've had a similar problem with x-if, see #3003 creating a pretty wild memory leak

Our solution was to use $nextTick()

@bakman2
Copy link

bakman2 commented Oct 27, 2022

I encounter issues with template swapping and wonder if this is the same issue or am I just missing something with this:

<body x-data="{test:['hello','bye'], show:true}">
    <template x-for="e in test">

        <template x-if="show">
            <div x-text="e"></div>
        </template>

        <template x-if="!show">
            <div>show is false</div>
        </template>

    </template>
</body>

If I manually set show:false the second template is not shown.

arg - ignore, i had to wrap the templates in a div (note to self: template can only a single root node)

ferranconde pushed a commit to ferranconde/alpine that referenced this issue Nov 21, 2022
…b is never dequeued from the queue, which would cause other jobs to be accidentally skipped.
@alpinejs alpinejs locked and limited conversation to collaborators Nov 24, 2022
@joshhanley joshhanley converted this issue into discussion #3286 Nov 24, 2022

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants