【问题标题】:Trouble implementing Infinite Scrolling With Rails 7, Stimulus, Kaminari gem and filtering/sorting functionality使用 Rails 7、Stimulus、Kaminari gem 和过滤/排序功能实现无限滚动时遇到问题
【发布时间】:2022-11-02 09:33:41
【问题描述】:

我正在尝试使用 Rails 7、Stimulus/Turbo 和 Kaminari gem 在鸡尾酒配方应用程序中实现无限滚动以进行分页。当我只是过滤从控制器发送的所有记录时,一切都正常运行。这意味着,当我向下滚动页面时,页码会正确增加,并且每个新的记录页面都会以正确的顺序更新。

但是,当我向控制器发送过滤或排序参数时,行为会变得异常不稳定。新的记录页继续从控制器发送,但它发送两个或三个重复页或将页码提前几页。

我花了太多时间试图弄清楚我在这里做错了什么,如果那里有任何亲切的灵魂可能知道我错过了什么愚蠢的东西,我将不胜感激。

这是我的鸡尾酒食谱控制器#index,我根据排序选项(所有成分、任何成分等)、鸡尾酒类别和/或一组特定选择的成分(杜松子酒、薄荷、苹果杰克)来提供记录, ETC。)

  def index
    @page = params[:page] || 1
    category_id = params[:categoryId]
    ingredient_ids = params[:ingredientIds] ? [*params[:ingredientIds]].map(&:to_i) : nil
    
    @recipes = Recipe.alphabetical.page(@page)

    case params[:sort_option]
    when ''
      params[:ingredientIds] ?
        @recipes = Recipe.alphabetical.filter_all_recipes(ingredient_ids, category_id).page(@page) :
        @recipes = Recipe.filter_all_by_category(category_id).page(@page)
      respond_to do |format|
        # needed to explicitly call formats: [:html] after adding turbo_stream option
        format.html { render partial: 'recipe_cards', formats: [:html] }
        format.turbo_stream
      end
    when 'All Recipes'
      params[:ingredientIds] ?
        @recipes = Recipe.alphabetical.filter_all_recipes(ingredient_ids, category_id).page(@page) :
        @recipes = Recipe.filter_all_by_category(category_id).page(@page)
      respond_to do |format|
        format.html { render partial: 'recipe_cards', formats: [:html] }
        format.turbo_stream
      end
    when 'Any Ingredient'
      params[:ingredientIds] ?
        @recipes = Recipe.alphabetical.match_any_subset(ingredient_ids, current_user.ingredients, category_id).page(@page) :
        @recipes = Recipe.alphabetical.match_any_ingredient(current_user.ingredients, category_id).page(@page)
      respond_to do |format|
        format.html { render partial: 'recipe_cards', formats: [:html] }
        format.turbo_stream
      end
    when 'All Ingredients'
      params[:ingredientIds] ?
        @possible_recipes = Recipe.match_all_subset(params[:recipeIds], ingredient_ids, category_id).page(@page) :
        @possible_recipes = Recipe.alphabetical.match_all_ingredients(current_user.ingredients, category_id).page(@page)
      respond_to do |format|
        format.html { render partial: 'recipe_cards', formats: [:html] }
        format.turbo_stream
      end
    end
  end

这是我的分页刺激控制器:

import { Controller } from "@hotwired/stimulus";

export default class extends Controller {

  // gets/sets record fetching flag
  static get fetching() { return this.fetching; }
  static set fetching(bool) {
    this.fetching = bool;
  } 

  // gets url and page number from target element
  static get values() { return {
    url: String,
    page: { type: Number, default: 1 },
  };}

  // adds the scroll event listener and sets fetching flag to false
  connect() {
    console.log("Pagination Controller Loaded");
    document.addEventListener('scroll', this.scroll);
    this.fetching = false;
  }

  // binds this to the controller rather than document
  initialize() {
    this.scroll = this.scroll.bind(this);
  }

  // calls loadRecords() when scroll reaches the bottom of the page
  scroll() {
    if (this.pageEnd && !this.fetching) {
      this.loadRecords(); 
    }
  }

  // record fetching function
  async loadRecords() {
    // get pre-configured url from helper method
    const url = getUrl(this.urlValue, this.pageValue);
    
    // sets fetching flag to true
    this.fetching = true;

    // sends a turbo_stream fetch request to the recipes controller
    await fetch(url.toString(), {
      headers: {
        Accept: 'text/vnd.turbo-stream.html',
      },
    }).then(r => r.text())
      .then(html => Turbo.renderStreamMessage(html));

    // sets fetching flag to false
    this.fetching = false;

    // increments the target element's 
    this.pageValue += 1;
  }

  // sets the boundary where the loadRecords() function gets called
  get pageEnd() {
    const { scrollHeight, scrollTop, clientHeight } = document.documentElement;
    return scrollHeight - scrollTop - clientHeight < 40; // can adjust to desired limit
  }
}

//  -------------  HELPER FUNCTIONS ----------------

// gets selected ingredient ids from liquor cabinet display
// options and returns them in an array
function getIngredientIds() {
  var ingredientIds = [...$('.cabinet-spirits').val(),
  ...$('.cabinet-modifiers').val(),
  ...$('.cabinet-sugars').val(),
  ...$('.cabinet-garnishes').val()];
  return ingredientIds;
}

// if there are ingredientIds, appends them as an array to searchParams
function appendIngredientIds(url) {
  var ingredientIds = getIngredientIds();
  if (ingredientIds.length != 0) {
    ingredientIds.map(i => url.searchParams.append('ingredientIds', i));
  }
  return url;
}

// configures url searchParams and returns the url
function getUrl(urlValue, pageValue) {
  var url = new URL(urlValue);
  url.searchParams.set('page', pageValue);
  url.searchParams.append('sort_option', $('.sort-options').val());
  url = appendIngredientIds(url);
  return url;
}

这是 index.turbo.erb:

<%= turbo_stream.append 'recipes' do %>
  <% @recipes.each do |recipe| %>
    <%= render partial: "recipe_card", locals: { recipe: recipe } %>
  <% end %>
<% end %>

最后,我将新记录附加到的目标 div:

<div class="container-fluid mt-2 mx-3">
  
  <div id="recipes" class="row row-cols-lg-5 row-cols-md-4 row-cols-sm-3 g-2" 
                    data-controller='pagination'
                    data-pagination-target='recipes'
                    data-pagination-url-value='<%= recipes_url %>'
                    data-pagination-page-value='<%= 2 %>'>

    <% @recipes.each do |recipe| %>
      <%= render partial: "recipe_card", locals: { recipe: recipe } %>
    <% end %>
  </div>
</div>

我已经在 devTools 中监视了页面增量,看起来对于配方控制器的每个额外的 ajax 调用,分页控制器都会被调用一次额外的时间。因此,如果我按“任何成分”对结果进行排序,我会在滚动时开始获得重复的页面。如果我然后按波旁酒过滤这些结果,3 页(不一定按顺序),开始加载滚动。我觉得我可能缺少一些明显的东西。

【问题讨论】:

    标签: infinite-scroll kaminari ruby-on-rails-7 stimulusjs turbo


    【解决方案1】:

    万一其他人遇到类似的问题,我想通了。可能是菜鸟的错误。

    问题来自我找到数据分页属性的位置。最初,我将它们放在一个部分的 div 中,当排序或过滤选项发送到控制器时,该部分会被替换。部分看起来像这样:

    <div class="container-fluid mt-2 mx-3">
      
      <div id="recipes" class="row row-cols-lg-5 row-cols-md-4 row-cols-sm-3 g-2" 
                        data-controller='pagination'
                        data-pagination-target='recipes'
                        data-pagination-url-value='<%= recipes_url %>'
                        data-pagination-page-value='<%= 2 %>'>
    
        <% @recipes.each do |recipe| %>
          <%= render partial: "recipe_card", locals: { recipe: recipe } %>
        <% end %>
      </div>
    </div>
    

    如前所述,无限滚动/分页工作正常,直到将排序或过滤选项发送到控制器。之后,滚动功能开始添加重复页面或乱序页面。

    当我将数据分页属性移动到包含部分的 div 时,不需要的行为停止了。这是外部div:

     <%# cocktail card container %>
        <div class='col offset-2 recipe-cards me-3' 
             data-controller='pagination'
             data-pagination-target='recipes'
             data-pagination-url-value='<%= recipes_url %>'
             data-pagination-page-value='<%= 2 %>'>
    
          <%= render partial: 'recipe_cards', locals: { recipes: @recipes } %>
          
        </div>
    

    我相信数据属性在部分更新后会持续存在,因此分页最终会响应多个目标。

    我还需要创建一个简单的 javascript 函数来在每次选择新的排序选项时重置 data-pagination-page-value。

    【讨论】:

      猜你喜欢
      • 2014-02-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多