The Spry periodic table takes advantage of many Spry techniques that the Spry community might be interested in. This document steps through the code section by section and explains the thinking and methods behind the code. Since this demo gets away from our common use patterns, it may serve to expand the ways in which Spry can be used on pages.
See a working version of the Spry periodic table here.
To complete this tutorial you will need to install the following software and files:
The Spry Periodic Table uses an HTML data set to populate the periodic table. The actual data source table is on the same page. This allows the page to load faster, allows the page to degrade gracefully, and gives search engines content to index. HTML tables are easier to create and edit than XML and you can use visual editors to modify them.
We then use Spry:repeats and Spry:regions, plus a good use of CSS classes to
position things so they look like a periodic table. These classes are also used
to highlight the different sections of the table via the links.
The tooltip widget is used with a detailregion to provide additional
information when mousing over an element.
The data table, located at the bottom of the page, is an HTML table:
| No | Atomicweight | Name | Symbol | MP | BP | Density |
|---|---|---|---|---|---|---|
| 1 | 1.0079 | Hydrogen | H | -259 | -253 | 0.09 |
| 2 | 4.0026 | Helium | He | -272 | -269 |
See a copy of the entire data table here.
The header row is used as the data reference names used throughout the page.
We use No (atomic number) frequently as a unique ID. We also use Group,
Period, and Element_Type for additional functionality, explained later.
Using an HTML table made it easy to add columns for additional data points and to find elements quickly.
As with all Spry pages, you need to attach files so you can get started.
<link href="SpryAssets/atom.css" rel="stylesheet" type="text/css" /> <script type="text/javascript" src="SpryAssets//SpryData.js"></script> <script type="text/javascript" src="SpryAssets//SpryHTMLDataSet.js"></script> <script type="text/javascript" src="SpryAssets//SpryTooltip.js"></script> <script type="text/javascript" src="SpryAssets/atom_functions.js"></script>
A big part of the functionality depends on class names. Open the atom.css file.
You see a set of class names that are named after the different
Element_Type references. This allows us to color the different element types using
a data reference, since the value of the data reference and the class name are
identical. Other classes are used for positioning things correctly within the
element div. (The element div is the div tag that contains the data for a
single element. )
The Periodic Table uses four data sets:
<script type="text/javascript" language="javascript">
var ds1 = new Spry.Data.HTMLDataSet(null,"atoms");
var types = new Spry.Data.HTMLDataSet(null,"atoms",{distinctOnLoad:true, distinctFieldsOnLoad:['Element_Type']});
var periods = new Spry.Data.HTMLDataSet(null,"atoms",{distinctOnLoad:true, distinctFieldsOnLoad:['Period']});
var groups = new Spry.Data.HTMLDataSet(null,"atoms",{distinctOnLoad:true, distinctFieldsOnLoad:['Group'],sortOnLoad:"Group",sortOrderOnLoad:"ascending"});
groups.setColumnType("Group","number");
...
</script>
var ds1 = new
Spry.Data.HTMLDataSet(null,"atoms"); atoms is the ID of the data table.var types = new
Spry.Data.HTMLDataSet(null,"atoms",{distinctOnLoad:true,
distinctFieldsOnLoad:['Element_Type']}); distinctOnLoad allows us to only get an Element type once.
distinctFieldsOnLoad tells Spry to get every distinct value in the
Element_Type column.var periods = new
Spry.Data.HTMLDataSet(null,"atoms",{distinctOnLoad:true,
distinctFieldsOnLoad:['Period']});var groups = new
Spry.Data.HTMLDataSet(null,"atoms",{distinctOnLoad:true,
distinctFieldsOnLoad:['Group'],sortOnLoad:"Group",
sortOrderOnLoad:"ascending"});groups.setColumnType("Group","number");,
which tells Spry that these are numbers. Without this line of code, Spry would treat
them as text and may not order them correctly. The sortOnLoad and
sortOrderOnLoad ensures that they display in the numeric order. If we
didn't set the sort, they would display in the order they appear in the
document.There is one more function in the <script>:
ds1.addObserver({onPostLoad: function(ds, data) {
Spry.Utils.removeClassName("mainRegion", "SpryHiddenClass");
}});
The CSS class SpryHiddenClass only sets display:none; This hides and
removes the element from the flow of the page.
The mainRegion div wraps the entire periodic table (but not the data
table) within it. It has the SpryHiddenClass class applied to it.
This is used for two reasons:
onPostLoad notification
and then runs the Spry.Utils.removeClassName. This removes
SpryHiddenClass from mainRegion. This will cause the periodic table
to show, all ready to go.SpryHiddenClass applied to it and
Spry will not remove it, since JavaScript is turned off. This allows the
data table to be displayed instead. Because the HTML data set is using a
table on the same page, Spry will hide the data table automatically.The HTML markup for the table is short, but contains a lot of logic for both layout and filtering. I will start with the main block of the periodic table and then explain the filters.
The bulk of the periodic table is a single div, which I refer to as the
element div since it contains the actual chemical information.
Using Spry, this one DIV is repeated for every element in the data table. The
rest of the work is simply positioning those element divs correctly, so it
looks like a real periodic table.
This element div has many classes applied to it. This allows us to identify each element and to wrap lines correctly as well as to filter easily.
<div tabindex="{No}" id="div_{No}" class="elementBlock {Element_Type} {Period} {Name} {Group}"
spry:if="({No} < 58 || ({No} > 71 && {No} < 90) || {No} > 103)" onmouseover="ds1.setCurrentRow('{ds_RowID}');" spry:hover="hover">
<span class="number">{No}</span> <span class="group"> {Group} </span>
<p align="center">{Symbol}</p>
<span class="weight">{Atomicweight}</span> </div>
</div>
The bulk of the code above is the actual content: The
<span> tags and the <p> contain the Spry data references for
Atomic Number {No}, Group Number {Group}, the element symbol {Symbol}, and
the atomic weight {Atomicweight}. The CSS classes number, group, and
weight are defined in atom.css to position these data references
within the element div.
The logic for the element div is in the attributes of the div tag.
tabIndex="{No}" is used to allow tabbing between
elements for accessibility reasons. This gives the tabindex a unique
number {No} to allow tabbing in atomic order.id="div_{no}" provides a unique ID for each of
the element divs. These are used in the filter functions to gray out the
other elements.class="elementBlock {Element_Type} {Period} {Name}
{Group}"
elementBlock is defined in atom.css. It sets the size of
the element div, the border, padding, and font size. Key here is the
float:left that lines up the divs horizontally.{Element_Type} are all defined in atoms.css. The data
reference that adds the class name that sets the background colors you
see on the table.{Period} and {Group} are added so we can filter on them.
They are not defined in atoms.css. They are only used for logic in
filtering the periods and groups.{Name} are used so we can define classes for specific
elements. For example, we use specific classes like .Hydrogen to great
the gap between it and Helium correctly.onmouseover="ds1.setCurrentRow('{ds_RowID}');"
is used for the tooltip to update the tooltip spry:detailregion. This
ensures that the tooltip always shows details for the current element.spry:hover="hover" sets the hover class for
changing the text color when mousing over an element.The spry:if takes some explanation. The two blue rows at the bottom are
special. They are the Lanthanide and Actinide series.
They are always displayed in separate columns below the main table, but
numerically, they are out of order. Note that they have a Number (upper left
corner) of 58–71 and 90–103. Note that on Periods six and seven (click the
Periods filter to highlight them), the number jumps from 57
straight to 72, and from 89 to 104.
So we have to logically not display those in the main table, and ensure they
only show up below. (This separation is the only reason we have to duplicate
code in this demo!)
The spry:if says:
spry:if="({No} < 58 || ({No} > 71 &&
{No} < 90) || {No} > 103)"
This is the logic that says: if the Number is less than 58 or greater than
71 and less than 90 but greater than 103, print it here. Note the ( ) that
surround the highlighted code. This separates the logic so everything works
correctly. You will see in the other element DIV block that an inverse
condition displays the other two rows.
For the Lanthanide block, the two blue rows at the bottom, twe need to display the elements that are not displayed above.
We simply repeat the markup from above, with a new spry:region and
spry:repeatchildren. The only difference in this block is that spry:if is the
reverse of the one above:
spry:if="({No} >= 58 && {No} <= 71) || ({No} >= 90 && {No} <= 103)"
Other than that, the two spry:regions that output the elements are the same.
The Lanthanide div has CSS that positions it left so it lines up
vertically correctly. The <br style="clear:both;" /> between
the two element blocks puts the Lanthanide block on a new line.
The mainRegion div wraps the entire periodic table. It is used as the
main spry:region and is used for CSS purposes. The <br> is a critical
component of the layout.
<div spry:region="ds1" spry:repeatchildren="ds1" class="SpryHiddenRegion" id="mainRegion">
<br spry:if="({No} == 3) || ({No} == 11) || ({No} == 19) || ({No} == 37) || ({No} == 55) || ({No} == 87)" style="clear:both;" />
This DIV is the spry:region for ds1, the main data set. We also
set a spry:repeatchildren on the data set. This is the attribute that causes
all the element divs and the breaks ( <br>) to repeat. The SpryHiddenRegion
class is used to hide the div while the page loads. The ID is needed because we
use it on the observer described in the Data sets section above.
So this code repeats the element div, which since it is float:left:, will
line up all the elements in a long horizontal line. In order to make the table
look correct, we need to start a new line at precise times, Helium, Neon, and so on.
Every time we hit a green element, we need to start a new line. We know the
element Numbers where this needs to happen.
The <br> has a spry:if that has a "clear:both;". This
clears the float and causes the remaining content to wrap to the next line.
We use the spry:if to determine when to add a <br> that will cause this
break. So we write a statement that says, "Break when the number {No}
equals 3 or 19 or 37 or 55 or 87". Note that we actually use the number that
is the first element of the new row.
This break, along with the float on the .elementBlock class, are the key
components of getting the layout correct. If we just had that, it would
have the correct rows, but we still need to create that gap in the first three rows
(Periods, hence the name periodic table), so the elements line up
correctly vertically. To do this, we take advantage of having the {Name} class
on every element div.
In any periodic table, the first three rows (Periods) have a gap between elements because they need to line up vertically in the correct column (Group). To do this , we create classes that use a specific element name and contain simple CSS.
To position these three rows correctly, we simply add a large margin to the correct elements. This pushes the elements to the right so they line up right. The float ensures it stays in line horizontally.
The atoms.css file contains classes called .Hydrogen, .Beryllium, and
.Magnesium. (Note that they are uppercase because CSS and JavaScript are case
sensitive.). The code is:
.Hydrogen {
margin-right:928px;
}
.Beryllium, .Magnesium {
margin-right:580px;
}
This pushes those rows over correctly. This, and a couple other details, are why we pass the element name as a class name.
We use a Spry Tooltip widget to show additional information about the element. The table has much more information in it than we can fit in the element div.
The Tooltip widget works by having a tooltip element, which contains the
tooltip content, and a trigger, which activates the tooltip. Since we want the
data to update for each element, we use a spry:detailregion for this. This is
tied to the onmouseover="ds1.setCurrentRow('{ds_RowID}');" attribute on the element divs. This causes the detailregion to update
on the onmouseover event.
Because the tooltip widget is a detailregion, we need to take steps to make sure it functions properly. Generally, with widgets and Spry data, we have to reactivate widgets that get wiped out when the data refreshes. In this case, we use observers to trigger the widget constructor after the detail region updates.
<div spry:detailregion="ds1" id="tooltip" class="SpryHiddenRegion"> Name:<strong>{Name}</strong><br />
Type:<strong>{Element_Type}</strong><br />
Group:<strong>{Group}</strong><br />
Symbol:<strong>{Symbol}</strong><br />
Period:<strong>{Period}</strong><br />
Boiling Point:<strong>{BP}</strong><br />
Melting Point:<strong>{MP}</strong><br />
Density:<strong>{Density}</strong><br />
Atomic Weight:<strong>{Atomicweight}</strong><br />
Electron Config:<strong>{Electron_configuration}</strong><br />
Year Discovered:<strong>{DiscoveryYear}</strong> </div>
<script type="text/javascript">
Spry.Data.Region.addObserver('mainRegion',{onPostUpdate:function(){var tt1 = new Spry.Widget.Tooltip('tooltip','.elementBlock',{showDelay:750, hideDelay:500});}});
Spry.Data.Region.addObserver('Lanthanide',{onPostUpdate:function(){var tt2 = new Spry.Widget.Tooltip('tooltip','.elementBlock',{showDelay:750, hideDelay:500});}});
</script>
</div>
The div is a detailregion tied to ds1, the main data set.
The id="tooltip" is used to identify the tooltip content for the
widget.
The SpryHiddenRegion class is used to automatically hide the markup as the page
loads. It is removed when Spry is done loading the page.
The <script> tags are within the detailregion. Spry has code that
finds script tags within regions and executes them after the data is finished
loading.
The highlighted regions are observers that recreate the tooltip every time
the detail region updates. We add an observer to elements by their ID. In this
case, we attach observers to the two spry:regions: mainRegion and
Lanthanide. The onPostUpdate notification runs the function()
after the data is finished updating in the region.
The function is the actual Tooltip constructor. tooltip is the ID of the
tooltip content: the detailregion.
.elementBlock is the class name that defines the trigger. Every time
someone mouses over an element with a class of elementBlock, the tooltip
appears. In this case, the elementBlock class is attached to every element
div. Recall that is the class that sets the size and border? So now the tooltip
will trigger every time you mouse over the element div. The onmouseover
attribute causes the data to update to the current element information and the
observer instantly creates the new tooltip.
At the top of the page, we have the links that highlight the particular part of the table. We can select element types, groups, or periods. They are highlighted by graying out the elements that don't belong to the selection.
We use Spry to output the text and simple custom functions to enable this
inverse highlighting. These two functions are in atom_functions.js. One
function does the highlighting. The other clears any current highlighting. The
hasClassName is a utility function that will be included in the next version
of Spry.
function GrayOutTable(activeClass)
{
var rows = ds1.getData(true);
var numRows = rows.length;
for (var i = 1; i <= numRows; i++)
{
var ele = Spry.$("div_" + i);
if (ele)
{
if (!Spry.Utils.hasClassName(ele, activeClass))
Spry.Utils.addClassName(ele,"grayOut");
else
Spry.Utils.removeClassName(ele,"grayOut");
}
}
}
There is a class defined in atom.css called grayOut that changes the
opacity of an element to 30%.
We pass in the name of the class that we want to highlight. We loop
through the data set and apply the grayOut class to all those element divs
that do not have the class name that is passed in. We use the
id="ds_{No}" that we set up on the element divs to identify the divs
that need this new class.
This function gets the data in ds1 and from that we get the number of rows (numRows).
We then loop through all the element divs. We use Spry.Utils to find out if
the element does not have the class name we are looking to highlight. If it
does not have it, we add the grayOut class. If it does have the class name we
are looking for, we leave it alone. That's the inverse highlighting. If it
already has the grayOut class applied, we assume it is from a previous
highlight and remove it.
The other function clears any highlighting by looping through the element
divs and removes any instance of the grayOut class.
To use these functions, we use the other data sets. All three work the same way. For example:
<div>
<span class="redText">Element Types: </span> <span spry:region="types" spry:repeat="types" class="SpryHiddenRegion"> <a href="#" onclick="GrayOutTable('{Element_Type}');">{Element_Type}</a> </span></div>
<span> simply adds the redText
class to the initial text.<span> defines the spry:region for, in this case,
the types data set. The spry:repeat is tied to the same data set. The
SpryHiddenRegion hides the markup until the page is ready.<a> has a null href so we get the hand cursor.
The onclick attribute is the actual function. The parameter in the
function is the Element_Type data reference and it is the value we are
sending to the function. We want to highlight that Element Type. The
actual text of the link is also the Element Type. The space ( )
is used to separate the links when we repeat through the data set.So this will loop through the data set, which has the distinct Element Types and attaches the function to each type. This easily creates the list of highlight types.
We use the same code for the groups and
periods data sets. They all go to the same function, since we are
attaching those values on the element div as class names. So we can use these
values to pick the right elements to highlight.
This is a nice example of how to use Spry for more complicated web layouts. With a small amount of markup, some CSS and a couple custom functions, you have a functional, interactive learning tool.
Hopefully, you have found an interesting technique here or expanded your ideas of how to use Spry.
Donald started his career working in Technical Support for Dreamweaver, Contribute and Authorware among other products. He joined the Dreamweaver Quality Engineering team 2 1/2 years ago. He has been a core member of the Spry team since its inception. Check out his travel photos at http://www.dbooth.net.