Posts Tagged “pug”

Sunday, November 28, 2021
  A Tour of myPrayerJournal v3: The User Interface

NOTE: This is the second post in a series; see the introduction for information on requirements and links to other posts in the series.

If you are a seasoned Single Page Application (SPA) framework developer, you likely think about interactivity in a particular way. Initially, I focused on replacing each interactive piece in isolation. In the end, though, requests for “pages” returned almost everything but the HTML head info and the displayed footer - and I was happy about it. Keep that in mind as I walk you down the path I have already traveled; keep an open mind, and read to the end before forming strong opinions either way.

The $.05 Tour of Pug and Giraffe View Engine

Understanding the syntax of both Pug and Giraffe View Engine will help you if you click any of the source code example links. While a complete explanation of these two templating languages would make this long post much longer than it already is, here are some short examples of their syntax. Using a string variable who with the contents “World”, we will show both languages rendering:

<p id="example" class="greeting">Hello <strong>World</strong></p>

myPrayerJournal v2 used Pug templates in Vue to render the user interface. Pug uses indentation-sensitive tag/content pairs (or blocks), with JavaScript syntax for attributes, to generate HTML. To generate the example paragraph, the shortest template would look like:

p.greeting(id="example") Hello #[strong= who]

myPrayerJournal v3 uses Giraffe View Engine, which uses F# lists to generate HTML from a very HTML-looking domain-specific language (DSL). The example paragraph would be generated with:

p [ _id "example"; _class "greeting" ] [ str "Hello "; strong [] [ who ] ]

Given those examples, let's dig into the conversion.

The Menu

The menu across the top of the application was one of the first items I needed to convert. The menu needs to be different, depending on whether there is a user logged on or not. Also, if a user is logged on, the menu can still be different; the “Snoozed” menu item only appears if the user has any snoozed requests. The application uses Auth0 to manage users (which is how it is open to Microsoft and Google accounts), and I wanted to preserve this; my requests are tied to the ID provided by Auth0, so that did not need to change.

In the Vue version, the system used Auth0's SPA library that exposed whether there was a user logged on or not. Also, once a user was logged on, the API sent all the user's active requests, which included snoozed requests; once this API call returned, the application can turn on the “Snoozed” menu item. In the htmx version, though, this information is all generated on the server. My initial process was to use an hx-get to get the menu HTML snippet, using an hx-trigger of load to fill in this spot of the page when the page was loaded. I also (initially) implemented a custom HTML header to include in responses, and if that header was found, I would trigger a refresh on the menu; the eventual solution included the navbar in “page” refreshes.

(See the Vue “Navigation” component that became the Giraffe View Engine “navbar” function)

“New Page” in htmx

This leads directly into a discussion of how myPrayerJournal is still considered a SPA. In the Vue version, “pages” were Vue Single-File Components (SFCs) under the /components directory. (In the years since myPrayerJournal v1, the default Vue template has changed to place these SFCs under /views, while /components is reserved for shared components.) These view components rendered into a custom component within the main tag (using Vue router's router-view tag), while the nav component was reactive, based on the user logging on/off and snoozing requests.

In myPrayerJournal v3, “page” views target the #top section element. If the request is for a full page load, the HTML head content is rendered, as is the body's footer content; none of these change until a new version of the application is released. If the request is an htmx request, though, the only thing rendered is a new #top section, which includes the navigation bar and the page content. While this does approach a full “page load”, there are some key differences:

  • The page contents are refreshed based on one HTTP request (no extra request or processing required for the navbar);
  • The HTML head content is responsible for most of the large HTTP requests, such as those for JavaScript libraries (and is excluded from non-full-page views);
  • The page footer is not included.

Note the difference between the full view layout and the partial layout. Also, within the application's request handlers, there is a partial return function that determines whether this is an htmx-initiated page view request (in which case a partial view is returned) or a full page request (which returns the entire template).

Updating the Page Title

One of the most unexpectedly-vexing parts of a SPA is determining how the browser's title bar will be updated when navigation occurs. (I understand why it's challenging; what I do not understand is why it took major frameworks so long to devise a built-in way of handling this.) Coming from that world, I had originally implemented yet another custom header, pushing the title from the server, and used a request listener to update the title if the header was present. As I dug in further, though, I learned that htmx will update the document title any time a request payload has an HTML title element in its head. If you look at both layouts in the preceding paragraph, you'll notice that they include a head element with a title tag. This is how easy it should be, and with htmx, this is how easy it is.

At this point, there is a pattern emerging. The thought process behind an htmx-powered website is much different than a JavaScript-based SPA framework; and, in the majority of cases, it has been less complex. Now, let me contradict what I just said.

POST-Redirect-GET

In myPrayerJournal v2, updating a prayer request followed this flow:

  • Display the edit page, with the request details filled in
  • When the user saved the request, return an empty 200 OK response
  • Using Vue, display a notification, refresh the journal, then re-render the page where the user had been when they clicked “Edit” (there are multiple places from which requests can be edited)

While there are no redirects here, this is the classic traditional-web-application scenario where the “POST-Redirect-GET” (P-R-G) pattern is used. By using this pattern, the “Back” button on the browser still works. If you try to go back to the result of a POST request, the browser will warn you that your action will result in the data being resubmitted (not usually what you want to do). By redirecting, though, the result of a POST becomes a GET, which does not change any data. For traditional web applications, this is the user-friendliest way to handle updates.

In the htmx examples, they show an example of inline editing. This led to my first plan - change the request edit “page” to be a component, where the HTML for the displayed list was replaced by the form, and then the “Save” action returns the new HTML. This requires no P-R-G, as these actions have no effect on the “Back” button. It worked fine, but there were some things that weren't quite right:

  • New requests needed their own page; I was going to have to duplicate the edit form for the “new” page, and introduce some complexity in determining how to render the results.
  • Some updates required refreshing the list of requests, not just replacing the text and action buttons.

At this point, I was also starting to realize “if you think something is hard to do in htmx, you probably aren't trying to do it correctly.” So, I decided to try to replicate the “edit page” flow of v2 in v3. Creating the page was easy enough, and I was able to use the returnTo parameter in the function to both provide a “Cancel” button and redirect the user to the right place after saving. Easy, right? Well… Not quite.

htmx uses XMLHttpRequest (XHR) to send its requests, which has some interesting behavior; it follows redirects! When I submitted my form, it received the request (with htmx's HX-Request header set), and the server returned the redirect. XHR saw this, and followed it; however, it used the same method. (It was POSTing to the new URL.) The fix for this, though, was not easy to find, but easy to implement; use HTTP response code 303 (see other) instead of 307 (moved temporarily). Using this, combined with using hx-target="#top" on the form, allowed the P-R-G pattern to work successfully without double-POSTing and without a full-page refresh.

htmx Support in Giraffe and Giraffe View Engine

As I developed this, I was also building up extensions for Giraffe to handle the htmx request and response headers, as well as the attributes needed to generate htmx-aware markup from Giraffe View Engine. This project, called Giraffe.Htmx, is now published on NuGet. There are two packages; Giraffe.Htmx provides server-side support for the request and response headers, and Giraffe.ViewEngine.Htmx provides support for generating htmx attributes. I wrote about it when it was released, so I won't rehash the entire thing here.

Final UI Thoughts

htmx is much less complex than any other front-end JavaScript SPA framework I have ever used - which, for context, includes Angular, Vue, React, Ember, Aurelia, and Elm. Both in development and in production use, I cannot tell that the payloads are slightly larger; navigation is fast and smooth. Though I have yet to change anything since going live with myPrayerJournal v3, I know that maintenance will be quite straightforward (to be further explored in the conclusion post).

The UI for myPrayerJournal uses Bootstrap, a UI framework which has its own script, and htmx plays quite nicely with it. The next post in this series will describe how I interact with both Bootstrap and htmx, using modals and toasts on this “traditional” web application.

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 , , , , , , , , , , , ,