From 05bbd68b9f9cb0536dad045548b5052ff4a2e13a Mon Sep 17 00:00:00 2001 From: Sangeeth Sudheer Date: Sat, 18 Jan 2020 00:10:29 +0530 Subject: [PATCH 1/3] 09-fetching-data: adds lesson and api lib --- app/src/lib/api.js | 17 ++ lessons/09-fetching-data.md | 316 ++++++++++++++++++++++++++++++++++++ 2 files changed, 333 insertions(+) create mode 100644 app/src/lib/api.js create mode 100644 lessons/09-fetching-data.md diff --git a/app/src/lib/api.js b/app/src/lib/api.js new file mode 100644 index 0000000..e0ca56b --- /dev/null +++ b/app/src/lib/api.js @@ -0,0 +1,17 @@ +const baseURL = 'https://hacker-news.firebaseio.com/v0'; + +const fetchItem = id => { + return fetch(`${baseURL}/item/${id}.json`).then(res => res.json()); +} + +const getTopStories = () => { + return fetch(baseURL + '/topstories.json').then(res => res.json()); +}; + +export const getPaginatedTopStories = async (page = 1, count = 10) => { + const allTopStories = await getTopStories(); + + const idsToFetch = allTopStories.slice((page - 1) * count, page * count); + + return Promise.all(idsToFetch.map(fetchItem)); +}; diff --git a/lessons/09-fetching-data.md b/lessons/09-fetching-data.md new file mode 100644 index 0000000..fe8a979 --- /dev/null +++ b/lessons/09-fetching-data.md @@ -0,0 +1,316 @@ +# Lesson 8 + +In this lesson, we'll connect to the publicly available HackerNews API and load real data onto our news list. We'll also implement the `` component, which will be linked to our router. There's quite a lot of stuff in this lesson so hop in. + +## Fetching data + +We'll first implement some convenient functions that'll use the Fetch API to get the data we need. We'll create these in `src/lib/api.js`: + +```javascript +const baseURL = 'https://hacker-news.firebaseio.com/v0'; + +const fetchItem = id => { + return fetch(`${baseURL}/item/${id}.json`).then(res => res.json()); +}; + +const getTopStories = () => { + return fetch(baseURL + '/topstories.json').then(res => res.json()); +}; + +export const getPaginatedTopStories = async (page = 1, count = 10) => { + const allTopStories = await getTopStories(); + + const idsToFetch = allTopStories.slice((page - 1) * count, page * count); + + return Promise.all(idsToFetch.map(fetchItem)); +}; +``` + +While you could handcraft these APIs yourself, that's not really necessary. The goal is to show how we can get the data loaded into our components for rendering. The functions implemented above are pretty straightforward, although we could've certainly optimized many things around: + +- `getTopStories`: fetches IDs of the top stories. About 500 of them by default. +- `fetchItem`: given an ID, gets the JSON representation of a news item. +- `getPaginatedTopStories`: combines above two functions to get us an array of items for a given page number. + +## Adding dynamic params to our routes + +We'll do a little refactoring of our routes. What we want to do now is: + +1. If user visits `/`, redirect to `/top` +2. If user visits `/top/2`, load data for page 2. + +So, we'll let our routes indicate part of our state. + +`vue-router` allows us to define dynamic components in our routes: + +```javascript +const routes = [ + { + path: '/', + name: 'home', + redirect: { name: 'top' } + }, + { + path: '/top/:page?', + name: 'top', + component: Home + }, + { + path: '/new', + name: 'new', + component: Home + }, + { + path: '/about', + name: 'about', + component: About + } +]; +``` + +As you can see above, we can indicate dynamic components of a route with `:param` syntax. We can access this from our components via the `this.$route.params` object. For the `:page` param above, that'd be `this.$route.params.page`. + +The other cool thing about this is that `$route.params` is reactive! As we'll see later, we'd be able to re-fetch the data when the param changes as a result of us clicking within our paginator component. + +Let's also make a small change in the `components/NavBar.vue` file so that "Top" link gets active state correctly: + +```html +Top +``` + +## Passing down the fetched data + +Let's go to `views/Home.vue` and make some changes: + +```vue + + + +``` + +Let's go over the changes. `` would need the current page we're at and the total number of pages. We create two computed properties: `currentPage` and `totalPages` that does this. As mentioned before, we can access the current page by `this.$route.params.page`. This will be a string value which we convert to number by using the unary `+` operator. If this param isn't present, we default to 1. + +`totalPages` is straightforward. We just return the length of the `items` data property. + +We also create a dummy method called `fetchStories` which we'll populate later. + +We use a watcher to notify whenever the `currentPage` computed property changes. `immediate: true` runs the `handler` function once at the beginning when the component is mounted, otherwise, it only gets run when the value of reactive property changes later. As you can see, it simply calls the `this.fetchStories` method which will make the API call and sets the `this.items` property. Let's code that up: + +```javascript +import { getPaginatedTopStories } from '@/lib/api'; + +// .. + +async fetchStories() { + const items = await getPaginatedTopStories(this.currentPage, 20); + + this.items = items.map(item => ({ + title: item.title, + author: item.by, + upvotes: item.score, + timestamp: new Date(item.time * 1000).toUTCString(), + commentCount: (item.kids || []).length + })); +} +``` + +Nothing too complicated. Although, we're mapping over the items we receive into the format we're expecting when we pass it down to ``. Alright, try saving and reload. You should see actual data being fetched and rendered! Alright! πŸ™Œ We're not done yet, we need to implement ``. + +## Building `` + +Remember, we're determining the current page state via router params. So, when someone clicks the Next/Previous buttons, we basically need to change the route. The reactivity takes care of the rest. + +You have two choices here: + +1. Use `` for the prev/next buttons and simply pass a `params` property with `page: currentPage - 1` and `page: currentPage + 1` respectievely. +2. Use the `this.$router.push()` function to do it imperatively from within a method. It accepts a route object, just like ``. Give it a shot on your own. Take a peek at the below implementation for some inspiration. + +```vue + + + + + +``` + +Try it out. You should now see that clicking the prev/next buttons would not only change the route, but also fetch data for the page you're viewing. Neato! + +## Improvising + +Let's add the popular `date-fns` package to format the timestamp value we receive into a more, human-readable string: + +```sh +yarn add date-fns +``` + +Open `components/NewsListItem.vue`: + +```diff + + + +``` + +## What next? + +Nice. You have a working HackerNews clone written in Vue.js. You could do so much more: + +1. Add a user view, item view with comment threads +2. Link to the actual post via the `url` from item response +3. Load data more efficiently: you could use the firebase SDK for realtime updates and cache already seen item responses so that you don't have to re-fetch them. +4. Make it a PWA! + +There's also lot more to learn about Vue.js. Check the excellent Vue.js docs: https://vuejs.org/ From 05eea8a712b083bd39022aee47b5f0e214bdda80 Mon Sep 17 00:00:00 2001 From: Sangeeth Sudheer Date: Sun, 19 Jan 2020 10:08:59 +0530 Subject: [PATCH 2/3] Fixes lesson 9 heading --- lessons/09-fetching-data.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lessons/09-fetching-data.md b/lessons/09-fetching-data.md index fe8a979..5dcfe25 100644 --- a/lessons/09-fetching-data.md +++ b/lessons/09-fetching-data.md @@ -1,4 +1,4 @@ -# Lesson 8 +# Lesson 9 In this lesson, we'll connect to the publicly available HackerNews API and load real data onto our news list. We'll also implement the `` component, which will be linked to our router. There's quite a lot of stuff in this lesson so hop in. From 235005081a5c1dfba48e08a8c8c7de5b306fe3f8 Mon Sep 17 00:00:00 2001 From: Sangeeth Sudheer Date: Sat, 18 Jan 2020 00:10:56 +0530 Subject: [PATCH 3/3] 09-fetching-data: adds app changes --- app/package.json | 1 + app/src/components/NavBar.vue | 2 +- app/src/components/NewsList.vue | 32 +++-- app/src/components/NewsListItem.vue | 25 +++- app/src/components/Paginator.vue | 73 ++++++++++ app/src/router/index.js | 8 ++ app/src/views/Home.vue | 199 +++++++--------------------- app/yarn.lock | 5 + 8 files changed, 174 insertions(+), 171 deletions(-) diff --git a/app/package.json b/app/package.json index 8b6a8d0..664592c 100644 --- a/app/package.json +++ b/app/package.json @@ -9,6 +9,7 @@ }, "dependencies": { "core-js": "^3.6.4", + "date-fns": "^2.9.0", "vue": "^2.6.11", "vue-router": "^3.1.3", "vue-simple-spinner": "^1.2.8" diff --git a/app/src/components/NavBar.vue b/app/src/components/NavBar.vue index 3240387..c6c17cd 100644 --- a/app/src/components/NavBar.vue +++ b/app/src/components/NavBar.vue @@ -5,7 +5,7 @@ diff --git a/app/src/components/NewsList.vue b/app/src/components/NewsList.vue index 127101c..6b191d5 100644 --- a/app/src/components/NewsList.vue +++ b/app/src/components/NewsList.vue @@ -1,16 +1,18 @@ @@ -32,10 +34,14 @@ export default { }; - diff --git a/app/src/components/NewsListItem.vue b/app/src/components/NewsListItem.vue index 07311be..49ccd58 100644 --- a/app/src/components/NewsListItem.vue +++ b/app/src/components/NewsListItem.vue @@ -4,10 +4,12 @@ {{ upvotes }}
-

{{ title }}

+

+ {{ title }} +

{{ author }}
-
{{ timestamp }}
+
{{ ago }}
{{ numComments }} comments
@@ -18,13 +20,25 @@ @@ -58,6 +72,11 @@ export default { color: #333; } +.title a { + text-decoration: none; + color: inherit; +} + .meta { color: #888; font-size: 0.8em; diff --git a/app/src/components/Paginator.vue b/app/src/components/Paginator.vue index e69de29..a0c4e47 100644 --- a/app/src/components/Paginator.vue +++ b/app/src/components/Paginator.vue @@ -0,0 +1,73 @@ + + + + + diff --git a/app/src/router/index.js b/app/src/router/index.js index 59b0282..7579266 100644 --- a/app/src/router/index.js +++ b/app/src/router/index.js @@ -10,6 +10,14 @@ const routes = [ { path: '/', name: 'home', + redirect: { name: 'top' } + }, + { + path: '/top/:page?', + params: { + page: 1 + }, + name: 'top', component: Home }, { diff --git a/app/src/views/Home.vue b/app/src/views/Home.vue index 6f32aa6..fee2853 100644 --- a/app/src/views/Home.vue +++ b/app/src/views/Home.vue @@ -1,170 +1,61 @@ diff --git a/app/yarn.lock b/app/yarn.lock index c1b186a..0673e94 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -2640,6 +2640,11 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +date-fns@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.9.0.tgz#d0b175a5c37ed5f17b97e2272bbc1fa5aec677d2" + integrity sha512-khbFLu/MlzLjEzy9Gh8oY1hNt/Dvxw3J6Rbc28cVoYWQaC1S3YI4xwkF9ZWcjDLscbZlY9hISMr66RFzZagLsA== + de-indent@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"