Introduction
What is Single Page Application (SPA)?
Ordinary HTML applications/web-sites fetch each page from the server every time the user wants to go to a new page.
SPA applications, however, bring all the pages of the application on the client's browser at the beginning and
use JavaScript in order to switch between the pages making parts of HTML visible and invisible depending on which page
is chosen. This makes the HTML application as responsive as a native or Silverlight application, since the navigation
between the pages does not require a round trip to the server. This advantage comes at the expense of a higher
application loading time. There are some ways to speed up the loading of an SPA application, making the
SPA loading experience for the user the same as when loading a usual HTML application, but
this topic is beyond this article.
If you think of it, an SPA application is very similar to a Silverlight application, in that most Silverlight
applications load all the pages at the beginning and later allow the users to navigate between the pages
without calling the server. So understanding of the SPA concepts should come very naturally to Silverlight
developers.
Navigation State
In its generic form, the browser navigation maps a unique URL to the specific navigation state of the browser
application.
The "navigation state of the application" determines which pages or regions within
the application are shown or hidden or how they are shown.
The browser interprets the URL and sets the application to the corresponding state.
This is required for e.g. enabling the user to navigate the browser application by pressing the
browser "back" and "forward" navigation buttons (the browser stores the URLs and when the user presses one
of the browser navigation buttons the URL changes and the application state changes correspondingly). Also,
this is useful when some external page refers to some specific page (or state) within the application which
is not the application's default page.
The reverse is also true: once the state of the application changes, e.g. because the user presses a tab or a hyperlink
to move to a different page, the browser URL should also change to the one corresponding to the new state.
For the ordinary (non-SPA) HTML applications the navigation comes naturally
(the page loads from the server based on the corresponding URL). The SPA applications, however, need to use
some JavaScript magic in order to achieve the one-to-one mapping between the URLs and the navigation states
of the application. Here we present a new functionality that makes the SPA navigation easier to achieve than
by using other already existing frameworks.
The same navigation concepts can apply to any native single window/multipage applications especially to those
built for smart phones and tablets and time permitting, I am going to write more articles about it.
Composition of an SPA Application
In my experience, building SPA application results in large HTML files since the application consists of
multiple pages and all of them are loaded together. Large HTML files can be difficult to understand and
modify. I created functionality allowing to split SPAs into multiple files and load and assemble them all
on the client. This functionality is also discussed here.
BPF Framework
Being a great fan of WPF and Silverlight, I ambitiously called the new framework BPF standing for
Browser Presentation Foundation. Its ultimate purpose is to provide the capabilities one can find
in WPF and Silverlight and more. Currently it provides some generic purpose utility functionality,
navigation and composition capabilities which will be described below. Composition part of the
framework depends on JQuery, the rest of the framework is self-sufficient, and can be used
without JQuery installed.
There are two SPAs that have been built with an earlier version of BPF library: my own business website -
awebpros.com and
paperplusne.com. Note that paperplusne.com website does not have correct pricing data yet for all its items (for items without pricing data I put $1000 as their price). You can still order some
plastic-ware through it and someone will contact you back about the prices.
SPA Server Communications
Just like a Silverlight or a desktop application, the SPA application can minimize the amount of information it loads
from the server by getting only the required data from the server (usually in JSON form) and not
the generated HTML code as ordinary HTML applications do. I provide some examples of an SPA communicating with
the server within this article.
Organization of this Article
We present the simplest possible samples of the ordinary and SPA
HTML/JavaScript sites. We talk about the differences between them.
We discuss the navigation and present the navigation capabilities of the BPF framework.
We discuss the composition using BPF framework.
We talk about SPA communications with the server using ASP.NET MVC and provide corresponding examples.
Important Note: This article concentrates on the business logic and not
on the UI design. We present the SPAs built in the simplest possible way without
any concern about how ugly they look.
Prerequisites
You need to have some knowledge of HTML and JavaScript in order to read and understand this article.
I refer to HTML5, JavaScript, Knockout, JQuery, Guide for Recovering Silverlight/WPF/C# Addicts. Part 1 - JavaScript and DOM as a good
HTML/JavaScript primer.
Throughout this article, I use some very basic JQuery functionality to detect when the HTML document is loaded
and also to provide a multiplatform way of binding to the DOM events, so some understanding of JQuery functionality
is needed. A couple of paragraphs from
JQuery and
DOM Event
should cover this information.
When discussing composition we also use JQuery selectors. You can follow the JQuery Selectors link to learn about them.
I am using JQuery-UI tabs
control extensively for navigation
but anything related to that is explained in the text.
Part that covers communications with the server might require some basic understanding of ASP MVC, even though
we try to explain everything within the article itself.
Part of a Larger Series
This article can be considered part 3 of
"HTML5, JavaScript, Knockout, JQuery, Guide for Recovering Silverlight/WPF/C# Addicts" series which also contain
HTML5, JavaScript, Knockout, JQuery, Guide for Recovering Silverlight/WPF/C# Addicts. Part 1 - JavaScript and DOM and
HTML5, JavaScript, Knockout, JQuery, Guide for Recovering Silverlight/WPF/C# Addicts. Part 2 - Solar System Animation built with SVG, Knockout and MVVM Pattern.
SPA Basics
Presenting Two Page SPA and Two Page Ordinary HTML Application Samples
Here we present two very simple two-page HTML applications: one ordinary and the other SPA. The code for the ordinary HTML
application is located under TwoPageOrdinaryHTMLApp solution, while the code for the SPA is located under TwoPageSPA solution.
Both applications provide exactly the same user experience. There are two hyperlinks at the top of the HTML window
corresponding to two pages. Clicking on top hyperlink shows first page's text, while clicking on the other one shows the
other page's text:
The ordinary HTML application has two HTML pages Page1.htm and Page2.htm. Both have hyper links at the top of the page
(one to itself and one to the other page) and the page text. For page1.htm page text is "This is page 1" and it is colored
red. For page2.htm the text is "This is page 2" and it is colored blue. Here is the page1.htm code:
<body>
<!-- hyper links for choosing the page -->
<ul>
<li><a href="Page1.htm">choose page1</a></li>
<li><a href="Page2.htm">choose page2</a></li>
</ul>
<!-- page 1 message colored in red -->
<p style="font-size:40px;color:red">This is page 1</p>
</body>
To start the TwoPageOrdinaryHTMLApp, right click on Page1.htm file within the Visual Studio's solution explorer and choose "View in Browser"
option if you want to view it in the default browser or "Browse with" option if you want to choose the browser.
You can switch between the pages by clicking the hyperlinks at the top.
The SPA solution uses JQuery (if you open Scripts folder you will see a bunch of JQuery files).
You can install JQuery as a NuGet package as described in
JQuery.
There is only one HTML file within TwoPageSPA project: Index.html. You can start the application by
right clicking on this file within solution explorer and choosing "Run in Browser".
Index.html has HTML part containing the hyperlinks and text for
both page. It also has a JavaScript code for switching between the pages.
Here is the HTML code within Index.html file:
<body>
<ul>
<!-- href='#' is needed only to help the links look like links -->
<li><a id="page1Link" href="#">choose page1</a></li>
<li><a id="page2Link" href="#">choose page2</a></li>
</ul>
<!-- page 2 message colored in blue -->
<!-- in the beginning page2 is not visible (display set to 'none') -->
<p id="page2" style="font-size:40px;color:blue;display:none">This is page 2</p>
<!-- page 1 message colored in red -->
<p id="page1" style="font-size:40px;color:red" >This is page 1</p>
</body>
As you can see, the page links do not contain valid references - their href
property is set to '#'
just so that they would look and behave like links (have bluish color and change the mouse cursor to "hand").
Both pages' text is placed below the links. Page 2 text is not visible in the beginning
(its display
property is set to none
and because of
that, it is placed first so that whatever HTML code follows it won't be shifted down because of it.
Page switching is done by JavaScript code placed below the HTML code:
$(document).ready(function () {
$(page1Link).bind("click", function () {
$(page1).show();
$(page2).hide();
});
$(page2Link).bind("click", function () {
$(page1).hide();
$(page2).show();
});
});
We use JQuery's bind function to attach event handlers to click
events on the hyperlinks. When the event fires
we show the contents of one of the pages and hide the contents of the other page.
One can see, however, there is a difference in behaviors of the two applications. When you change pages in the ordinary HTML application,
the browser URL also changes (e.g. from http://localhost:23033/Page1.htm to http://localhost:23033/Page2.htm). After switching the pages,
you can use "back" button on your browser to go to the previous page. Also if you want the users to access Page2.htm without accessing
Page1 (which is a default page for the application) you can simply give a hyperlink http://localhost:23033/Page2.htm pointing straight
to Page2.
If you try to switch pages in the SPA, however, you will notice that the URL does not change, back button is not working and there is no
way to give the users a straight link to page2. This problem will be addressed shortly
in a section of the article dedicated to SPA navigation.
Using JQuery UI Tabs for SPA Pages
It looks nicer to have tabs instead of hyperlinks when you want to switch between the pages.
JQuery UI tabs can be used for this purpose.
JQuery UI is a GUI package built on top of JQuery. You can install it via NuGet in the same fashion as JQuery.
The code sample using JQuery UI tabs is located under TwoPagesSPAWithTabs solution. It consists of Index.htm file
as well as the files from JQuery and JQuery UI packages. Index.htm file contains reference to a JQuery UI
style sheet: <link href="Content/themes/base/jquery.ui.all.css" rel="stylesheet" type="text/css" />
at the top of the page (the style sheet is part of the JQuery UI package installation).
Here is the HTML code within Index.htm file:
<body>
<ul id="pageTabs">
<!-- hrefs of the links point to the ids of the pages' contents -->
<li><a href="#page1">choose page1</a></li>
<li><a href="#page2">choose page2</a></li>
</ul>
<!-- page 1 message colored in red -->
<p id="page1" style="font-size:40px;color:red" >This is page 1</p>
<!-- page 2 message colored in blue -->
<p id="page2" style="font-size:40px;color:blue">This is page 2</p>
</body>
Note that the href
attributes of the hyperlinks at the top of the code are pointing to id
s of the
HTML tags containing page content (e.g. hyperlink with href="#page1"
points to the tag <p id="page1" ...
).
This is to let JQuery UI functionality figure out which content belongs to which tab.
The JavaScript code is also very simple:
$(document).ready(function () {
$("body").tabs();
});
Here is how the application looks:
Just like in previous SPA sample, the URL is not connected to the tabs - changing tabs won't change the
URL and changing URL won't change the tabs.
BPF Framework and Navigation
Here we present navigation functionality of the BPF framework.
Many SPAs use Sammy.js framework for the navigation.
Sammy.js allows to map different URL hashes into
various JavaScript functions. When SPA has a lot of different navigation states with some
possible hierarchy, such mapping might be different to implement. BPF library, however, implements the mapping
between the navigation states and the URL hashes naturally and with very little extra code.
There is a limitation to the current version of BPF framework, in that it relies on
hashchange
document event to detect the changes of the hash and change the
navigation states of the SPA. IE7 does not support hashchange
event
so the navigation functionality will on work on it. The share of
IE7 browser users is currently smaller than 1% and rapidly declining, so I believe this issue
does not have to be addressed.
Code for BPF framework is located under BPF folder. It is also available from
github.com/npolyak/bpf. The only file you
need in order to access all of its functionality is bpf.min.js; so far (in the
beginning of December 2012, it is pretty small - about 12Kb). This file
was obtained by bundling and minification of other JavaScript files which can
also be found in BPF folder and to which I will be referring from time to time.
Two Page SPA with Navigation
We'll start by using BPF functionality to modify the simple two page SPA with tabs
presented above in such a way that it can be navigated.
The sample is located under
TwoPageSPAWithNavigation solution.
The only difference between this project and TwoPageSPAWithTabs lies in JavaScript code
at the bottom of Index.html file. In TwoPageSPAWithTabs project JavaScript code consisted
of one line $("body").tabs();
within $(document).ready()
method.
The purpose of this line was to turn the hyperlinks into the JQuery
tabs with the corresponding content. The JavaScript code within TwoPageSPAWithNavigation is more complex:
var tabs = $("body").tabs();
var tabsAdaptor = new bpf.nav.JQTabsNavAdaptor(tabs);
var tabNode = new bpf.nav.Node(tabsAdaptor);
bpf.nav.connectToUrlHash(tabNode);
return bpf.nav.setKeySegmentToHash(tabNode);
Do not try to understand it all, since detailed navigation functionality design
will be discussed shortly. In short, we use bpf.nav.Node
functionality to connect the navigation states to the parts of the browser url.
bpf.nav.JQTabsNavAdaptor
adapts JQuery tabs so that the resulting adapted object
has methods expected by bpf.nav.Node
. The line bpf.nav.connectToUrlHash(tabNode)
makes a two way connection between the nodes and the URL (nodes react to URL and vice versa).
Last line bpf.nav.setKeySegmentToHash(tabNode)
sets the initial URL to match
the one obtained from bpf.nav.Node
s structure.
Running the sample shows SPA very similar to TwoPageSPAWithTabs, but the tabs have unique URL mapped to
them and when you change the tabs, the URL also changes. Correspondingly "Back" and "Forward" browser
buttons now work and switch between the tabs. Also typing "http://localhost:48476/Index.html#page2." for a URL
in a browser, will get you straight to the second tab.
A More Complex Example With JQuery Tabs
HierarchicalSPAWithNavigation sample presents a more interesting of tab hierarchy. The top
level tabs contain sub-tabs. Each tab/sub-tab combination maps to its own URL.
Try starting the application and playing with the tabs. Here is how the application looks:
Pay attention that we show SubPage2 within Page2. This combination
corresponds to the URL hash: "#page2.page2SubTab2.".
If we change the tabs, the hash will change accordingly.
You can see that the hash always start with '#' and ends with '.' character.
The links corresponding to different levels of the navigation state hierarchy are separated
by the period ('.') character. Such links in this sample, match the href
values of the links of the tabs stripped of '#' prefix character.
Here is the code for creating connecting the navigation (again do not try very hard to understand it as
we'll discuss it in the next subsection):
var tabs = $("body").tabs();
var page1SubTabs = $("#page1SubTabs").tabs();
var page2SubTabs = $("#page2SubTabs").tabs();
var topLevelNode = bpf.nav.getJQTabsNode(tabs);
bpf.nav.addJQTabsChild(topLevelNode, "page1", page1SubTabs);
bpf.nav.addJQTabsChild(topLevelNode, "page2", page2SubTabs);
bpf.nav.connectToUrlHash(topLevelNode);
return bpf.nav.setKeySegmentToHash(topLevelNode);
Detailed Design of the Navigation Functionality
Navigation functionality is located under "namespace" bpf.nav
within BPF library. It relies on the bpf.utils
functionality and
also parts of it depend on JQuery and JQuery UI (though it can work without them).
The purpose of Navigation functionality is to allow the developers to
define a number of navigation states within an SPA and
map each of the states into a unique browser URL so that when the state changes
the URL changes also and vice versa - the URL change triggers the navigation
state change.
The navigation state is assumed to be hierarchical - there can be a number of
states at the top level (e.g. page 1 and page 2 from the previous sample). Each
one can have sub-states (sub-pages). Each of the sub-states can have its own
sub-states etc. The total navigation state of the application
is determined by the unique selection of states at each level (or level states).
Each level state maps to a link bound by period ('.') characters within the URL hash.
It is attached to a link corresponding to the parent level state.
The image below depicts a sample of the state hierarchy with the tree links corresponding
to the level states and the hash strings shown next to the corresponding end nodes: e.g.
Page1/SubPage2 selection will result in hash="#Page1.SubPage2.":
Each part of the URL's hash bound by periods we call hash segment. For example
hash "#Page1.SubPage2" has two segments: "Page1" and "SubPage2". Each segment maps
into the level state selected at that level.
At each level no more than one level state can be selected (sometimes,
it is convenient to assume that no level state is selected at some level).
Navigation states consist of level states. Each navigation node can have
several level child states that belong to it. One or none of these level child states
may be selected as the current level state. Each of the level states has a
string associated with it. This string uniquely identifies
that state among the children states of the same node. This string becomes a segment bound
by two period characters within the URL's hash when the level state is selected.
The total hash is uniquely determined by the sequence of such segments corresponding to
the level states starting from the higher level states and going down to the more
refined levels.
The main functionality for capturing information about the tree of level states is bpf.nav.Node
located within NavigationNode.js file.
It kind of extends or subclasses (in C# sense) the functionality provided by bpf.nav.NodeBase
located in
NavigationNodeBase.js file. This subclassing is achieved by bpf.nav.Node
constructor
calling bpf.nav.NodeBase
constructor within its code. We need subclassing because another class
bpf.nav.ProductNode
is also derived from the same bpf.nav.NodeBase
class.
bpf.nav.ProductNode
functionality as will be discussed below. This inheritance relationship is
shown at the diagram below:
bpf.nav.Node
references its parent and children (if any). bpf.nav.Node
's parent and children
are also derived from bpf.nav.NodeBase
class. The children of bpf.nav.Node
are accessible by
string keys via function bpf.nav.NodeBase.getChild(key)
. The key maps into the corresponding segment
of the URL's hash.
Each bpf.nav.Node
object wraps some other object in charge of switching states at the node's level.
As the above samples show, such object can be JQuery UI tabs object. bpf.nav.Node
assumes that the
wrapped object satisfies certain requirements - it should have some methods and fields - in Java or C# that requirement
would be that the wrapped object should implement some interface. Here is the interface that it should implement written in
C# notations:
public interface ISelectable
{
event Action onSelectionChagned;
string getSelectedKey();
void select(string key);
void unselect();
}
Most entities within JavaScript do not support the above interface so we have to use an adaptor to
adapt the objects we are dealing with to ISelectable
interface. The adaptor for JQuery UI
tabs object is called bpf.nav.JQTabsNavAdaptor
and it is part of the BPF library.
Adaptor contains the object that controls the selection (in this case it is JQuery UI tabs object) and
implements ISelectable
interface by providing the methods that bpf.nav.Node
expects.
Another adaptor built into BPF - a check box adaptor comes with bpf.nav.CheckboxNavAdaptor
class.
It provides a way to associate the level states with checkbox's checked and unchecked states.
bpf.nav.Node
(via bpf.nav.NodeBase
) has the following important methods:
setSelectedKeySegments(urlHash)
- sets the selections for the node and its descendant
to match the segments within URL's hash passed as a string to the function -
getTotalHash()
- returns a URL's hash that corresponds to the selections within the
node itself and its descendants. The hash has a leading pound ('#') and trailing period ('.')
characters.
These two methods provide a way to update the selection within some selectable objects (e.g. JQuery UI tabs)
once the URL's hash changes and vice versa - to change the URL once a selection changes within the
bpf.nav.Node
hierarchy.
View Model Selection Example
This section requires some knowledge of Knockoutjs library.
You can learn about Knockoutjs by using this link or many other
resources including
Solar System Animation built with SVG, Knockout and MVVM Pattern.
This section's sample is located under ParamNavigationTest solution. We have a View Model created
by the functionality within the file StudentViewModel.js file under Scripts/ViewModels directory. It defines
a collection of studentVM.students
of objects of type Student
. Each Student
has a name
field and a collection of course
objects. Each course has two fields
courseName
and grade
. The View Model studentVM
has selectedStudent
Knockoutjs observable field. Also each Student
object
has an observable selectedCourse
field.
The View is located within HTML/Index.html file. It provides hyperlinks to the Student
objects
at the top level. The links display student's name. Once a link is clicked the corresponding student is selected and
the browser window shows the name of the courses taken by the selected student under the student's hyperlink.
When the course link is clicked the browser shows the student name, course name and the student's grade for that course:
Our task is to provide a unique URL hash for each student - course selection.
We can, of course, use the bpf.nav.Node
functionality described above for every student and course object.
This, however, can be problematic if the number of such objects is very high or if the student or course collections
change throughout the application.
BPF library has a class bpf.nav.FactoryNode
located within NavigationFactoryNode.js file,
that produces bpf.nav.Node
objects as they are needed
by the application and adds them to their parent node's childNodes collection.
We use bpf.nav.KoObservableNavAdaptor
as an adaptor object to adapt the observable
objects. bpf.nav.KoObservableNavAdaptor
constructor takes 3 parameters:
- the observable
- the function to get the object by the hash segment (or key) from the View Model collection
- the function to get the key from each object within View Model collection
Here is how we connect the navigation states to the View Model within Index.html file:
var studentLevelObservableAdaptor =
new bpf.nav.KoObservableNavAdaptor
(
studentVM.selectedStudent,
function (key) {
var foundStudent = _.chain(studentVM.students).filter(function (student) {
return student.name === key;
}).first().value();
return foundStudent;
},
function (student) {
return student.name;
}
);
var topLevelNode = new bpf.nav.FactoryNode(
studentLevelObservableAdaptor,
null,
function (key, data) {
var childObj = data.getChildObjectByKey(key);
if (!childObj)
return;
var adaptedChildObj =
new bpf.nav.KoObservableNavAdaptor
(
childObj.selectedCourse,
function (courseKey) {
return _.chain(childObj.courses).
filter(function (universityCourse) {
return universityCourse.courseName === courseKey;
}).first().value();
},
function (course) {
return course.courseName;
}
);
return new bpf.nav.Node(
adaptedChildObj
);
}
);
bpf.nav.connectToUrlHash(topLevelNode);
bpf.nav.setKeySegmentToHash(topLevelNode);
Cartesian Product Navigation
Suppose that your browser is divided into two halves. Each of the halves has tabs. You can select one tab in each of the
halves. You also want to assign a URL for every possible tab combination. The resulting space of navigation states
will be a Cartesian products of the states within each of the halves of the browser.
We can further complicate the task by dividing the browser into more than two parts. Also we can assume that
such Cartesian product can occur not only at the top state, but at any state level.
SimpleProductNavigationTest solution contains precisely the scenario we described in the beginning of this section.
The browser page is divided into two parts. Each part has tabs. We record every combination of tab selections
in both parts of the page:
Note that total URL hash for on the figure above is "#(topics/TopicA)(othertopics/SomeOtherTopicA).". The parts
of the hash corresponding to the components of the Cartesian product are placed within a parenthesis.
The part of the hash within a parenthesis starts with a key that uniquely identifies that part within
the Cartesian product. The key is separated from the rest of the string within the parenthesis by a forward slash.
We use bpf.nav.ProductNode
to map the URL hash into the Cartesian product of the states. Its usage example
is located at the bottom of Index.html file within SimpleProductNavigationTest project:
var topics = $("#MyTopics").tabs();
var otherTopics = $("#SomeOtherTopics").tabs();
var topLevelNode = new bpf.nav.ProductNode();
bpf.nav.addJQTabsChild(topLevelNode, "topics", topics);
bpf.nav.addJQTabsChild(topLevelNode, "othertopics", otherTopics);
bpf.nav.connectToUrlHash(topLevelNode);
bpf.nav.setKeySegmentToHash(topLevelNode);
bpf.nav.ProductNode
is located within ProductNavigationNode.js file. Just like bpf.nav.Node
,
it is kind of a sub-class of bpf.nav.NodeBase
. It overrides the implementation
of the functions setSelectedKeySegmentsRecursive
, getUrlRecursive
and chainUnselect
.
A considerably more complex example can be found in ComplexProductNavigationTest solution. It involves
a long sequence of tabs within tabs with Cartesian products occurring at two levels - the top one and
under TopicA tab.
Here is how the SPA looks:
The total hash part of the URL for the figure above is
"#(topics/TopicA.(a1subs1/A1Subtopic1.A1Sub1Sub2)(a1subs2/A2Subtopic2))(othertopics/SomeOtherTopicA).".
You can play with the application to see that the browser navigation buttons are working indeed.
Here is the HTML code of the application:
<table>
<tr>
<td style="vertical-align: top">
<div id="MyTopics">
<ul>
<li><a href="#TopicA">TopicA</a></li>
<li><a href="#TopicB">TopicB</a></li>
</ul>
<div id="TopicA">
My topic A
<table>
<tr>
<td style="vertical-align: top;">
<div id="A1Subtopics">
<ul>
<li><a href="#A1Subtopic1">A1Sub1</a></li>
<li><a href="#A1Subtopic2">A1Sub2</a></li>
</ul>
<div id="A1Subtopic1">
A1 Sub Topic1
<div id="A1Sub1Sub">
<ul>
<li><a href="#A1Sub1Sub1">A1S1S1</a></li>
<li><a href="#A1Sub1Sub2">A1S1S2</a></li>
</ul>
<div id="A1Sub1Sub1">A1 Sub1 Sub1</div>
<div id="A1Sub1Sub2">A1 Sub1 Sub2</div>
</div>
</div>
<div id="A1Subtopic2">A1 Sub Topic2</div>
</div>
</td>
<td style="vertical-align: top;">
<div id="A2Subtopics">
<ul>
<li><a href="#A2Subtopic1">A2Sub1</a></li>
<li><a href="#A2Subtopic2">A2Sub2</a></li>
</ul>
<div id="A2Subtopic1">A2 Sub Topic1</div>
<div id="A2Subtopic2">A2 Sub Topic2</div>
</div>
</td>
</tr>
</table>
</div>
<div id="TopicB">
The Topic B
</div>
</div>
</td>
<td style="vertical-align: top">
<div id="SomeOtherTopics">
<ul>
<li><a href="#SomeOtherTopicA">AnotherA</a></li>
<li><a href="#SomeOtherTopicB">AnotherB</a></li>
</ul>
<div id="SomeOtherTopicA">Some Other A </div>
<div id="SomeOtherTopicB" style="background-color: pink">Some Other B</div>
</div>
</td>
</tr>
</table>
And here is the JavaScript code within $(document).ready()
function:
var topics = $("#MyTopics").tabs();
var otherTopics = $("#SomeOtherTopics").tabs();
var A1Subtopics = $("#A1Subtopics").tabs();
var A2Subtopics = $("#A2Subtopics").tabs();
var A1Sub1Sub = $("#A1Sub1Sub").tabs();
var topLevelNode = new bpf.nav.ProductNode();
var topicsNode = bpf.nav.addJQTabsChild(topLevelNode, "topics", topics);
bpf.nav.addJQTabsChild(topLevelNode, "othertopics", otherTopics);
var aSubtopicsProductNode = new bpf.nav.ProductNode();
topicsNode.addChildNode("TopicA", aSubtopicsProductNode);
var A1SubNode = bpf.nav.addJQTabsChild(aSubtopicsProductNode, "a1subs1", A1Subtopics);
bpf.nav.addJQTabsChild(aSubtopicsProductNode, "a1subs2", A2Subtopics);
bpf.nav.addJQTabsChild(A1SubNode, "A1Subtopic1", A1Sub1Sub);
bpf.nav.connectToUrlHash(topLevelNode);
bpf.nav.setKeySegmentToHash(topLevelNode);
Using BPF Framework for SPA Composition
As I mentioned in Introduction, SPA might result in large HTML files due to the fact that all its pages
are loaded together and switching the pages simply means that parts of HTML change their visibility.
BPF framework provides a way to break HTML functionality into smaller files (called BPF plugins or
simply plugins) and assemble all of them together on the client side. It also allows the plugins
to refer to some other plugins creating a plugin hierarchy. Moreover, you can place JavaScript code
dealing with the functionality provided by the plugin into the same HTML file and later call this functionality
either during the composition or at a later stage. This was my attempt to
imitate the WPF/Silverlight code-behind concept within HTML/JavaScript universe.
All of the BPF composition related functionality is located within Composite.js file and it relies heavily
on JQuery.
Simple Plugin Sample
Let us take a look at the sample under SimpleCompositionSample project. HTML directory of this project
contains two html files Index.html - the main file and APlugin.html - a file containing
the plugin code.
To start the application, right click on Index.html file within Solution Explorer and choose
"View in Browser" option.
Here is the HTML code within Index.html file:
<div style="font-size:30px;color:red">This is the main Module</div>
<img id="busyIndicator" src="../Images/busy_indicator.gif" style="vertical-align:central;margin-left:50%" />
<!-- plugin will get into this div below -->
<div id="APluginContainer1"></div>
<!-- call plugin's function to change its color to 'blue' for the plugin above -->
<button id="changePluginColorButton1">Change 1st Plugin Text to Blue</button>
<!-- plugin will get into this div below -->
<div id="APluginContainer2" style="margin-top:40px"></div>
<!-- call plugin's function to change its color to 'blue' for the plugin above -->
<button id="changePluginColorButton2">Change 2nd Plugin Text to Blue</button>
The only text visible within main HTML file is "This is the main Module".
There are two <div> tags for inserting plugin content:
"APluginContainer1" and "APluginContainer2". Each one of them has a
button under it that calls the plugin's code-behind to change
the color of the text of the corresponding plugin instance to blue.
You may notice that the same plugin is being inserted into two places within the main module.
Here is the JavaScript code of Index.html:
var compositionReady = new bpf.utils.EventBarrier();
compositionReady.addSimpleEventHandler(function (success) {
$("#busyIndicator").hide();
$("#changePluginColorButton1").click(function () {
bpf.control("#APluginContainer1").call("changeColorToBlue");
});
$("#changePluginColorButton2").click(function () {
bpf.control("#APluginContainer2").call("changeColorToBlue");
})
});
bpf.cmpst.getPlugin("APlugin.html", null, "#APluginContainer1", compositionReady);
bpf.cmpst.getPlugin("APlugin.html", null, "#APluginContainer2", compositionReady);
The two bpf.cmpst.getPlugin
calls at the bottom,
load the plugin into the main module inserting it into two different places within the DOM.
The compositionReady
event handler is called after all the plugins and their descendants
are loaded into the main module. It hides the "busyIndicator" and
it assigns the callback to the buttons' events calling changeColorToBlue
function of the plugin's code=behind:
bpf.control("#APluginContainer1").call("changeColorToBlue");
The function bpf.control(jquery-selector)
returns
the code-behind for the plugin contained within the element pointed to
by the jquery-selector
passed to it. We use the function
call()
created by the BPF framework to call the
code-behind's method. The function's first argument is the name of the method
you want to call. If the method has some input arguments, you can
add them after the method name.
Now, let us take a look at the plugin code:
<div id="aPlugin" style="font-size:25px">
This is a plugin
</div>
<!--script tag should be marked by data-type="script-interface" in
order for it to be recognized as containing the plugin's 'code-behind' -->
<script type="text/javascript" data-type="script-interface">
(function () {
// this function returns the plugin's 'code-behind' -
return {
"postRender": function (compositionReadyEventBarrier) {
this.currentDOM.find("#aPlugin").css("color", "green");
},
"changeColorToBlue": function () {
this.currentDOM.find("#aPlugin").css("color", "blue");
}
};
})();
</script>
The HTML of the plugin simply shows text "This is a plugin".
The <script> tag marked with
data-type="script-interface"
attribute is considered to contain
the plugin's code-behind. There should only be one <script> attribute like that.
The rest of the script attributes can be added to the plugin in order to enhance intellisense, but
will be removed when the plugin is loaded into the main module. All the
JavaScript references contained in <script> required for the plugins to work
should be placed in the main module.
The code-behind <script> tag contains an anonymous function that returns
a JSON object defining various methods. There are two
predefined code-behind methods that are called (if they exist) when the plugin is loaded
into the main module: preRender
and postRender
- the first one is called
before and the second one after the plugin is loaded.
All the code-behind methods except for preRender
receive a special object as
their this
variable. This object contains currentDOM
field
which is a JQuery DOM object for the element within the parent module into which
the plugin was attached.
Important Note: if you want to find a DOM element belonging to a plugin within the code-behind
you should do it via a call to this.currentDOM.find(selector)
function, not by
using $(selector)
. The reason is that if the same plugin is inserted in multiple
places within the main module, unless you use this.currentDOM.find
method,
you'll probably find multiple instances of the same plugin instead of that particular instance,
so that all of your modification will be applied to multiple instances instead of the particular
instance you need. BTW, note that that since our plugin has and id: id="aPlugin"
,
and the plugin is inserted twice into the main module, you'll have two different elements
with the same id within the main module. It seemed to be working fine in my experience due to the
fact that I was always using this.currentDOM.find
to resolve the instance. If you
feel that it is wrong (after all it is the ABCs of the HTML that there should be no
two elements with the same id within the DOM), you can avoid using ids within plugins
and use unique class names instead.
The postRender
function changes the color of the text of the plugin to green once
the plugin is inserted into the main module.
Code-behind's method changeColorToBlue
changes the color of the
plugin's text to blue.
Here is how we call the changeColorToBlue
function from the JavaScript code
of the main module (file Index.html):
bpf.control("#APluginContainer1").call("changeColorToBlue");
When you run the application you will get the text for both instances of the plugin colored in green.
When you click the button under the corresponding plugin instance that plugin instance will turn blue,
while the other stays the same color.
Here is the image of the application after the top plugin instance's button was clicked:
The text of the top plugin instance changed to blue, while the text of the bottom
plugin instance stayed green.
Chain of the Plugins Composition Sample
ChainOfPluginsCompositionSample solution contains a sample where the main module loads
a plugin, which, in turn load another plugin we call it sub-plugin.
All the HTML files of the sample are located
under HTML folder. Index.html is the file containing the main module. You can start the application
by right clicking on it and choosing "View in Browser" option.
Here is the image of the application once it starts:
The buttons change the color of the text of the plugin and sub-plugin correspondingly.
The code of Index.html file does not contain anything new in comparison to the previous sample. Most
"interesting" code is located within APlugin.html file:
<div id="aPlugin" style="font-size:25px">
This is a plugin
<div id="subPluginContainer"></div>
<Button id="changePluginColorButton">Change Plugin Color to Blue</Button>
<Button id="changeSubPluginColorButton">Change SUB Plugin Color to Black</Button>
</div>
<script src="../Scripts/jquery-1.8.3.js"></script>
<script src="../Scripts/BPF/bpf.js"></script>
<script type="text/javascript" data-type="script-interface">
(function () {
return {
"postRender": function (compositionReadyEventBarrier) {
var self = this;
self.currentDOM.find("#aPlugin").css("color", "green");
var subCompositionReady =
compositionReadyEventBarrier.createChildEventBarrier();
subCompositionReady.addSimpleEventHandler(function () {
$("#changePluginColorButton").click(function () {
self.currentDOM.find("#aPlugin").css("color", "blue");
});
$("#changeSubPluginColorButton").click(function () {
bpf.control("#subPluginContainer", self).call("changeColorToBlack");
});
});
bpf.cmpst.getPlugin("ASubPlugin.html", self, "#subPluginContainer", subCompositionReady);
}
};
})();
</script>
Note that when we call getPlugin()
function within the plugin, we pass self
(
which is the same as this
variable) as the second argument, not null
as we do in the main module. Also note that in order to get access within a plugin
to the code-behind object of the sub-plugin, we need to pass self
as
the second argument to the bpf.control()
function: bpf.control("#subPluginContainer", self).call("changeColorToBlack");
.
Composition and Navigation Sample
CompositionAndNavigationSample solution shows how to combine BPF composition and navigation.
We create two tabs at the main module level. We also have tabs within a plugin.
The plugin is added to the content of one of the tabs within the main module. We use the
bpf.nav.Node
objects create a navigation node around the plugin's tabs and
connect it as a child of the main module's node structure.
Here is the main modules code within Index.html file:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<link href="../Content/themes/base/jquery-ui.css" rel="stylesheet" />
<title></title>
</head>
<body>
<!-- links to be converted to tabs -->
<ul id="pageTabs">
<li><a href="#page1">Page1</a></li>
<li><a href="#page2">Page2</a></li>
</ul>
<!-- page 1 message colored in red -->
<div id="page1" style="color:red" >
This is page 1
<div id="pluginContainer"></div>
</div>
<!-- page 2 message colored in blue -->
<div id="page2" style="color:blue">This is page 2</div>
</body>
</html>
<script src="../Scripts/jquery-1.8.3.min.js"></script>
<script src="../Scripts/jquery-ui-1.9.2.min.js"></script>
<script src="../Scripts/BPF/bpf.js"></script>
<script>
$(document).ready(function () {
var topLevelTabs = $("body").tabs();
var topLevelNode = bpf.nav.getJQTabsNode(topLevelTabs);
var compositionReady = new bpf.utils.EventBarrier();
compositionReady.addSimpleEventHandler(function () {
bpf.control("#pluginContainer").call("connectToParentNode", topLevelNode);
bpf.nav.connectToUrlHash(topLevelNode);
bpf.nav.setKeySegmentToHash(topLevelNode);
});
bpf.cmpst.getPlugin("APlugin.html", null, "#pluginContainer", compositionReady);
});
</script>
To connect the plugin's navigation node to that of the main module, we call function
connectToParentNode
on the plugin's code-behind, passing the parent node
(topLevelNode) as a parameter to it:
bpf.control("#pluginContainer").call("connectToParentNode", topLevelNode);
Here is the plugin's code (from APlugin.html file):
<div id="subTabPlugin">
<ul id="subTabs">
<li><a href="#subPage1">SUB Page1</a></li>
<li><a href="#subPage2">SUB Page2</a></li>
</ul>
<!-- page 1 message colored in red -->
<div id="subPage1" style="color:green" >This is SUB page 1</div>
<!-- page 2 message colored in blue -->
<div id="subPage2" style="color:purple">This is SUB page 2</div>
</div>
<script src="../Scripts/jquery-1.8.3.min.js"></script>
<script src="../Scripts/jquery-ui-1.9.2.min.js"></script>
<script src="../Scripts/BPF/bpf.js"></script>
<script type="text/javascript" data-type="script-interface">
(function () {
var pluginTabs;
return {
"postRender": function (compositionReadyEventBarrier) {
pluginTabs = this.currentDOM.find("#subTabPlugin").tabs();
},
"connectToParentNode": function (parentNode) {
bpf.nav.addJQTabsChild(parentNode, "page1", pluginTabs);
}
};
})();
</script>
The method postRender
creates the tabs out of the hyperlinks and stores the
reference to them within the plugin by calling
pluginTabs = this.currentDOM.find("#subTabPlugin").tabs();
.
Connecting to the parent's navigation node is taken care of by the method
connectToParentNode
. This method is called from the main module. (Of course
we could have connected the navigation nodes within postRender
function
but we wanted to demonstrate more composition functionality by creating a separate method
connectToParentNode
and calling it from the main module).
Parameterized Plugins
ParameterizedPluginSample shows how to create instances of the same plugin that differ in their looks.
Just like in SimpleCompositionSample, two instances of the same plugin are inserted into the main module
contained within Index.html file:
bpf.cmpst.getPlugin(
"APlugin.html",
null,
"#APluginContainer1",
compositionReady,
{
fontSize: "50px",
color: "blue"
}
);
bpf.cmpst.getPlugin(
"APlugin.html",
null,
"#APluginContainer2",
compositionReady,
{
fontSize: "20px",
color: "green"
}
);
The last argument to bpf.cmpst.getPlugin
function is the JSON object which can
be accessed from within postRender
function of the plugin's code-behind via
this.postRenderArguments
object. Here is the code-behind of the plugin:
{
"postRender": function (compositionReadyEventBarrier) {
var fontSize = this.postRenderArguments.fontSize;
var color = this.postRenderArguments.color;
this.currentDOM.find("#aPlugin").css("font-size", fontSize);
this.currentDOM.find("#aPlugin").css("color", color);
}
}
As a result we have the same plugin text displayed with different font-size and color:
The top plugin instance has font size of 50px and blue color, while the bottom one -
20px and green color.
Generic Silverlight Plugin Sample
Here I describe a generic BPF plugin for Silverlight applications.
Silverlight apps are usually packaged as one file with .xap extension. In order to embed a
Silverlight app into a page, you have to have the page reference Silverlight.js file
(which comes from Microsoft). You also have to write the following HTML code within your
HTML application:
<object type="application/x-silverlight-2"
style="display: block; text-align: center; width: 100%; height: 100%">
<param name="source" value="../ClientBin/MySilverlightApp.xap" /> <!-- url to Silverlight app -->
<param name="onError" value="onSilverlightError" />
<param name="background" value="white" />
<param name="minRuntimeVersion" value="5.0.61118.0" />
<param name="autoUpgrade" value="true" />
<a href="http://go.microsoft.com/fwlink/?LinkID=149156&v=5.0.61118.0" style="text-decoration: none">
<img src="http://go.microsoft.com/fwlink/?LinkId=161376" alt="Get Microsoft Silverlight"
style="border-style: none" />
</a>
</object>
This is a large chunk of HTML code and my idea was to create a BPF parameterized plugin for re-using it
in multiple places within HTML applications. One of the parameters of the BPF plugin
will be, of course, the Silverlight app's URL. Other parameters might be needed for positioning
the application, e.g. width, height, margins.
You would probably want to know why Silverlight plugin is different from any other BPF plugin.
Why can't we simply create a parameterized plugin using the information from the previous subsection?
My answer is "redirect your question to Microsoft". The <object> tag behaves in a very strange way in
conjunction with JQuery on IE8 and IE9 - trying to use JQuery to parse the DOM of the object tag
totally mangles it. Besides, from my experience, the Silverlight app URL has
to be set before the HTML code is inserted into the main module - IE won't change the Silverlight
application when the Silverlight URL changes if the code has already been inserted.
In fact it is because of these issues that I added preRender
function to the
code-behind to be called before the plugin is added to the main module.
The code for this sample is located under SilverlighPluginSample solution. Here is what you get
if you run the application - (hey, after a week of toiling on this article I can
allow myself a bit of self promotion - AWebPros.com
is my company):
To get around the issues with Silverlight plugin that I describe above, we change the plugin code
substituting <object> tag for <myobject>. Also in order to place the URL parameter
in the correct place, we use "___XAPFilePlaceHolder___" string as a placeholder.
The substitution of these strings by the correct ones takes place within preRender
method, before the code is inserted into the main module. We get the HTML code
of the Silverlight plugin by using this.getDownloadedHtml()
function
(which, within preRender
method indeed returns the full plugin HTML code).
Here is the code within SilverlightContainerPlugin.html file:
<div class="slContainerDiv">
<myobject type="application/x-silverlight-2"
style="display: block; text-align: center; width: 100%; height: 100%">
<param name="source" value="___XAPFilePlaceHolder___" />
<param name="onError" value="onSilverlightError" />
<param name="background" value="white" />
<param name="minRuntimeVersion" value="5.0.61118.0" />
<param name="autoUpgrade" value="true" />
<a href="http://go.microsoft.com/fwlink/?LinkID=149156&v=5.0.61118.0" style="text-decoration: none">
<img src="http://go.microsoft.com/fwlink/?LinkId=161376" alt="Get Microsoft Silverlight"
style="border-style: none" />
</a>
</myobject>
</div>
<script src="../Scripts/jquery-1.8.2.js"></script>
<script type="text/javascript" data-type="script-interface">
(function () {
return {
"preRender": function (preRenderArgs) {
var originalHtml = this.getDownloadedHtml()
var modifiedHtml = originalHtml.replace(/myobject/gi, "object").replace("___XAPFilePlaceHolder___", preRenderArgs.slAppUrl);
this.setDownloadedHtml(modifiedHtml);
},
"postRender": function () {
var args = this.postRenderArguments;
var slContainerDiv = this.currentDOM.find(".slContainerDiv");
if (args) {
slContainerDiv.css("margin-top", args.marginTop);
slContainerDiv.css("margin-bottom", args.marginBottom);
slContainerDiv.css("width", args.width);
slContainerDiv.css("height", args.height);
}
},
}
})();
</script>
Here is how the plugin is inserted within the main module:
bpf.cmpst.getPlugin(
"SilverlightContainerPlugin.html",
this,
"#AWebProsLogo",
compositionReady,
{
marginTop: "auto",
marginBottom: "auto",
width: "150px",
height: "150px"
},
{
slAppUrl: "../ClientBin/AnimatedBanner.xap"
}
);
This plugin is not part of BPF library (which consists only of JavaScript files) but
it can be very useful and as such it is published on
github.com/npolyak/bpf/tree/master/BPF/UsefulBPFPlugins
SPA Server Communications Using ASP.NET MVC
Here we show how SPA can communicate with the ASP.NET MVC server. Unlike the previous two
sections, this section does not contain anything new - it is simply a tutorial providing
a number of different examples of SPA communicating with the server.
Simple GET Request Sample Returning String from the Server
As was mentioned in Introduction, once the SPA is running, it should only load
string or JSON data for the application and not the HTML code.
A very simple example of an SPA sending and getting some string data from an ASP server can be found
under SPAServerStringCommunications solution.
To start the solution, right mouse button click on Index.html file and choose "View in Browser".
Here is how the application looks after it is started:
After you enter your name into the editable area and click "Get Server String" button
the browser will send your name to the server and receive a string "Hello <yourname>" from the server
and display above the button in red:
SPAServerStringCommunications project was created as ASP.NET MVC 4 empty project. After the project was created,
I removed Model and Views folders and added a "Hello" controller to the Controllers folder. I also removed
all the scripts from the Scripts folder and installed JQuery as a NuGet package.
HelloControler
contains only one method GetHelloMessage
that takes name
argument and returns string "Hello" + name
wrapped in Json
function:
public ActionResult GetHelloMessage(string name)
{
return Json("Hello " + name, JsonRequestBehavior.AllowGet);
}
In this case Json
function simply wraps a string and so, the string is returned to the server.
The relative URL to GetHelloMessage
function is obtained by concatenating the controller name,
forward slash and the function name: "/hello/gethellomessage".
The project's HTML/JavaScript client is located within Index.html file. Here is the HTML part of the client code:
<body>
<div>Enter name</div>
<!-- name input -->
<input id="nameInput" type="text" name="value" style="width:200px" />
<!-- div to display the text returning from the server -->
<div id="serverText" style="min-height:50px;color:red;margin-top:10px"></div>
<!-- button to trigger communication with the server -->
<button id="getHelloStringButton">Get Server String</button>
</body>
And here is the JavaScript:
$(document).ready(function () {
$("#getHelloStringButton").click(function () {
var name = nameInput.value;
$.get("/hello/gethellomessage", "name=" + name, function (returnedMessage) {
$("#serverText").text(returnedMessage);
});
});
});
JavaScript code uses JQuery's $.get
function to send a GET request to relative url "/hello/gethellomessage" and
to call a function to
insert whatever come from the server into serverText
element.
The structure of the query (which is part of the GET request from the client) is "name=<value-that-you've-entered>".
The name
within the query will map into name
argument of the controller function
and the value will become the value of the name
argument of the controller function, so no
extra parsing is necessary on the server side.
In general, when a GET query has multiple arguments, the query should be built as
name1=<val1>&name2=<val1>...
and on the server side the
controller function arguments should have the same argument names as the GET query. In that case,
parsing will happen automatically and the values of the arguments will be the same as the values within the query.
GETting a Complex Object from the Server
In the previous sample, we sent a string to the server to get a string back. SPAComplexObjectGetter project returns
a JSON object with some structure to it in response to a GET HTTP request. To run the project, open it in Visual Studio
right mouse click on Index.html file in Solution Explorer and choose "View in Browser". Here is what you will see after
clicking button "Get Cinema Info":
Unlike the previous sample, SPAComplexObjectGetter project has a non-empty Models folder with two classes in it:
Cinema
and Movie
. Each Movie
has a Title
and a Year
of
the release.
Cinema
has Name
, Description
and a collection of Movie
objects
called Movies
. Cinema
's default constructor creates a Cinema
object and populates
it with some data including two Movie
objects. The purpose of this sample is to show how this cinema
information can be requested by the client, transferred to it from the server in JSON format and displayed on the
client side.
"Controllers" folder contains DataController.cs file with only one method:
public ActionResult GetCinema()
{
Cinema cinema = new Cinema();
return Json(cinema, JsonRequestBehavior.AllowGet);
}
The relative URL to call method GetCinema
within DataController
is
"/data/getcinema".
Now let us look at the client code within Index.html file.
HTML code is very simple consisting of a div
to add the cinema info to and of
a button for triggering the exchange with the server:
<body>
<div id="cinemaInfo" style="margin-bottom:20px"></div>
<Button id="getCinemaInfoButton">Get Cinema Info</Button>
</body>
JavaScript code is more complex, but just like the code of the previous sample, it uses JQuery
function $.get
to send GET request to the server and process
whatever information is returned from the server:
$(document).ready(function () {
$("#getCinemaInfoButton").click(function () {
$.get("/data/getcinema", "", function (cinema) {
$("#cinemaInfo").contents().remove();
$("#cinemaInfo").append("Name: " + cinema.Name);
$("#cinemaInfo").append("<div></div>");
$("#cinemaInfo").append("Description: " + cinema.Description);
$("#cinemaInfo").append("<div style='margin-top:5px'></div>");
$("#cinemaInfo").append("Movies:");
$("#cinemaInfo").append("<div></div>");
for (var idx = 0; idx < cinema.Movies.length; idx++) {
var movie = cinema.Movies[idx];
$("#cinemaInfo").append
(
" " +
"Title: " + movie.Title +
", " +
"year: " + movie.Year
);
$("#cinemaInfo").append("<div></div>");
}
});
});
});
The query part of the function $.get
is empty, since
our GET request does not have any parameters.
The object returned from the server is represented by variable cinema
which is an input variable to the function called on GET request success. Within that
function we form the text and append it to the "cinemaInfo" div tag. The returned cinema
object
has fields Name
and
Description
as well as an array of movie
objects called Movies
.
Each movie
object has Title
and Year
fields. In fact,
the returned JSON object mimics the server side C# object of type Cinema
.
You can view the JSON object returned from the server by opening a browser e.g. Chrome and
typing the URL to the controller function GetCinema
: "http://localhost:29863/data/getcinema".
You will see the returned JSON string in the browser.
POSTing Complex Data Object to the Server
GET requests presented in the two previous samples build the query as part of the URL and as such
GET requests are perfect when an object you want to send a set of names and values to the server.
When a complex JSON object needs to be transferred from the client to the server,
POST request should be utilized.
In this sub-section we will consider the same Cinema
model as in the previous one,
only the model will be created on the client, sent to the server for modifications, returned
by the server back to the client and displayed on the client.
This sub-section's sample is located under SPA_POSTingObjectToServer solution.
The server has the same model classes:
Cinema
and Movie
, but Cinema
's default constructor is empty
since the data is coming from the client. DataController
's method ChangeCinemaData
changes the data coming from the client by adding another movie "Anne of Planet Mars" to the collection
of the movies. Then it returns the modified Cinema
object back to the client.
To cover the case of the server failure, I also made the server request fail every other time.
To mark the failure, the server changes the Response
's StatusCode and returns
"serverError" as a string.
Here is the server code:
static bool evenRequest = true;
public ActionResult ChangeCinemaData(Cinema cinema)
{
evenRequest = !evenRequest;
if (evenRequest)
{
Response.StatusCode = (int)System.Net.HttpStatusCode.InternalServerError;
return Json("serverError");
}
cinema.Movies.Add
(
new Movie
{
Title = "Anne of Planet Mars",
Year = 3000
}
);
return Json(cinema);
}
Note, that the server will interpret the data coming from the client as
an object of type Cinema
as long as the object has correct structure.
The client code is located in Index.html file. HTML consist of two div
tags - one for
the visual representation of the original Cinema
object - the other one for holding
the visual representation of the modified Cinema
object returned from the server.
There is also a button placed between them to trigger the exchange with the server.
The visual representation of the server reply is colored in red.
<body>
<!-- Here we place the original cinema data-->
<div id="originalCinemaInfo" style="margin-bottom:20px"></div>
<!-- clicking this button sends the cinema object to the server
and places the result returning from the server under
serverCinemaInfo tag -->
<Button id="changeCinemaInfoFromServerButton">Change Cinema Info via Server</Button>
<div style="color:blue;margin-top:20px">Reply from the Server:</div>
<!-- here we create a visual representation for Cinema object
coming from the server -->
<div id="serverCinemaInfo" style="margin-bottom:20px;color:Red">
</div>
</body>
Here is the JavaScript code:
$(document).ready(function () {
var cinema = {
Name: "Mohawk Mall Cinema",
Description: "An OK Cinema",
Movies: [
{
Title: "Anne of Green Gables",
Year: 1985
},
{
Title: "Anne of Avonlea",
Year: 1987
}
]
};
displayCinemaInfo($("#originalCinemaInfo"), cinema);
var cinemaJsonString = JSON.stringify(cinema);
$("#changeCinemaInfoButton").click(function () {
$.ajax({
type: "POST",
dataType: "json",
contentType: "application/json; charset=utf-8;",
url: "/data/changecinemadata",
data: cinemaJsonString,
success: function (changedCinemaFromServer) {
displayCinemaInfo($("#serverCinemaInfo"), changedCinemaFromServer);
},
error: function (resultFromServer) {
$("#serverCinemaInfo").contents().remove();
$("#serverCinemaInfo").append("Server Error");
}
});
});
});
We use generic $.ajax
method to send a POST request to the server. This method allows
us to also specify the function to be called in case of a server error as well as the content type
for the request.
The data we send within the message body is our Cinema
JSON object
turned into string by JSON.stringify
function. Some older browsers
still do not support this function and because of that, the reference to json2.min.js
file was added. This file came from NuGet installation of the json2 package.
Here is how the SPA looks after successful reply from the server:
and here is how it looks in case of failure:
Summary
This article talks about the Single Page Applications (SPAs). We define the SPA
and present numerous SPA samples.
All the information and functionality within the article is something I wished I knew and had several months
ago when I just started building the SPAs.
This article introduces a new JavaScript library called BPF (Browser Presentation Foundation). This library is used for two
purposes:
- Navigation - enabling to navigate your SPA using browser URLs and navigation buttons.
- Composition - showing how one can use BPF library to compose the SPA from different HTML pages
on the client.
Eventually, I hope that the BPF library will include a lot of other functionality.
At the end of the article, I talk about how the SPA applications contact the server.
Acknowledgment
I was introduced to SPA by John Papa's presentation on Pluralsight at
Single Page Apps with HTML5, Web API, Knockout and jQuery. Since then I am building web sites as SPAs.
My warm thanks to him and other Pluralsighters.