Spry Periodic Case study

The Spry Periodic Table takes advantage of many Spry techniques that the Spry community might be interested in. This document will go through the code section by section and explain 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.

Basic functionality

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 regions, plus a good use of CSS classes to position things to they look like a periodic table. These class 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

The data table, located at the bottom of the page, is a straight HTML table:

No Atomicweight Name Symbol MP  BP Density Earthcrust DiscoveryYear Group Period Electron_configuration Element_Type
1 1.0079 Hydrogen H -259 -253 0.09 0.14 1776 1 one 1s1 NonMetal
2 4.0026 Helium He -272 -269 1895 18 one 1s2 Inert

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 a HTML table made it easy to add columns for additional data points and to find elements quickly.

Setting up the page

As with all Spry pages, we need to attach files so we 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>

The CSS

A big part of the functionality depends on class names. Open the 'atom.css' file.

We have a set of class names that are named after the different 'Element_Types'. This will allow 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. )

Data Sets

The Periodic Table uses 4 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>

There is one more function in the <script>

ds1.addObserver({onPostLoad: function(ds, data) {
 Spry.Utils.removeClassName("wholeContainer", "SpryHiddenClass");
 }});

The CSS class 'SpryHiddenClass' only sets 'display:none;' This will hide and remove the element from the flow of the page.

The 'wholeContainer' 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 2 reasons:

  1. When the page is loading, the periodic table will not be visible. This hides the Spry code before it is ready to go. When the data is loaded and ready to go, the observer gets the 'onPostLoad' notification and then runs the Spry.Utils.removeClassName. This removes 'SpryHiddenClass' from 'wholeContainer. This will cause the periodic table to show, all ready to go.
  2. When javascript it turned off, the Spry code will not display. This is because it has the '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

The HTML markup for the table is short, but contains a lot of logic for both layout and filtering. We will start with the main block of the periodic table and then explain the filters.

The element DIV

The bulk of the periodic table is a single DIV, which we 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 allows us to wrap lines correctly and allows us to filter easily.

<div tabindex="{No}" id="div_{No}" class="elementBlock {Element_Type} {Period} {Name} {Group}"  
spry:if="({No} &lt; 58 || ({No} &gt; 71 &amp;&amp; {No} &lt; 90) || {No} &gt; 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 logic for the element DIV is in the attributes of the DIV tag.

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. Notice that they have a Number (upper left corner) of 58-71 and 90-103. Notice 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 our 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. Notice the () that surround the highlighted code. This separates the logic so everything works correctly. We will see in the other element DIV block that we have an inverse condition to display the other 2 rows.

For the "Lanthanide' block, the 2 blue rows at the bottom, we need to display the elements we didn't display 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} &gt;= 58 &amp;&amp; {No} &lt;= 71) || ({No} &gt;= 90 &amp;&amp; {No} &lt;= 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 wholeContainer

The 'wholeContainer' DIV wraps the entire periodic table. It use 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", our 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 <br>s 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, etc. 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 will clear the float and cause 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". Notice 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. But if we just had that, it would have the correct rows, but we still need to create that gap in the first 3 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.

Creating the Horizontal spacing

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 3 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.

In 'atoms.css', there are classes called '.Hydrogen', '.Beryllium' and '.Magnesium' (Notice they are uppercase because CSS and javascript are case sensitive.). The code is:

.Hydrogen {
 margin-right:928px;
 }
 .Beryllium, .Magnesium {
 margin-right:580px;
 }

And this pushes those rows over correctly! This, and a couple other details, are why we pass the Element name as a class name.

The Tooltip

We use a Spry Tooltip widget to show additional information about the element. Our 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 onmouseover.

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 our 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', our 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.

This 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 our 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: our 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 our case, the 'elementBlock' class is attached to every element Div. Recall that is the class that sets the size and border? So now our tooltip will trigger every time we 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.

Highlighting functions

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 them 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 2 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;
	Spry.$$('.elementBlock', 'wholeContainer').forEach(
			function(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 'grayIt' that changes the opacity of an element to 30%.

We pass in the name of the class name that we want to highlight. We loop through the data set and apply the 'grayIt' class to all those element Divs that do NOT have the class name that is passed in. We use the class="elementBlock" 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 'grayIt' 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 'grayIt' 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 'grayIt' class.

To use these functions, we use our 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>&nbsp; </span></div>

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' data set and the "periods" data set. 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.

Conclusion

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.


Copyright © 2007. Adobe Systems Incorporated.
All rights reserved.