Building a RSS Viewer With Vue: Part 2

Welcome to Part 2 of this mini-series on building a RSS viewer with Vue. In the last post, I walked through how I built my demo using Vue.js and Vuetify on the front end and Webtask on the back end. When I built that initial version, I knew it was exactly thatmdash;an “initial” version. I took some time to work on a few updates, and while I won’t dare call this a “perfect” version, I do think I’ve made some improvements and I’d like to share them with you.

Article Series:

  1. Setup and first iteration
  2. Refinements and final version (This Post)

Before I get started, here are links to the completed demo and source code.

View Demo View Code

Feel free to fork, file PRs, and report bugs to your heart’s content!

The Plan

When I shared the initial version in Part 1, I outlined some ideas to improve the RSS reader, including:

  • Moving to Vuex.
  • Starting to switch to components in the layout. (Well, I was already using Vuetify components, but I meant custom components for my application.)
  • Using IndexedDB to store feed items for quicker access and offline support.

That was the plan, and like most plans, I wasn’t necessarily able to hit everything in this update (and I’ll explain why at the end). But hopefully you’ll see the improvements as a general “moving in the right direction” for the application. With that out of the way, let’s get started!

Implementing Vuex

I’ll start off discussing the biggest change to the application, the addition of Vuex. As I said in the previous post, Vuex describes itself as a “state management pattern + library” on their “What is Vuex” page. No offense to their documentation, but I had a difficult time wrapping my head around exactly what this meant, from a practical sense.

After having using it in a few small projects now, I’m coming to appreciate what it provides. To me, the core benefit is providing a central interface to your data. If I’ve got a basic Vue app working with an array of values, I may have multiple different methods that modify it. What happens when I begin to have certain rules that must be applied before the data changes? As a simple example, imagine an array of RSS feeds. Before I add a new one, I want to ensure it doesn’t already exist in the list. If I have one method that adds to the feed list, that isn’t a problem, but if I have more, it may become cumbersome to keep that logic in sync across the different methods. I could simply build a utility to do this, but what happens when I have other components in play as well?

While it is absolutely not a one-to-one comparison, I feel like Vuex reminds me of how Providers or Services work in Angular. If I ever want to do work with any data, I’ll ensure I use a central provider to handle all access to that data. That’s how I look at Vuex.

So the big change in this application was to migrate all the data related items to a store. I began by adding the library to my HTML:

<script src="https://unpkg.com/vuex"></script>

Woot! Half-way done! (OK maybe not.)

I then created an instance of my store in my JavaScript file:

const feedStore = new Vuex.Store({ // lots of stuff here
});

and included it in my Vue app:

let app = new Vue({ el: '#app', store:feedStore, // lots of stuff here too...
});

Now comes the interesting part. Any time my Vue application needs data, which primarily consists of the list of feeds and the items from those feeds, it’s going to ask the store for them. So, for example, my feeds value is now computed:

feeds() { return feedStore.state.feeds;
},

This is now defined in the state portion of my store:

state: { allItems: [], feeds: [], selectedFeed: null
},

Notice that feeds defaults to an empty array. I had previously used the created event of my Vue app to read in the data from localStorage. Now, I ask the store to do that:

created() { feedStore.dispatch('restoreFeeds');
},

Back in the store, the logic is pretty much the same:

restoreFeeds(context) { let feedsRaw = window.localStorage.getItem('feeds'); if(feedsRaw) { try { let feeds = JSON.parse(feedsRaw); context.state.feeds = feeds; context.state.feeds.forEach(f => { context.dispatch('loadFeed', f); }); } catch(e) { console.error('Error restoring feed json'+e); // bad json or other issue, nuke it window.localStorage.removeItem('feeds'); } }
},

I say “pretty much the same” except now I’m doing a bit of error-checking on the value read in from localStorage. But here’s the crucial bit. I already said I failed in terms of switching to IndexedDB, but in theory, I could build a third version of this application with an updated store and my Vue app won’t know the difference. And that’s where I started to get really excited. The more I worked, the more “dumb” my Vue app became and the less tied it was to any particular implementation of storage. Let’s look at the complete Vue app now:

let app = new Vue({ el: '#app', store:feedStore, data() { return { drawer:true, addFeedDialog:false, addURL:'', urlError:false, urlRules:[], selectedFeed:null } }, computed: { showIntro() { return this.feeds.length == 0; }, feeds() { return feedStore.state.feeds; }, items() { return feedStore.getters.items; } }, created() { feedStore.dispatch('restoreFeeds'); }, methods:{ addFeed() { this.addFeedDialog = true; }, allFeeds() { feedStore.dispatch('filterFeed', null); }, addFeedAction() { this.urlError = false; this.urlRules = []; feedStore.dispatch('addFeed', {url:this.addURL}) .then(res => { this.addURL = ''; this.addFeedDialog = false; }) .catch(e =>{ console.log('err to add', e); this.urlError = true; this.urlRules = ["URL already exists."]; }); }, deleteFeed(feed) { feedStore.dispatch('deleteFeed', feed); }, filterFeed(feed) { feedStore.dispatch('filterFeed', feed); } }
})

What you’ll notice is that pretty much all of the actual logic is now gone and all I’m really doing here is here, although I apologize for lumping everything together in one file.

Adding a Component

One of the other changes I mentioned was beginning to “component-ize” the view layer. I ended up only making one component, feed-item. This reduced the total number of lines in the HTML a bit:

<v-flex xs12 v-for="item in items" :key="item.link"> <feed-item :title="item.title" :content="item.content" :link="item.link" :feedtitle="item.feedTitle" :color="item.feedColor" :posted="item.pubDate"></feed-item>
</v-flex>

It isn’t a huge change by any means, but it did make it bit easier for me when I started working on the feed display. As I’m not using a fancy builder yet, I defined my component straight in JavaScript like so:

Vue.component('feed-item', { props:[ 'color','title','content','link','feedtitle', 'posted' ], template: ` <v-card :color="color"> <v-card-title primary-title> <div class="headline">{{title}} ({{posted | dtFormat}})</div> </v-card-title> <v-card-text> {{content | maxText }} </v-card-text> <v-card-actions> <v-btn flat target="_new" :href="link">Read on {{feedtitle}}</v-btn> </v-card-actions> </v-card> `
});

I’m not doing anything at all fancy in heremdash;there’s no dynamic logic or events or anything like that, but I could certainly add that later where it makes sense. I did finally get around to adding the date and time of posting. If you’re curious about how I built the formatter used for it, read my article Build A i18n Filter Using Vue.js & Native Web Specs.”

The Power of Delete!

Oh, and I finally added a way to delete feeds:

Trash can icon, FTW!

This just fires off a method on the Vue object that, in turn, fires off a call to the store that takes care of removing the feed and items from the UI and then persisting it. A small thing, but, wow, did I wish I had that in the first version when testing. And here is a final shot of everything:

The awesome app in all it’s awesomeness

Next Steps… and What Happened to IndexedDB?

As I said in the beginning, this version is still not perfect but I definitely feel better about it. I highly encourage you to share tips, suggestions, and bug reports in the comments below or on the GitHub repo.

So what happened to IndexedDB support? The issue I ran into was how to properly initialize the database. Vuex stores don’t have a concept of a created process. I could have done something like this:

// dummy code for getting feeds
dispatch('getDB')
.then(() => // do stuff
);

Where the getDB action returns a promise and handles doing a one-time IndexedDB opening and storing the value in the state. I may give this a shot later, and again, what I love about Vuex is that I know I can safely do that without interfering with the rest of the application.


Article Series:

  1. Setup and first iteration
  2. Refinements and final version (This Post)

The post Building a RSS Viewer With Vue: Part 2 appeared first on CSS-Tricks.

Building a RSS Viewer With Vue: Part 1

As I explore, learn, and most importantly, play with Vue.js, I’ve been building different types of apps as a way to get practice with and improve my use of it. A few weeks ago, I was reading about the shut down of Digg’s RSS Reader and while great alternatives exist, I thought it would be fun to build my own with Vue. In this article, I’m going to explain how I put it together and also what’s wrong with it. I knew getting into this that I was going to make some compromises, so the plan is to follow up this version with a nicer one in a follow-up post.

Article Series:

  1. Setup and first iteration (This Post)
  2. Refinements and final version

Let’s start by looking at the app and explaining the various components.

View Demowebpack for this application — just a simple script include with no build process.

The Vuetify, a very nice material design framework that is easy to use. I’m still learning it, so you can be sure that my design could be better, though I’m really happy with how it looks now.

Persistence is done via localStorage. I store the feed metadata retrieved from the RSS feed. This typically includes things like the name of the site, the main URL, and a description. I do not store feed items which means every time you load the site, I re-fetch items. The next version will keep items locally using IndexedDB.

So, how do I load feed information? I could just make a network request to the URL, but most RSS feeds aren’t making use of CORS which means the browser would be blocked from loading it. To get around this, I wrote a quick serverless function with Webtask. It handles both creating a CORS-friendly endpoint as well as parsing the feeds’ XML into friendly JSON.

Now that I’ve covered the various parts of the application, let’s start looking at the code!

The Layout

Let’s start with the layout. As I said, I’m using Vuetify for the UI. I started off using the dark sample layout. This is what creates the header, footer, and left column used for the menu.

Application template

I used the card component for individual feed items. I’m not quite happy with the layout here. For example, I don’t have publication dates rendered yet because I had trouble finding a nice way to render it. I decided to simply punt and wait till the next version, which we’ll **see in Part 2 of this series.

Instead of dumping the entire source code on you at once, let’s look at the individual parts. First, here’s the introductory/help text before any feeds have been added:

<div v-if="showIntro"> <p> Welcome to the RSS Reader, a simple way to manage RSS feeds and read content. To begin using the RSS Reader, add your first feed by clicking the button below. </p> <p> <v-btn color="primary" large @click="addFeed"> <v-icon>add</v-icon> Add Feed </v-btn> </p>
</div>

When you do have feeds, items are displayed as a list of cards:

<v-container fluid grid-list-lg> <v-layout row wrap> <v-flex xs12 v-for="item in items"> <v-card :color="item.feedColor"> <v-card-title primary-title> <div class="headline">{{item.title}}</div> </v-card-title> <v-card-text> {{item.content | maxText }} </v-card-text> <v-card-actions> <v-btn flat target="_new" :href="item.link">Read on {{item.feedTitle}}</v-btn> </v-card-actions> </v-card> </v-flex> </v-layout>
</v-container>

Note the button for reading a feed item uses a target to open it up in a new tab.

To display feeds, I use a list component:

<v-list dense> <v-list-tile @click="allFeeds"> <v-list-tile-action> <v-icon>dashboard</v-icon> </v-list-tile-action> <v-list-tile-content> <v-list-tile-title>All Feeds</v-list-tile-title> </v-list-tile-content> </v-list-tile> <v-list-tile @click="filterFeed(feed)" v-for="feed in feeds" :value="feed == selectedFeed"> <v-list-tile-action> <v-icon :color="feed.color">bookmark</v-icon> </v-list-tile-action> <v-list-tile-content> <v-list-tile-title>{{ feed.title }} </v-list-tile-title> </v-list-tile-content> </v-list-tile> <v-list-tile @click="addFeed"> <v-list-tile-action> <v-icon>add</v-icon> </v-list-tile-action> <v-list-tile-content> <v-list-tile-title>Add Feed</v-list-tile-title> </v-list-tile-content> </v-list-tile>
</v-list>

Finally, here is the modal layout:

<v-dialog v-model="addFeedDialog" max-width="500px"> <v-card> <v-card-title>Add Feed</v-card-title> <v-card-text> Add the RSS URL for a feed below, or the URL for the site and I'll try to auto-discover the RSS feed. <v-text-field v-model="addURL" label="URL" :error="urlError" :rules="urlRules"></v-text-field> </v-card-text> <v-card-actions> <v-btn color="primary" @click.stop="addFeedAction">Add</v-btn> <v-btn color="primary" flat @click.stop="addFeedDialog=false">Close</v-btn> </v-card-actions> </v-card>
</v-dialog>

The Logic

Now for the fun part — JavaScript! As before, I’m not going to dump the entire file on you. Instead, let’s tackle it bit by bit.

On start up, I load any existing feeds that may have defined and then display the introduction text, if needed:

created() { this.restoreFeeds(); if (this.feeds.length === 0) this.showIntro = true;
},

The restoreFeeds method handles checking LocalStorage for existing feeds.

restoreFeeds() { let feeds = localStorage.getItem('feeds'); if (feeds) { this.feeds = JSON.parse(feeds); this.feeds.forEach((feed,idx) => { feed.color = colors[idx % (colors.length-1)]; this.loadFeed(feed); }); }
},

Note that this method handles assigning a color (which is a simple array) and then loading feed data.

Speaking of that, how do I handle loading RSS information? Currently there are two times where this happens. First is when you initially add the feed and second when you reload the application and the feed was already defined. In both cases, I call one URL — the serverless task defined with Webtask. This task will return everything — the metadata about the feed and the items itself. I only care about the metadata on the *first* call and, in theory, I could have made the code a bit quicker by removing the metadata at the server side and strip that out but it didn’t seem like it was worth the effort.

That serverless function is rather simple:

'use strict'; const Parser = require('rss-parser');
const parser = new Parser(); module.exports = function(context, cb) { let url = ''; if(context.body && context.body.url) url = context.body.url; if(context.query && context.query.url) url = context.query.url; if(url === '') cb(new Error('URL parameter not passed.')); console.log('gonna parse '+url); parser.parseURL(url) .then(feed => { console.log(feed); cb(null, {feed:feed}); }) .catch(e => { cb(e); }); }

All I’m doing here is wrapping the npm package rss-parser and that handles all the converting for me. The if statements you see in the beginning handle looking for the url parameter. When calling my webtask, I can either pass a query string variable or send it as part of a HTTP body. Either way, I simply use the rss-parser module and return the result.

The endpoint for this function is:

https://wt-c2bde7d7dfc8623f121b0eb5a7102930-0.sandbox.auth0-extend.com/getRss

You’re welcome to try it out yourself. You can see this being used in the method handling adding a feed:

addFeedAction() { this.urlError = false; this.urlRules = []; //first, see if new if(this.feeds.findIndex((feed) => { return (feed.rsslink === this.addURL); }) >= 0) { this.urlError = true; this.urlRules = ["URL already exists."]; return; } else { fetch(rssAPI+encodeURIComponent(this.addURL)) .then(res => res.json()) .then(res => { // ok for now, assume no error, cuz awesome this.addURL = ''; //assign a color first res.feed.color = colors[this.feeds.length % (colors.length-1)]; // ok, add the items (but we append the url as a fk so we can filter later) res.feed.items.forEach(item => { item.feedPk = this.addURL; item.feedColor = res.feed.color; this.allItems.push(item); }); // delete items delete res.feed.items; // add the original rss link res.feed.rsslink = this.addURL; this.feeds.push(res.feed); this.addFeedDialog = false; //always hide intro this.showIntro = false; //persist the feed, but not the items this.storeFeeds(); }); } },

This method first checks if a feed already exists and, if it doesn’t, it hits the serverless endpoint to get the details. I’ve got a bit of data duplication going on when I store items. I didn’t want to store items “under” a feed object and instead use a global Vue data value, allItems. Therefore, I copy the feed identifier and color into each item. The idea was to make it easier to do item display and filtering later. This feels “wrong” to me, but again, this is my first draft. I’m using a computed property for items and you can see that logic here:

items:function() { if(this.allItems.length === 0) return []; // filter let items = []; if(this.selectedFeed) { console.log('filtered'); items = this.allItems.filter(item => { return item.feedPk == this.selectedFeed.rsslink; }); } else { items = this.allItems; } items = items.sort((a, b) => { return new Date(b.isoDate) - new Date(a.isoDate); }); return items;
}

Looking at it now, I could gather my items from each feed instead of storing one global array, though I could address this later, if I want. I love that Vue gives me options for how to solve things like this.

Where Next?

When I started this article, I explicitly thought *this* *is* a first draft. I’ve pointed out things here and there that I like and don’t like, but what exactly do I plan for the next version?

  • I want to move all the data access into Vuex. Vuex is described as a “state management pattern + library” for Vue. If that doesn’t make much sense to you, don’t worry. I had no idea what it meant at first either. To me, Vuex provides a way to handle more complex data in an encapsulated manner. This becomes even more important as you start building more components that need to share data.
  • Speaking of components, I should consider making “item” a proper Vue component. That’s an easy win.
  • I want to start storing feed items in IndexedDB so you’ll get content the second you open the application. This will make the application much more performant and provide basic offline support. Of course, you can’t read the full entries if you’re offline, but at least *something* could be provided.
  • …and anything you suggest! Take a look at the code and feel free to make suggestions and point out mistakes!

Stay tuned for the second post!

The post Building a RSS Viewer With Vue: Part 1 appeared first on CSS-Tricks.