JavaScript: Category Archive

Posts about programming in JavaScript, including other projects that use it

Thursday, February 9, 2023
  Page-Level JavaScript Initialization with htmx

Development with htmx focuses on generating content server-side and replacing portions of the existing page (or the entire content) with the result, with no build step and no additional JavaScript apart from the library itself. The only JavaScript required is whatever the application may need for interactivity on the page. This can be tricky, though, when the front-end uses a library that requires a JavaScript initialization call; how do we know when (or how, or whether) to initialize it?

The answer is HTML Events. For regular page loads, the DOMContentLoaded event is the earliest in the page lifecycle where the content is available; references to elements with particular ids will work. htmx offers its own events, and the one that corresponds to DOMContentLoaded is htmx:afterSwap. (Swapping is the process of updating the existing page with the new content; when it is complete, the old elements are gone and the new ones are available.) The other difference is that DOMContentLoaded is fired from the top-level Window element (accessible via the document element), while htmx:afterSwap is fired from the body tag.

All that being said, the TL;DR process is:

  • If the page loads normally, initialize it on DOMContentLoaded
  • If the page is loaded via htmx, initialize it on htmx:afterSwap

and looks something like:

document.addEventListener([event], () => { [init-code] }, { once: true })

When htmx makes a request, it includes an HX-Request header; using this as the flag, the server can know if it is preparing a response to a regular request or for htmx. (There are several libraries for different server-side technologies that integrate this into the request/response pipeline.) As the server is in control of the content, it must make the determination as to how this initialization code should be generated.

This came up recently with code I was developing. I had a set of contact information fields used for websites, e-mail addresses, and phone numbers (type, name, and value), and the user could have zero to lots of them. ASP.NET Core can bind arrays from a form, but for that to work properly, the name attributes need to have an array index in them (ex. Contact[2].Name). To add a row in the browser, I needed the next index; to know the next index, I needed to know how many I rendered from the server.

To handle this, I wrote a function to render a script tag to do the initialization. (This is in F# using Giraffe View Engine, but the pattern will be the same across languages and server technologies.)

let jsOnLoad js isHtmx =
    script [] [
        let (target, event) =
            if isHtmx then "document.body", "htmx:afterSettle"
            else "document", "DOMContentLoaded"
        rawText (sprintf """%s.addEventListener("%s", () => { %s }, { once: true })"""
                    target event js)
    ]

Then, I call it when rendering my contact form:

    jsOnLoad $"app.user.nextIndex = {m.Contacts.Length}" isHtmx

Whether the user clicks to load this page (which uses htmx), or goes there directly (or hits F5), the initialization occurs correctly.

Categorized under ,
Tagged , , , ,

Sunday, September 2, 2018
  A Tour of myPrayerJournal: Conclusion

NOTE: This is the final post of an 8-post series; see the introduction for all of them, and the requirements for which this software was built.

Over the course of this tour, we've meandered through client side code, server side code, a database, and documentation. However, the experience of developing an application is more than just the sum of the assembled technologies and techniques. Let's take a look at some of these “lessons learned” and opinions formed through this process. (This post will use the first-person singular pronouns “I” / “me” / “my” a lot more than the previous ones.)

Vue Is Awesome – But…

As I tried different SPA frameworks, they were interesting and fun, but a lot more work than I expected. Then, I came across Vue, and its paradigm and flow just clicked. Single file components are great; it was so nice to not have to go digging through a site-wide CSS file looking for styles that affected the elements in the component. I just had to scroll down! While I did put the common CSS in App.vue, the application's top component, anything unique that component did was right there. There are also all kinds of Vue-aware packages that you can add and use, that add their own elements/components to the process; Element UI, Bootstrap Vue, and Vue-Awesome were three of the ones I used at different points in development. Since it's a JavaScript framework, you can use vanilla JS packages as well; myPrayerJournal uses moment.js to display relative dates (“last activity 8 minutes ago”) and format dates for display.

Then… I ran the build process, and the bundles were huge – as in, multiple megabytes huge! We changed our implementation of Vue-Awesome to only import the icons we used in the application (to be fair to them, that is the project's recommendation). Element also seemed to be rather heavy. One of the last issues I worked before final release was removing Bootstrap Vue and just using straight HTML/CSS for layout and flow (which is another lesson we'll explore below). There are guides on configuring Webpack to help make moment's bundle smaller (and that project has an open issue to refactor so that it's more friendly to a “just import the part you need” paradigm).

None of this is meant as a knock of any of the fine projects I've named up to this point. It was also near the end of the project when I converted to the Vue CLI v3 template, which uses a version of Webpack that has a much better “tree-shaking” algorithm. (This means that, if it can establish that code is never executed, it excludes it from the bundle.) myPrayerJournal v1.0's modern “vendor” bundle (the one with the libraries) is 283K, while the legacy bundle is 307K; while that's not lightning fast on mobile, it's also comparable to a lot of other sites, and since page updates happen through the API, it is fast once it's loaded.1

Lesson: Be smart about what you import.

Lesson: The JavaScript ecosystem evolves quickly. This was published September 2nd, 2018, describing development that occurred September 2016 through August 2018; a good bit of this is likely already obsolete. :)

You Might Not Need…

We mentioned above that the site eventually was written with simple HTML and CSS. Many of the more popular packages and utilities were created to make up for deficiencies, either in the browser ecosystem or among the differing browser vendors. With the recent efforts by browser vendors to support published standards, though, many of these packages are used for reasons that distill to comfort and inertia. As before, this is not a knock on these projects; they filled a definite need, and continue to work as the basis for a lot of deployed, executing code.

For new development, though, existing standards – and their support – may be sufficient. There are some great sites that detail how certain things can be done using plain JavaScript or CSS.

I used the last one quite a bit. I also extensively referred to CSS Tricks' “A Complete Guide to Flexbox” post. When I decided to rework the layout without Bootstrap, I thought the replacement would be CSS Grid; however, Flexbox was more than enough.

Lesson: Use a framework if you want, but don't assume it's the only way to do things.

Lesson: If you want to shrink your bundle size, 20-30 lines of your own code can sometimes save you 20-30K (or more).

Learn Go

Ladies and gentlemen of the Internet class of 2018 -
  Learn Go.
If I could offer you only one tip for the future,
   Go would be it.
with apologies to Baz Luhrmann

Go is a systems programming language. It was developed at Google, to help them better utilize their hardware. It natively supports concurrent processing (which can be done in parallel, but is distinct from “parallel programming”); has an opinionated code formatter; forces you to address calls that may error; and is terribly efficient. When myPrayerJournal was running with the Go backend, the working size in RAM was around 10MB. Let me say that again, this time with feeling - the working size for a database-accessing, HTTP-listening, dynamic web service was 10MB of RAM! If you have ever profiled a web server process, you know that it's nearly ludicrous how small this is. For comparison, the process working set for the F#/Giraffe/EF Core version of the backend runs between 60-80MB, and includes another ~256MB of shared working set memory.2 (An Apache2 process running PHP can run in the 256MB range as well.)

Why am I recommending a technology that I ultimately moved away from before the v1.0 release? Well, other than "did you read the last paragraph?!?!", the short answer is “it's the future, and will change how you code in every other language.” The fact that it forces you to deal with every single thing that may error makes it robust; but, if you learn to develop with it, you will find yourself thinking about error handling more fully than you did before – and I say this as a person who already coded error handlers as I coded the happy path.

Why did I move away from a technology on which I'm so bullish? Well, for starters, F# is the language that “clicks” for me in the same way that Vue did as a client-side framework; its development paradigm just makes sense with the way I think about structuring code. I completed a project that used F# and Giraffe (which I hope to take open-source soon), and that gave me the confidence to move forward with a third attempt at an F# API. (Third time's the charm, right?) Also, while Entity Framework generated some pretty verbose SQL statements, EF Core more or less generates what I would have written anyway, plus it takes care of populating the objects it returns from the database.

I also found the development process awkward, though not unwieldy. (They're probably not going to adopt that as their slogan…) Much has been written about the GOPATH environment variable, but once you get into it, it starts to make sense. go get is great at pulling down your dependencies, and the way it does it, you can peruse the source code to see exactly what they are doing. Also, it was not too difficult to develop on Windows, but build executables for Linux – which, in the “systems” programming work, is a really cool feature.

Lesson: Learn Go.

Write about Your Code

This wasn't a lesson I learned on this project; this was one I'd known for a while. There are (at least) two distinct benefits to writing about your code:

  • It can help you learn even more than you may have learned when you were writing the code itself. As developers, we sometimes forget to step back and look at the body of work we've created. If you write about it, you have to form a coherent view about it so you can explain it to others; this helps you long-term. Also, people who know more about the environment can chime in on what you've written, which also not only helps you learn, ...
  • It helps build community. If you hit a snag, and someone in the community around that technology helps you get past it, writing about your experience means that the next person to encounter that issue may not have to ask; if their searching leads them to your post, they can fix it and move on. This applies doubly if you could not get help from the community; you might be the one who surfaces this issue/technique, and moves the entire community forward.

Lesson: Write about your code; participate in the community around your technology to whatever extent you are able.

 

If you've been with us for this entire tour – thank you. I hope you've learned something; I know I have, not just through the development of myPrayerJournal, but through the course of writing about it. And, certainly, if you feel that this application could help you in any way, help yourself. It is and will always be free, and Bit Badger Solutions (and DJS Consulting before it) has, as of this writing, a 14-year streak of no known data breaches; your prayer requests are safe with us.


1 There are chunk-splitting techniques that can be used to make the initial download smaller, and load other portions on-demand. Moment.js, for example, isn't needed for the default “Welcome to myPrayerJournal” page. We could defer loading that until the user has logged in, as the journal page definitely needs it; we'd still have to download the same amount, but it would be spread out across 2 requests. Opportunities to save some size in the initial download are still out there, but 283K is just above the 244K suggested bundle size, so we went forward with it.

2 The server on which I host myPrayerJournal already has other .NET Core processes running on it, so the shared memory size has already been allocated.

Categorized under , , ,
Tagged , , , , , , , , , , , , , , , , ,

Thursday, August 30, 2018
  A Tour of myPrayerJournal: Authentication

NOTES:

  • This is post 5 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.

At this point in our tour, we're going to shift to a cross-cutting concern for both app and API - authentication. While authentication and authorization are distinct concerns, the authorization check in myPrayerJournal is simply “Are you authenticated?” So, while we'll touch on authorization, and it will seem like a synonym for authentication, remember that they would not be so in a more complex application.

Deciding on Auth0

Auth0 provides authentication services; they focus on one thing, and getting that one thing right. They support simple username/password authentication, as well as integrations with many other providers. As “minimalist” was one of our goals, not having to build yet another user system was appealing. As an open source project, Auth0 provides these services at no cost. They're the organization behind the JSON Web Token (JWT) standard, which allows base-64-encoded, encrypted JSON to be passed around as proof of identity.

This decision has proved to be a good one. In the introduction, we mentioned all of the different frameworks and server technologies we had used before settling on the one we did. In all but one of these “roads not further traveled”1, authentication worked. They have several options for how to use their service; you can bring in their library and host it yourself, you can write your own and make your own calls to their endpoints, or you can use their hosted version. We opted for the latter.

Integrating Auth0 in the App

JavaScript seems to be Auth0's primary language. They provide an npm package to support using the responses that will be returned from their hosted login page. The basic flow is:

  • The user clicks a link that executes Auth0's authorize() function
  • The user completes authorization through Auth0
  • Auth0 returns the result and JWT to a predefined endpoint in the app
  • The app uses Auth0's parseHash() function to extract the JWT from the URL (a GET request)
  • If everything is good, establish the user's session and proceed

myPrayerJournal's implementation is contained in AuthService.js (mpj:AuthService.js). There is a file that is not part of the source code repository; this is the file that contains the configuration variables for the Auth0 instance. Using these variables, we configure the WebAuth instance from the Auth0 package; this instance becomes the execution point for our other authentication calls.

Using JWTs in the App

We'll start easy. The login() function simply exposes Auth0's authorize() function, which directs the user to the hosted log on page.

The next in logical sequence, handleAuthentication(), is called by LogOn.vue (mpj:LogOn.vue) on line 16, passing in our store and the router. (In our last post, we discussed how server requests to a URL handled by the app should simply return the app, so that it can process the request; this is one of those cases.) handleAuthentication() does several things:

  • It calls parseHash() to extract the JWT from the request's query string.
  • If we got both an access token and an ID token:
    • It calls setSession(), which saves these to local storage, and schedules renewal (which we'll discuss more in a bit).
    • It then calls Auth0's userInfo() function to retrieve the user profile for the token we just received.
    • When that comes back, it calls the store's (mpj:store/index.js) USER_LOGGED_ON mutation, passing the user profile; the mutation saves the profile to the store, local storage, and sets the Bearer token on the API service (more on that below as well).
    • Finally, it replaces the current location (/user/log-on?[lots-of-base64-stuff]) with the URL /journal; this navigates the user to their journal.
  • If something didn't go right, we log to the console and pop up an alert. There may be a more elegant way to handle this, but in testing, the only way to reliably make this pop up was to mess with things behind the scenes. (And, if people do that, they're not entitled to nice error messages.)

Let's dive into the store's USER_LOGGED_ON mutation a bit more; it starts on line 68. The local storage item and the state mutations are pretty straightforward, but what about that api.setBearer() call? The API service (mpj:api/index.js) handles all the API calls through the Axios library. Axios supports defining default headers that should be sent with every request, and we'll use the HTTP Authorization: Bearer [base64-jwt] header to tell the API what user is logged in. Line 18 sets the default authorization header to use for all future requests. (Back in the store, note that the USER_LOGGED_OFF mutation (just above this) does the opposite; it clears the authorization header. The logout() function in AuthService.js clears the local storage.)

At this point, once the user is logged in, the Bearer token is sent with every API call. None of the components, nor the store or its actions, need to do anything differently; it just works.

Maintaining Authentication

JWTs have short expirations, usually expressed in hours. Having a user's authentication go stale is not good! The scheduleRenewal() function in AuthService.js schedules a behind-the-scenes renewal of the JWT. When the time for renewal arrives, renewToken() is called, and if the renewal is successful, it runs the result through setSession(), just as we did above, which schedules the next renewal as its last step.

For this to work, we had to add /static/silent.html as an authorized callback for Auth0. This is an HTML page that sits outside of the Vue app; however, the usePostMessage: true parameter tells the renewal call that it will receive its result from a postMessage call. silent.html uses the Auth0 library to parse the hash and post the result to the parent window.2

Using JWTs in the API

Now that we're sending a Bearer token to the API, the API can tell if a user is logged in. We looked at some of the handlers that help us do that when we looked at the API in depth. Let's return to those and see how that is.

Before we look at the handlers, though, we need to look at the configuration, contained in Program.fs (mpj:Program.fs). You may remember that Giraffe sits atop ASP.NET Core; we can utilize its JwtBearer methods to set everything up. Lines 38-48 are the interesting ones for us; we use the UseAuthentication extension method to set up JWT handling, then use the AddJwtBearer extension method to configure our specific JWT values. (As with the app, these are part of a file that is not in the repository.) The end result of this configuration is that, if there is a Bearer token that is a valid JWT, the User property of the HttpContext has an instance of the ClaimsPrincipal type, and the various properties from the JWT's payload are registered as Claims on that user.

Now we can turn our attention to the handlers (mpj:Handlers.fs). authorize, on line 72, calls user ctx, which is defined on lines 50-51. All this does is look for a claim of the type ClaimTypes.NameIdentifier. This can be non-intuitive, as the source for this is the sub property from the JWT3. A valid JWT with a sub claim is how we tell we have a logged on user; an authenticated user is considered authorized.

You may have noticed that, when we were describing the entities for the API, we did not mention a User type. The reason for that is simple; the only user information it stores is the sub. Requests are assigned by user ID, and the user ID is included with every attempt to make any change to a request. This eliminates URL hacking or rogue API posting being able to get anything meaningful from the API.

The userId function, just below the user function, extracts this claim and returns its value, and it's used throughout the remainder of Handlers.fs. add (line 160) uses it to set the user ID for a new request. addHistory (line 192) and addNote (line 218) both use the user ID, as well as the passed request ID, to try to retrieve the request before adding history or notes to it. journal (line 137) uses it to retrieve the journal by user ID.

 

We now have a complete application, with the same user session providing access to the Vue app and tying all API calls to that user. We also use it to maintain data security among users, while truly outsourcing all user data to Microsoft or Google (the two external providers currently registered). We do still have a few more stops on our tour, though; the next is the back end data store.


1 Sorry, Elm; it's not you, it's me…

2 This does work, but not indefinitely; if I leave the same browser window open from the previous day, I still have to sign in again. I very well could be “doing it wrong;” this is an area where I probably experienced the most learning through creating this project.

3 I won't share how long it took me to figure out that sub mapped to that; let's just categorize it as “too long.” In my testing, it's the only claim that doesn't come across by its JWT name.

Categorized under , , ,
Tagged , , , , , , , ,

Sunday, August 26, 2018
  A Tour of myPrayerJournal: State in the Browser

NOTES:

  • This is post 3 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.

Flux (a pattern that originated at Facebook) defines state, as well as actions that can mutate that state. Redux is the most popular implementation of that pattern, and naturally works very well with React. However, other JavaScript framewoks use this pattern, as it ensures that state is managed sanely. (Well, the state is sane, but so is the developer!)

As part of Vue, the Vuex component is a flux implementation for Vue that provides a standard way of managing state. They explain it in much more detail, so if the concept is a new one, you may want to read their “What is Vuex?” page before you continue. Once you are ready, let's continue and take a look at how it's used in myPrayerJournal.

Defining the State

The store (mpj:store/index.js) exports a single new Vuex.Store instance, with its state property defining the items it will track, along with initial values for those items. This represents the initial state of the app, and is run whenever the browser is refreshed.

In looking at our store, there are 4 items that are tracked; two items are related to authentication, and two are related to the journal. As part of authentication (which will get a further exploration in its own post), we store the user's profile and identity token in local storage; the initial values for those items attempt to access those values. The two journal related items are simply initialized to an empty state.

Mutating the State

There are a few guiding principles for mutations in Vuex. First, they must be defined as part of the mutations property in the store; outside code cannot simply change one state value to another without going through a mutation. Second, they must be synchronous; mutations must be a fast operation, and must be accomplished in sequence, to prevent race conditions and other inconsistencies. Third, mutations cannot be called directly; mutations are “committed” against the current store. Mutations receive the current state as their first parameter, and can receive as many other parameters as necessary.

(Side note: although these functions are called “mutations,” Vuex is actually replacing the state on every call. This enables some really cool time-traveling debugging, as tools can replay states and their transformations.)

So, what do you do when you need to run an asynchronous process - like, say, calling an API to get the requests for the journal? These processes are called actions, and are defined on the actions property of the store. Actions receive an object that has the state, but it also has a commit property that can be used to commit mutations.

If you look at line 87 of store/index.js, you'll see the above concepts put into action1 as a user's journal is loaded. This one action can commit up to 4 mutations of state. The first clears out whatever was in the journal before, committing the LOADED_JOURNAL mutation with an empty object. The second sets the isLoadingJournal property to true via the LOADING_JOURNAL mutation. The third, called if the API call resolves successfully, commits the LOADED_JOURNAL mutation with the results. The fourth, called whether it works or not, commits LOADING_JOURNAL again, this time with false as the parameter.

A note about the names of our mutations and actions - the Vuex team recommends defining constants for mutations and actions, to ensure that they are defined the same way in both the store, and in the code that's calling it. This code follows their recommendations, and those are defined in action-types.js and mutation-types.js in the store directory.

Using the Store

So, we have this nice data store with a finite number of ways it can be mutated, but we have yet to use it. Since we looked at loading the journal, let's use it as our example (mpj:Journal.vue). On line 56, we wrap up the computed properties with ...mapState, which exposes data items from the store as properties on the component. Just below that, the created function calls into the store, exposed as $store on the component instance, to execute the LOAD_JOURNAL action.

The template uses the mapped state properties to control the display. On lines 4 and 5, we display one thing if the isLoadingJournal property is true, and another (which is really the rest of the template) if it is not. Line 12 uses the journal property to display a RequestCard (mpj:RequestCard.vue) for each request in the journal.

I mentioned developer sanity above; and in the last post, I said that the logic that has RequestCard making the decision on whether it should show, instead of Journal deciding which ones it should show, would make sense. This is where we put those pieces together. The Vuex store is reactive; when data from it is rendered into the app, Vue will update the rendering if the store changes. So, Journal simply displays a “hang on” note when the journal is loading, and “all the requests” once it's loaded. RequestCard only displays if the request should be displayed. And, the entire “brains” behind this is the thing that starts the entire process, the call to the LOAD_JOURNAL action. We aren't moving things around, we're simply displaying the state of things as they are!

Navigation (mpj:Navigation.vue) is another component that bases its display off state, and takes advantage of the state's reactivity. By mapping isAuthenticated, many of the menu items can be shown or hidden based on whether a user is signed in or not. Through mapping journal and the computed property hasSnoozed, the “Snoozed” menu link does not show if there are no snoozed requests; however, the first time a request from the journal is snoozed, this one appears just because the state changed.

This is one of the things that cemented the decision to use Vue for the front end2, and is one of my favorite features of the entire application. (You've probably picked up on that already, though.)

 

We've now toured our stateful front end; next time, we'll take a look at the API we use to get data into it.


1 Pun not originally intended, but it is now!

2 The others were the lack of ceremony and the Single File Component structure; both of those seem quite intuitive.

Categorized under , ,
Tagged , , , , , , , , , , ,

Saturday, August 25, 2018
  A Tour of myPrayerJournal: The Front End

NOTES:

  • 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.

Vue is a front-end JavaScript framework that aims to have very little boilerplate and ceremony, while still presenting a componentized abstraction that can scale to enterprise-level if required1. Vue components can be coded using inline templates or multiple files (splitting code and template). Vue also provides Single File Components (SFCs, using the .vue extension), which allow you to put template, code, and style all in the same spot; these encapsulate the component, but allow all three parts to be expressed as if they were in separate files (rather than, for example, having an HTML snippet as a string in a JavaScript file). The Vetur plugin for Visual Studio Code provides syntax coloring support for each of the three sections of the file.

Layout

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.)

Routing

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 App.vue, the 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 :), ensure props: true is part of the route configuration

The 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. router-link).

Components

When software developers hear “components,” they generally think of reusable pieces of software that can be pulled together to make a system. While that isn't wrong, it's important to understand that “reusable” does not necessarily mean “reused.” For example, the privacy policy (mpj:PrivacyPolicy.vue) is a component, but reusing it throughout the application would be… well, let's just say a “sub-optimal” user experience.

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 script section, while the privacy policy component only has a template section.

Component Interaction

Before we dive into the specifics of events, let's look again at Journal and 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 requestUnsnoozed and 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 Journal and 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.

Categorized under , ,
Tagged , , , , , , , , , , , ,

Friday, August 24, 2018
  A Tour of myPrayerJournal: Introduction

Recently, we released version 1.0 of myPrayerJournal, a minimalistic prayer journaling application. This series aims to provide a tour of the code, with several stops along the way:

From a technical perspective, this application was going to be a learning experience. We knew we wanted to use a Single Page Application (SPA) framework with an API; we'd built APIs before, but had yet to build a SPA. For front-end frameworks, we started with Angular, went through Aurelia and Elm, then decided on Vue. For the back-end API, we started with Suave, then went live on Node.js with Koa; later, we moved it to Go, and after .NET Core 2.1 was released, landed on Giraffe. The “learning experience” part was a success; through all these attempts, we utilized 5 different languages and 3 different database access techniques.

To understand the requirements, a short explanation of the process will help. “Prayer journaling” is a discipline where a person will write down the things for which they are praying; this provides a defined list to help guide their prayer, and helps them not forget things. Then, as the situation changes, they can record updates, through to the resolution of the situation (also called the request being “answered”). This discipline not only helps to focus efforts, it also provides a record of requests and answers. Although people have successfully used a notebook, or something similar, for a long time, that approach does have some downsides:

  • For long term requests, you can run out of room for updates.
  • A physical journal can only be in one place at one time.
  • Answered requests coexist with unanswered requests, so you have to flip pages past them.
  • Books can end up under stacks of other things, falling victim to “out of sight, out of mind.”

Looking to address some of those, the initial requirements started as the first three bullets below. The remaining requirements emerged through using the application as it was being developed.

  • List unanswered requests, in a way that they can be marked as prayed or answered, and be updated
  • List answered requests, and allow full requests (and their history) to be viewed
  • Do the above in a way that will not be distracting
  • Allow notes to be recorded for a request; not every update on a situation requires a change in the verbiage of the request
  • Allow requests to be “snoozed” (removed from the journal, with a specified date when they will reappear), and list snoozed requests so that the snooze can be expired (returning the request to the journal immediately)
  • Allow requests to be prioritized (this became the request recurrence feature)

Armed with these requirements, we will pick up next time with a look at the Vue front end.

Categorized under , , , ,
Tagged , , , , , , , , , , , , , , ,