Create an Aurelia Application with ASP.NET Core 1.0, Part 4 - Screens

In this final post of the series, I demonstrate how to convert a couple of JavaScript screens into Aurelia screens.

Written by Jonathan "JD" Danylko • Last Updated: • Develop •
Train Tracks

Aurelia has shown a lot of promise in this project so far since we setup our dev environment, our navigation, and finally, our web services.

Today, we'll pull all of the components together into the application and convert our old JavaScript into Aurellia screens.

A couple of notes before we proceed with the screens:

  • I consider Aurelia's View/ViewModel similar to Microsoft's WebForms. You have a View (.aspx) and a code-behind file (.aspx.cs). I know the JavaScript is called a ViewModel, but old habits die hard...and since Rob is a front-end Microsoft-stack developer, I think he subconciously went this route. ;-)
  • There are three kinds of binding (as i came to find out) to control the direction of how you want your data to flow: one-way, two-way, or one-time. For our purposes, we'll be using the bind method which automatically selects which binding method to use. For forms, it uses two-way binding, but for everything else, it uses one-way binding.
  • I added my old CSS and the boostrap Slate theme from the Optimization Series to bring the design back to life.

So let's finish this Aurelia application with some screens.

Some Assembly Required

In our last post, we created a simple speakers View/ViewModel to confirm we were receiving our data from the web services.

Since we exposed a property called SpeakerList, we can expand our view to show a list of our speakers. We just need to update our View.

wwwroot/views/Speakers.html

<template>
    <div class="speaker-list">
        <a href="#" repeat.for="speaker of speakerList"
           class="list-group-item speaker">
            <div class="media">
                <div class="media-object pull-left">
                    <img src="${speaker.gravatarUrl}" 
                         alt="${speaker.firstName} ${speaker.lastName}"
                         class="speaker-img img-rounded" />
                </div>
                <div class="media-body">
                    <h4 class="media-heading">
                        ${speaker.firstName} ${speaker.lastName}
                    </h4>
                </div>
            </div>
        </a>
    </div>
</template>

Looks a little futuristic, doesn't it?...but definitely understandable.

The anchor tag has a strange attribute called repeat-for. This is the equivalent of a for..each in C# or JavaScript.

The speakerList property in our ViewModel contains our speakers and we loop through each one storing each speaker in the speaker variable inside this tag. Note that you can attach this to ANY element to achieve a list of items no matter what the tag.

The great news is we already had our partial view working from our ASP.NET MVC application. All we had to do is remove the Razor syntax and replace it with the properties of the speaker object.

The Go-Nowhere Link

Ooooo...we have an issue. If you notice the anchor, we are going to a hash (or pound-sign for you old-schoolers).

We need a way to display the details of this speaker.

So how do we define the Url for a new speaker?

Remember when I said the routing is the key to understanding how everything fits together? No? Well, I'm saying it now. 

We can access the route by specifying a route-href attribute in the anchor tag and asking for a particular route and have it return a Url for us.

We can replace the href="#" with this route-href attribute.

<a route-href="route:speakerDetail; params.bind:{ id:speaker.id }"
   repeat.for="speaker of speakerList"
   class="list-group-item speaker">

Since we have access to the routing, we can tell the router that we want to use the speakerDetail route and pass in the following parameters, which is the speaker.id. This will provide us with our Url.

If you are familiar with ASP.NET MVC, consider this to be similar to naming your routes and calling them by name.

So now we have our speakerDetail route.

Oh wait...Umm...what speakerDetail route?

Yes, we need to add the speakerDetail screen and route.

Since our navigation is controlled in our App.js, we need another entry for our speakerDetail page.

wwwroot/App.js

export class App {

    configureRouter(config, router) {         config.title = 'Codemash App';
        config.map([           { route: ['','codemash'], name: 'codemash', moduleId: './views/Codemash', nav: true, title:'Home'},           { route: 'speakers', name: 'speakers', moduleId: './views/Speakers', nav: true, title:'Speakers'},           { route: 'speakerDetail', name: 'speakerDetail', moduleId: './views/SpeakerDetails', nav: false, title:'Speaker Details'}         ]);
        this.router = router;     } }

We now have our speakerDetail in place for our router to know where the speakerDetail is located in our application.

Our Speaker Detail page will get a little tricky based on our services we built.

Let's dig into that aspect of the app.

Give Me Details

As you know, we have some interesting things to deal with here, such as how do we grab the id from url or how do we use multiple services in one ViewModel?

Whoa, whoa...one question at a time. ;-)

Remember the anchor with the route-href where we defined the bound parameters which was the speaker.id?

Those parameters are passed by convention over to the activate method which is part of the Screen Activation Lifecycle. On activation of the speakerDetails screen, we receive a nicely-packaged model of our parameters we require to load our speaker.

With this speaker detail screen, we also want to know what sessions this presenter will be...umm...presenting. We also need the session web service. How do we add another web service to the speaker details view model?

By injecting it.

wwwroot/views/SpeakerDetails.js

import {SpeakerWebService} from '../services/speakerWebService';
import {SessionWebService} from '../services/sessionWebService';
import {inject} from 'aurelia-framework';

@inject(SpeakerWebService, SessionWebService) export class SpeakerDetails {
    constructor(speakerService, sessionService) {         this.speakerService = speakerService;         this.sessionService = sessionService;     }

    activate(params) {         this.speakerService.getSpeakerById(params.id)             .then(speaker => {                 this.speaker = speaker;             });         this.sessionService.getSessionsBySpeakerId(params.id)             .then(sessions => {                 this.sessions = sessions;             });     } }

We import the SessionWebService like any other service and inject it into the constructor so we can use it later.

You may be wondering how we "getSessionsBySpeakerId." Here is an updated sessionsWebService.js.

wwwroot/services/sessionWebService.js

import {HttpClient} from 'aurelia-fetch-client';
import {inject} from 'aurelia-framework';

@inject(HttpClient) export class SessionWebService {
    constructor(http) {         this.http = http;         this.url = 'http://localhost:37432/api/Session'; // TODO: fix the url     }
    getSessions() {         return this.http.fetch(this.url)                     .then(response => response.json())                     .catch(error => console.log(error));     }
    getById(id) {         return this.http.fetch(this.url)             .then(response => response.json())             .then(response => {                 var sessionId = parseInt(id);                 var item = response.find(x => x.id === sessionId);                 return item;             })             .catch(error => console.log(error));     }
    getSessionsBySpeakerId(id) {         return this.http.fetch(this.url)             .then(response => response.json())             .then(response => {                 var item = response.filter(session =>                      session.speakers.find(speaker => speaker.id === id));                 return item;             })             .catch(error => console.log(error));     } }

The one thing that I'm appreciating more with ES6 is the ability to have LINQ-style queries with lambdas (as you can see). 

Now back to our activate method.

In our activate method, we load the speaker and sessions and set a property in the class for each one. These two properties will be available at the class level for our View.

wwwroot/views/SpeakerDetails.html

<template>
    <a route-href="route:speakers" class="back-button"><i class="fa fa-angle-double-left"></i> Back</a>
    <div class="well">
        <div class="text-center">
            <img src="${speaker.gravatarUrl}"
                 title="${speaker.firstName} ${speaker.lastName}" 
                 alt="${speaker.firstName} ${speaker.lastName}"
                 class="img-rounded" />
            <h3>${speaker.firstName} ${speaker.lastName}</h3>
        </div>
        <p>${speaker.biography}</p>
        <h4 class="list-group-item-heading">Sessions</h4>
        <div class="list-group">
            <div repeat.for="session of sessions">
                <div class="list-group-item">
                    <h4 class="list-group-item-heading">${session.title}</h4>
                </div>
            </div>
        </div>
    </div>
</template>

If you notice, I added a "Back" link to go back to our main speakers page using the route-href attribute. We used the name route and let the router handle the rest.

This template is similar to the Speakers, but we are only dealing with one speaker and multiple sessions.

With me so far?

Good! Let's keep going with the Sessions.

I will be going at a faster pace since most of this is going to be repetitive, but I will stop to highlight the important parts.

Creating the Session Routes

Of course, we need to add the Sessions and SessionDetails pages as well as add the route in the app.js.

wwwroot/app.js

export class App {
    
    configureRouter(config, router) {
        config.title = 'Codemash App';
        config.map([
          { route: ['','codemash'], name: 'codemash', moduleId: './views/Codemash', nav: true, title:'Home'},
          { route: 'speakers', name: 'speakers', moduleId: './views/Speakers', nav: true, title:'Speakers'},
          { route: 'sessions', name: 'sessions', moduleId: './views/Sessions', nav: true, title:'Sessions'},
          { route: 'sessionDetail', name: 'sessionDetail', moduleId: './views/SessionDetails', nav: false, title:'Session Details'},
          { route: 'speakerDetail', name: 'speakerDetail', moduleId: './views/SpeakerDetails', nav: false, title:'Speaker Details'}
        ]);
        this.router = router;
    }
}

Pay attention to the speakers and sessions routes. They have a property called nav. This is meant for display purposes in your routing. This will automatically appear in your navigation if it's true. If not, it's another screen for you to access.

Notice the speakerDetail and sessionDetail navs are false.

The big one: Session Screens

The session screens were interesting to deal with.

It definitely showed me a way to create "calculated properties" and formatting at a data level.

At first, I was performing all of my formatting in each screen. Of course, I didn't want to duplicate any code, so I dropped it down to the data level.

My new session web service was refactored.

wwwroot/services/sessionWebService.js

import {HttpClient} from 'aurelia-fetch-client';
import {inject} from 'aurelia-framework';

@inject(HttpClient) export class SessionWebService {

    constructor(http) {         this.http = http;         this.url = 'http://localhost:37432/api/Session';     }
    getSessions() {         return this.http.fetch(this.url)             .then(response => response.json())             .then(response => {                 for (var session of response) {                     session.formattedSessionTime = this.updateSessionTimes(session);                     session.formattedPresenters = this.updateSpeakers(session);                     session.formattedRooms = this.updateRoomList(session);                 }                 return response;             })             .catch(error => console.log(error));     }
    getById(id) {         return this.http.fetch(this.url)             .then(response => response.json())             .then(response => {                 var sessionId = parseInt(id);                 var item = response.find(x => x.id === sessionId);                 item.formattedSessionTime = this.updateSessionTimes(item);                 item.formattedPresenters = this.updateSpeakers(item);                 item.formattedRooms = this.updateRoomList(item);                 return item;             })             .catch(error => console.log(error));     }
    getSessionsBySpeakerId(id) {         return this.http.fetch(this.url)             .then(response => response.json())             .then(response => {                 var item = response.filter(session =>                      session.speakers.find(speaker => speaker.id === id));                 for (var session of item) {                     session.formattedSessionTime = this.updateSessionTimes(session);                     session.formattedPresenters = this.updateSpeakers(session);                     session.formattedRooms = this.updateRoomList(session);                 }                 return item;             })             .catch(error => console.log(error));     }

    updateRoomList(session) {         return session.rooms.join(", ");     }
    updateSpeakers(session) {         var speakerList = [];         for (var i = 0; i < session.speakers.length; i++) {             speakerList.push(session.speakers[i].firstName +                  " " + session.speakers[i].lastName);         }         return speakerList.join(", ");     }
    updateSessionTimes(session) {         var result = "N/A";         if (session !== null) {             var start = new Date(session.sessionStartTime);             var stop = new Date(session.sessionEndTime);             result = start.getHours() +                 ":" +                 (start.getMinutes() < 10 ? '0' : '') +                 start.getMinutes() +                 " - " +                 stop.getHours() +                 ":" +                 (stop.getMinutes() < 10 ? '0' : '') +                 stop.getMinutes();         }         return result;     } }

I can already hear some people commenting about the session times.

  • "Why don't you use <blah> date/time JavaScript library?" - I may add that into the code at a later date, but for now, I wanted to move forward quickly.
  • "It would make more sense to place that code into the domain." - Yes, yes it does. Again, when I become a little more familiar with Aurelia, I will transfer it to a session domain object with formatted properties.

For now, our Views and ViewModels are all ready to go.

wwwroot/views/Sessions.html

<template>
    <div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
        <div class="panel panel-default">
            <div class="panel-heading" role="tab" id="headingTwo">
                <h4 class="panel-title">
                    <a class="collapsed" role="button" data-toggle="collapse"
                       data-parent="#accordion" href="#collapseTwo" aria-expanded="false"
                       aria-controls="collapseTwo">
                        Sessions
                    </a>
                </h4>
            </div>
            <div id="collapseTwo" class="panel-collapse collapse in" role="tabpanel"
                 aria-labelledby="headingTwo">
                <ul class="list-group">
                    <li repeat.for="session of SessionsList">
                        <a route-href="route:sessionDetail; params.bind:{ id:session.id }"
                           class="session list-group-item">
                            <h4 class="list-group-item-heading">${session.title}</h4>
                            <!--<i class="favorite fa fa-heart fa-2x pull-right"></i>-->
                            <ul class="list-inline list-unstyled small">
                                <li>
                                    <i class="fa fa-user fa-fw"></i>
                                    ${session.formattedPresenters}
                                </li>
                                <li>
                                    <i class="fa fa-calendar fa-fw"></i>
                                    ${session.startdate}
                                    from
                                    ${session.formattedSessionTime}
                                </li>
                                <li>
                                    <i class="fa fa-map-marker fa-fw"></i>
                                    ${session.formattedRooms}
                                </li>
                            </ul>
                        </a>
                    </li>
                </ul>
            </div>
        </div>

    </div> </template>

wwwroot/views/Sessions.js

import {SessionWebService} from '../services/sessionWebService';
import {inject} from 'aurelia-framework';
@inject(SessionWebService)
export class Sessions {

    constructor(service) {         this.service = service;     }

    activate() {         this.service.getSessions()             .then(data => {                 this.SessionsList = data;             });     } }

wwwroot/views/SessionDetails.html

<template>
    <a route-href="route:sessions" class="back-button">
        <i class="fa fa-angle-double-left"></i> Back
    </a>
    <div class="list-group-item session-info" data-id="${session.id}">
        <h4 class="list-group-item-heading">${session.title}</h4>
        <ul class="list-inline list-unstyled small">
            <li>
                <i class="fa fa-user fa-fw"></i>
                ${session.formattedPresenters}
            </li>
            <li>
                <i class="fa fa-calendar fa-fw"></i>
                ${session.startdate}
                from
                ${session.formattedSessionTime}
            </li>
            <li>
                <i class="fa fa-map-marker fa-fw"></i>
                ${session.formattedRooms}
            </li>
            <li></li>
        </ul>

        <p class="list-group-item-text">${session.abstract}</p> 
            </div> </template>

wwwroot/views/SessionDetails.js

import {SessionWebService} from '../services/sessionWebService';
import {inject} from 'aurelia-framework';
@inject(SessionWebService)
export class SessionDetails {

    constructor(service) {         this.service = service;     }

    activate(params) {         this.service.getById(params.id)             .then(session => {                 this.session = session;             });     } }

Sorry for the amount of code, but since everything is in the data layer, most of the Views and ViewModels were small and simple to implement.

"We need Sessions...Can Ya Hold?"

One final thing to mention is latency issues.

While we are "on hold" and waiting for our data, we need something to entertain ourselves. ;-)

We need a spinner.

In our views/navigation.html, I added a simple FontAwesome spinner to the header on the far right.

<div class="navbar-right">
    <span if.bind="router.isNavigating"
          class="loading-status fa fa-spinner fa-spin fa-2x">
    </span>
</div>

We use the if.bind check to see if we are "navigating" to another page. This says that if we "IsNavigating", display this span which contains our spiny icon.

Entertaining spinner is finished.

Conclusion

As you can see, once you have the pattern of creating Views with ViewModels, writing Aurelia applications becomes simple to understand.

For me, it's the initial shock of learning the ES6/Aurelia way of doing things.

Honestly, if you know C#, I feel it brings you a little bit closer to the ES6 way of writing JavaScript code.

I hope this gives you a glimpse into what Aurelia can achieve. I feel like I've only scratched the surface.

If you want more of Aurelia, please let me know in the comments below.

To continue your journey, visit the Aurelia DocHub.

Was this a good insight? Did you understand the code? Did I miss something? Post your comments below.

ASP.NET 8 Best Practices on Amazon

ASP.NET 8 Best Practices by Jonathan Danylko


Reviewed as a "comprehensive guide" and a "roadmap to excellence" with over 120 Best Practices for ASP.NET Core 8, Jonathan's first book by Packt Publishing explores proven techniques for every phase of the SDLC.

Learn industry-standard concepts to improve your coding, debugging, and deployment of ASP.NET Core websites.

Order now on Amazon.com button

Picture of Jonathan "JD" Danylko

Jonathan "JD" Danylko is an author, web architect, and entrepreneur who's been programming for over 30 years. He's developed websites for small, medium, and Fortune 500 companies since 1996.

He currently works at Insight Enterprises as an Architect.

When asked what he likes to do in his spare time, he replies, "I like to write and I like to code. I also like to write about code."

comments powered by Disqus