Vue.js 2.0 Forum Description
Demo link: https://vue-forums.firebaseapp.com/
Video link: https://youtu.be/33WeNhsAcBk
How to run application: https://ashot72.github.io/Vue2Forum/index.html
Git Repository: https://github.com/Ashot72/Vue2Forum
Please read How to run application first.
This is Vue.js 2.0 forum web application using Single File Components approach. Instead of dividing the codebase into three huge layers that interweave with one another,
it makes much more sense to divide them into loosely-coupled components and compose them. Inside a component, its template, logic and styles are inherently coupled,
and collocating them actually makes the component more cohesive and maintainable.
The app is based on
Vue.js https://vuejs.org/ is the progressive JavaScript Framework for building user interfaces. Unlike other monolithic frameworks, Vue is designed from the ground up to be incrementally adoptable.
Vuex https://vuex.vuejs.org/ is a state management pattern + library for Vue.js applications. It serves as a centralized store for all the components in an application,
with rules ensuring that the state can only be mutated in a predictable fashion.
Vue Axios https://www.npmjs.com/package/vue-axios is a small wrapper for integrating axios to Vuejs. Axios is a Promised based HTTP client for the browser and node.js.
Vue Router https://router.vuejs.org/ deeply integrates with Vue.js core to make building Single Page Application with Vue.js a breeze.
Vuelidate https://monterail.github.io/vuelidate/ is a simple, lightweight model-based validation for Vue.js 2.0.
Bootstrap + Vue https://bootstrap-vue.js.org/ provides one of the most comprehensive implementations of Bootstrap V4 components and grid system available for
Vue.js 2.4+, complete with extensive and automated WAI-ARIA accessibility markup.
Forum web application is hosted on Firebase https://vue-forums.firebaseapp.com/ and uses Firebase database. Firebase Database REST API is used to authenticate and access Firebase Database URI as a REST endpoint.
No Firebase SDK is used. The reason is that any other REST API endpoints can be integrated easily.
Forum web application implements many Vue.js features such as props, watchers, named slots, mixins, event bus, transitions/animations, lazy loading, global/in-component guards
scrolling behavior etc.
When SPA (Single Page Application) grows in complexity, so does the size of the application bundle. Vue Router supports webpack's built in async module loading system.
Bundles can be loaded on-demand when the route is accessed.
Figure 1
You can see import statement which is equired to load separate chunks. Import statements start with @ sign.
Figure 2
This is done with Webpack resolve.alias configuration option and isn't specific to Vue. It gives you a relative path from the src file and it removes the requirement of the .vue at the end
of the import path. You do not need to have an import something like '../../../src' in your code.
Figure 3
Similarly, we point to noimage.jpg which is the profile picture on posts page.
Figure 4
You will see 4 JavaScript files loaded in the Chrome's console by refreshing the forums initial page.
Figure 5
Click SharePoint 2013 forum link and navigate to topics page. You will see another chunk loaded.
Figure 6
Click Search result order for different language topic link and navigate to posts page. Another chunk loaded. Loading chunks on demand is critical in large scale web applications.
We do not load all the code when application is starting. With lazy loading we are taking one segment of code, chunk and loading it on demand as the app requests it.
Application's state management is based on Vuex which a state management pattern is + library for Vue.js applications.
Figure 7
State: Vuex uses a single state tree -that is, this single object contains all your application level state and serves as the single source of truth. This also means usually you will
have only one store for each application.
Figure 8
Getters: Sometimes you may need to compute derived state based on store state, for example, authentication based on idToken state.
Figure 9
Mutations: The only way to actually change state in Vuex store is by committing a mutation.
Figure 10
Actions: are similar to mutations, the differences being that:
Instead of mutation
the state, actions commit mutations
Actions can contain
arbitrary asynchronous operations.
Figure 11
Modules: Due to using a single state tree, all state of your application is contained inside one big object. However, as our application grows in scale,
the store can get really bloated. To help with that, Vuex allows us to divide our store into modules. Each module can contain its own state, mutations,
actions, getters and even nested modules.
Figure 12
Namespacing: By default, actions, mutations and getters inside modules are still registered under the global namespace: this allows multiple modules to react
to the same mutation/action type. When the module is registered, all of its getters, actions and mutations will be automatically namespaced based on path the module
is registered at.
Figure 13
Namespaces are auth, forum, topic and post.
Figure 14
From the post actions to dispatch forums or topics we must use namespace. We should also use { root: true } which is required to dispatch actions or commit mutations in the global namespace.
You can see we dispatch in the code with forum namespace and UPDATEFORUM with { root: true }. If we omit { root: true } then the path will be forum/post/${ UPDATEFORUM } which is a wrong path.
Figure 15
mapState: Create component options that return the sub tree of the Vuex store. When a component needs to make use of multiple store state properties or getters, declaring all these
computed properties can get repetitive and verbose. To deal with this we can make use of the mapState helper which generates computed getter functions for us. Note, the use of topic and
auth namespaces.
Figure 16
Here is accessing the forums without mapState just via $store.state.
Figure 17
mapGetters: Create component computes options that return the evaluated value of a getter. The mapGetters helper simply maps store getters to local computes properties.
Figure 18
Here is accessing AUTH without mapGetters via $store.getters.
Figure 19
mapActions: Create component methods options that dispatch an action.
Figure 20
Here is dispatching without mapActions via $store.dispatch.
Figure 21
We also defined plugins for store mutations. It is called after every mutation and receives the mutation descriptor and post-mutation state as arguments.
Figure 22
Based on mutation type AUTHDATA or CLEARAUTHDATA we add a record to local storage or remove it respectively.
Figure 23
Vue Plugins add global-level functionality to Vue. There is no strictly defined scope for a plugin: there are typically several types of plugins you can write.
One of them is adding some Vue instance methods by attaching them to Vue.prototype. A Vue.js plugin should expose an install method. The method will be called with
the Vue constructor as the first argument, along with possible options. The options we passed are key and loader callback. This plugin is used for Firebase authentication.
Figure 24
Second Vue plugin is used for forums operations and exchanging a refresh token for an access token which will be discussed later.
Figure 25
Here are forums operations such as updateTopic, fetchPosts etc.
Figure 26
To access a Vue plugin instance we do it via this. In our case we want to access the Vue plugin from Vuex so there is no this in this case. For that reason
we create a vue instance let v = new Vue() and access via that instance v.$auth.
Figure 27
We call plugins using Vue.use. You can see that we use 2 axios instances for the plugins. Loader callback calls EventBus.$emit.
EventBus is used to communicate between Vue.js components. The EventBus is used to show or hide loading icons. We emit event LOADER_EVENT
Figure 28
In App.vue we register a listener which listens for event
LOADER_EVENT and based on the value loading
which is either true or false
we either show the loader or hide it respectively.
Figure 29
loader callback is called with false or true values in axios request and response interceptors.
Figure 30
User is authenticated to Firebase after he is Signed In or Signed Up. It uses token-based authentication. Expiration date is 3600 seconds (1 hour). This means that after an hour
the access token (id token in Firebase case) is expired and user either have to be logged out or the application should exchange the refresh token for an access token.
In our case we take the second approach: exchanging the refresh token for an access token. Usually refresh tokens have longer lifetime that access token such us 6 months.
In Firebase case refresh token never expires which means that the user can infinitely be logged on unless he is logged out by himself.
Figure 31
email, expiresIn, idToken and refreshToken are kept in local storage.
Figure 32
Suppose one hour passed after a user has been logged on. He is clicking a post link to load posts for example. If Chrome's console is opened he will see 401 (Unauthorized)
requests. He will still be redirected to posts page as a new IdToken token will be obtained via refreshToken for the next one hour. If the user refreshes the local storage entry (Figure 31)
he will see new IdToken instead of the old one and so on. The application is implemented using Firebase REST API purely, no Firebase SDK. The reason is that you can connect to another REST endpoint
easily.
Figure 33
Firebase requires ?auth=idTOken be appended to the request but for other cases you may need to pass bearer-token via headers.
Figure 34
The forums initial page can be accessed anonymously. If you try to access other pages such as topics page you have to authenticate.
Figure 35
In router.js file we specify { auth: true } for meta tag. We specify meta tag only for topics and posts routing.
Figure 36
Router.beforeEach(to, from, next): is global guard. Global before guards are called in creation order, whenever a navigation is triggered. Guards may be resolved asynchronously,
and the navigation is considered pending before all hooks have been resolved. In our app we dispatch AUTOSIGNIN action first. Application auto-signs the user after a page has been refreshed.
If there are valid local storage entries (Figure 31) then the apps set AUTH state to true. Next, we check if the route defines meta tag (Figure 35) and if the auth state is false (possibly user has been logout)
the we call forums route with query param e=401, otherwise we call next();
next function must be called to resolve the hook. The action depends on the arguments provided to next.
next(): move on to the next hook in the pipeline
next(false): abort the current navigation. If the browser URL was changed (either manually by the user or via back button), it will be reset to that of the from route.
next('/'): redirect to different location. The current navigation will be aborted and a new one will be started.
Figure 37
beforeRouteUpdate(to, from, next): is In-Component guard. beforeRouteUpdate is called when the route renders this component has changed
but the component is reused in the new route. In our app if the route is changed from
https://vue-forums.firebaseapp.com/forums to https://vue-forums.firebaseapp.com/forums?e=401 then the beforeRouteUpdate(to, from, next) will be called.
In that case we just show the dialog (Figure 34). We also check it in beforeRouteEnter(to, from, next).The reason is that
if a user is not authenticated (logged out) and try to go to https://vue-forums.firebaseapp.com/posts/-LEP35jzM4gEKsrShvlg/-LEP41EK8DCWnyJRA4V8
directly then he will be redirected to forums page and dialog will be displayed in this case. beforeRouteEnter(to, from, next) is described below in detail
but one thing that I want to mention is though the beforeRouteEnter does NOT have an access to this however you can access the instance by passing a callback to next.
The callback will be called when the navigation is confirmed, and the component instance will be passed to the callback as the argument. That is how we could
run vm.$refs.modal.show() in beforeRouteEnter.
Figure 38
Vue.js defines Lifecycle Hooks. Lifecycle hooks are an important part of any serious component. You often need to know when your component is created, added to the DOM, updated, or destroyed.
We call FETCHFORUMS action to load forums in created() lifecycle hook. In created hook, you will be able to access reactive data and events are active. Templates and Virtual DOM have not
yet been mounted or rendered.
Figure 39
Here is how the forums page looks like.
Figure 40
Let's modify page and add setTimout() of 5 seconds.
Figure 41
You see that page has rendered but data has not been arrived yet. Data will available in 5 seconds and you will have a fully loaded page (Figure 40)
That may not be the case you expect. You may want to see pages rendered as soon as data are available.
There is another way however using what's known as a route resolver, which allows you to get data before navigating to the new route.
Figure 42
We load topics page another way.
beforeRouteEnter(to, from, next): is In-Component guard and is called before the route renders this component is confirmed. It does NOT have access to this
component instance, because it has not been created yet when this guard is called.
As we do not have an access to this we import store and call dispatch via store. In this case as opposed to forum's created() lifecycle hook case the page will be rendered as
soon as data arrives.
Figure 43
Vue.js supports watchers. We can watch route changes and based on it render navigation. For example, if the route is posts we render posts navigation.
Figure 44
Here is posts navigation rendered via route watching.
Figure 45
Vue provides a variety of ways to apply transition effects when items are inserted, updated or removed form the DOM. In our app we
implement page transition fade effect with vue-router using third party library Animate.css. Watch the video to see the transition.
Figure 46
Animate.css library.
Figure 47
When using client-side routing, we may want to scroll to top when navigating to a new route or preserve the scrolling of history entries just like real page load does.
vue-router allows you to achieve these and even better, allows you to completely customize the scroll behavior on route navigation. This feature only works if the browser
supports history.pushState. When creating the router instance, you can provide the scrollBehavior function. The scrollBehavior function receives the to and from route objects.
The third argument, savedPosition, is only available if this is a popstate navigation (triggered by the browse's back/forward buttons).
Returning the savedPosition will result in a native-like behavior when navigating with back/forward buttons
If we want to simulate the 'scroll to anchor' behavior we use selector: to.hash.
Figure 48
If you hover Last Post link you will see hash #last attached.
Figure 49
Clicking the link, you will be navigated to Posts page and the scroller position will be the last post position.
Figure 50
If you look the scrollBevaior code you could see that instead of returning position we resolve promise with setTimout() of 2 seconds.
If return just position then the scrollbar will always go to the top of the page. The reason for that is transition discussed above.
What we have to do is return value as soon as the translon finishes (Watch the video).
Figure 51
fadeInRight animation takes one second, fadeOutLeft takes one second as well. Overall animation takes 2 seconds. That is the reason we put
2 seconds.
Figure 52
Vuelidate: is simple, lightweight model-based validation for Vue.js 2.0
Figure 53
We import validators and under validations object we define
properties. Password is required and
must be at least 6 characters in length. Password
and Confirm Password
must be the same.
Figure 54
Note, we also defined our custom async validator for email which is called unique. When user is typing an email field we send an axios request to Firebase Database
to find out If the user (email) already exists or not.
Figure 55
Firebase Password-Based account is used to store users. As you see ashota@gmail.com
is already registered so getting message Email
ashota@gmail.com is taken (Figure 54).
Figure 56
We keep a user email in the firebase database as soon as a user is successfully signed up. Custom async unique validator checks the user existence in users document.
Figure 57
Bootstrap-vue is used in the app which provides one of the comprehensive implementations of Bootstrap v4 components and grid system. If you look Form Input section you could notice
that forum inputs are rendered in different borders based on :state prop.
Figure 58
Considering form input state and vuelidate's error detection mechanism (e.g. $error, $valid, $dirty) we can render borders with different colors and show different error messages based on user's input.
Figure 59
:state and other props in template.
Figure 60
Mixins are a flexible way to distribute reusable functionalities for Vue components. A mixin object can contain any component options. When a component
uses a mixin, all options in the mixin will be 'mixed' into the component's own options. We defined formMixin.js which is used both in Sign Up and Sign In components.
Figure 61
mixins reference in SignupForm.vue file. Note, new instance of formMixin is created for each component.
Figure 62
App defines 4 documents forums, posts, topics and users. Forums keeps topics' references.
Figure 63
Topics keeps posts references which is required in our application.
Figure 64
This is the app database rules. Users must be authenticated to read from and write into the database. Users document does not require authentication as it is used after the Sign Up
and for async validator (Figure 53). Forums page can be accessed anonymously so forums document can be read without authentication.
Figure 65
.indexOn rule is specified to improve query performance.