Sarah Drasner
CSS-Tricks,
IBM,
Microsoft, Salesforce
Smashing Magazine,
NetMag,
Zillow, Workflo,
O’Reilly, Frontend Masters, & Mule Design
Microsoft
“We’ve evolved to perform actions that flow more or less seamlessly.
"We aren’t wired to deal with the fits and starts of human-computer interaction.”
Sensory memory: Your occipital lobe (AKA “the memory store”) works in 100ms bursts.
-Tammy Everts
consider how things look to your user
Spatial or otherwise
Paul Bakaus
More in my CSS-Tricks Article
this pen.
Creates a relationship between the data in the instance/component and a form input, so you can dynamically update values
Directives
Accepting user input and managing it in a responsible manner
new Vue({
el: '#app',
data() {
return {
message: 'This is a good place to type things.'
}
}
});
<div id="app">
<h3>Type here:</h3>
<textarea v-model="message" rows="5" maxlength="75"/>
<p>{{ message }}</p>
</div>
This pen.
<app-modal v-if="isShowing">
...
</app-modal>
This pen.
<transition name="fade">
<app-modal v-if="isShowing">
...
</app-modal>
</transition>
<app-modal v-if="isShowing">
...
</app-modal>
Encapsulate what is changing declaratively
Default 'v-' prefix, otherwise name="foo"
Example:
.v-enter-active {
transition: opacity 1s ease;
}
.fade-enter-active, .fade-leave-active {
transition: opacity 0.25s ease-out;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
Reusable for other components
This pen.
But...
<div :class="[isShowing ? blurClass : '', bkClass]">
<h3>Let's trigger this here modal!</h3>
<button @click="toggleShow">
<span v-if="isShowing">Hide child</span>
<span v-else>Show child</span>
</button>
</div>
.bk {
transition: all 0.05s ease-out;
}
.blur {
filter: blur(2px);
opacity: 0.4;
}
This pen.
Transition Modes
🏆
🏆
This pen
Without transition modes
The current element waits until the new element is done transitioning in to fire
The current element transitions out and then the new element transitions in.
<transition name="flip" mode="out-in">
<slot v-if="!isShowing"></slot>
<img v-else src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/28963/cartoonvideo14.jpeg" />
</transition>
HTML
enter-active-class="toasty"
leave-active-class="bounceOut"
Still <transition /> component, but
This pen.
<transition
enter-active-class="bouncein"
leave-active-class="rollout">
<div v-if="isShowing">
<app-child class="child"></app-child>
</div>
</transition>
Bounce a ball
@mixin ballb($yaxis: 0) {
transform: translate3d(0, $yaxis, 0);
}
@keyframes bouncein {
1% { @include ballb(-400px); }
20%, 40%, 60%, 80%, 95%, 99%, 100% { @include ballb() }
30% { @include ballb(-80px); }
50% { @include ballb(-40px); }
70% { @include ballb(-30px); }
90% { @include ballb(-15px); }
97% { @include ballb(-10px); }
}
.bouncein {
animation: bouncein 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
}
.ballmove-enter {
@include ballb(-400px);
}
Keep it DRY
<transition
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@enter-cancelled="enterCancelled"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
@leave-cancelled="leaveCancelled"
:css="false">
</transition>
Custom Naming
<transition
@enter="enterEl"
@leave="leaveEl"
:css="false">
<!-- put element here-->
</transition>
methods: {
enterEl(el, done) {
//entrance animation
done();
},
leaveEl(el, done) {
//exit animation
done();
},
}
This pen.
<transition @before-enter="beforeEnter" @enter="enter" :css="false">
<p class="booktext" v-if="load">
{{ message }}
</p>
</transition>
new Vue({
...
methods: {
beforeEnter(el) {
TweenMax.set(el, {
transformPerspective: 600,
perspective: 300,
transformStyle: "preserve-3d",
autoAlpha: 1
});
},
enter(el, done) {
tl.add("drop");
for (var i = 0; i < wordCount; i++) {
tl.from(split.words[i], 1.5, {
z: Math.floor(Math.random() * (1 + 150 - -150) + -150),
ease: Bounce.easeOut
}, "drop+=0." + (i/ 0.5));
...
}
}
});
FLIP stands for First, Last, Invert, Play
Rosario's article
<transition-group name="cell" tag="div" class="container">
<div v-for="cell in cells" :key="cell.id">
{{ cell.number }}
</div>
</transition-group>
From the guide
Snipcart's post
& Vue's Reactivity System
Reactive programming is programming with asynchronous data streams.
A stream is a sequence of ongoing events ordered in time that offer some hooks with which to observe it.
When we use reactive premises for building applications, this means it's very easy to update state in reaction to events.
More Resources:
Despite the name, React is not Reactive- it uses a "pull" approach (rather than "push")
We're going to 'watch' any data property declared on the Vue instance
SVG is good for this because it's built with MATH
SVG!
Built with math
<!--xaxis -->
<g targetVal="targetVal" class="xaxis">
<line x1="0" y1="1" x2="350" y2="1"/>
<g v-for="(select, index) in targetVal">
<line y1="0" y2="7" v-bind="{ 'x1':index*10, 'x2':index*10 }"/>
<text v-if="index % 5 === 0" v-bind="{ 'x':index*10, 'y':20 }">{{ index }}</text>
</g>
</g>
watch: {
selected: function(newValue, oldValue) {
var tweenedData = {}
var update = function() {
let obj = Object.values(tweenedData);
obj.pop();
this.targetVal = obj;
}
var tweenSourceData = { onUpdate: update, onUpdateScope: this}
for (let i = 0; i < oldValue.length; i++) {
let key = i.toString()
tweenedData[key] = oldValue[i]
tweenSourceData[key] = newValue[i]
}
TweenMax.to(tweenedData, 1, tweenSourceData)
}
}
watch: {
checked() {
let period = this.timeVal.slice(-2),
hr = this.timeVal.slice(0, this.timeVal.indexOf(":"));
const dayhr = 12,
rpos = 115,
rneg = -118;
if ((period === 'AM' && hr != 12) || (period === 'PM' && hr == 12)) {
this.spin(`${rneg - (rneg/dayhr) * hr}`)
this.animTime(1-hr/dayhr, period)
} else {
this.spin(`${(rpos/dayhr) * hr}`)
this.animTime(hr/dayhr, period)
}
}
},
methods: {
//this formats the hour info without a library
getCurrentHour(zone) {
let newhr = new Date().toLocaleTimeString('en', {
hour: '2-digit',
minute: '2-digit',
hour12: true,
timeZone: zone
})
return newhr
},
...
}
Side note: get rid of big libraries
React to events, change an animation mid-stream
Interpolation with style bindings- this pen
In the instance:
new Vue({
el: '#app',
data() {
return {
x: 0,
y: 0
}
},
methods: {
coords(e) {
this.x = e.clientX / 10;
this.y = e.clientY / 10;
},
}
})
In the template:
<div id="contain" :style="{ perspectiveOrigin: `${x}% ${y}%` }">
Emotions are tied to your limbic system and easier to remember
This pen.
<div id="app" @mousemove="coordinates">
coordinates(e) {
const audio = new Audio('https://s3-us-west-2.amazonaws.com/s.cdpn.io/28963/Whoa.mp3'),
walleBox = document.getElementById('walle').getBoundingClientRect(),
walleCoords = walleBox.width / 2 + walleBox.left;
...
TweenMax.set("#eyes", {
scaleX: 1 + (1 - e.clientX / walleCoords) / 5
});
TweenMax.set("#walle", {
x: ((e.clientX / walleCoords) * 50) - 40
});
this.startArms.progress(1 - (e.clientX / walleCoords)).pause();
}
},
In <template>
In Vue Instance
Encapsulate what is changing - repo
Encapsulate what is changing:
export const store = new Vuex.Store({
state: {
showWeather: false,
template: 0
},
mutations: {
toggle: state => state.showWeather = !state.showWeather,
updateTemplate: (state) => {
state.showWeather = !state.showWeather;
state.template = (state.template + 1) % 4;
}
}
});
<transition @leave="leaveDialog" :css="false">
<app-dialog v-if="showWeather"></app-dialog>
</transition>
<transition @leave="leaveDroparea" :css="false">
<g v-if="showWeather">
<app-droparea v-if="template == 1"></app-droparea>
<app-windarea v-else-if="template == 2"></app-windarea>
<app-rainbowarea v-else-if="template == 3"></app-rainbowarea>
<app-tornadoarea v-else></app-tornadoarea>
</g>
</transition>
export default {
computed: {
template() {
return this.$store.state.template;
}
},
methods: {
toggle() {
this.$store.commit('toggle');
}
},
mounted() {
//enter weather
const tl = new TimelineMax();
tl.add("enter");
tl.fromTo("#dialog", 2, {
opacity: 0
}, {
opacity: 1
}, "enter");
tl.fromTo("#dialog", 2, {
rotation: -4
}, {
rotation: 0,
transformOrigin: "50% 100%",
ease: Elastic.easeOut
}, "enter");
}
}
Lifecycle hooks
This pen.
const Child = {
beforeCreate() {
console.log("beforeCreate!");
},
...
};
Vue.directive('tack', {
bind(el, binding, vnode) {
el.style.position = 'fixed'
}
});
<p v-tack>I will now be tacked onto the page</p>
Vue.directive('tack', {
bind(el, binding, vnode) {
el.style.position = 'fixed'
el.style.top = binding.value + 'px'
}
});
<div id="app">
<p>Scroll down the page</p>
<p v-tack="70">Stick me 70px from the top of the page</p>
</div>
Vue.directive('tack', {
bind(el, binding, vnode) {
el.style.position = 'fixed';
el.style.top = binding.value.top + 'px';
el.style.left = binding.value.left + 'px';
}
});
<p v-tack="{ top: '40', left: '100' }">Stick me 40px from the top of the
page and 100px from the left of the page</p>
Pass arguments
Vue.directive('scroll', {
inserted: function(el, binding) {
let f = function(evt) {
if (binding.value(evt, el)) {
window.removeEventListener('scroll', f);
}
};
window.addEventListener('scroll', f);
},
});
// main app
new Vue({
el: '#app',
methods: {
handleScroll: function(evt, el) {
if (window.scrollY > 50) {
TweenMax.to(el, 1.5, {
y: -10,
opacity: 1,
ease: Sine.easeOut
})
}
return window.scrollY > 100;
}
}
});
<div class="box" v-scroll="handleScroll">
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. A atque amet harum aut ab veritatis earum porro praesentium ut corporis. Quasi provident dolorem officia iure fugiat, eius mollitia sequi quisquam.</p>
</div>
Custom Directives + D3
export default {
methods: {
totalImpact: function(evt, el) {
if (window.scrollY > 1100) {
TweenMax.to(el, 0.75, {
opacity: 0
})
let circ = d3.selectAll("circle")
.attr("cx", function(d) {
let lat = d["Longitude (Deg)"];
if (lat.includes("E")) {
return midX - parseInt(lat) * incByW;
} else {
return midX + (parseInt(lat) * incByW);
}
})
...
.attr("r", 5)
.attr("fill", "url(#radgrad)")
}
return window.scrollY > 1300;
},
Update the circle's coordinates
<div class="box accelerate impact" v-dscroll="totalImpact">
<h3>Total Impact</h3>
<p>Most alksdjflkjasd laksdjfl;kasjdf laksd falksdjf lsdj f</p>
</div>
By rendering on the server, you can cache the final shape of your data
-Karl Seguin
npm install -g vue-cli
--------
vue init nuxt/starter my-project
cd my-project
yarn
npm run dev
Templates in the pages directory
<nuxt-link to="/product">Product</nuxt link>
Transition hook already available
name="page"
.page-enter-active, .page-leave-active {
transition: all .25s ease-out;
}
.page-enter, .page-leave-active {
opacity: 0;
transform: scale(0.95);
transform-origin: 50% 50%;
}
Animation as well
.page-enter-active {
animation: acrossIn .45s ease-out both;
}
.page-leave-active {
animation: acrossOut .65s ease-in both;
}
export default {
transition: {
mode: 'out-in',
css: false,
enter (el, done) {
let tl = new TimelineMax({ onComplete: done }),
spt = new SplitText('h1', {type: 'chars' }),
chars = spt.chars;
TweenMax.set(chars, {
transformPerspective: 600,
perspective: 300,
transformStyle: 'preserve-3d'
})
tl.add('start')
tl.from(el, 0.8, {
scale: 0.9,
transformOrigin: '50% 50%',
ease: Sine.easeOut
}, 'start')
...
tl.timeScale(1.5)
}
...
Functions as a Service
is an actually interesting thing with the most clickbaity title
Demo and repo w/ OSS code
getGeo(makeIterator(content), (updatedContent, err) => {
if (!err) {
// we need to base64 encode the JSON to embed it into the PUT (dear god, why)
let updatedContentB64 = new Buffer(
JSON.stringify(updatedContent, null, 2)
).toString('base64');
let pushData = {
path: GH_FILE,
message: 'Looked up locations, beep boop.',
content: updatedContentB64,
sha: data.sha
};
...
};
We're going to retrieve the geo-information for each item in the original data
function getGeo(itr, cb) {
let curr = itr.next();
if (curr.done) {
// All done processing- pass the (now-populated) entries to the next callback
cb(curr.data);
return;
}
let location = curr.value.Location;
Single element
<div id="container"></div>
Call on mounted
mounted() {
//we have to load the texture when it's mounted and pass it in
let earthmap = THREE.ImageUtils.loadTexture('/world7.jpg');
this.initGlobe(earthmap);
}
//from
const geometry = new THREE.SphereGeometry(200, 40, 30);
//to
const geometry = new THREE.IcosahedronGeometry(200, 0);
makes it extraordinarily simple
to create complex and beautiful interactions
that feel seamless for our users.
We can connect states and reduce cognitive load for things that are changing in our application with ease.
These slides:
slides.com/sdrasner/animating-vue-e17