Skip to content

Instantly share code, notes, and snippets.

@seanCodes
Created May 26, 2020 08:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save seanCodes/9c98a73ca6ec373fa182e7575164f343 to your computer and use it in GitHub Desktop.
Save seanCodes/9c98a73ca6ec373fa182e7575164f343 to your computer and use it in GitHub Desktop.
Ember Toasts Test
<div
class="toast {{if this.isShown "show"}}"
role="alert"
aria-live="assertive"
aria-atomic="true"
{{did-insert this.show}}
{{on-transition-end "opacity" this.transitionEnd}}
>
<div class="toast-header">
<svg class="bd-placeholder-img rounded mr-2" width="20" height="20" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid slice" focusable="false" role="img">
<rect width="100%" height="100%" fill="#007aff"></rect>
</svg>
<strong class="mr-auto">
{{@toast.title}} {{@toast.id}}
</strong>
<small class="text-muted">
{{@toast.time}}
</small>
<button type="button" class="close overflow-hidden px-2 pb-1 my-n1" data-dismiss="toast" aria-label="Close" {{on "click" this.hide}}>
<span aria-hidden="true">
&times;
</span>
</button>
</div>
<div class="toast-body">
{{@toast.content}}
</div>
</div>
import { action } from '@ember/object'
import { htmlSafe } from '@ember/template'
import { later } from '@ember/runloop'
import { tracked } from '@glimmer/tracking'
import Component from '@glimmer/component'
export default class ToastComponent extends Component {
@tracked
isShown = false
@action
show(elem) {
// Updating `this.isShown` immediately causes the `.show`
// class to be applied too soon (before layout/paint), so
// no transition takes place. Calling `later()` with no
// specific wait time (which is essentially “as soon as
// you get a chance”) works most of the time but is
// occasionally too fast. 10ms seems to be just right.
later(() => {
this.isShown = true
if (this.args.onShow) {
this.args.onShow(this.args.toast)
}
}, 10)
}
@action
hide() {
this.isShown = false
if (this.args.onHide) {
this.args.onHide(this.args.toast)
}
}
@action
transitionEnd() {
if (!this.isShown && this.args.onHidden) {
this.args.onHidden(this.args.toast)
} else if (this.args.onShown) {
this.args.onShown(this.args.toast)
}
}
}
<div
class="toaster fixed-bottom"
{{did-insert this.registerElement}}
{{will-destroy this.unregisterElement}}
>
{{#each @toasts key="id" as |toast index|}}
<Toaster::Toast
@toast={{toast}}
@onShow={{fn this.reflow "show"}}
@onHide={{fn this.reflow "hide"}}
@onHidden={{@onToastDismiss}}
/>
{{/each}}
</div>
import { action, set } from '@ember/object'
import { cancel, later, run } from '@ember/runloop'
import { tracked } from '@glimmer/tracking'
import Component from '@glimmer/component'
export default class extends Component {
elem = null
reflowTimer = -1
@action
registerElement(elem) {
this.elem = elem
}
@action
unregisterElement() {
this.elem = null
}
@action
reflow(changeType) {
if (this.reflowTimer >= 0) {
cancel(this.reflowTimer)
}
const DELAY = changeType === 'show' ? '10' : '100'
// Reflowing immediately doesn’t allow enough time for the
// `.show` class to be removed. Calling `later()` with no
// specific wait time (which is essentially “as soon as
// you get a chance”) works most of the time but is
// occasionally too fast. 10ms seems to be just right.
this.reflowTimer = later(() => {
const toastElems = Array.from(this.elem.children)
.reverse()
.filter(toastElem =>
toastElem.classList.contains('show')
)
const TOAST_SPACING = 12
let y = 0
run(() =>
toastElems.forEach((toastElem, index) => {
y -= TOAST_SPACING + toastElem.clientHeight
toastElem.style.transform = `translateY(${y}px)`
})
)
}, DELAY)
}
}
import { action } from '@ember/object'
import { tracked } from '@glimmer/tracking'
import Controller from '@ember/controller'
export default class ApplicationController extends Controller {
appName = 'Ember Toasts Test'
@tracked
toasts = []
@tracked
nextToastId = 0
@action
makeToast() {
const id = this.nextToastId++
const randomCount = Math.floor(Math.random() * 4) + 1
this.toasts.pushObject({
id,
title: 'Title',
content: 'Animate me I’m a piece of toast. '.repeat(randomCount),
time: (new Date()).toLocaleTimeString(),
})
}
@action
removeToast(toast) {
this.toasts.removeObject(toast)
}
}
import { modifier } from 'ember-modifier'
export default modifier(function onTransitionEnd(elem, [property, callback]) {
const handler = (evt) => {
const { propertyName, target } = evt
if (
(property && property !== propertyName) ||
(target !== elem)
) {
return
}
callback()
}
elem.addEventListener('transitionend', handler)
return () => elem.removeEventListener('transitionend', handler)
})
@import "https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.0/css/bootstrap.min.css";
.toaster {
/*outline: solid 3px gray; /* DEBUG */
/*bottom: 150px; /* DEBUG */
overflow: visible;
}
.toast {
min-width: 350px;
position: absolute;
left: auto;
right: 12px;
top: 0;
transition:
opacity 0.4s ease-out 0.1s,
transform 0.5s cubic-bezier(0.2, 0, 0, 1.1);
pointer-events: none; /* block clicks when not shown */
-webkit-backdrop-filter: blur(4px); /* override 10px */
backdrop-filter: blur(4px); /* override 10px */
}
.toast.show {
z-index: 1;
pointer-events: auto;
}
.toast .close {
margin-right: -0.8rem;
}
<main class="container">
<h1>
{{this.appName}}
</h1>
{{outlet}}
<button class="btn btn-outline-secondary" type="button" {{on "click" this.makeToast}}>
Test
</button>
<Toaster @toasts={{this.toasts}} @onToastDismiss={{this.removeToast}} />
</main>
{
"version": "0.17.1",
"EmberENV": {
"FEATURES": {},
"_TEMPLATE_ONLY_GLIMMER_COMPONENTS": true,
"_APPLICATION_TEMPLATE_WRAPPER": false,
"_JQUERY_INTEGRATION": false
},
"options": {
"use_pods": false,
"enable-testing": false
},
"dependencies": {
"ember": "3.18.1",
"ember-template-compiler": "3.18.1",
"ember-testing": "3.18.1"
},
"addons": {
"@glimmer/component": "1.0.0",
"ember-modifier": "1.0.3",
"@ember/render-modifiers": "1.0.2"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment