- This is post 2 in a series; see the introduction for all of them, and the requirements for which this software was built.
- Links that start with the text “mpj:” are links to the 1.0.0 tag (1.0 release) of myPrayerJournal, unless otherwise noted.
Using the default template,
main.js is the entry point; it creates a Vue instance and attaches it to an element named
app. This file also supports registering common components, so they do not have to be specifically imported and referenced in components that wish to use them. For myPrayerJournal, we registered our common components there (mpj:main.js). We also registered a few third-party Vue components to support a progress bar (activated during API activity) and toasts (pop-up notifications).
App.vue is also part of the default template, and is the component that
main.js attaches to the
app elements (mpj:App.vue). It serves as the main template for our application; if you have done much template work, you'll likely recognize the familiar pattern of header/content/footer.
This is also our first look at an SFC, so let's dig in there. The top part is the template; we used Pug (formerly Jade) for our templates. The next part is enclosed in
script tags, and is the script for the page. For this component, we import one additional component (
Navigation.vue) and the version from
package.json, then export an object that conforms to Vue's expected component structure. Finally, styles for the component are enclosed in
style tags. If the
scoped attribute is present on the style tag, Vue will generate data attributes for each element, and render the declared styles as only affecting elements with that attribute. myPrayerJournal doesn't use scoped styles that much; Vue recommends classes instead, if practical, to reduce complexity in the compiled app.
Also of note in
App.js is the code surrounding the use of the
toast component. In the template, it's declared as
toast(ref='toast'). Although we registered it in
main.js and can use it anywhere, if we put it in other components, they create their own instance of it. The
ref attribute causes Vue to generate a reference to that element in the component's
$refs collection. This enables us to, from any component loaded by the router (which we'll discuss a bit later), access the toast instance by using
this.$parent.$refs.toast, which allows us to send toasts whenever we want, and have the one instance handle showing them and fading them out. (Without this, toasts would appear on top of each other, because the independent instances have no idea what the others are currently showing.)
Just as URLs are important in a regular application, they are important in a Vue app. The Vue router is a separate component, but can be included in the new project template via the Vue CLI. In
router-view item renders the output from the router; we wire in the router in
main.js. Configuring the router (mpj:router.js) is rather straightforward:
- Import all of the components that should appear to be a page (i.e., not modals or common components)
- Assign each route a path and name, and specify the component
- For URLs that contain data (a segment starting with
props: true is part of the route configuration
scrollBehavior function, as it appears in the source, makes the Vue app mimic how a traditional web application would handle scrolling. If the user presses the back button, or you programmatically go back 1 page in history, the page will return to the point where it was previously, not the top of the page.
To specify a link to a route, we use the
router-link tag rather than a plain
a tag. This tag takes a
:to parameter, which is an object with a
name property; if it requires parameters/properties, a
params property is included. mpj:Navigation.vue is littered with the former; see the
showEdit method in mpj:RequestCard.vue for the structure on the latter (and also an example of programmatic navigation vs.
However, that does not mean that none of our components will be reused.
RequestCard, which we referenced above, is used in a loop in the
Journal component (mpj:Journal.vue); it is reused for every request in the journal. In fact, it is reused even for requests that should not be shown; behavior associated with the
shouldDisplay property makes the component display nothing if a request is snoozed or is in a recurrence period. Instead of the journal being responsible for answering the question "Should I display this request?", the request display answers the question "Should I render anything?". This may seem different from typical server-side page generation logic, but it will make more sense once we discuss state management (next post).
Looking at some other reusable (and reused) components, the page title component (mpj:PageTitle.vue) changes the title on the HTML document, and optionally also displays a title at the top of the page. The “date from now” component (mpj:DateFromNow.vue) is the most frequently reused component. Every time it is called, it generates a relative date, with the actual date/time as a tool tip; it also sets a timeout to update this every 10 seconds. This keeps the relative time in sync, even if the router destination stays active for a long time.
Finally, it's also worth mentioning that SFCs do not have to have all three sections defined. Thanks to conventions, and depending on your intended use, none of the sections are required. The “date from now” component only has a
Before we dive into the specifics of events, let's look again at
RequestCard. In the current structure,
RequestCard will always have
Journal as a parent, and
Journal will always have
App as its parent. This means that
RequestCard could, technically, get its toast implementation via
this.$parent.$parent.toast; however, this type of coupling is very fragile2. Requiring
toast as a parameter to
RequestCard means that, wherever
RequestCard is implemented, if it's given a
toast parameter, it can display toasts for the actions that would occur on that request.
Journal, as a direct descendant from
App, can get its reference to the toast instance from its parent, then pass it along to child components; this only gives us one layer of dependency.
In Vue, generally speaking, parent components communicate with child components via props (which we see with passing the toast instance to
RequestCard); child components communicate with parents via events. The names of events are not prescribed; the developer comes up with them, and they can be as terse or descriptive as desired. Events can optionally have additional data that goes with it. The Vue instance supports subscribing to event notifications, as well as emitting events. We can also create a separate Vue instance to use as an event bus if we like. myPrayerJournal uses both of these techniques in different places.
As an example of the first, let's look at the interaction between
ActiveRequests (mpj:ActiveRequests.vue) and
RequestListItem (mpj:RequestListItem.vue). On lines 41 and 42 of
ActiveRequests (the parent), it subscribes to the
requestNowShown events. Both these events trigger the page to refresh its underlying data from the journal.
RequestListItem, lines 67 and 79, both use
this.$parent.$emit to fire off these events. This model allows the child to emit events at will, and if the parent does not subscribe, there are no errors. For example,
AnswerdRequests (mpj:AnsweredRequests.vue) does not subscribe to either of these events. (
RequestListItem will not show the buttons that cause those events to be emitted, but even if it did, emitting the event would not cause an error.)
An example of the second technique, a dedicated parent/child event bus, can be seen back in
RequestCard. Adding notes and snoozing requests are modal windows3. Rather than specifying an instance of these per request, which could grow rather quickly,
Journal only instantiates one instance of each modal (lines 19-22). It also creates the dedicated Vue instance (line 46), and passes it to the modal windows and each
RequestCard instance (lines 15, 20, and 22). Via this event bus, any
RequestCard instance can trigger the notes or snooze modals to be shown. Look through
NotesEdit (mpj:NotesEdit.vue) to see how the child listens for the event, and also how it resets its state (the
closeDialog() method) so it will be fresh for the next request.
That wraps up our tour of Vue routes and components; next time, we'll take a look at Vuex, and how it helps us maintain state in the browser.
1 That's my summary; I'm sure they've got much more eloquent ways to describe it.
2 ...and kinda ugly, but maybe that's just me.
3 Up until nearly the end of development, editing requests was a modal as well. Adding recurrence made it too busy, so it got to be its own page.