Alpine.js x-data 不响应

问题描述 投票:0回答:1

我正在使用 Alpine.js 通过 API 渲染视频列表。进行 API 调用后,响应数据成功填充我的视频数组,但页面未更新以显示新数据。

以下是所发生事件的详细说明:

我正在发出 Axios API 请求并将生成的视频推送到视频数组。 我已通过控制台日志确认视频填充了正确的数据。 尽管如此,模板不会渲染更新后的数组。

谢谢!

<div x-data="app" class="flex flex-wrap justify-between">
     <template x-for="video in videos" :key="video.id">
         <!--video content-->
         <div>Test Video</div>
     </template>
</div>
  const app = {
        triggerElement: null,
        page: 1,
        lastPage: 24,
        itemsPerPage: 24,
        observer: null,
        isObserverPolyfilled: false,
        videos: [],
        debug: true,
        loaded: false,

        init: function () {

            window.addEventListener('urlChange', () => {
                app.getItems();
            });

            app.triggerElement = document.querySelector('#infinite-scroll-trigger');

            document.addEventListener('DOMContentLoaded', function () {
              //  app.getItems();
                app.loaded = true;
            });

            app.infiniteScroll();

            (app.debug) ? console.log('init' )  : '';


        },
        infiniteScroll: function () {

            (app.debug) ? console.log('infiniteScroll' )  : '';

            // Check if browser can use IntersectionObserver which is waaaay more performant
            if (!('IntersectionObserver' in window) ||
                !('IntersectionObserverEntry' in window) ||
                !('isIntersecting' in window.IntersectionObserverEntry.prototype) ||
                !('intersectionRatio' in window.IntersectionObserverEntry.prototype)) {
                // Loading polyfill since IntersectionObserver is not available
                this.isObserverPolyfilled = true

                // Storing function in window so we can wipe it when reached last page
                window.alpineInfiniteScroll = {
                    scrollFunc() {
                        var position = app.triggerElement.getBoundingClientRect()

                        if (position.top < window.innerHeight && position.bottom >= 0) {
                            if (app.loaded) {
                                (app.debug) ? console.log('getItems 1' )  : '';

                                app.getItems();
                            }

                        }
                    }
                }

                window.addEventListener('scroll', window.alpineInfiniteScroll.scrollFunc)
            } else {
                // We can access IntersectionObserver
                this.observer = new IntersectionObserver(function (entries) {
                    if (entries[0].isIntersecting === true) {
                        if (app.loaded) {
                            (app.debug) ? console.log('getItems 2' )  : '';

                            app.getItems();
                        }
                    }
                }, {threshold: [0]})

                this.observer.observe(this.triggerElement)
            }
        },
        getItems: function () {
            // TODO: Do fetch here for the content and concat it to populated items
            // TODO: Set last page from API call - ceil it

            let currentURL = new URL(window.location.href);
            currentURL.hostname = 'api.' + currentURL.hostname;


            axios.post(currentURL, {
                page: this.page,
                perPage: this.itemsPerPage,

            })
                .then(response => {
                    const data = response.data;
                    //  this.lastPage = data.total_pages;
                    app.videos = data.data.videos;
                    //  this.page++;
                    (app.debug) ? console.log(response)  : '';

                })
                .catch(error => {
                    console.error('Error loading videos:', error);
                    (app.debug) ? console.log(error)  : '';
                    this.loading = false;
                });


            // Next page
            this.page++

            // We have shown the last page - clean up
            if (this.lastPage && this.page > this.lastPage) {
                if (this.isObserverPolyfilled) {
                    window.removeEventListener('scroll', window.alpineInfiniteScroll.scrollFunc)
                    (app.debug) ? console.log('alpineInfiniteScroll')  : '';

                } else if (this.observer && this.triggerElement) {
                    try {
                        this.observer.unobserve(this.triggerElement);
                    } catch (e) {
                        console.error('Failed to unobserve element:', e);
                    }
                }

                if (this.triggerElement && this.triggerElement.parentNode) {
                    this.triggerElement.parentNode.removeChild(this.triggerElement);
                    this.triggerElement = null; // Prevent further access to the removed element
                }

            }
        }
    };
  • 更新视频后使用
    this.$nextTick()
    确保 Alpine.js 更新。
  • 手动触发 Alpine.js 反应性。
  • 直接操作 DOM 作为临时解决方法(并不理想)。

这些方法都没有解决问题。如果您了解 Alpine.js 可能不重新渲染更新后的数据或任何替代解决方案的原因,我将不胜感激。

javascript axios alpine.js
1个回答
0
投票

我尝试在本地计算机上重现您的问题,并最终编写了一个完整的工作示例。我希望它能帮助您与实际代码库进行比较,看看有什么问题:

index.html

确保将所有内容都用

div
x-data
包裹到
x-init
中。 您还会注意到,我创建了一个函数,该函数返回一个应用程序对象,并对您的代码进行了一些更改,如下面的 js 示例所示。通过在组件中定义 init 并通过 x-init 调用它,您可以确保您的设置代码在组件初始化时运行。

<div x-data="app()" x-init="init">
  <!-- Video List -->
  <div id="video-list">
    <template x-for="video in videos" :key="video.id">
      <div class="video">
        <h3 x-text="video.title"></h3>
        <p x-text="video.description"></p>
      </div>
    </template>
  </div>

  <!-- Infinite Scroll Trigger -->
  <div id="infinite-scroll-trigger"></div>
</div>

app.js

function app() {
  return {
    // Data properties
    triggerElement: null,
    page: 1,
    lastPage: null,
    itemsPerPage: 10,
    observer: null,
    isObserverPolyfilled: false,
    videos: [],
    debug: true,
    loading: false,

    // Initialization method
    init() {
      this.triggerElement = document.querySelector("#infinite-scroll-trigger");
      this.infiniteScroll();

      if (this.debug) console.log("Initialization complete.");
    },

    // Method to set up infinite scroll
    infiniteScroll() {
      if (this.debug) console.log("Setting up infinite scroll.");

      const supportsIntersectionObserver = "IntersectionObserver" in window;

      if (supportsIntersectionObserver) {
        // Use IntersectionObserver for better performance
        this.observer = new IntersectionObserver(
          this.handleIntersection.bind(this),
          {
            threshold: 0,
          }
        );
        this.observer.observe(this.triggerElement);
      } else {
        // Fallback for browsers without IntersectionObserver support
        this.isObserverPolyfilled = true;
        this.scrollFunc = this.handleScroll.bind(this);
        window.addEventListener("scroll", this.scrollFunc);
      }
    },

    // Handler for IntersectionObserver
    handleIntersection(entries) {
      if (entries[0].isIntersecting && !this.loading) {
        if (this.debug)
          console.log("Trigger element intersected. Loading items...");
        this.getItems();
      }
    },

    // Handler for scroll event (polyfill)
    handleScroll() {
      const position = this.triggerElement.getBoundingClientRect();

      if (
        position.top < window.innerHeight &&
        position.bottom >= 0 &&
        !this.loading
      ) {
        if (this.debug)
          console.log("Trigger element visible. Loading items...");
        this.getItems();
      }
    },

    // Method to fetch items from the mock API
    async getItems() {
      if (this.loading) return; // Prevent multiple calls
      this.loading = true;

      try {
        const response = await axios.get("http://localhost:3000/videos", {
          params: {
            _page: this.page,
            _limit: this.itemsPerPage,
          },
        });

        const totalItems = response.headers["x-total-count"];
        this.lastPage = Math.ceil(totalItems / this.itemsPerPage);

        this.videos = this.videos.concat(response.data);

        if (this.debug) console.log(`Loaded page ${this.page}.`, response.data);

        this.page++;

        // Check if we've reached the last page
        if (this.page > this.lastPage) {
          this.cleanup();
        }
      } catch (error) {
        console.error("Error loading videos:", error);
      } finally {
        this.loading = false;
      }
    },

    // Cleanup method to remove observers and event listeners
    cleanup() {
      if (this.isObserverPolyfilled && this.scrollFunc) {
        window.removeEventListener("scroll", this.scrollFunc);
        if (this.debug) console.log("Removed scroll event listener.");
      }

      if (this.observer) {
        this.observer.disconnect();
        if (this.debug) console.log("Disconnected IntersectionObserver.");
      }

      if (this.triggerElement) {
        this.triggerElement.remove();
        this.triggerElement = null;
        if (this.debug) console.log("Removed trigger element.");
      }
    },
  };
}

JavaScript 文件的关键更新:

  • 通过函数实例化app对象,并通过
    this
    语句而不是
    app
    引用属性和方法。在 Alpine 组件的方法中, this 指的是组件的数据对象。使用它,您可以访问和修改反应属性。
© www.soinside.com 2019 - 2024. All rights reserved.