A happy day deserves another happy blog post, right? A few weeks ago I signed up for an interesting listserv called Data is Plural. This is a weekly newsletter of "interesting" datasets. I put that in quote not because it's dangerous or anything, but the datasets are incredibly varied from week to week. I want to thank my super-smart former coworker Erin McKean for sharing this resource.
In the last newsletter, one of the datasets caught my eye - HappyDB. HappyDB is a collection of one hundred thousand different "moments". Simply put - 100K little messages of joy. So for example:
My Mother gave me a surprise visit at my home.
And...
I got engaged with my parents support.
To be clear, this isn't all deep or emotional stuff. Sometimes the data contains stuff like this:
Frankly, I am constipated over the past 2 days and not able to defecate, today my bowel got cleared that moment really makes me feel happy and my bowel feels well that moment makes me feel happy really.
So yeah, there's that, but I'm going to focus on the fact that this is a giant pile of happiness. That's good, right? I thought it would be cool to turn this data into a little Vue.js application that randomly displayed one happy message at a time.
Now - as always - I know this isn't a practical idea. But I also knew that I'd learn a bit while building it. The first issue I ran into was getting the data into a form that would be usable by the web. The data is in CSV, and while I know libraries exist for this, I also figured I could do a one time conversion to JSON locally. I also knew that 100K strings would be a bit large. I decided to take a "slice" of the original data since no one was going to sit there and watch my app go through all 100k messages. With that in mind, here's what I built to prepare my data.
const fs = require('fs');
const parse = require('csv-parse/lib/sync');
let contents = fs.readFileSync('./cleaned_hm.csv','UTF-8');
let rawData = parse(contents, { from:2, to:5001});
let data = [];
data = rawData.map(d => {
return d[4];
});
fs.writeFileSync('./quotes.json', JSON.stringify(data));
console.log('Done - wrote '+data.length+ ' records.');
Yeah, not terribly complex. I used a nice little csv parsing library, read the file, and worked with 5000 entries. The end result was a JSON file that "weighed" 482K which is pretty big, but I thought it was acceptable. In theory I could zip it and use a client-side JavaScript Zip parser (yes, of course one exists), but for a fun little demo I thought that was overkill. Another idea I considered and threw out was storing the data in IndexedDB. I actually think that would make sense, but again, KISS (Keep it Simple, Stupid).
For the front end, I decided to use a simple transition. I'd show a message - fade it out and then fade in the new one (selected by random). This turned out to be difficult for me. Vue has native support for transitions, but the docs really confused me. To be clear, there's nothing particularly wrong with them, but I had a hard time wrapping my head around them. Also, look at this:
<transition name="fade">
<p v-if="show">hello</p>
</transition>
The fact that a visual effect is in markup just didn't sit well in my head. I mean technically the v-if
part of the paragraph tag represents something that can appear and disappear, but it doesn't bug me like the transition does. And again, I'm not saying Vue is doing it wrong. Just that mentally - my brain had a hard time with it. I'll also point out that Sarah Drasner has an article on the feature that is worth checking out as well: Intro to Vue.js: Animations .
Alright, so with that said, let me share the code I wrote. First, the front end:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Happy</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Happy+Monkey" rel="stylesheet">
<link rel="stylesheet" type="text/css" media="screen" href="main.css" />
</head>
<body>
<div id="app" v-cloak>
<transition name="fade" mode="out-in">
<p :key="text" v-bind:style="{fontStyle:loading}">{{text}}</p>
</transition>
</div>
<div id="footer">Quotes are sourced from <a href="https://rit-public.github.io/HappyDB/">HappyDB</a>.</div>
<script src="https://unpkg.com/vue@2.5.13/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>
The app is really just one paragraph tag wrapped in a transition. Now let's look at the code.
const app = new Vue({
el:'#app',
data:{
index:-1,
quotes:[],
text:'Loading the Happy...',
loading:'italic'
},
created() {
fetch('quotes.json')
.then(res => res.json())
.then(res => {
this.quotes = res;
console.log('loaded');
this.newQuote();
setInterval(() => {
this.newQuote();
}, 6000);
});
},
methods:{
newQuote() {
index = getRandomInt(0, this.quotes.length-1);
app.text = this.quotes[index];
app.loading='';
}
}
});
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min)) + min; //The maximum is exclusive and the minimum is inclusive
}
The code handles loading in the JSON file, parsing it, and setting the local quotes array equal to the result. Then I select a random one and kick off an interval to load a new one every six seconds. Definitely not rocket science, but you can check out the live version here: https://cfjedimaster.github.io/webdemos/happy/.
This one was beautiful:
I took my mom to a small hill at Lake Elsinore that was filled with a lot of flowers.
Archived Comments
Nice. How about support for manually swiping to the next quote?
To clarify: an override swipe, but if I do nothing it still runs on the timer.
So that's a great question. Because initially it sounds simple - just listen for the event and do X. But then you have to ensure you cancel the next automatic change. Let me chew on that. :)
Actually, I think this is what I'd do. Have a flag: skipUpdate=false. When you swipe, tap, whateer, do an update and set skipUpdate to true. In the interval, if the value is true, don't update, and reset it to false.
Bam - done.
Great idea with fun content. I'm newer to Vue than you are and it's been great reading your posts. I checked out your demo and, as luck would have it, the second message as a super long one. Before I got half way through reading it (6000 ms in your code) it transitioned to the next one.
First, my psyche was left in complete suspense about the ending of the happy moment (did it stay happy, or take a dark turn?? I'll never know!), and second I was wondering what a simple way to affect some dynamic timing would be. What do you think about calculating the # of milliseconds based on a word count? It feels like a hack and could go complete wrong... but at least it's a simple algorithm. Maybe a trial idea of, say, 2000 ms per 10 words. Or 200 ms per each word. Just a thought.
In order for it to be dynamic, you would have to switch from an interval to a timeout, and then ensure you call it at the end of every new message load. Doable for sure!
Hi Raymond,
I have some VUE.js questions.
1. How to remove header and footers in one component.. like for example I don't want to use the header (include: header, logo, navigation.) and the footer. which this is common to all pages. But some pages not required, but of course I have already choped those into components, since I always use the css and js files on each page.
2. Kinda related with Q1. I have a modal box, that I want to call a URL (component.vue), and I want to update the modal content on whatever the component contents, so I don't want to use header and footer (everything including the css and js files..)
Hopefully its possible, and you have idea on how to do it.
Thanks in advanced!
~zar
Hey Zar:
It appears as if you are asking general questions about Vue, and not questions related to this post. In general, I'd suggest posting these to the Vue forum: https://forum.vuejs.org/. That would be more appropriate.
1. If you have a component that uses headers and footers and need to make it conditional, then I'd simply allow for a passed in property to determine if they should be shown.
2. No idea on that one. Obviously you can load in stuff via a simple Ajax call, but you want to dynamically run a Vue component here and I'm not sure how that would be done.