Skip to content

Modals

Modals are easy. Lets imagine a scenario where we have two urls:

  1. /posts
  2. /posts/new

When a user visits /posts/new from /posts, we want a modal to appear overlaying the existing list of posts. The overlay should work if a user chooses instead to directly visit /posts/new.

The setup

Arriving at both urls results in a seeing list of posts. Lets set up the controller and the page_to_page_mapping.js the same way.

Same template different action

Notice that we're rendering the index for the new action. While the content is the same, the componentIdentifier is different as that has been setup to use the controller and action name.

# app/controllers/posts_controller.rb

def index
  @posts = Post.all
end

def new
  @posts = Post.all
  render :index
end

Info

Similarly, we tie the componentIdentifier to the same page component.

import PostIndex from '../views/posts/index'

export const pageIdentifierToPageComponent = {
  'posts/index': PostIndex,
  'posts/new': PostIndex,
};

Imagine a list of posts, lets add a button somewhere on the index page to direct the user to /posts/new. As seen previously, both /posts and /posts/new render the same thing.

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

...

json.newPostPath new_post_path
export default PostIndex = ({
  newPostPath,
  ...rest
}) => {

  return (
    ...
    <a
      href={newPostPath}
      >
      New Post
    </a>
    ...
  )
}

The modal

Now the link appears and we're able to navigate to /posts/new, but /posts/new is missing a modal. Not surprising as both routes are rendering the same content.

Lets add a modal.

Info

For simplicity, we'll use a "Hello World" as the modal contents

# app/views/posts/index.json

...

json.newPostPath new_post_path

+ json.createPostModal do
+   json.greeting "Hello World"
+ end
+ import Modal from './Modal'

export default PostIndex = ({
  newPostPath,
  createPostModal
  ...rest
}) => {

  return (
    ...
    <a
      href={newPostPath}
    >
      New Post
    </a>
+   <Modal {...createPostModal} />
    ...
  )
}

Info

This is a simplified modal, in practice you'll use this with <Dialog> or other modal library.

import Modal from './Modal'

export default Modal = ({
  greeting
}) => {
  return (
    <div className="my-modal">{greeting}</div>
  )
}

Too many modals

Unfortunately, now BOTH routes have modals! Lets fix that by adding a conditional render.

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

...

json.newPostPath new_post_path

json.createPostModal do
  json.greeting "Hello World"
+ json.showModal @show_modal
end
# app/controllers/posts_controller.rb

def index
  @posts = Post.all
+ @show_modal = false
end

def new
  @posts = Post.all
+ @show_modal = true
  render :index
end
import Modal from './Modal'

export default Modal = ({
  greeting,
+  showModal
}) => {
  return (
-   <div className="my-modal">{greeting}</div>
+   {showModal && <div className="my-modal">{greeting}</div>}
  )
}

Finish!

Awesome! We have modals! Unfortunately, clicking <a href={newPostPath}>New Post</a> will cause a new page load. We can move the page load by adding data-sg-visit to the link. With data-sg-visit, Superglue will navigate to the next page without reloading the page, just like Turbo.

posts/index.js

import Modal from './Modal'

export default PostIndex = ({
  newPostPath,
  createPostModal,
  ...rest
}) => {

  return (
    ...
    <a
      href={newPostPath}
+     data-sg-remote
    >
      New Post
    </a>
    <Modal {...createPostModal} />
    ...
  )
}

Optimization

With the above, a click on New Post while on /posts will

  1. Fetch /posts/new with format=json
  2. Save the page to the store
  3. Swap the page components
  4. Change the url

Unfortunately, step 1 is still a full page load. Commonly, we just want to load the modal without loading the entire page.

Lets fix that!

index.json

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

# app/views/posts/index.json

...

- json.newPostPath new_post_path
+ json.newPostPath new_post_path(props_at: `data.createPostModal`)

json.createPostModal do
  json.greeting "Hello World"
  json.showModal @show_modal
end

posts/index.js

Lastly add data-sg-placeholder to the link.

import Modal from './Modal'

export default PostIndex = ({
  newPostPath,
  createPostModal,
  ...rest
}) => {

  return (
    ...
    <a
      href={newPostPath}
      data-sg-visit
+     data-sg-placeholder="/posts"
      >
      New Post
    </a>
    <Modal {...createPostModal} />
    ...
  )
}

With the placeholder, the sequence becomes:

  1. Copy the state in /posts to /posts/new in the store.
  2. Fetch /posts/new?props_at=data.createPostModal
  3. Graft the result to the store at /posts/new
  4. Swap the page components
  5. Change the url

Info

Normally, props_at cannot be used with data-sg-visit, that's because the state needs to exist for Superglue to know where to graft. With data-sg-placeholder, we take an existing page and copy that over as a placeholder for what doesn't exist yet.