Archives

Archives / 2008 / February
  • ScriptOnly - The Opposite of a NOSCRIPT

    Despite all of the advances in client-side scripting, the wonderful JavaScript libraries like Prototype and Scriptaculous, and the ease of writing AJAXy code in ASP.NET, there’s still one aspect of modern web development that can be a complete pain in the butt: accessibility for users without JavaScript. If you’re lucky – perhaps you’re developing an Intranet application, or the like – a simple <noscript>Error: This application requires JavaScript enabled</noscript> is all it takes. But other times, you need to take that extra step and make it work for those with and without JavaScript enabled.

    There’s a lot of ways that this can be accomplished, but one of the more popular ways is with the SCRIPT/NOSCRIPT combo...

    <script type="text/javascript">
        document.write('Only users with JavaScript will see me.');    
    </script>
    <noscript>
        Only users without JavaScript will see me.
    </noscript>
    

    While this works fine in a lot of scenarios, it can get especially tricky when you want to put server-side controls on the SCRIPT side of things. A lot of developers resort to something like this...

    <div id="javaScriptOnly" style="display:none">
        Only users with JavaScript will see me.
        <asp:LinkButton runat="server" ... />
    </div>
    <div id="noJavaScript" style="display:block">
        Only users without JavaScript will see me.
        <asp:Button runat="server" ... />
    </div>
    <script type="text/javascript">
        document.getElementById('javaScriptOnly').style.display = 'block';
        document.getElementById('noJavaScript').style.display = 'none';
    </script>
    

    ... and of course, things quickly get much uglier once you do this in the real world.

    One solution that I use is a simple, custom-control called ScriptOnly. It works just like this...

    <inedo:ScriptOnly runat="server">
        Only users with JavaScript will see me.
        <asp:LinkButton runat="server" onClick="doSomething" ... />
    </inedo:ScriptOnly>
    <noscript>
        Only users without JavaScript will see me.
        <asp:Button runat="server" onClick="doSomething" ... />
    </noscript>

    JavaScript users see a LinkButton, while non-JavaScript users see a plain old submit button. What’s neat about this technique is that you can put any type of content - server-controls, html, script tags, etc - and that content will only be displayed for JavaScript users. In essense, it works like a reverse NOSCRIPT tag.

    Behind the scenes, ScriptOnly is a very simple control...

    [ParseChildren(false)]
    public class ScriptOnly : Control
    {
        protected override void Render(HtmlTextWriter writer)
        {
            //Render contents to a StringWriter
            StringWriter renderedContents = new StringWriter();
            base.Render(new HtmlTextWriter(renderedContents));
    
            //write out the contents, line by line
            writer.WriteLine("<script type=\"text/javascript\">");
            StringReader sr = new StringReader(renderedContents.ToString());
            while (sr.Peek() >= 0)
            {
                // This could be optimized to write on one line; but
                // I've found this makes it easier to debug when
                // looking at a page's source
                writer.WriteLine(
                    "document.writeln('{0}');",
                    jsEscapeText(sr.ReadLine()).Trim());
            }
            writer.WriteLine("</script>");
        }
    
        private string jsEscapeText(string value)
        {
            if (string.IsNullOrEmpty(value)) return value;
    
            // This, too, could be optimzied to replace character
            // by character; but this gives you an idea of
            // what to escape out
            return value
                /*  \ --> \\ */
                .Replace("\\", "\\\\")
                /*  ' --> \' */
                .Replace("'", "\\'")
                /*  " --> \" */
                .Replace("\"", "\\\"")
                /*  (newline) --> \n */
                .Replace("\n", "\\n")
                /*  (creturn) --> \r */
                .Replace("\r", "\\r")
                /* </script> string */
                .Replace("</script>", "</scri'+'pt>");
        }
    }

    When "pre-reistered" in your web.config, it works just as well as the NOSCRIPT tag.