Breezy
main
main
  • README
  • Code of conduct
  • Contributing Guide
  • Changelog
  • Security Policy
  • docs
    • configuration
    • Cross cutting concerns
    • deferments
    • Demo Application
    • Digging
    • Overview
    • Installation
    • NavigationContext
    • The page response
    • Rails utils
    • The store shape
    • Requests
    • Tutorial
    • The return of Rails UJS
    • recipes
      • Infinite scroll
      • Modals
      • progress-bar
      • Shopping cart
      • SPA (Single Page Application) Pagination
      • Server-Side Rendering
      • Replicating Turbo behavior
      • Usage with vite
    • Modules
      • Functions
      • Functions
      • index
      • types.actions
      • types
      • Interfaces
Powered by GitBook
On this page
  • Starting point
  • Add gems
  • Add pagination
  • Smooth navigation
  • Optimize!

Was this helpful?

  1. docs
  2. recipes

SPA (Single Page Application) Pagination

In this recipe, we'll be adding pagination that works without reloading the page.

Starting point

Lets pretend that we're already able to see a list of posts.

=== "posts_controller.rb" ```ruby # app/controllers/posts_controller.rb

def index
  @posts = Post.all
end
```

=== "index.json.props" !!! info In this example, we have a sleep that we will optimize for later

```ruby
# app/views/posts/index.json.props

json.header do
  json.name "bob"
  sleep 2
end

json.posts do
  json.list do
    json.array! @posts do |post|
      json.id post.id
      json.body post.body
      json.editPostPath edit_post_path(post)
    end
  end
end
```

=== "index.js" !!! info Let's assume Header is a simple component that exist.

```js
// app/views/posts/index.js

import React from 'react'
import {useContent} from '@thoughtbot/superglue'
import Header from './Header'

export default PostIndex = () => {
  const {
    posts,
    header
  } = useContent()

  return (
    <>
      <Header {...header}/>
      <div>
        {
          posts.list.map(({id, body}) => (
            <p key={id}>{body}</p>
          ))
        }
      </div>
      <PostList items={posts}>
    </>
  )
}

```

Add gems

Lets also add Kaminari to your gem file

gem 'kaminari'

and bundle

Add pagination

The changes here are almost same with the .erb counterpart. We're using path_to_next_page and path_to_prev_page which come with Kaminari, both methods return nil if there are no subsequent pages.

=== "posts_controller.rb" ```diff # app/controllers/posts_controller.rb

def index
  @posts = Post.all
+   .page(params[:page_num])
+   .per(10)
+   .order(created_at: :desc)
end
```

=== "index.json.props" ```diff # app/views/posts/index.json.props

json.posts do
  json.list do
    json.array! @posts do |post|
      json.id post.id
      json.body post.body
      json.editPostPath edit_post_path(post)
    end
  end
+
+ json.pathToNextPage path_to_next_page(@posts)
+ json.pathToPrevPage path_to_prev_page(@posts)
end
```

=== "index.js"

```diff
// app/views/posts/index.js

import React from 'react'
import {useContent} from '@thoughtbot/superglue'
import Header from './Header'

export default PostIndex = () => {
  const {
    posts,
    header,
+   pathToNextPage,
+   pathToPrevPage
  } = useContent()

  return (
    <>
      <Header {...header}/>
      <div>
        {
          posts.list.map(({id, body}) => (
            <p key={id}>{body}</p>
          ))
        }
      </div>
+     {pathToPrevPage && <a href={pathToPrevPage}>Prev Page</a>}
+     {pathToNextPage && <a href={pathToNextPage}>Next Page</a>}
    </>
  )
}
```

Smooth navigation

The above adds pagination, but each click on Next Page is a new page load.

index.js

// app/views/posts/index.js

import React from 'react'
import {useContent} from '@thoughtbot/superglue'
import PostList from './PostList'
import Header from './Header'

export default PostIndex = () => {
  const {
    posts,
    header,
    pathToNextPage,
    pathToPrevPage
  } = useContent()

  return (
    <>
      <Header {...header}/>
      <div>
        {
          posts.list.map(({id, body}) => (
            <p key={id}>{body}</p>
          ))
        }
      </div>
-     {pathToPrevPage && <a href={pathToPrevPage}>Prev Page</a>}
+     {pathToPrevPage && <a href={pathToPrevPage} data-sg-visit>Prev Page</a>}
-     {pathToNextPage && <a href={pathToNextPage}>Next Page</a>}
+     {pathToNextPage && <a href={pathToNextPage} data-sg-visit>Next Page</a>}
    </>
  )
}

Optimize!

Lets skip data.header when navigating and dig for data.posts. For the user, only the posts lists change, but the header stays the same.

index.json.props

# app/views/posts/index.json.props

json.header do
  ...
end

json.posts do
  json.list do
    json.array! @posts do |post|
      json.id post.id
      json.body post.body
      json.editPostPath edit_post_path(post)
    end
  end

- json.pathToNextPage path_to_next_page(@posts)
+ json.pathToNextPage path_to_next_page(@posts, props_at: 'data.posts')
- json.pathToPrevPage path_to_prev_page(@posts)
+ json.pathToPrevPage path_to_prev_page(@posts, props_at: 'data.posts')
end

- [:octicons-arrow-right-24: Interested in infinite-scroll?](./infinite-scroll.md) for `visit`

PreviousShopping cartNextServer-Side Rendering

Last updated 4 months ago

Was this helpful?

!!! info Some like paginate output HTML instead of JSON, but we can still use more primitives methods.

Lets navigate without a reload. In this example, we're using the helper data-sg-visit, which would set the current page's state to the response without changing the URL.

!!! info In effect, this achieves the same functionality as , but Superglue leans more on Unobtrusive Javascript for better ergonomics.

Recall how for content works. We'll add a props_at that digs for the json.posts while skipping other content on that page.

helpers
UJS
Turbo Frames
digging