{"id":15459060,"url":"https://github.com/ntamvl/infinitescrollstimulusexample","last_synced_at":"2026-05-05T05:39:13.511Z","repository":{"id":204634356,"uuid":"712311265","full_name":"ntamvl/InfiniteScrollStimulusExample","owner":"ntamvl","description":"Infinite Scroll with Stimulus Example","archived":false,"fork":false,"pushed_at":"2023-11-03T05:09:00.000Z","size":87,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-10-18T19:29:46.587Z","etag":null,"topics":["hotwire-stimulus","rails","stimulus"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ntamvl.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-10-31T08:08:15.000Z","updated_at":"2024-02-21T10:57:43.000Z","dependencies_parsed_at":"2024-12-07T18:06:20.411Z","dependency_job_id":"166a9445-5cf3-4c7a-ada5-675b0b7d075b","html_url":"https://github.com/ntamvl/InfiniteScrollStimulusExample","commit_stats":{"total_commits":23,"total_committers":1,"mean_commits":23.0,"dds":0.0,"last_synced_commit":"f63ee111f28a435aa2f3882c3529126a2ccfb566"},"previous_names":["ntamvl/infinitescrollstimulusexample"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ntamvl%2FInfiniteScrollStimulusExample","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ntamvl%2FInfiniteScrollStimulusExample/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ntamvl%2FInfiniteScrollStimulusExample/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ntamvl%2FInfiniteScrollStimulusExample/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ntamvl","download_url":"https://codeload.github.com/ntamvl/InfiniteScrollStimulusExample/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246009692,"owners_count":20709002,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["hotwire-stimulus","rails","stimulus"],"created_at":"2024-10-01T23:04:23.215Z","updated_at":"2026-05-05T05:39:08.492Z","avatar_url":"https://github.com/ntamvl.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Infinite Scroll with Stimulus\n\nInfinite scroll is a pagination mechanism where whenever the user reaches the end of the scroll area more content is loaded till there are no more content to load.\n\n**Introduction**\n\nHotwire is a new set of tools extracted from Hey by Basecamp. It uses Asynchronous HTML and HTTP (also known as AHAH) to render partial updates to the DOM without full browser reload. You build your servers with any language of your choice and let Turbo handle the partial updates for you. Which makes your application to have a speed of an SPA while having the benefits of server-rendered partials.\n\nHOTWire is not a single tool, but three tools that allow you to build super fast apps while not having to write tons of client-side JavaScript to manage the updates. The tools within HOTWire are\n\n1- Turbo: which is responsible for the navigation in your application and rendering the server responses to update the correct partial in the DOM.\n\n2- Stimulus: Sometimes we would like to add a little bit of client-side behaviour to our site, the feature is too simple to let Turbo manage it and doesn't require a round trip to the server. There, Stimulus comes into play. You add behaviour to your HTML and sprinkles of JavaScript for this.\n\n3- Strada: Standardizes the way that web and native parts of a mobile hybrid application talk to each other via HTML bridge attributes\n\nReferences:\n- https://stimulus.hotwired.dev/\n\n## Setup Rails project\n```bash\nrails new InfiniteScrollStimulusExample  -c=bootstrap -j=esbuild\n\ncd InfiniteScrollStimulusExample\nbundle add kaminari faker\nyarn add @rails/request.js\n\nrails g scaffold Post title body:text\n```\n\n**Modify action index in app/controllers/posts_controller.rb**\n```rb\n# app/controllers/posts_controller.rb\nclass PostsController \u003c ApplicationController\n  ...\n\n  def index\n    @page = params[:page] || 1\n    @posts = Post.page @page\n  end\n\n  ...\nend\n```\n\n**Create infinitive_pagination stimulus controller**\n```bash\nrails g stimulus infinitive_pagination\n```\n\n```js\n// infinitive_pagination_controller.js\nimport { Controller } from \"@hotwired/stimulus\"\nimport { get } from \"@rails/request.js\"\n\n// Connects to data-controller=\"infinitive-pagination\"\nexport default class extends Controller {\n  static targets = ['lastPage', 'loadMoreButton']\n\n  static values = {\n    url: String,\n    page: Number,\n  }\n\n  initialize() {\n    this.handleScroll = this.handleScroll.bind(this)\n    this.pageValue = this.pageValue || 1\n    this.loading = false\n  }\n\n  connect() {\n    window.loadMoreButtonTarget = this.loadMoreButtonTarget\n    window.addEventListener(\"scroll\", this.handleScroll)\n  }\n\n  disconnect() {\n    window.removeEventListener(\"scroll\", this.handleScroll)\n  }\n\n  handleScroll() {\n    const reachEndPage = this.hasReachEndPage()\n    if (reachEndPage \u0026\u0026 !this.hasLastPageTarget) {\n      this.loadMore()\n    } else {\n      this.hideLoadMoreButton()\n    }\n  }\n\n  hasReachEndPage() {\n    const bottomHeight = 20\n    let body = document.body, html = document.documentElement\n    let height = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight)\n    const distance = height - window.innerHeight - bottomHeight\n    const reachEndPage = window.scrollY \u003e= distance\n    return reachEndPage\n  }\n\n  hasReachEndPage2() {\n    const bottomHeight = 20\n    const { scrollHeight, scrollTop, clientHeight } = document.documentElement\n    const distance = scrollHeight - scrollTop - clientHeight\n    return distance \u003c bottomHeight\n  }\n\n  async loadMore() {\n    if (this.loading) {\n      return\n    }\n\n    this.loading = true\n    this.pageValue += 1\n    const url = new URL(this.urlValue)\n    const currentSearchParams = new URLSearchParams(window.location.search)\n    for (const [key, value] of currentSearchParams) {\n      url.searchParams.set(key, value)\n    }\n    url.searchParams.set('page', this.pageValue)\n    await get(url.toString(), { responseKind: 'turbo-stream' })\n    this.loading = false\n  }\n\n  async handleLoadMoreButton(e) {\n    await this.loadMore()\n    e.target.blur()\n  }\n\n  hideLoadMoreButton() {\n    this.loadMoreButtonTarget.classList.add('d-none')\n  }\n}\n\n```\n\n**Modify index.html.erb**\n```html\n\u003cp style=\"color: green\"\u003e\u003c%= notice %\u003e\u003c/p\u003e\n\u003ch1\u003ePosts\u003c/h1\u003e\n\u003c%= link_to \"New post\", new_post_path %\u003e\n\u003cdiv data-controller=\"infinitive-pagination\"\n  data-infinitive-pagination-url-value=\"\u003c%= posts_url %\u003e\"\n  data-infinitive-pagination-page-value=\"1\"\n\u003e\n  \u003cdiv id=\"posts\"\u003e\n    \u003c% @posts.each do |post| %\u003e\n      \u003c%= render post %\u003e\n      \u003cp\u003e\n        \u003c%= link_to \"Show this post\", post %\u003e\n      \u003c/p\u003e\n    \u003c% end %\u003e\n  \u003c/div\u003e\n  \u003cbutton data-action=\"click-\u003einfinitive-pagination#handleLoadMoreButton\" data-infinitive-pagination-target=\"loadMoreButton\"\u003e\n    Load more\n  \u003c/button\u003e\n\u003c/div\u003e\n```\n\n**Create index.turbo_stream.erb**\n```html\n\u003c%= turbo_stream.append \"posts\" do %\u003e\n  \u003c%= render @posts %\u003e\n  \u003c% if @posts.page(@page.to_i + 1).out_of_range? %\u003e\n    \u003cspan class=\"hidden\" data-infinitive-pagination-target=\"lastPage\"\u003e\u003c/span\u003e\n  \u003c% end %\u003e\n\u003c% end %\u003e\n```\n\nModify `db/seeds.rb`\n```rb\n# db/seeds.rb\n500.times do\n  Post.create title: Faker::Movie.title, body: Faker::Quote.famous_last_words\nend\n```\n\nMigrate database and seed data\n```bash\nrails db:migrate db:seed\n```\n\nCurrently I am running many apps with many different ports, while the `Rails app` will run on the default port `3000`, so I need to update the `Procfile.dev` file to run on another port, here I will use the port `4001` to avoid conflicts, like the Procfile.dev content below::\n```\nweb: env RUBY_DEBUG_OPEN=true bin/rails server -p 4001\njs: yarn build --watch\ncss: yarn watch:css\n```\n\n**Run app**\n```bash\n./bin/dev\n```\n\nOpen your browser and goto `http://localhost:4001/posts`\n\nEnjoy!!! :smile:\n\nIf you have any questions, please do not hesitate to contact me via X (Twitter) [@nguyentamvn](https://twitter.com/nguyentamvn) or Facebook [@nguyentamvinhlong](https://www.facebook.com/nguyentamvinhlong\n)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fntamvl%2Finfinitescrollstimulusexample","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fntamvl%2Finfinitescrollstimulusexample","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fntamvl%2Finfinitescrollstimulusexample/lists"}