A place to cache linked articles (think custom and personal wayback machine)
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

5 роки тому
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. title: Progressive enhancement with handlers and enhancers
  2. url: https://hiddedevries.nl/en/blog/2015-04-03-progressive-enhancement-with-handlers-and-enhancers
  3. hash_url: ba6ef65c13ffa3b92e95fb486cfc6fe0
  4. <p class="intro">Recently I adopted a different way to manage bits of JavaScript in websites I was building. It exercises progressive enhancement (PE) by declaring handler and enhancer functions on <span class="caps">HTML</span> elements.</p>
  5. <p><strong>TL;DR</strong>: When JavaScript is used to handle user interactions like clicks, or enhance the page by manipulating the <span class="caps">DOM</span>, traditionally you&#8217;d have JavaScript find that <span class="caps">HTML</span> element in your page, and hook some code into it. But what if you&#8217;d switch that around, and have the <span class="caps">HTML</span> element “tell” JavaScript what function to execute?</p>
  6. <p>How? I declare JavaScript functions on <span class="caps">HTML</span> elements. Separating them to functions that happen on click and those that happen on page load, I use two attributes (<code>data-handler</code> and <code>data-enhancer</code>) that both get space-separated function names as their values. Then, with a little bit of JavaScript, I make sure the functions execute when they need to.</p>
  7. <p>Note that this works best for websites for which the mark-up is rendered on the server, to which JavaScript is added as an extra layer. Websites that rely on JavaScript for rendering content will probably have options to do the same within the framework they are built with.</p>
  8. <h2>Separating to handlers and enhancers</h2>
  9. <p>On many small to medium sized websites, the required JavaScript can be drilled down to two types of usage: </p>
  10. <ol>
  11. <li>things that need to happen on a click</li>
  12. <li>enhancements after the inital page load</li>
  13. </ol>
  14. <p>Surely, there are often other events we want to use for triggering JavaScript (scroll, anyone?), but let&#8217;s focus on these two types first.</p>
  15. <p>Many simple websites will have both types of script in a single <code>scripts.js</code> file, which is also used to query the nodes interactions need to happen on, and to add click handlers to those nodes.</p>
  16. <p>This year, <a href="http://krijnhoetmer.nl">Krijn</a> and <a href="http://brinkhu.is">Matijs</a> introduced me to a new way to go about this. It has proven to be a very powerful pattern in some of my recent projects, which is why I’d like to share it here. Credits for the pattern, and for the initialisation functions I will discuss later, go to them.</p>
  17. <h2>Including JavaScript functions to the declarative model</h2>
  18. <p>The pattern starts from the idea that web pages have three layers to them: structure (<span class="caps">HTML</span>), lay-out (<span class="caps">CSS</span>) and enhancement (JavaScript). <span class="caps">HTML</span> is a declarative language: as an author, you declare what you’d like something to be, and that is what it will be. </p>
  19. <blockquote class="in-content">
  20. <p class="in-content">Let this be a header, let this be a list item, let this be a block quote.</p>
  21. </blockquote>
  22. <p>This is great, browsers can now know that ‘this’ is a list item, and treat it as such. Screenreaders may announce it as a list, your <span class="caps">RSS</span> reader or mobile Safari “reader view” can view it as a list, et cetera.</p>
  23. <p>With <span class="caps">CSS</span> we declare what things look like:</p>
  24. <blockquote class="in-content">
  25. <p class="in-content">Let headers be dark blue, let list items have blue bullets and let block quotes render in slightly smaller type.</p>
  26. </blockquote>
  27. <p>This works rather well for us, because now we don’t need to think about what will happen if we add another list item. It being a list item, it will have the <span class="caps">CSS</span> applied to it, so it will have blue bullets. </p>
  28. <p>The idea I’d like to share here, is that of making JavaScript functions part of this declarative model. If we can declare what something looks like in <span class="caps">HTML</span>, why not declare what its behaviour is, too?</p>
  29. <h2 id="handlers">Handlers: things that happen on a click</h2>
  30. <p>The idea is simple: we introduce a <code>data-handler</code> attribute to all elements that need to trigger function execution. As their value, we add one or more functions name(s) that need(s) to execute on click. </p>
  31. <p>For example, here’s a link:</p>
  32. <pre class="language-markup"><code>&lt;a href=&quot;#punk-ipa&quot;&gt;More about Punk IPA&lt;/a&gt;</code></pre>
  33. <p>This is an in-page link to a section that explains more about Punk <span class="caps">IPA</span>. It will work regardless of JavaScript, but can be improved with it.</p>
  34. <p>Cooler than linking to a section about Punk <span class="caps">IPA</span>, is to have some sort of overlay open, with or without a fancy animation. We add a <code>data-handler</code> attribute:</p>
  35. <pre class="language-markup"><code>&lt;a href=&quot;#punk-ipa&quot; data-handler=&quot;overlay&quot;&gt;More about Punk IPA&lt;/a&gt; </code></pre>
  36. <p>In the data-handler, the value holds a function name, in this case ‘overlay’. The idea is that the overlay function executes when the link is clicked. Naturally, you would be able to add more than one function name, and separate function names by spaces. This works just like <code>class=&quot;foo bar&quot;</code>.</p>
  37. <p>Within the function declaration, we will know which element was clicked, so we can access attributes. We can access the <code>href</code> or any data attribute. With that, it can grab the content that’s being linked to, append it into some sort of overlay, and smoothly transition the overlay into the page.</p>
  38. <p>Note that this is similar to doing <code>&lt;a onclick=&quot;overlayfunction(); anotherfunction();&quot;&gt;</code>, but with the added benefit that a <code>data-handler</code> only gets meaning once JavaScript is active and running, and that it contains strings like <span class="caps">CSS</span> classes, instead of actual JavaScript code. This way, the scripting is separated in the same way as the style rules are.</p>
  39. <p>Also note that is best practice to only add handlers to <span class="caps">HTML</span> elements that are made for click behaviour, like <code>&lt;button&gt;</code>s and <code>&lt;a&gt;</code>.</p>
  40. <h3>Adding function definitions</h3>
  41. <p>In our JavaScript (e.g. an included <code>scripts.js</code> file), we add all functions for click behaviour to one object: </p>
  42. <pre class="language-javascript"><code>var handlers = {
  43. &#039;function-name&#039; : function(e) {
  44. // This function is executed on click of any element with
  45. // &#039;function-name&#039; in its data-handler attribute.
  46. // The click event is in &#039;e&#039;, $(this).attr(&#039;data-foo&#039;) holds the
  47. // value of data-foo of the element the user clicked on
  48. },
  49. &#039;another-function-name&#039; : function(e) {}
  50. };</code></pre>
  51. <p>Those working in teams could consider making the handler object global. That way functions can have their own files, making collaboration through version control easier.</p>
  52. <h3>Adding click handling: one handler to rule them all</h3>
  53. <p>If we set all our click-requiring functionality up within <code>data-handler</code> attributes, we only need to attach one click handler to our document. <a href="http://davidwalsh.name/event-delegate">Event delegation</a> can then be used to do stuff to the actual element that is clicked on, even when that actual element did not exist on page load (i.e. was loaded in with <span class="caps">AJAX</span>).</p>
  54. <p>This function (jQuery) can be used to handle clicks, then search through the handler functions and apply the ones specified in the data-handler function:</p>
  55. <pre class="language-javascript"><code>$(function() {
  56. &#039;use strict&#039;;
  57. // generic click handler
  58. $(document).on(&#039;click&#039;, &#039;[data-handler]&#039;, function(event) {
  59. var handler = this.getAttribute(&#039;data-handler&#039;);
  60. // honour default behaviour when using modifier keys when clicking
  61. // for example:
  62. // cmd + click / ctrl + click opens a link in a new tab
  63. // shift + click opens a link in a new window
  64. if (this.tagName === &#039;A&#039; &amp;&amp; (event.metaKey || event.ctrlKey || event.shiftKey)) {
  65. return;
  66. }
  67. if (handlers &amp;&amp; typeof handlers[handler] === &#039;function&#039;) {
  68. handlers[handler].call(this, event);
  69. }
  70. else {
  71. if (window.console &amp;&amp; typeof console.log === &#039;function&#039;) {
  72. console.log(&#039;Non-existing handler: &quot;%s&quot; on %o&#039;, handler, this);
  73. }
  74. }
  75. });
  76. });</code></pre>
  77. <p>(<a href="https://gist.github.com/matijs/5b2d6675265ec440bcba">Source</a>; see also <a href="https://gist.github.com/matijs/9bb314a9a184b53952e2">its vanilla JavaScript version</a>)</p>
  78. <h2 id="enhancers">Enhancers: things that happen after page load</h2>
  79. <p>We can run functions to enhance elements in a similar way, by adding their function names to a <code>data-enhancer</code> attribute. The corresponding functions go into a <code>enhancers</code> object, just like the <code>handlers</code> object above.</p>
  80. <p>For example, a page element that needs to display tweets. Again, here&#8217;s a link:</p>
  81. <pre class="language-markup"><code>&lt;a href=&quot;https://twitter.com/bbc&quot;&gt;Tweets of the BBC&lt;/a&gt;</code></pre>
  82. <p>To enhance this link to the <span class="caps">BBC</span>&#8217;s tweets, we may want to load a widget that displays actual tweets. The function to do that may add some container <code>&lt;div&gt;</code>s, and run some calls to the Twitter <span class="caps">API</span> to grab tweets. To trigger this function:</p>
  83. <pre class="language-markup"><code>&lt;a href=&quot;https://twitter.com/bbc&quot; data-enhancer=&quot;twitter-widget&quot;&gt;
  84. Tweets of the BBC&lt;/a&gt;</code></pre>
  85. <p>To find out whose Twitter widget to display, our function could analyse the <span class="caps">URL</span> in the <code>href</code> attribute, or we can add an extra attribute:</p>
  86. <pre class="language-markup"><code>&lt;a href=&quot;https://twitter.com/bbc&quot; data-enhancer=&quot;twitter-widget&quot;
  87. data-twitter-user=&quot;bbc&quot;&gt;Tweets of the BBC&lt;/a&gt;</code></pre>
  88. <p>Another example: of a large amount of text, we want to hide all but the first paragraph, then add a “Show all” button to show the remainder. The <span class="caps">HTML</span> will contain all of the content, and we will hide the remainder with JavaScript.</p>
  89. <pre class="language-markup"><code>&lt;section&gt;
  90. &lt;p&gt;Some text&lt;/p&gt;
  91. &lt;p&gt;Some more text&lt;/p&gt;
  92. &lt;p&gt;Some more text&lt;/p&gt;
  93. &lt;/section&gt;</code></pre>
  94. <p>To the block of text we add a data-enhancer function that makes sure everything but the first paragraph is hidden, and a “Show all” button is added.</p>
  95. <pre class="language-markup"><code>&lt;section data-enhancer=&quot;only-show-first-paragraph&quot;&gt;
  96. &lt;p&gt;Some text&lt;/p&gt;
  97. &lt;p&gt;Some more text&lt;/p&gt;
  98. &lt;p&gt;Some more text&lt;/p&gt;
  99. &lt;/section&gt;</code></pre>
  100. <p>A function named ‘only-show-first-paragraph’ could then take care of removing the content, and adding a button that reveals it (this button could have a <code>data-handler</code> for that behaviour).</p>
  101. <h3>Running all enhancements</h3>
  102. <p>Assuming all our <code>enhancer</code> functions are in one <code>enhancer</code> object, we can run all <code>enhancers</code> on a page with one function. The function looks for all elements with a <code>data-enhancer</code> attribute, and calls the appropriate functions.</p>
  103. <pre class="language-javascript"><code>$(function() {
  104. &#039;use strict&#039;;
  105. // kick off js enhancements
  106. $(&#039;[data-enhancer]&#039;).each(function() {
  107. var enhancer = this.getAttribute(&#039;data-enhancer&#039;);
  108. if (enhancers &amp;&amp; typeof enhancers[enhancer] === &#039;function&#039;) {
  109. enhancers[enhancer].call(this);
  110. }
  111. else {
  112. if (window.console &amp;&amp; typeof console.log === &#039;function&#039;) {
  113. console.log(&#039;Non-existing enhancer: &quot;%s&quot; on %o&#039;, enhancer, this);
  114. }
  115. }
  116. });
  117. });</code></pre>
  118. <h2>Wrap-up</h2>
  119. <p>So the basic idea is: functions for click behaviour are handlers, and those that happen on page load are enhancers. They are stored in a <code>handlers</code> and <code>enhancers</code> object respectively, and triggered from <span class="caps">HTML</span> elements that have <code>data-handler</code> and <code>data-enhancer</code> attributes on them, to declare which functions they need.</p>
  120. <p>In summary: </p>
  121. <ol>
  122. <li>All functions that need to execute on click (or touch or pointer events), are declared on the <span class="caps">HTML</span> element that should trigger the click, in a <code>data-handler</code> attribute</li>
  123. <li>All functions that need to change/enhance stuff on the <span class="caps">DOM</span>, are declared on the <span class="caps">HTML</span> element they need to change, in a <code>data-enhancer</code> attribute</li>
  124. <li>Two JavaScript functions run through <code>data-handler</code> and <code>data-enhancer</code> attributes respectively, and execute all functions when they are required</li>
  125. </ol>
  126. <h2>Thoughts?</h2>
  127. <p>This pattern is not new, similar things have been done by others. Rik Schennink wrote about <a href="http://rikschennink.nl/thoughts/controlling-behaviour/">controlling behaviour</a> before, and his <a href="http://conditionerjs.com">conditioner.js</a> deserves special mention. It is a library that not only manages modules of JavaScript, it also activates them based on responsive breakpoints, something the pattern described here does not do out of the box (it could).</p>
  128. <p>For me and teams I worked in, the above has proven to be a useful way to declare actions and enhancements within pages. It adds maintainability, because it helps keeping functions organised. It also promotes reusability: although new functions can be added for each use, multiple elements can make use of the same function. </p>
  129. <p>The method is not perfect for every project, and it can definitely be improved upon. For example, we could add the function that triggers all <code>data-handler</code> functions to an enhancer (<code>&lt;html data-enhancer=&quot;add-handlers&quot;&gt;</code>), as it is an enhancement (of the page) by itself. In big projects with many people working on the same codebase, it may be useful to have all functions in separate files and have a globally available handlers object.</p>
  130. <p><em>Update 07/04/2015</em>: I have opened comments, would love to hear any feedback</p>