Phát triển Javascript - part 22 pdf

10 192 0
Phát triển Javascript - part 22 pdf

Đang tải... (xem toàn văn)

Thông tin tài liệu

ptg 9.3 Do Not Make Assumptions 183 9.3 Do Not Make Assumptions “Do not make assumptions” is perhaps the most important rule of unobtrusive JavaScript. It sums up most aspects of clean JavaScript in a single phrasing. In this section we will go over the most common assumptions and why they make it challenging to write robust scripts for the web. 9.3.1 Don’t Assume You Are Alone Never assume that scripts run in isolation. This applies to application developers as much as library authors, because most websites today use code from at least one external source. Assuming scripts runin isolation makes running them alongside scripts we don’t control harder. For the last few years, all the sites I’ve worked on use at least one external analytics script and most of these scripts use document.write as a last resort. document.write has a nasty side-effect of wiping the entire document if used after the DOM has fully loaded. This means that asynchronously loading content invoking the offending code will cause the site’s analytics script to effectively break it completely. I’ve seen maintenance developers break down in tears as they realize what is causing their site to fail, and it ain’t a pretty sight. 9.3.1.1 How to Avoid The less we contribute to the global environment, the less we will depend on it. Keeping our global footprint small reduces chances of conflicts with other scripts. Techniques to minimize global impact were described in Chapter 6, Applied Func- tions and Closures. Besides keeping the number of global objects low, we need to watch out for other forms of global state, such as assigning to window.onload or using the aforementioned document.write. 9.3.2 Don’t Assume Markup Is Correct When separating concerns, we should strive to keep as much markup in the doc- ument as possible. In practice this equates to using the “fallback” solution as a basis for the scripted solution as much as possible. However, this also means that scripts are no longer in complete control of markup, so we need to be careful. The original markup may be compromised in many ways; it may be compromised by other scripts, by document authors, or by invalid markup that results in a different document structure when parsed. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 184 Unobtrusive JavaScript 9.3.2.1 How to Avoid Check the required markup using script before applying a given feature. It is par- ticularly important to verify that the complete structure required is available when initializing widgets, so we don’t accidentally start initializing a widget only to abort halfway because of unexpected changes in the document structure, effectively leav- ing the user with a broken page. 9.3.3 Don’t Assume All Users Are Created Equal Reaching a wide audience means meeting a lot of different needs. The web content accessibility guidelines (WCAG) instruct us not to tie functionality to a single input mechanism, such as the mouse. Triggering functionality using the mouseover event effectively removes the feature for users unable to handle a mouse, or handle it well enough. Besides, mouseover doesn’t make any sense on touch devices, which are becoming increasingly popular. 9.3.3.1 How to Avoid WCAG advices to use redundant input methods, i.e., provide keyboard alternatives for mouse-specific events. This is a good piece of advice, but there is more to keyboard accessibility than adding a focus event handler with every mouseover event handler (not even possible on some elements). Ultimately, the only way to create truly keyboard accessible websites is to test, test, and test. Ideally, those tests are carried out by actual users, both the mouse, keyboard, and possibly even the touch inclined. 9.3.4 Don’t Assume Support Never use features that may not be available; test for the existence of features before using them. This is also known as feature detection or feature testing, and we will deal with it in more detail in Chapter 10, Feature Detection. 9.4 When Do the Rules Apply? Although most of the principles presented in this chapter are general characteristics of solid craftsmanship, some rules can be challenging in given cases. For instance, a JavaScript intense application such as Gmail could prove difficult to develop using progressive enhancement. Gmail has solved this problem by providing a scriptless environment completely detached from its main interface. This solution certainly From the Library of WoweBook.Com Download from www.eBookTM.com ptg 9.5 Unobtrusive Tabbed Panel Example 185 honors accessibility by allowing clients unable to use the main application access to a less demanding one that can more easily support their needs. Additionally, a more lightweight, but still heavily scripted application, is available for mobile devices with smaller screens and less capable browsers. However, providing alternate versions is no excuse for writing sloppy code, ignoring the fact that people use different input methods or tightly coupling scripts with the document structure. Many developers feel that unobtrusive JavaScript is too idealistic, and that it does not apply in “the real world,” in which projects have budgets and deadlines. In some cases they are right, but mostly it’s more about planning and attacking a problem from the right angle. Quality always takes a little more effort than spewing out anything that seems to work in whatever browser the developer keeps handy for testing. Like TDD, coding JavaScript unobtrusively will probably slow you down slightly as you start out, but you will reap the benefits over time because it makes maintenance a lot easier, causes fewer errors, and produces more accessible solutions. This translates to less time spent fixing bugs, less time spent handling complaints from users, and possibly also less serious trouble as accessibility laws get more comprehensive. In 2006, target.com, an American online retailer, was sued for lack of accessi- bility after refusing to deal with accessibility complaints since early 2005. Two years later the company agreed to a $6 million settlement. I’m guessing that slightly raised development costs outrank civil action any day. Note that a website is not necessarily a web application in terms of the user interface. Buying music, managing contacts, paying bills, and reading news rarely need functionality that cannot be offered in a simplified way without scripts. On the other hand, applications such as spreadsheets, real-time chat rooms, or collaborative office tools can be hard to reproduce in a meaningful way without them. 9.5 Unobtrusive Tabbed Panel Example We have learned a few things about unobtrusive JavaScript, and we’ve seen the manifestation of unmaintainable obtrusive JavaScript. In this section we will walk quickly through developing an unobtrusive tabbed panel backed by tests. To keep this example somewhat brief, we won’t go into details on every step of the test-driven development process taken to develop this solution. Part III, Real- World Test-Driven Development in JavaScript, goes into the nitty-gritty of the process and provides several complete and narrated TDD projects. In this section we will focus on the concepts used to create an unobtrusive tabbed panel. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 186 Unobtrusive JavaScript 9.5.1 Setting Up the Test To support the tabbed panel we will build a tabController interface, one test case at a time. Each test case will target a single method in this interface, which controls the state of the tabs and offers a callback that fires anytime the active tab changes. In order for tests to share the setup code, which creates the minimum markup and keeps a reference to it available for the tests, we wrap the test cases in an anonymous closure that is immediately executed. Inside it we can add a shortcut to the namespaced object and a local setUp function. The setup code can be viewed in Listing 9.3. Listing 9.3 Test setup using a shared setUp (function () { var tabController = tddjs.ui.tabController; // All test cases can share this setUp function setUp() { /*:DOC += <ol id="tabs"> <li><a href="#news">News</a></li> <li><a href="#sports">Sports</a></li> <li><a href="#economy">Economy</a></li> </ol>*/ this.tabs = document.getElementById("tabs"); } // Test cases go here }()); In addition to this setup, we will use the two helpers in Listing 9.4, which simply adds and removes class names from an element’s class attribute. Listing 9.4 Adding and removing class names (function () { var dom = tddjs.namespace("dom"); function addClassName(element, cName) { var regexp = new RegExp("(^|\\s)" + cName + "(\\s|$)"); if (element && !regexp.test(element.className)) { cName = element.className + " " + cName; From the Library of WoweBook.Com Download from www.eBookTM.com ptg 9.5 Unobtrusive Tabbed Panel Example 187 element.className = cName.replace(/^\s+|\s+$/g, ""); } } function removeClassName(element, cName) { var r = new RegExp("(^|\\s)" + cName + "(\\s|$)"); if (element) { cName = element.className.replace(r, " "); element.className = cName.replace(/^\s+|\s+$/g, ""); } } dom.addClassName = addClassName; dom.removeClassName = removeClassName; }()); These two methods require the tddjs object and its namespace method from Chapter 6, Applied Functions and Closures. To code along with this example, set up a simple JsTestDriver project as described in Chapter 3, Tools of the Trade, and save the tddjs object and its namespace method along with the above helpers in lib/tdd.js. Also save the Object.create implementation from Chapter 7, Objects and Prototypal Inheritance, in lib/object.js. 9.5.2 The tabController Object Listing 9.5 shows the first test case, which covers the tabController object’s create method. It accepts a container element for the tab controller. It tests its markup requirements and throws an exception if the container is not an element (determined by checking for the properties it’sgoing to use). If the element is deemed sufficient, the tabController object is created and a class name is appended to the element, allowing CSS to style the tabs as, well, tabs. Note how each of the tests test a single behavior. This makes for quick feedback loops and reduces the scope we need to concentrate on at any given time. The create method is going to add an event handler to the element as well, but we will cheat a little in this example. Event handlers will be discussed in Chapter 10, Feature Detection, and testing them will be covered through the example project in Chapter 15, TDD and DOM Manipulation: The Chat Client. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 188 Unobtrusive JavaScript Listing 9.5 Test case covering the create method TestCase("TabControllerCreateTest", { setUp: setUp, "test should fail without element": function () { assertException(function () { tabController.create(); }, "TypeError"); }, "test should fail without element class": function () { assertException(function () { tabController.create({}); }, "TypeError"); }, "should return object": function () { var controller = tabController.create(this.tabs); assertObject(controller); }, "test should add js-tabs class name to element": function () { var tabs = tabController.create(this.tabs); assertClassName("js-tab-controller", this.tabs); }, // Test for event handlers, explained later }); The implementation shown in Listing 9.6 is fairly straightforward. Staying out of the global namespace, the tabController object is implemented inside the existing tddjs namespace. The method makes one possibly unsafe assumption: The DOM 0 event listener (the onclick property). The assumption the script implicitly is making is that no other script will hijack the ol element’s onclick listener. This might seem like a reasonable expectation, but using DOM 2 event listeners is a much safer choice. As mentioned previously, we will defer their use to Chapter 15, TDD and DOM Manipulation: The Chat Client, in which we’ll also discuss how to test them. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 9.5 Unobtrusive Tabbed Panel Example 189 Note that we’re using event delegation here, by registering a single event han- dler for the whole list element and then passing along the event object to the event handler. Listing 9.6 Implementation of create (function () { var dom = tddjs.dom; function create(element) { if (!element || typeof element.className != "string") { throw new TypeError("element is not an element"); } dom.addClassName(element, "js-tab-controller"); var tabs = Object.create(this); element.onclick = function (event) { tabs.handleTabClick(event || window.event || {}); }; element = null; return tabs; } function handleTabClick(event) {} tddjs.namespace("ui").tabController = { create: create, handleTabClick: handleTabClick }; }()); The event is handled by the tab controller’s handleTabClick method. Be- cause we will discuss working around the cross-browser quirks of event handling in Chapter 10, Feature Detection, we will skip its test case for now. The tabCon- troller test case should concern itself with the behavior of tabs, not differing implementations of event handling. Such tests belong in a test case dedicated to an event interface whose purpose is to smooth over browser differences. In many cases this role is filled by a third party JavaScript library, but there is nothing stopping us from keeping our own set of tools for those cases in which we don’t need everything that comes with a library. Listing 9.7 shows the resulting method. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 190 Unobtrusive JavaScript Listing 9.7 Implementation of handleTabClick function handleTabClick(event) { var target = event.target || event.srcElement; while (target && target.nodeType != 1) { target = target.parentNode; } this.activateTab(target); } The handler grabs the element that triggered the event. This means the target property of the event object in most browsers, and srcElement in In- ternet Explorer. To accommodate browsers that occasionally fire events directly on text nodes, it makes sure it got an element node. Finally, it passes the originating element to the activateTab method. 9.5.3 The activateTab Method The activateTab method accepts an element as its only argument, and given that its tag name is of the expected type, it activates it by adding a class name. The method also deactivates the previously activated tab. The reason we check the tag name is the event delegation. Any element inside the containing element will cause a click event to fire, and the tabTagName prop- erty allows us to configure which elements are considered “tabs.” Given a selector engine, we could allow more fine-grained control of this feature by allowing arbi- trary CSS selectors decide if an element is a tab. Another possibility is to expose an isTab(element) method that could be overridden on specific instances to provide custom behavior. If and when the method changes the tabs state, it fires the onTabChange event, passing it the current and previous tabs. Listing 9.8 shows the entire test case. Listing 9.8 Test case covering the activateTab method TestCase("TabbedControllerActivateTabTest", { setUp: function () { setUp.call(this); this.controller = tabController.create(this.tabs); this.links = this.tabs.getElementsByTagName("a"); this.lis = this.tabs.getElementsByTagName("li"); }, From the Library of WoweBook.Com Download from www.eBookTM.com ptg 9.5 Unobtrusive Tabbed Panel Example 191 "test should not fail without anchor": function () { var controller = this.controller; assertNoException(function () { controller.activateTab(); }); }, "test should mark anchor as active": function () { this.controller.activateTab(this.links[0]); assertClassName("active-tab", this.links[0]); }, "test should deactivate previous tab": function () { this.controller.activateTab(this.links[0]); this.controller.activateTab(this.links[1]); assertNoMatch(/(^|\s)active-tab(\s|$)/, this.links[0]); assertClassName("active-tab", this.links[1]); }, "test should not activate unsupported element types": function () { this.controller.activateTab(this.links[0]); this.controller.activateTab(this.lis[0]); assertNoMatch(/(^|\s)active-tab(\s|$)/, this.lis[0]); assertClassName("active-tab", this.links[0]); }, "test should fire onTabChange": function () { var actualPrevious, actualCurrent; this.controller.activateTab(this.links[0]); this.controller.onTabChange = function (curr, prev) { actualPrevious = prev; actualCurrent = curr; }; this.controller.activateTab(this.links[1]); assertSame(actualPrevious, this.links[0]); assertSame(actualCurrent, this.links[1]); } }); From the Library of WoweBook.Com Download from www.eBookTM.com ptg 192 Unobtrusive JavaScript Implementation, as seen in Listing 9.9, is fairly straightforward. As the tests indicate, the method starts by checking that it actually received an element, and that its tag name matches the tabTagName property. It then proceeds to add and remove class names as described above, and finally calls the onTabChange method. Finally, we add a no-op onTabChange, ready for users to override. Listing 9.9 The activateTab method function activateTab(element) { if (!element || !element.tagName || element.tagName.toLowerCase() != this.tabTagName) { return; } var className = "active-tab"; dom.removeClassName(this.prevTab, className); dom.addClassName(element, className); var previous = this.prevTab; this.prevTab = element; this.onTabChange(element, previous); } tddjs.namespace("ui").tabController = { /* */ activateTab: activateTab, onTabChange: function (anchor, previous) {}, tabTagName: "a" }; 9.5.4 Using the Tab Controller Using the tabController object we can recreate the tabbed panel in an unob- trusive way. The improved panel will be based on the markup shown in Listing 9.2. The script in Listing 9.10 grabs the ol element containing links to each section and creates a tab controller with it. Doing so will cause the tabs to have the active- tab class name toggled as we click them. We then hook into the tab controller’s onTabChange callback and use the semantic relationship between the anchors and the sections of information to toggle active state for panels, disabling the pre- vious panel and enabling the current selected one. Finally, the first tab anchor is fetched and activated. From the Library of WoweBook.Com Download from www.eBookTM.com . on every step of the test-driven development process taken to develop this solution. Part III, Real- World Test-Driven Development in JavaScript, goes into the nitty-gritty of the process and. tabController.create(this.tabs); assertObject(controller); }, "test should add js-tabs class name to element": function () { var tabs = tabController.create(this.tabs); assertClassName("js-tab-controller", this.tabs); }, //. handleTabClick method. Be- cause we will discuss working around the cross-browser quirks of event handling in Chapter 10, Feature Detection, we will skip its test case for now. The tabCon- troller test

Ngày đăng: 04/07/2014, 22:20

Tài liệu cùng người dùng

  • Đang cập nhật ...