Modal

by spacejack

Level: advanced • Mithril.js Version: latest

In this example we can see a modal example that is built with an Overlay, an OverlayContainer, and the Modal component itself.

The Overlay component provides a full-screen cover element. It is mounted to a separate V/DOM tree appended to the body. Children supplied to Overlay are rendered into this tree. The Overlay component can be nested anywhere within your app's view but will be rendered to display overtop everything else.

The Modal component uses the Overlay component to provide a full screen cover and renders a dialog-like widget within that waits for the user to click a button. A Modal instance can be nested anywhere within your app's view and will be rendered on top of everything else. For the Modal component at least one button should be provided otherwise there will be no way to close the modal.

Live Example

Dependencies

Type Name URL
scriptmithril@latesthttps://unpkg.com/mithril@latest

Overlay.js

// Overlay.js
const Overlay = function() {
	let dom
	let children

	const OverlayContainer = {
		view: () => children
	}

	return {
		oncreate(v) {
			children = v.children
			// Append a container to the end of body
			dom = document.createElement('div')
			dom.className = 'overlay'
			document.body.appendChild(dom)
			m.mount(dom, OverlayContainer)
		},
		onbeforeupdate(v) {
			children = v.children
		},
		onbeforeremove(v) {
			// Add a class with fade-out exit animation
			dom.classList.add('hide')
			return new Promise(r => {
				dom.addEventListener('animationend', r)
			})
		},
		onremove() {
			m.mount(dom, null)
			// Destroy the overlay dom tree. Using m.mount with
			// null triggers any modal children removal hooks.
			document.body.removeChild(dom)
		},
		view() {}
	}
}

Modal.js

// Modal.js
/**
Expected attrs are as follows:

interface Attrs {
  title: m.Children
  content: m.Children
  buttons: {id: string, text: string}[]
  onClose(id: string): void
}
*/

const Modal = function(v) {
  let clickedId

  return {
    view({attrs: {title, content, buttons, onClose}}) {
      if (clickedId != null) {
        // We need to allow the Overlay component execute its
        // exit animation. Because it is a child of this component,
        // it will not fire when this component is removed.
        // Instead, we need to remove it first before this component
        // goes away.
				// When a button is clicked, we omit the Overlay component
        // from this Modal component's next view render, which will
        // trigger Overlay's onbeforeremove hook.
        return null
      }
      return m(Overlay,
        {
          onremove() {
            // Wait for the overlay's removal animation to complete.
            // Then we fire our parent's callback, which will
            // presumably remove this Modal component.
            Promise.resolve().then(() => {
              onClose(clickedId)
              m.redraw()
            })
          }
        },
        m('.modal',
          m('h3', title),
          m('.modal-content', content),
          m('.modal-buttons',
            buttons.map(b =>
              m('button',
                {
                  type: 'button',
                  disabled: clickedId != null,
                  onclick() {
                    clickedId = b.id
                  }
                },
                b.text
              )
            )
          )
        )
      )
    }
  }
}

app.js

// app.js
const App = function() {
  let showModal = false

  return {
    view: () => m('.app',
      m('h1', 'Modal Demo'),
      m('p', 'Click below to open a modal'),
      m('p',
        m('button',
          {
            type: 'button',
            onclick() {
              showModal = true
            }
          },
          'Open Modal'
        ),
        // Even though this modal is nested within our App vdom,
        // it will appear on top of everything else, appended
        // to the end of document body.
        showModal && m(Modal, {
          title: 'Hello Modal!',
          content: 'Click the button below to close.',
          buttons: [{id: 'close', text: 'Close'}],
          onClose(id) {
            showModal = false
          }
        })
      )
    )
  }
}

m.mount(document.body, App)

CSS

body {
  margin: 0;
  padding: 0;
  font-family: sans-serif;
  font-size: 16px;
}

.app {
  padding: 2em;
}

@keyframes fade-in {
  from {opacity: 0;}
  to {opacity: 1;}
}

@keyframes fade-out {
  from {opacity: 1;}
  to {opacity: 0;}
}

.overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(255,255,255,0.75);
  display: flex;
  justify-content: center;
  align-items: center;
  animation: fade-in 0.5s;
}
.overlay.hide {
  animation: fade-out 0.5s;
}

.modal {
  text-align: center;
  padding: 2em 4em 3em 4em;
  background-color: #FFF;
  box-shadow: 0.5em 0.5em 2em #999;
  border: #CCC 1px solid;
}

.modal-buttons {
  margin-top: 1em;
}
.modal-buttons button {
  margin: 0 0.5em;
}

The snippet requires the latest version of Mithril.js framework. More advanced users should be able to follow this example, however it contains some first difficulties.

In this example we can see some Mithril.js API methods like m.mount or m.redraw, besides Mithril.js' basic m() hyperscript function. It also demonstrates, how Mithril.js' lifecycle methods (aka hooks) like onbeforeupdate, onremove, onbeforeremove and oncreate can be used.

The code sample was authored by spacejack. It was last modified on 26 October 2021. Want to see more examples written by spacejack? Then Click here.

Contribute

Do you see some improvements, that could be addressed here? Then let me know by opening an issue. As an alternative, you can fork the repository on GitHub, push your commits and send a pull request. To start your work, click on the edit link below. Thank you for contributing to this repo.

See more code examples  •  Edit this example on GitHub