Spinning SVG Animation

Interested in learning a few capabilities of SVG animation this evening from JavaScript in a web page, I put together a simple demonstration.

Click on the image to launch the demo.

image

Animating SVG in a modern browser (including IE9) is generally easy enough. This example was slightly more interesting in that I wanted the thicker line to rotate based on the center point of the image, rather than the location of the line itself.

Not what I wanted:

image

Desired rotation:

image

The white bar would rotate around the center point (marked by the red arrow above).

I’ve included all of the code at the bottom of the post. (There’s quite a lot of path information).

The JavaScript just used an interval to move the line around the point:

(function () {
    window.onload = loaded;
    function loaded() {
        var colorTemp = document.getElementById("color-temp");
        var reading = document.getElementById('current-reading');
        var currentAngle = 0;
        var fill;

        var direction = 1;
        setInterval(function () {
            currentAngle += direction;
            if (currentAngle >= 120 || currentAngle <= -120) {
                direction *= -1;
            } else if (currentAngle === 0) {
                fill = direction === 1 ? "#BE1E2D" : "#10A2DC";
                colorTemp.setAttribute("fill", fill);
            }
            // adjust the opacity
            colorTemp.setAttribute("opacity", Math.abs(currentAngle) / 120.0);

            reading.setAttribute("transform", "rotate(" + currentAngle + ")");

        }, 25);
    }
})();

The angle is fixed between 120 and –120 degrees. When the angle reaches zero, the fill color is toggled.

As the angle adjusts, the line is updated as well as the opacity of the fill.

The overall size of the SVG drawing is 600×600. Knowing that (and wanting the exact center), I translated a group to an offset of 300x, 300y containing the line:

<g id="temp-transform" transform="translate(300,300)">
        <line id="current-reading" fill="none" stroke="#FFFFFF"
              stroke-width="5" stroke-linecap="round"
              stroke-miterlimit="10" x1="0" y1="-180" x2="0" y2="-120"/>
</g>

This effectively made the rotation now work from the center, once the line coordinates were adjusted to reflect the new translation.

This works because the contents of the outer group caused the new starting point (origin) to be 300x, 300y rather than the default 0x, 0y. You can see the line is from y –120 to y –180. That’s because I wanted the line to start in the top middle (as the x is set to 0 for the line).

I used Adobe Illustrator CS 5.5 to create the SVG image. Illustrator typically does not always handle fonts well when exporting as SVG. The final text node was this:

<text transform="matrix(1 0 0 1 254.1387 83.3198)" fill="#D1D3D4" font-family="'Arial'"
font-weight="bold" font-size="29.4616">thermo</text>

To make it work across browsers, I needed to modify the text node below slightly. Note the font name and missing bold attribute. It’s not a hard switch, but it’s annoying if you need to make changes to your SVG multiple times.

<text transform="matrix(1 0 0 1 254.1387 83.3198)" fill="#D1D3D4" 
font-family="'Arial-BoldMT'" font-size="29.4616">thermo</text>

If you have any questions, please leave a comment!

<!DOCTYPE HTML>
<html>
<head>
    <title>Svg Spinner Demo</title>
    <style>
        #demo1 svg {
            width: 300px;
            height: 300px;
        }

    </style>
</head>
<body>

<div id="demo1">
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
     width="600px" height="600px" viewBox="0 0 600 600" enable-background="new 0 0 600 600" xml:space="preserve">
<g>
    <radialGradient id="SVGID_1_" cx="298.9106" cy="196.146" r="399.267" gradientUnits="userSpaceOnUse">
        <stop  offset="0" style="stop-color:#D9D9D9"/>
        <stop  offset="1" style="stop-color:#C2C2C2"/>
    </radialGradient>
    <path fill="url(#SVGID_1_)" d="M300.646,595.41c-78.957,0-153.188-30.747-209.02-86.579C35.795,453,5.048,378.77,5.048,299.812
        c0-78.957,30.748-153.188,86.579-209.02S221.689,4.214,300.646,4.214c78.958,0,153.188,30.748,209.019,86.579
        c55.832,55.831,86.579,130.063,86.579,209.02c0,78.958-30.747,153.188-86.579,209.019
        C453.834,564.663,379.604,595.41,300.646,595.41z"/>
    <path fill="#808285" d="M300.646,6.214c39.637,0,78.086,7.762,114.281,23.071c34.961,14.787,66.359,35.957,93.324,62.922
        s48.135,58.364,62.923,93.325c15.309,36.193,23.07,74.643,23.07,114.28s-7.762,78.086-23.07,114.281
        c-14.788,34.961-35.958,66.359-62.923,93.324s-58.363,48.135-93.324,62.923c-36.194,15.309-74.644,23.07-114.281,23.07
        s-78.087-7.762-114.28-23.07c-34.961-14.788-66.36-35.958-93.325-62.923s-48.135-58.363-62.922-93.324
        C14.81,377.898,7.048,339.449,7.048,299.812s7.762-78.087,23.071-114.28c14.787-34.961,35.957-66.36,62.922-93.325
        s58.364-48.135,93.325-62.922C222.559,13.976,261.009,6.214,300.646,6.214 M300.646,2.214
        C136.287,2.214,3.048,135.453,3.048,299.812S136.287,597.41,300.646,597.41s297.598-133.239,297.598-297.598
        S465.005,2.214,300.646,2.214L300.646,2.214z"/>
</g>
<g>
    <radialGradient id="SVGID_2_" cx="299.0464" cy="204.2632" r="368.0053" gradientUnits="userSpaceOnUse">
        <stop  offset="0" style="stop-color:#141414"/>
        <stop  offset="1" style="stop-color:#080808"/>
    </radialGradient>
    <path fill="url(#SVGID_2_)" d="M300.646,572.266c-72.775,0-141.194-28.34-192.654-79.8c-51.459-51.46-79.8-119.879-79.8-192.654
        s28.34-141.194,79.8-192.654s119.879-79.8,192.654-79.8s141.194,28.34,192.654,79.8c51.46,51.46,79.8,119.879,79.8,192.654
        s-28.34,141.194-79.8,192.654S373.421,572.266,300.646,572.266z"/>
    <path d="M300.646,29.358c36.512,0,71.931,7.15,105.271,21.252c32.205,13.622,61.129,33.123,85.968,57.962
        c24.84,24.839,44.341,53.763,57.963,85.968c14.102,33.34,21.252,68.759,21.252,105.271c0,36.512-7.15,71.931-21.252,105.271
        c-13.622,32.205-33.123,61.129-57.963,85.968c-24.839,24.84-53.763,44.341-85.968,57.963
        c-33.34,14.102-68.759,21.252-105.271,21.252c-36.513,0-71.931-7.15-105.271-21.252c-32.205-13.622-61.129-33.123-85.968-57.963
        c-24.839-24.839-44.34-53.763-57.962-85.968c-14.102-33.34-21.252-68.759-21.252-105.271c0-36.513,7.15-71.931,21.252-105.271
        c13.622-32.205,33.123-61.129,57.962-85.968s53.763-44.34,85.968-57.962C228.715,36.509,264.133,29.358,300.646,29.358
         M300.646,25.358c-151.577,0-274.454,122.877-274.454,274.454c0,151.576,122.877,274.454,274.454,274.454
        c151.576,0,274.454-122.878,274.454-274.454C575.1,148.235,452.222,25.358,300.646,25.358L300.646,25.358z"/>
</g>
<g>
    <radialGradient id="SVGID_3_" cx="299.5176" cy="232.4409" r="259.4771" gradientUnits="userSpaceOnUse">
        <stop  offset="0" style="stop-color:#212121"/>
        <stop  offset="1" style="stop-color:#0D0D0D"/>
    </radialGradient>
    <path fill="url(#SVGID_3_)" d="M300.646,491.916c-105.927,0-192.104-86.178-192.104-192.104
        c0-105.927,86.178-192.104,192.104-192.104c105.926,0,192.104,86.178,192.104,192.104
        C492.75,405.738,406.572,491.916,300.646,491.916z"/>
    <path d="M300.646,109.708c25.666,0,50.561,5.026,73.996,14.938c22.638,9.575,42.968,23.283,60.429,40.743
        c17.46,17.46,31.167,37.791,40.742,60.428c9.912,23.435,14.938,48.331,14.938,73.996s-5.025,50.561-14.938,73.996
        c-9.575,22.638-23.282,42.968-40.742,60.429c-17.461,17.46-37.791,31.167-60.429,40.742c-23.435,9.912-48.33,14.938-73.996,14.938
        s-50.562-5.025-73.996-14.938c-22.637-9.575-42.968-23.282-60.428-40.742c-17.46-17.461-31.168-37.791-40.743-60.429
        c-9.912-23.435-14.938-48.33-14.938-73.996s5.026-50.562,14.938-73.996c9.575-22.637,23.283-42.968,40.743-60.428
        s37.791-31.168,60.428-40.743C250.084,114.733,274.98,109.708,300.646,109.708 M300.646,105.708
        c-107.201,0-194.104,86.903-194.104,194.104c0,107.201,86.903,194.104,194.104,194.104c107.201,0,194.104-86.903,194.104-194.104
        C494.75,192.611,407.847,105.708,300.646,105.708L300.646,105.708z"/>
</g>
<g opacity="0.8">
    <path id="color-temp" fill="#BE1E2D" opacity="0" d="M300.646,489.309c-104.489,0-189.497-85.008-189.497-189.497c0-104.489,85.008-189.497,189.497-189.497
        c104.489,0,189.497,85.008,189.497,189.497C490.143,404.301,405.135,489.309,300.646,489.309z"/>
    <path d="M300.646,110.815c25.515,0,50.266,4.997,73.566,14.852c22.505,9.519,42.718,23.146,60.075,40.504
        s30.984,37.57,40.504,60.075c9.854,23.3,14.852,48.051,14.852,73.566s-4.997,50.266-14.852,73.566
        c-9.52,22.505-23.146,42.718-40.504,60.075s-37.57,30.984-60.075,40.504c-23.3,9.854-48.051,14.852-73.566,14.852
        s-50.266-4.997-73.566-14.852c-22.505-9.52-42.718-23.146-60.075-40.504s-30.985-37.57-40.504-60.075
        c-9.855-23.3-14.852-48.051-14.852-73.566s4.997-50.266,14.852-73.566c9.519-22.505,23.146-42.718,40.504-60.075
        s37.57-30.985,60.075-40.504C250.38,115.812,275.131,110.815,300.646,110.815 M300.646,109.815
        c-104.933,0-189.997,85.064-189.997,189.997c0,104.932,85.064,189.997,189.997,189.997c104.932,0,189.997-85.064,189.997-189.997
        C490.643,194.879,405.578,109.815,300.646,109.815L300.646,109.815z"/>
</g>
<g id="temperature-markings">
        <line fill="none" stroke="#C7C8CA" stroke-width="5" stroke-miterlimit="10"
              x1="300.646" y1="123.751" x2="300.646" y2="161.603"/>
        <line fill="none" stroke="#C7C8CA" stroke-width="5" stroke-miterlimit="10"
              x1="422.102" y1="371.971" x2="454.881" y2="390.896"/>
        <line fill="none" stroke="#C7C8CA" stroke-width="5" stroke-miterlimit="10"
              x1="146.41" y1="390.896" x2="179.191" y2="371.97"/>
</g>
<g id="temp-transform" transform="translate(300,300)">
        <line id="current-reading" fill="none" stroke="#FFFFFF"
              stroke-width="5" stroke-linecap="round"
              stroke-miterlimit="10" x1="0" y1="-180" x2="0" y2="-120"/>
</g>
    <text transform="matrix(1 0 0 1 254.1387 83.3198)" fill="#D1D3D4" font-family="'Arial'"
          font-weight="bold" font-size="29.4616">thermo</text>
</svg>


</div>


<script type="text/javascript">

    (function () {

        window.onload = loaded;

        function loaded() {
            var colorTemp = document.getElementById("color-temp");
            var reading = document.getElementById('current-reading');
            var currentAngle = 0;
            var fill;

            var direction = 1;
            setInterval(function () {
                currentAngle += direction;
                if (currentAngle >= 120 || currentAngle <= -120) {
                    direction *= -1;
                } else if (currentAngle === 0) {
                    fill = direction === 1 ? "#BE1E2D" : "#10A2DC";
                    colorTemp.setAttribute("fill", fill);
                }
                // adjust the opacity
                colorTemp.setAttribute("opacity", Math.abs(currentAngle) / 120.0);

                reading.setAttribute("transform", "rotate(" + currentAngle + ")");

            }, 25);
        }
    })();


</script>
</body>
</html>

Paging Data with SQL Server Compact

image

If you’re using SQL Server Compact Editition version 4 or higher, there’s finally a decent and efficient way to do data paging:

var streets = db.Query<StreetName>(@"SELECT * FROM StreetNames
                   ORDER BY Id
                   OFFSET @offset ROWS
                   FETCH NEXT @rows ROWS ONLY",
                        new { offset = group * GroupSize, 
                            rows = GroupSize });

The code above uses Dapper-dot-net (with Contrib Extensions) to make the syntax of executing the Query succinct. (Recently, I’ve all but abandoned the Entity Framework in favor of Dapper).

The above code should be straightforward.

Given a table named, StreetNames created thusly:

db.Execute(@"CREATE TABLE StreetNames (          
            Id int IDENTITY NOT NULL,
            Name nvarchar(24) NOT NULL,
            CONSTRAINT pk_id PRIMARY KEY (Id))");

with some sample data inserted into the table:

foreach (var name in GetStreetNames())
{
    db.Execute("INSERT INTO StreetNames (Name) Values (@Name)",
        new { Name = name });
}

you’ll likely want to grab the data in pages. To get the data as pages, you’ll need to pick an index or something ordered (in this case, I used the Id column), specify the starting index and the total number to fetch:

OFFSET @offset ROWS
FETCH NEXT @rows ROWS ONLY

So, using the sample database:

using (var db = new SqlCeConnection(connectionString))
{
    db.Open();
    for (var group = 0; ; group++)
    {
        Console.WriteLine(string.Format("== GROUP {0} ==", group));
        var streets = db.Query<StreetName>(@"SELECT * FROM StreetNames
                           ORDER BY Id
                           OFFSET @offset ROWS
                           FETCH NEXT @rows ROWS ONLY",
                                new { offset = group * GroupSize, 
                                    rows = GroupSize });

        foreach (var street in streets)
        {
            Console.WriteLine(string.Format("{0} ({1})",
                street.Name, street.Id));
        }
        if (streets.Count() < GroupSize) { break; }
    }
}

The code loops until it can’t load a full GroupSize.

const int GroupSize = 8;

I added an extension method to IDbConnection to check whether a table exists:

public static class DbExtensions
{
    /// <summary>
    /// Determines whether the table exists
    /// </summary>
    /// <param name="connection">Existing, opened, database connection</param>
    /// <param name="tableName">The name of the table to test for.</param>
    /// <returns>True if table exists.</returns>
    public static bool TableExists(this IDbConnection connection, string tableName)
    {
        Debug.Assert(connection != null);
        Debug.Assert(!string.IsNullOrWhiteSpace(tableName));

        var cmd = connection.CreateCommand();
        cmd.CommandText = @"SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES 
                            WHERE TABLE_NAME=@TableName";
        var p1 = cmd.CreateParameter();
        p1.DbType = DbType.String;
        p1.ParameterName = "TableName";
        p1.Value = tableName;
        cmd.Parameters.Add(p1);

        var result = cmd.ExecuteScalar();
        return (int)result == 1;
    }
}

I really like the simple syntax it permits:

if (!db.TableExists("StreetNames"))

It’s used like this:

private static string CreateDatabase()
{
    var connectionString = string.Format("DataSource={0}", "test.sdf");
    SqlCeEngine engine = new SqlCeEngine(connectionString);
    try
    {
        engine.CreateDatabase();
    }
    catch (SqlCeException ex)
    {
        // file exists? (if so, we'll just ignore it ...)
        if (ex.NativeError != 25114) { throw; }
    }

    using (IDbConnection db = new SqlCeConnection(connectionString))
    {
        db.Open();
        // create a sample table 
        if (!db.TableExists("StreetNames"))
        {
            db.Execute(@"CREATE TABLE StreetNames (                
                        Id int IDENTITY NOT NULL,
                        Name nvarchar(24) NOT NULL,
                        CONSTRAINT pk_id PRIMARY KEY (Id))");
            foreach (var name in GetStreetNames())
            {
                db.Execute("INSERT INTO StreetNames (Name) Values (@Name)",
                    new { Name = name });
            }
        }
    }
    return connectionString;
}

Oddly, Sql Compact doesn’t throw proper (specific) exceptions, so you’ll be forced to check for NativeErrors or against strings rather than specific Exception types. (Seriously!)

Using the SqlCeEngine class, the code attempts to create a new Database file. If it already exists, it throws an exception. All but the file exists exception are rethrown.

Using an open connection and the TableExists extension, it creates a sample table called StreetNames, populated with this data:

private static IEnumerable<string> GetStreetNames()
{
    // Most common street names in UK, apparently :)
    yield return "High Street"; yield return "Station Road"; yield return "Main Street";
    yield return "Church Street"; yield return "Victoria Road"; yield return "Park Road";
    yield return "Church Road"; yield return "London Road"; yield return "Manor Road";
    yield return "New Road"; yield return "Park Avenue"; yield return "Queens Road";
    yield return "Kings Road"; yield return "Church Lane"; yield return "Green Lane";
    yield return "Alexandra Road"; yield return "The Crescent"; yield return "George Street";
    yield return "Grange Road"; yield return "Main Road"; yield return "King Street";
    yield return "The Avenue"; yield return "New Street"; yield return "North Street";
    yield return "Victoria Street"; yield return "West Street"; yield return "Queen Street";
    yield return "Springfield Road"; yield return "Stanley Road"; yield return "Albert Road";
    yield return "Albert Street"; yield return "Park Lane"; yield return "Chapel Street";
    yield return "Highfield Road"; yield return "The Green"; yield return "Mill Lane";
    yield return "Broadway"; yield return "St. Johns Road"; yield return "Marlborough Road";
    yield return "Windsor Road"; yield return "Forest Road"; yield return "South Street";
    yield return "Warwick Road"; yield return "Grove Road"; yield return "Kingsway";
    yield return "York Road"; yield return "Woodlands Road"; yield return "Clarence Road";
    yield return "School Lane"; yield return "Cromwell Road";
}

(I’d wanted to use the top 50 USA street names, but they’re far more boring as they’re like: Fourth Street, Fifth Street, Oak Street. Smile ).

There’s a tiny class to represent a StreetName that’s used later by the Dapper extension functions:

public class StreetName
{
    public int Id { get; set; }
    public string Name { get; set; }
}

The full main function:

static void Main(string[] args)
{
    const int GroupSize = 8;

    var connectionString = CreateDatabase();
    using (var db = new SqlCeConnection(connectionString))
    {
        db.Open();
        for (var group = 0; ; group++)
        {
            Console.WriteLine(string.Format("== GROUP {0} ==", group));
            var streets = db.Query<StreetName>(@"SELECT * FROM StreetNames
                               ORDER BY Id
                               OFFSET @offset ROWS
                               FETCH NEXT @rows ROWS ONLY",
                                    new { offset = group * GroupSize, 
                                        rows = GroupSize });

            foreach (var street in streets)
            {
                Console.WriteLine(string.Format("{0} ({1})",
                    street.Name, street.Id));
            }
            if (streets.Count() < GroupSize) { break; }
        }
    }

    Console.WriteLine("** Done **");
    Console.ReadKey();
}

The db.Query<StreetName> function is Dapper provided. Given the type and a Query, it automatically and efficiently constructs/deserializes objects of type StreetName for each row returned from the SQL query.

While the syntax isn’t quite as slick as  “Entity Framework” by any means, the resulting run-time efficiency far makes up for that as far as I’m concerned (and if you care about performance, you might want to re-evaluate your use of the EntityFramework as well).

I took the photo above years ago at the Boeing Museum of Flight in Seattle (which inspired me to look for videos). For fun, check out this video of a model SR-71 blackbird. It takes off around 2:21.

Model SR-71 Blackbird in flight.

Get path or location of currently executing batch/command file in Windows

I didn’t know it was this simple, and am posting this information on my blog so I find it in the future, but hopefully this will help someone else!

I’ve created a number of batch files over the years which routinely copy files from one location to another, usually as part of a backup strategy. However, I’ve always just hard-coded the paths of the drives, etc. into the batch files. While this works for drives which are permanently attached (or internal), it’s more fragile with external (flash/USB) drives. As I’ve never understood the logic of drive letter selection in Windows (letters usually are the same, but occasionally not), it meant that I was tweaking the drive letter in the batch file before running. Annoying, but it worked.

Thanks to more than a few web sites, I now know there is a much better way!

There are basically two decent options, depending on your scenario and requirements.

Option 1

If you are using drive letters (and not a mapped drive\network share), then you can use the variable %CD%.

It contains the “current directory.” So, that actually may be more than you wanted if the current directory isn’t the root of the drive.

image

Simple, just chop it off:

image

%CD:~0,2%

The colon and tilde character is a flag which indicates that a substring should be returned rather than the entire string. The first value is the zero-based starting index and the second is the number of characters you want to return:

image

The above starts at character 4, and includes 3 characters.

For fun, you can use negative values:

image

With only a single negative parameter, it returns the number of characters requested starting with the rightmost character.  (Check here for a bunch of examples on string manipulation.)

So, you could use knowledge in a batch file:

image

The above line uses robocopy (available in modern versions of Windows without an extra install) to copy from the folder \\server\Backups to the current path appended with \server\backups. So, if the batch file containing the robocopy command was executing on the J: drive, the resulting robocopy command would be:

image

By using the :~0,2 syntax, regardless of the folder the batch file is located in, it always copies to the root of the J drive (as the first two characters are J and : ).

Option 2

The other option is a bit different as it only works in a batch or command file.

image

Parameter zero (%0) in batch file represents the full path of the currently executing file (path and filename). The (dp) modifiers expand the value to be the “drive” and the “path,” excluding the file name.

image

You can manipulate the value as well:

image

I’m immediately going to adopt the first option into all of my “robocopy” batch files.

JavaScript: isScrolledIntoView

image

I needed a simple way to detect when a placeholder DIV (that would contain an image) had scrolled into the current viewport of the browser. I’ve seen a few solutions that worked, and a few that didn’t (hello? test your code!). Here’s my simple solution:

function isScrolledIntoView(elem) {
    elem = $(elem);
    var win = $(window);
    var docViewTop = win.scrollTop();
    var docViewBottom = docViewTop + win.height();
    var elemTop = elem.offset().top;
    var elemBottom = elemTop + elem.height();

    var a = (elemTop > docViewBottom);
    if (a) { return false; }
    var b = (elemBottom > docViewTop);
    if (!b) { return false; }
    return !(a && b);
}

Nothing fancy, except it does require jQuery.

When one of the place holder DIVs scrolled into view, the code would trigger a queued load of the image.

SmugMupBrowser-live

As the SmugMug API is poorly designed in a few places, in particular as it relates to showing a thumbnail for a gallery/album, when an album/gallery is scrolled into view, the app loads a list of ALL of the images from the now visible album and selects one of them to show as the thumbnail. It’s extremely inefficient unfortunately. Instead, if they could have included that extra piece of data in the gallery list (thumbnail image and thumbnail image key), boom!

I’ve also included a delay so that the auto loading only occurs after a 1 second pause (either the window being resized or scrolling occurring):

function delayLoadNewVisible() {
    if (visibilityDelayTimerId == 0) {
        visibilityDelayTimerId = window.setTimeout(function () {
            visibilityDelayTimerId = 0;
            loadNewVisible();
        }, 1000);
    }
}

I’m not going to post the code for the other useful aspect of this functionality, but I’ll tell you about it, and leave coding it as an exercise for the reader. Smile

When the user scrolls new albums into view, after the pause, they’re added to a load queue. However, as it’s just as likely that the user will scroll the album off the screen, my load code also can remove something from the load queue if it hasn’t been loaded already. This way, visible albums are given precedence. By using the queue in this way, there are a manageable number of outstanding requests at any given time, and ideally only those that are relevant to what’s on the user’s screen.

Knockout.JS: Dictionary/Index and ObservableArray, Part 2

Ryan suggested an alternative in a comment (and corresponding jsFiddle) to the technique that I’d used in my previous Knockout.JS post.

Following his suggestion, I made a few tweaks to my original function (and renamed it yet again):

ko.observableArray.fn.withIndex = function (keyName) {        
    var index = ko.computed(function () {
        var list = this() || [];    // the internal array
        var keys = {};              // a place for key/value
        ko.utils.arrayForEach(list, function (v) {
            if (keyName) {          // if there is a key
                keys[v[keyName]] = v;    // use it
            } else {
                keys[v] = v;
            }
        });
        return keys;
    }, this);

    // also add a handy add on function to find
    // by key ... uses a closure to access the 
    // index function created above
    this.findByKey = function (key) {
        return index()[key];
    };

    return this;
};

To use, just tag on the withIndex function call:

var ViewModel = function () {
    this.uniqueNumber = 0;
    this.list = ko.observableArray([]).withIndex('id');
};

In the example above, the objects stored in the “list” observableArray will be indexed by the “id” property.

By adding the withIndex function call to the observableArray creation, an additional function is added to the array object, findByKey.

$("#btnAdd").on("click", function () {
    var id = vm.uniqueNumber++;
    vm.list.push({
            id: id,
            time: new Date().toLocaleTimeString()});
    var data = vm.list.findByKey(id);
});

In the above example, a new “log” object is added to the list, and then retrieved by the key a moment later (dumb, yes, but hopefully instructive).

I intentionally added the findByKey only to array instances that include the indexing functionality added by withIndex. One reason was to not have the function be available for all arrays (as it doesn’t work for non-indexed arrays) and the second was that it makes possible a bit of slick JavaScript closure value hiding. This way, the index is never stored anywhere on the original view model. Instead, it’s kept entirely within the closure.

But there’s more!

What if you want to have multiple keys or don’t like the name of the find function?

Here’s the kicked up a notch version:

ko.observableArray.fn.withIndex = function (keyName, useName) {
    /// keyName == the name of the property used as the index
    ///            value.
    /// useName == when false, a function named findByKey 
    ///            is added to the observableArray.
    ///            when true, the function is named based
    ///            on the name of the index property &
    ///            capitalized (like id becomes findById)
    var index = ko.computed(function () {
        var list = this() || [];    // the internal array
        var keys = {};              // a place for key/value
        ko.utils.arrayForEach(list, function (v) {
            if (keyName) {          // if there is a key
                keys[v[keyName]] = v;    // use it
            } else {
                keys[v] = v;
            }
        });
        return keys;
    }, this);

    var fnName = "";
    if (useName && keyName) {
        var cap = keyName.substr(0, 1).toUpperCase();
        if (keyName.length > 1) {
            fnName = cap + keyName.substring(1);
        } else {
            fnName = cap;
        }
    } else {
        fnName = "Key";
    }

    var fnName = "findBy" + fnName;
    this[fnName] = function (key) {
        return index()[key];
    };

    return this;
};

This optionally uses the name of the key parameter to build a findByXYZ function using the pattern of findBy{KeyName}. It also capitalizes the function name to follow typical JavaScript coding conventions. Here it is in when initialized:

this.list = ko.observableArray([])
                .withIndex('id', true)
                .withIndex('time', true);

(note that they’re chained above) … and in use:

var data = vm.list.findById(id);
data = vm.list.findByTime(time);

You’ll see how the “findBy” functions are named “findById” and “findByTime” as the original call to “withIndex” used the optional second parameter set to true. I suppose this follows a tiny bit of the automatic mix-in style of Ruby on Rails (but is optional).