feat: initial nav

This commit is contained in:
Andrew
2015-04-01 19:38:27 -06:00
parent fee7520291
commit fd74dc937e
5 changed files with 132 additions and 81 deletions

View File

@ -1,16 +1,41 @@
.nav-view { .nav-view {
opacity: 0;
transition: 0.25s;
width: 100%;
height: 100%;
}
.nav-view.in {
opacity: 1;
}
.nav-view.out {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
opacity: 0; width: 100%;
height: 100%;
display: none;
&.shown {
display: block;
}
&[animate] {
display: block;
transition: 1s linear;
}
&[animate=push-enter] {
transform: translate3d(100%,0,0);
&.start {
transform: translate3d(0,0,0);
}
}
&[animate=pop-enter] {
transform: translate3d(-100%,0,0);
&.start {
transform: translate3d(0,0,0);
}
}
&[animate=push-leave] {
transform: translate3d(0,0,0);
&.start {
transform: translate3d(-100%,0,0);
}
}
&[animate=pop-leave] {
transform: translate3d(0,0,0);
&.start {
transform: translate3d(100%,0,0);
}
}
} }

View File

@ -1,7 +1,7 @@
import {Component, Template, For} from 'angular2/angular2' import {Component, Template, For} from 'angular2/angular2'
import {ComponentConfig} from 'ionic2/config/component-config' import {ComponentConfig} from 'ionic2/config/component-config'
import {NavView} from 'ionic2/components/nav-view/nav-view' import {NavView} from 'ionic2/components/nav-view/nav-view'
import * as util from 'ionic2/util' import {array as arrayUtil, dom as domUtil} from 'ionic2/util'
@Component({ @Component({
selector: 'ion-nav-viewport', selector: 'ion-nav-viewport',
@ -11,15 +11,15 @@ import * as util from 'ionic2/util'
}) })
@Template({ @Template({
inline: ` inline: `
<section class="nav-view" *for="#item of stack.rawItems()" [item]="item"> <section class="nav-view" *for="#item of creator._array" [item]="item">
</section> </section>
`, `,
directives: [NavView, For] directives: [NavView, For]
}) })
export class NavViewport { export class NavViewport {
constructor( constructor() {
) { this.stack = []
this.stack = new NavStack(); this.creator = new ItemCreator()
} }
set initial(Class) { set initial(Class) {
@ -39,9 +39,18 @@ export class NavViewport {
* @param view the new view * @param view the new view
* @param shouldAnimate whether to animate * @param shouldAnimate whether to animate
*/ */
// TODO only animate if state hasn't changed
// TODO make sure the timing is together
// TODO allow starting an animation in the middle (eg gestures). Leave
// most of this up to the animation's implementation.
push(Class, opts = {}) { push(Class, opts = {}) {
//TODO animation let item = new NavItem(Class, opts)
return this.stack.push(Class) this.stack.push(item)
return this.creator.instantiate(item).then(() => {
let current = this.getPrevious(item)
current && current.pushLeave()
return item.pushEnter()
})
} }
/** /**
@ -50,8 +59,16 @@ export class NavViewport {
* @param shouldAnimate whether to animate * @param shouldAnimate whether to animate
*/ */
pop() { pop() {
// TODO animation let current = this.stack.pop()
return this.stack.pop() let previous = this.stack[this.stack.length - 1]
previous.popEnter()
current.popLeave().then(() => {
this.creator.destroy(current)
})
}
getPrevious(item) {
return this.stack[ this.stack.indexOf(item) - 1 ]
} }
// Animate a new view *in* // Animate a new view *in*
@ -65,79 +82,70 @@ export class NavViewport {
} }
class NavStack { class ItemCreator {
constructor(navView) { constructor() {
// array to communicate with angular for loop. When an element is in this array, it really
// exists in the DOM.
this._array = [] this._array = []
} }
rawItems() { instantiate(navItem) {
return this._array this._array.push(navItem)
return navItem.waitForSetup()
} }
destroy(navItem) {
push(ComponentClass) { return arrayUtil.remove(this._array, navItem)
const item = new NavStackItem(ComponentClass)
let last = this._array[this._array.length - 1]
this._array.push(item)
return item.setupPromise.then(() => {
// Once the item is successfully instantiated, add it to our public history
item.animateIn()
last && last.animateOut()
})
}
pop() {
// public registry: instantly remove to make the removal seem 'synchronous' for our data
const current = this._array[this._array.length - 1]
const previous = this._array[this._array.length - 2]
current.animateOut().then(() => {
util.array.remove(current)
})
return previous && previous.animateIn()
} }
} }
class NavStackItem { class NavItem {
constructor(ComponentClass) { constructor(ComponentClass) {
this.setupPromise = new Promise((resolve) => { this.Class = ComponentClass
this.finishSetup = (navView, componentInstance) => { this._setupPromise = new Promise((resolve) => {
this._resolveSetupPromise = resolve
})
}
waitForSetup() {
return this._setupPromise
}
finishSetup(navView, componentInstance) {
this.navView = navView this.navView = navView
this.instance = componentInstance this.instance = componentInstance
resolve() this._resolveSetupPromise()
} }
setAnimation(state) {
if (!state) {
this.navView.domElement.removeAttribute('animate')
this.navView.domElement.classList.remove('start')
} else {
this.navView.domElement.setAttribute('animate', state)
}
}
setShown(isShown) {
this.navView.domElement.classList[isShown?'add':'remove']('shown')
}
startAnimation() {
this.navView.domElement.classList.add('start')
}
_animate({ isShown, animation }) {
this.setAnimation(animation)
this.setShown(isShown)
// We have to wait two rafs for the element to show. Yawn.
return domUtil.rafPromise().then(domUtil.rafPromise).then(() => {
this.startAnimation()
return domUtil.transitionEndPromise(this.navView.domElement).then(() => {
this.setAnimation(null)
})
}) })
this.Class = ComponentClass
} }
setInstance(instance) { pushEnter() {
this.instance = instance return this._animate({ isShown: true, animation: 'push-enter' })
}
popEnter() {
return this._animate({ isShown: true, animation: 'pop-enter' })
}
pushLeave() {
return this._animate({ isShown: false, animation: 'push-leave' })
}
popLeave() {
return this._animate({ isShown: false, animation: 'pop-leave' })
} }
setNavView(navView) {
this.navView = navView
} }
animateIn() {
this.navView.domElement.classList.remove('out')
this.navView.domElement.classList.add('in')
return new Promise(resolve => {
this.navView.domElement.addEventListener('transitionend', (ev) => {
if (ev.target !== this.navView.domElement) {
return
}
resolve()
})
})
}
animateOut() {
this.navView.domElement.classList.add('out')
this.navView.domElement.classList.remove('in')
return new Promise(resolve => {
this.navView.domElement.addEventListener('transitionend', (ev) => {
if (ev.target !== this.navView.domElement) {
return
}
resolve()
})
})
}
}

View File

@ -1,3 +1,3 @@
<ion-view nav-title="Second Page"> <ion-view nav-title="Second Page">
<button (click)="prevPage()">Pop This Page</button> <button (click)="pop()">Pop (Go back)</button>
</ion-view> </ion-view>

View File

@ -15,7 +15,7 @@ export class SecondPage {
) { ) {
this.viewport = viewport this.viewport = viewport
} }
prevPage() { pop() {
this.viewport.pop() this.viewport.pop()
} }
} }

View File

@ -32,3 +32,21 @@ if (window.ontransitionend === undefined && window.onwebkittransitionend !== und
css.transition = 'transition' css.transition = 'transition'
css.transitionEnd = 'tranistionend' css.transitionEnd = 'tranistionend'
} }
export function transitionEndPromise(el:Element) {
return new Promise(resolve => {
css.transitionEnd.split(' ').forEach(eventName => {
el.addEventListener(css.transitionEnd, onTransitionEnd)
})
function onTransitionEnd(ev) {
// Don't allow bubbled transitionend events
if (ev.target !== el) {
return
}
css.transitionEnd.split(' ').forEach(eventName => {
el.removeEventListener(css.transitionEnd, onTransitionEnd)
})
resolve(ev)
}
})
}