Sunday 24 March 2019

Template Tricks for a Dainty DOM

I'm somewhat into code golf. Placing restrictions on what you're "allowed" to do in code and seeing what the happens as a result. I'd like to share with you something that came out of some recent dabblings.

Typically I spend a good amount of time playing with TypeScript. Either working on build tools or making web apps with it. (Usually with a portion of React on the side.) This is something different.

I have a side project on the go which is essentially a mini analytics dashboard. For the purposes of this piece let's call it "StatsDash". When I was starting it I thought: let's try something different. Let's build StatsDash with HTML only. The actual HTML is hand cranked by me and generated in ASP.Net Core / C# using a combination of LINQ and string interpolation. (Who needs Razor? 😎) I'll say it's pretty fun - but the back end is not what I want to focus on.

I got something up and running pretty quickly in pure HTML. The first lesson I learned was this: HTML alone is hella ugly. So I relaxed my criteria; I allowed CSS to come play as long as I didn't have to write any / much myself. There followed some experimentation with different CSS frameworks. For a while I rolled with Bootstrap (old school!), then Bulma and finally I settled on Materialized. Materialized is a heavily inspired by Google's Material Design and is hence quite beautiful. With my HTML and Materialize's CSS we were rolling. Beautiful stats - no JS.

"Oh All Right; Just a Splash"

Lovely as things were, StatsDash quickly got to the point where there was too much information on the screen. It was time to make some changes. If data is to convey a message, it must first be comprehensible.

I needed a way to hide and show data as people interacted with StatsDash. I wanted to achieve this without starting to render on the client side and also without going back to the server each time.

If you want interactions in your UI all roads lead to JS. It's certainly possible to do some tricks with CSS but that's a round of code golf I'm ill equipped to play. So, I took a look at what Materialized had to offer. Usefully it has a Modal component. With that in play I'd be able to separate the detailed information into different modals which the users could show and hide as required. Perfect!

It required a little JS. What's a line or two between friends? Dear reader, I compromised once more.

The DOM Bunker

With my handy modals, StatsDash was now a one stop shop for a great deal of information. Info which took the form of DOM nodes. Lots of them. And by "lots of them" I want you to think along the lines of "space is big, really big...".

This was impacting users. Clicking to open a modal resulted in a noticeable lag. It would take 2+ seconds for the browser to respond. Users found themselves clicking multiple times; wondering why nothing seemed to occur. In the end the modal would shuffle into view. However, this wasn't the best experience. The lack of responsiveness was getting in the way of users enjoying all StatsDash had to offer.

Running an audit of StatsDash in Chrome DevTools there was no doubt we had a DOM problem:

What to do? I still didn't want to go back to the server on each click in StatsDash. And I didn't want to start writing rendering code on the client as well either. I have in the past mixed client and server side rendering and I know well that it's a first class ticket to a confusing codebase.

Smuggling DOM in Templates

There's a mechanism that supports this use case directly: the <template> element. To quote MDN:

The HTML Content Template (<template>) element is a mechanism for holding client-side content that is not to be rendered when a page is loaded but may subsequently be instantiated during runtime using JavaScript.

Think of a template as a content fragment that is being stored for subsequent use in the document.

This is exactly what I'm after. I can keep my rendering server side, but instead wrap content that isn't immediately visible to users inside a <template> element and render that only when users need it.

So in the case of my modals (where most of my DOM lives), I can tuck the contents of each modal into a <template> element. Then, when the user clicks to open a modal we move that template content into the DOM so they can see it. Likewise, as they close a modal we can clear out the modal's DOM content to ease the load on the dear old browser.

"That Sounds Complicated..."

It's not. Let me show you how easily this is accomplished. First of all, wrap all your modal contents into <template> elements. They should look a little something like this:

<div>
    <button data-target="modalId" class="btn modal-trigger">Open the Modal!</button>

    <template>
        <!--
        loads of DOM nodes
        -->
    </template>

    <div id="modalId" class="modal modal-fixed-footer"></div>
</div>

Next, where you initialise your modals you need to make a little tweak:

document.addEventListener('DOMContentLoaded', function() {
    M.Modal.init(document.querySelectorAll('.modal'), {
        onOpenStart: modalDiv => {
            const template = modalDiv.parentNode.querySelector('template');

            modalDiv.appendChild(document.importNode(template.content, true));
        },
        onCloseEnd: modalDiv => {
            while (modalDiv.firstChild) {
                modalDiv.removeChild(modalDiv.firstChild);
            }
        }
    });
});

That's it! As you can see, before we open our modals, the onOpenStart callback will fire which creates the actual DOM elements based upon the template. And when the modals finish closing the onCloseEnd callback runs to remove those DOM elements once more.

For this minimal change, the client gets a dramatically different user experience. StatsDash went from super laggy to satisfyingly fast. Using templates, The number of initial DOM nodes dropped from more than 20,000 to 200. That's right πŸ’― times smaller!

Do It Yourself

The code examples above rely upon the Materialize modals. However the principles used here are broadly applicable. It's easy for you to take the approach outlined here and apply it in a different situation.

If you're interested in some of the other exciting things you can do with templates then I recommend Eric Bidelman's post on the topic.

Friday 22 March 2019

Google Analytics API and ASP.Net Core

Some of my posts are meaningful treaties on the nature of software development. Some are detailed explanations of approaches you can use. Some are effectively code dumps. This is one of those.

I recently had need to be able to access the API for Google Analytics from ASP.Net Core. Getting this up and running turned out to be surprisingly tough because of an absence of good examples. So here it is; an example of how you can access a simple page access stat using the API:

async Task<SomeKindOfDataStructure[]> GetUsageFromGoogleAnalytics(DateTime startAtThisDate, DateTime endAtThisDate)
{
    // Create the DateRange object. Here we want data from last week.
    var dateRange = new DateRange
    {
        StartDate = startAtThisDate.ToString("yyyy-MM-dd"),
        EndDate = endAtThisDate.ToString("yyyy-MM-dd")
    };
    // Create the Metrics and dimensions object.
    // var metrics = new List<Metric> { new Metric { Expression = "ga:sessions", Alias = "Sessions" } };
    // var dimensions = new List<Dimension> { new Dimension { Name = "ga:pageTitle" } };
    var metrics = new List<Metric> { new Metric { Expression = "ga:uniquePageviews" } };
    var dimensions = new List<Dimension> { 
        new Dimension { Name = "ga:date" },
        new Dimension { Name = "ga:dimension1" } 
    };

    // Get required View Id from configuration
    var viewId = $"ga:{"[VIEWID]"}";

    // Create the Request object.
    var reportRequest = new ReportRequest
    {
        DateRanges = new List<DateRange> { dateRange },
        Metrics = metrics,
        Dimensions = dimensions,
        FiltersExpression = "ga:pagePath==/index.html",
        ViewId = viewId
    };

    var getReportsRequest = new GetReportsRequest {
        ReportRequests = new List<ReportRequest> { reportRequest }
    };
        
    //Invoke Google Analytics API call and get report
    var analyticsService = GetAnalyticsReportingServiceInstance();
    var response = await (analyticsService.Reports.BatchGet(getReportsRequest)).ExecuteAsync();

    var logins = response.Reports[0].Data.Rows.Select(row => new SomeKindOfDataStructure {
        Date = new DateTime(
            year: Convert.ToInt32(row.Dimensions[0].Substring(0, 4)), 
            month: Convert.ToInt32(row.Dimensions[0].Substring(4, 2)), 
            day: Convert.ToInt32(row.Dimensions[0].Substring(6, 2))),
        NumberOfLogins = Convert.ToInt32(row.Metrics[0].Values[0])
    })
    .OrderByDescending(login => login.Date)
    .ToArray();

    return logins;
}

/// <summary>
/// Intializes and returns Analytics Reporting Service Instance
/// </summary>
AnalyticsReportingService GetAnalyticsReportingServiceInstance() {
    var googleAuthFlow = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer {
        ClientSecrets = new ClientSecrets {
            ClientId = "[CLIENTID]",
            ClientSecret = "[CLIENTSECRET]"
        }
    });

    var responseToken = new TokenResponse {
        AccessToken = "[ANALYTICSTOKEN]",
        RefreshToken = "[REFRESHTOKEN]",
        Scope = AnalyticsReportingService.Scope.AnalyticsReadonly, //Read-only access to Google Analytics,
        TokenType = "Bearer",
    };

    var credential = new UserCredential(googleAuthFlow, "", responseToken);

    // Create the  Analytics service.
    return new AnalyticsReportingService(new BaseClientService.Initializer {
        HttpClientInitializer = credential,
        ApplicationName = "my-super-applicatio",
    });
}

You can see above that you need various credentials to be able to use the API. You can acquire these by logging into GA. Enjoy!

Wednesday 6 March 2019

The Big One Point Oh

It's time for the first major version of fork-ts-checker-webpack-plugin. It's been a long time coming :-)

A Little History

The fork-ts-checker-webpack-plugin was originally the handiwork of Piotr OleΕ›. He raised an issue with ts-loader suggesting it could be the McCartney to ts-loader's Lennon:

Hi everyone!

I've created webpack plugin: fork-ts-checker-webpack-plugin that plays nicely with ts-loader. The idea is to compile project with transpileOnly: true and check types on separate process (async). With this approach, webpack build is not blocked by type checker and we have semantic check with fast incremental build. More info on github repo :)

So if you like it and you think it would be good to add some info in README.md about this plugin, I would be greatful.

Thanks :)

We did like it. We did think it would be good. We took him up on his kind offer.

Since that time many people have had their paws on the fork-ts-checker-webpack-plugin codebase. We love them all.

One Point Oh

We could have had our first major release a long time ago. The idea first occurred when webpack 5 alpha appeared. "Huh, look at that, a major version number.... Maybe we should do that?" "Great idea chap - do it!" So here it is; fresh out the box: v1.0.0

There are actually no breaking changes that we're aware of; users of 0.x fork-ts-checker-webpack-plugin should be be able to upgrade without any drama.

Incremental Watch API on by Default

Users of TypeScript 3+ may notice a performance improvement as by default the plugin now uses the incremental watch API in TypeScript.

Should this prove problematic you can opt out of using it by supplying useTypescriptIncrementalApi: false. We are aware of an issue with Vue and the incremental API. We hope it will be fixed soon - a generous member of the community is taking a look. In the meantime, we will not default to using the incremental watch API when in Vue mode.

Compatibility

As it stands, the plugin supports webpack 2, 3, 4 and 5 alpha. It is compatible with TypeScript 2.1+ and TSLint 4+.

Right that's it - enjoy it! And thanks everyone for contributing - we really dig your help. Much love.