A place to cache linked articles (think custom and personal wayback machine)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.html 25KB

5 lat temu
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690
  1. <!doctype html><!-- This is a valid HTML5 document. -->
  2. <!-- Screen readers, SEO, extensions and so on. -->
  3. <html lang=fr>
  4. <!-- Has to be within the first 1024 bytes, hence before the <title>
  5. See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
  6. <meta charset=utf-8>
  7. <!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
  8. <!-- The viewport meta is quite crowded and we are responsible for that.
  9. See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
  10. <meta name=viewport content="width=device-width,minimum-scale=1,initial-scale=1,shrink-to-fit=no">
  11. <!-- Required to make a valid HTML5 document. -->
  12. <title>5 astuces sur les directives AngularJS et leurs tests (archive) — David Larlet</title>
  13. <!-- Generated from https://realfavicongenerator.net/ such a mess. -->
  14. <link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons/apple-touch-icon.png">
  15. <link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons/favicon-32x32.png">
  16. <link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons/favicon-16x16.png">
  17. <link rel="manifest" href="/manifest.json">
  18. <link rel="mask-icon" href="/static/david/icons/safari-pinned-tab.svg" color="#5bbad5">
  19. <link rel="shortcut icon" href="/static/david/icons/favicon.ico">
  20. <meta name="apple-mobile-web-app-title" content="David Larlet">
  21. <meta name="application-name" content="David Larlet">
  22. <meta name="msapplication-TileColor" content="#da532c">
  23. <meta name="msapplication-config" content="/static/david/icons/browserconfig.xml">
  24. <meta name="theme-color" content="#f0f0ea">
  25. <!-- That good ol' feed, subscribe :p. -->
  26. <link rel=alternate type="application/atom+xml" title=Feed href="/david/log/">
  27. <meta name="robots" content="noindex, nofollow">
  28. <meta content="origin-when-cross-origin" name="referrer">
  29. <!-- Canonical URL for SEO purposes -->
  30. <link rel="canonical" href="http://blog.ninja-squad.com/2015/01/27/5-astuces-sur-les-directives-et-leurs-tests/">
  31. <style>
  32. /* http://meyerweb.com/eric/tools/css/reset/ */
  33. html, body, div, span,
  34. h1, h2, h3, h4, h5, h6, p, blockquote, pre,
  35. a, abbr, address, big, cite, code,
  36. del, dfn, em, img, ins,
  37. small, strike, strong, tt, var,
  38. dl, dt, dd, ol, ul, li,
  39. fieldset, form, label, legend,
  40. table, caption, tbody, tfoot, thead, tr, th, td,
  41. article, aside, canvas, details, embed,
  42. figure, figcaption, footer, header, hgroup,
  43. menu, nav, output, ruby, section, summary,
  44. time, mark, audio, video {
  45. margin: 0;
  46. padding: 0;
  47. border: 0;
  48. font-size: 100%;
  49. font: inherit;
  50. vertical-align: baseline;
  51. }
  52. /* HTML5 display-role reset for older browsers */
  53. article, aside, details, figcaption, figure,
  54. footer, header, hgroup, menu, nav, section { display: block; }
  55. body { line-height: 1; }
  56. blockquote, q { quotes: none; }
  57. blockquote:before, blockquote:after,
  58. q:before, q:after {
  59. content: '';
  60. content: none;
  61. }
  62. table {
  63. border-collapse: collapse;
  64. border-spacing: 0;
  65. }
  66. /* http://practicaltypography.com/equity.html */
  67. /* https://calendar.perfplanet.com/2016/no-font-face-bulletproof-syntax/ */
  68. /* https://www.filamentgroup.com/lab/js-web-fonts.html */
  69. @font-face {
  70. font-family: 'EquityTextB';
  71. src: url('/static/david/css/fonts/Equity-Text-B-Regular-webfont.woff2') format('woff2'),
  72. url('/static/david/css/fonts/Equity-Text-B-Regular-webfont.woff') format('woff');
  73. font-weight: 300;
  74. font-style: normal;
  75. font-display: swap;
  76. }
  77. @font-face {
  78. font-family: 'EquityTextB';
  79. src: url('/static/david/css/fonts/Equity-Text-B-Italic-webfont.woff2') format('woff2'),
  80. url('/static/david/css/fonts/Equity-Text-B-Italic-webfont.woff') format('woff');
  81. font-weight: 300;
  82. font-style: italic;
  83. font-display: swap;
  84. }
  85. @font-face {
  86. font-family: 'EquityTextB';
  87. src: url('/static/david/css/fonts/Equity-Text-B-Bold-webfont.woff2') format('woff2'),
  88. url('/static/david/css/fonts/Equity-Text-B-Bold-webfont.woff') format('woff');
  89. font-weight: 700;
  90. font-style: normal;
  91. font-display: swap;
  92. }
  93. @font-face {
  94. font-family: 'ConcourseT3';
  95. src: url('/static/david/css/fonts/concourse_t3_regular-webfont-20190806.woff2') format('woff2'),
  96. url('/static/david/css/fonts/concourse_t3_regular-webfont-20190806.woff') format('woff');
  97. font-weight: 300;
  98. font-style: normal;
  99. font-display: swap;
  100. }
  101. /* http://practice.typekit.com/lesson/caring-about-opentype-features/ */
  102. body {
  103. /* http://www.cssfontstack.com/ Palatino 99% Win 86% Mac */
  104. font-family: "EquityTextB", Palatino, serif;
  105. background-color: #f0f0ea;
  106. color: #07486c;
  107. font-kerning: normal;
  108. -moz-osx-font-smoothing: grayscale;
  109. -webkit-font-smoothing: subpixel-antialiased;
  110. text-rendering: optimizeLegibility;
  111. font-variant-ligatures: common-ligatures contextual;
  112. font-feature-settings: "kern", "liga", "clig", "calt";
  113. }
  114. pre, code, kbd, samp, var, tt {
  115. font-family: 'TriplicateT4c', monospace;
  116. }
  117. em {
  118. font-style: italic;
  119. color: #323a45;
  120. }
  121. strong {
  122. font-weight: bold;
  123. color: black;
  124. }
  125. nav {
  126. background-color: #323a45;
  127. color: #f0f0ea;
  128. display: flex;
  129. justify-content: space-around;
  130. padding: 1rem .5rem;
  131. }
  132. nav:last-child {
  133. border-bottom: 1vh solid #2d7474;
  134. }
  135. nav a {
  136. color: #f0f0ea;
  137. }
  138. nav abbr {
  139. border-bottom: 1px dotted white;
  140. }
  141. h1 {
  142. border-top: 1vh solid #2d7474;
  143. border-bottom: .2vh dotted #2d7474;
  144. background-color: #e3e1e1;
  145. color: #323a45;
  146. text-align: center;
  147. padding: 5rem 0 4rem 0;
  148. width: 100%;
  149. font-family: 'ConcourseT3';
  150. display: flex;
  151. flex-direction: column;
  152. }
  153. h1.single {
  154. padding-bottom: 10rem;
  155. }
  156. h1 span {
  157. position: absolute;
  158. top: 1vh;
  159. left: 20%;
  160. line-height: 0;
  161. }
  162. h1 span a {
  163. line-height: 1.7;
  164. padding: 1rem 1.2rem .6rem 1.2rem;
  165. border-radius: 0 0 6% 6%;
  166. background: #2d7474;
  167. font-size: 1.3rem;
  168. color: white;
  169. text-decoration: none;
  170. }
  171. h2 {
  172. margin: 4rem 0 1rem;
  173. border-top: .2vh solid #2d7474;
  174. padding-top: 1vh;
  175. }
  176. h3 {
  177. text-align: center;
  178. margin: 3rem 0 .75em;
  179. }
  180. hr {
  181. height: .4rem;
  182. width: .4rem;
  183. border-radius: .4rem;
  184. background: #07486c;
  185. margin: 2.5rem auto;
  186. }
  187. time {
  188. display: bloc;
  189. margin-left: 0 !important;
  190. }
  191. ul, ol {
  192. margin: 2rem;
  193. }
  194. ul {
  195. list-style-type: square;
  196. }
  197. a {
  198. text-decoration-skip-ink: auto;
  199. text-decoration-thickness: 0.05em;
  200. text-underline-offset: 0.09em;
  201. }
  202. article {
  203. max-width: 50rem;
  204. display: flex;
  205. flex-direction: column;
  206. margin: 2rem auto;
  207. }
  208. article.single {
  209. border-top: .2vh dotted #2d7474;
  210. margin: -6rem auto 1rem auto;
  211. background: #f0f0ea;
  212. padding: 2rem;
  213. }
  214. article p:last-child {
  215. margin-bottom: 1rem;
  216. }
  217. p {
  218. padding: 0 .5rem;
  219. margin-left: 3rem;
  220. }
  221. p + p,
  222. figure + p {
  223. margin-top: 2rem;
  224. }
  225. blockquote {
  226. background-color: #e3e1e1;
  227. border-left: .5vw solid #2d7474;
  228. display: flex;
  229. flex-direction: column;
  230. align-items: center;
  231. padding: 1rem;
  232. margin: 1.5rem;
  233. }
  234. blockquote cite {
  235. font-style: italic;
  236. }
  237. blockquote p {
  238. margin-left: 0;
  239. }
  240. figure {
  241. border-top: .2vh solid #2d7474;
  242. background-color: #e3e1e1;
  243. text-align: center;
  244. padding: 1.5rem 0;
  245. margin: 1rem 0 0;
  246. font-size: 1.5rem;
  247. width: 100%;
  248. }
  249. figure img {
  250. max-width: 250px;
  251. max-height: 250px;
  252. border: .5vw solid #323a45;
  253. padding: 1px;
  254. }
  255. figcaption {
  256. padding: 1rem;
  257. line-height: 1.4;
  258. }
  259. aside {
  260. display: flex;
  261. flex-direction: column;
  262. background-color: #e3e1e1;
  263. padding: 1rem 0;
  264. border-bottom: .2vh solid #07486c;
  265. }
  266. aside p {
  267. max-width: 50rem;
  268. margin: 0 auto;
  269. }
  270. /* https://fvsch.com/code/css-locks/ */
  271. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  272. font-size: 1rem;
  273. line-height: calc( 1.5em + 0.2 * 1rem );
  274. }
  275. h1 {
  276. font-size: 1.9rem;
  277. line-height: calc( 1.2em + 0.2 * 1rem );
  278. }
  279. h2 {
  280. font-size: 1.6rem;
  281. line-height: calc( 1.3em + 0.2 * 1rem );
  282. }
  283. h3 {
  284. font-size: 1.35rem;
  285. line-height: calc( 1.4em + 0.2 * 1rem );
  286. }
  287. @media (min-width: 20em) {
  288. /* The (100vw - 20rem) / (50 - 20) part
  289. resolves to 0-1rem, depending on the
  290. viewport width (between 20em and 50em). */
  291. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  292. font-size: calc( 1rem + .6 * (100vw - 20rem) / (50 - 20) );
  293. line-height: calc( 1.5em + 0.2 * (100vw - 50rem) / (20 - 50) );
  294. margin-left: 0;
  295. }
  296. h1 {
  297. font-size: calc( 1.9rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  298. line-height: calc( 1.2em + 0.2 * (100vw - 50rem) / (20 - 50) );
  299. }
  300. h2 {
  301. font-size: calc( 1.5rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  302. line-height: calc( 1.3em + 0.2 * (100vw - 50rem) / (20 - 50) );
  303. }
  304. h3 {
  305. font-size: calc( 1.35rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  306. line-height: calc( 1.4em + 0.2 * (100vw - 50rem) / (20 - 50) );
  307. }
  308. }
  309. @media (min-width: 50em) {
  310. /* The right part of the addition *must* be a
  311. rem value. In this example we *could* change
  312. the whole declaration to font-size:2.5rem,
  313. but if our baseline value was not expressed
  314. in rem we would have to use calc. */
  315. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  316. font-size: calc( 1rem + .6 * 1rem );
  317. line-height: 1.5em;
  318. }
  319. p, li, pre, details {
  320. margin-left: 3rem;
  321. }
  322. h1 {
  323. font-size: calc( 1.9rem + 1.5 * 1rem );
  324. line-height: 1.2em;
  325. }
  326. h2 {
  327. font-size: calc( 1.5rem + 1.5 * 1rem );
  328. line-height: 1.3em;
  329. }
  330. h3 {
  331. font-size: calc( 1.35rem + 1.5 * 1rem );
  332. line-height: 1.4em;
  333. }
  334. figure img {
  335. max-width: 500px;
  336. max-height: 500px;
  337. }
  338. }
  339. figure.unsquared {
  340. margin-bottom: 1.5rem;
  341. }
  342. figure.unsquared img {
  343. height: inherit;
  344. }
  345. @media print {
  346. body { font-size: 100%; }
  347. a:after { content: " (" attr(href) ")"; }
  348. a, a:link, a:visited, a:after {
  349. text-decoration: underline;
  350. text-shadow: none !important;
  351. background-image: none !important;
  352. background: white;
  353. color: black;
  354. }
  355. abbr[title] { border-bottom: 0; }
  356. abbr[title]:after { content: " (" attr(title) ")"; }
  357. img { page-break-inside: avoid; }
  358. @page { margin: 2cm .5cm; }
  359. h1, h2, h3 { page-break-after: avoid; }
  360. p3 { orphans: 3; widows: 3; }
  361. img {
  362. max-width: 250px !important;
  363. max-height: 250px !important;
  364. }
  365. nav, aside { display: none; }
  366. }
  367. ul.with_columns {
  368. column-count: 1;
  369. }
  370. @media (min-width: 20em) {
  371. ul.with_columns {
  372. column-count: 2;
  373. }
  374. }
  375. @media (min-width: 50em) {
  376. ul.with_columns {
  377. column-count: 3;
  378. }
  379. }
  380. ul.with_two_columns {
  381. column-count: 1;
  382. }
  383. @media (min-width: 20em) {
  384. ul.with_two_columns {
  385. column-count: 1;
  386. }
  387. }
  388. @media (min-width: 50em) {
  389. ul.with_two_columns {
  390. column-count: 2;
  391. }
  392. }
  393. .gallery {
  394. display: flex;
  395. flex-wrap: wrap;
  396. justify-content: space-around;
  397. }
  398. .gallery figure img {
  399. margin-left: 1rem;
  400. margin-right: 1rem;
  401. }
  402. .gallery figure figcaption {
  403. font-family: 'ConcourseT3'
  404. }
  405. footer {
  406. font-family: 'ConcourseT3';
  407. display: flex;
  408. flex-direction: column;
  409. border-top: 3px solid white;
  410. padding: 4rem 0;
  411. background-color: #07486c;
  412. color: white;
  413. }
  414. footer > * {
  415. max-width: 50rem;
  416. margin: 0 auto;
  417. }
  418. footer a {
  419. color: #f1c40f;
  420. }
  421. footer .avatar {
  422. width: 200px;
  423. height: 200px;
  424. border-radius: 50%;
  425. float: left;
  426. -webkit-shape-outside: circle();
  427. shape-outside: circle();
  428. margin-right: 2rem;
  429. padding: 2px 5px 5px 2px;
  430. background: white;
  431. border-left: 1px solid #f1c40f;
  432. border-top: 1px solid #f1c40f;
  433. border-right: 5px solid #f1c40f;
  434. border-bottom: 5px solid #f1c40f;
  435. }
  436. </style>
  437. <h1>
  438. <span><a id="jumper" href="#jumpto" title="Un peu perdu ?">?</a></span>
  439. 5 astuces sur les directives AngularJS et leurs tests (archive)
  440. <time>Pour la pérennité des contenus liés. Non-indexé, retrait sur simple email.</time>
  441. </h1>
  442. <section>
  443. <article>
  444. <h3><a href="http://blog.ninja-squad.com/2015/01/27/5-astuces-sur-les-directives-et-leurs-tests/">Source originale du contenu</a></h3>
  445. <p>S’il y a bien un sujet compliqué en Angular, c’est l’écriture de directives. J’espère que les chapitres de <a href="https://books.ninja-squad.com">notre livre</a> aident à passer un cap sur ce problème, mais il manque sur les internets un article un peu complet sur la façon de tester celles-ci.</p>
  446. <p>Angular est très bien pensé pour les tests, avec un système de mock, d’injection de dépendance, de simulation des requêtes HTTP, bref la totale. Mais les tests de directive restent souvent le parent pauvre de tout ça.</p>
  447. <p>Une directive un peu complète va contenir un template, un scope à elle avec différentes valeurs initialisées, et un ensemble de méthodes de comportement. Essayons de prendre une exemple pratique et pas trop compliqué :</p>
  448. <pre><code>angular.module('myProject.directives').directive('gravatar', function() {
  449. return {
  450. restrict: 'E',
  451. replace: true,
  452. scope: {
  453. user: '=',
  454. size: '@'
  455. },
  456. template: '&lt;img class="gravatar" ng-src="http://www.gravatar.com/avatar/{{ user.gravatar }}?s={{ sizePx }}&amp;d=identicon"/&gt;',
  457. link: function(scope) {
  458. if (scope.size === 'lg') {
  459. scope.sizePx = '40';
  460. } else {
  461. scope.sizePx = '20';
  462. }
  463. }
  464. };
  465. });
  466. </code></pre>
  467. <p>Cette directive permet d’afficher le gravatar d’un utilisateur (passé en paramètre <code>user</code>), avec 2 tailles possibles : 20px par défaut et 40px si le paramètre <code>size</code> est précisé avec la valeur <code>lg</code>. Cette logique de composant est assez agréable à manipuler, puisque pour l’utiliser, il suffit de mettre dans un template :</p>
  468. <pre><code>&lt;gravatar user="user" size="lg"&gt;&lt;/gravatar&gt;
  469. </code></pre>
  470. <p>Tester une directive ressemble à un test classique, avec quelques instructions en plus qui ressemblent à des incantations de magie noire quand on débute, et que l’on copie/colle religieusement en espérant que personne ne nous pose de questions sur leur signification.</p>
  471. <pre><code>beforeEach(inject(function($rootScope, $compile) {
  472. scope = $rootScope;
  473. scope.user = {
  474. gravatar: '12345',
  475. name: 'Cédric'
  476. };
  477. gravatar = $compile('&lt;gravatar user="user" size="lg"&gt;&lt;/gravatar&gt;')(scope);
  478. scope.$digest();
  479. }));
  480. </code></pre>
  481. <h1 id="cest-quoi-ce-bordelnbsp">1. C’est quoi ce bordel ?!</h1>
  482. <p>On commence par créer une chaîne de caractères avec le HTML que l’on veut interpréter. Celui-ci doit, bien sûr, contenir la directive que vous voulez tester :</p>
  483. <pre><code>'&lt;gravatar user="user" size="lg"&gt;&lt;/gravatar&gt;'
  484. </code></pre>
  485. <p>Ensuite l’élément est compilé : c’est peut-être la première fois que vous voyez le service <code>$compile</code>. Celui-ci est un service fourni par Angular, utilisé par le framework lui-même, mais rarement dans notre code. A l’exception des tests donc.
  486. Pour le compiler, on lui passe un scope, qui correspond aux variables auxquelles la directive aura accès. La nôtre a, par exemple, besoin d’un utilisateur : on crée donc un scope avec une variable <code>user</code> qui contient l’id gravatar qui va bien.</p>
  487. <p>Le <code>$digest()</code> à la fin permet de déclencher les watchers, c’est à dire résoudre toutes les expressions contenues dans notre directive : <code>user.gravatar</code> et <code>sizePx</code>.</p>
  488. <p>Une fois compilée, on récupère un élément Angular, comme lorsque l’on utilise la méthode <a href="https://docs.angularjs.org/api/ng/function/angular.element">angular.element</a> qui wrappe un élément de DOM ou du HTML sous forme de chaîne de caractères pour en faire un élément jQuery.</p>
  489. <p>Et voilà, le setup est fait. Maintenant, nous allons pouvoir passer au test proprement dit.</p>
  490. <p>Ce que vous ne savez probablement pas, c’est qu’un élément Angular offre de petits bonus. Ainsi, nous pouvons accéder au scope de la directive, qu’il soit isolé ou non. Dans notre cas, la directive <code>gravatar</code> utilise un scope isolé, donc notre test ressemblerait à quelque chose comme ça :</p>
  491. <pre><code>it('should have the correct size on scope', function() {
  492. expect(gravatar.isolateScope().sizePx).toBe('40');
  493. });
  494. </code></pre>
  495. <p>Si le scope n’était pas isolé, on utiliserait <code>scope()</code> :</p>
  496. <pre><code>it('should have the correct size on scope', function() {
  497. expect(gravatar.scope().sizePx).toBe('40');
  498. });
  499. </code></pre>
  500. <p>On peut aussi s’assurer que le HTML produit par la directive est conforme à ce que l’on attend. Vous pouvez utiliser la méthode <code>html()</code> qui renvoie le HTML de l’élément sous forme de chaîne de caractères, mais cela donne des tests un peu pénibles à maintenir. On peut faire quelque chose d’un peu plus sympa, pour tester la validité de l’élément, des classes ou attributs avec :</p>
  501. <pre><code>it('should create a gravatar image with large size', function() {
  502. expect(gravatar[0].tagName).toBe('IMG');
  503. expect(gravatar.hasClass('gravatar')).toBe(true);
  504. expect(gravatar.attr('src')).toBe('http://www.gravatar.com/avatar/12345?s=40&amp;d=identicon');
  505. });
  506. </code></pre>
  507. <p>Il est pas beau ce test ? Mais on peut encore mieux faire…</p>
  508. <h1 id="la-logique-dans-un-controller">2. La logique dans un controller</h1>
  509. <p>La logique d’une directive peut être un pénible à tester. Le plus simple est de l’externaliser dans un controller dédié, que l’on peut tester comme un controller classique :</p>
  510. <pre><code>angular.module('myProject.directives').directive('gravatar', function() {
  511. return {
  512. restrict: 'E',
  513. replace: true,
  514. scope: {
  515. user: '=',
  516. size: '@'
  517. },
  518. template: '&lt;img class="gravatar" ng-src="http://www.gravatar.com/avatar/{{ user.gravatar }}?s={{ sizePx }}&amp;d=identicon"/&gt;',
  519. controller: 'GravatarDirectiveController'
  520. };
  521. });
  522. </code></pre>
  523. <p>C’est d’autant plus utile si votre controller grossit et devient plus complexe.</p>
  524. <h1 id="externaliser-le-template">3. Externaliser le template</h1>
  525. <p>De la même façon, si le template grossit trop, n’hésitez pas à l’extraire dans un fichier à part.</p>
  526. <pre><code>angular.module('myProject.directives').directive('gravatar', function() {
  527. return {
  528. restrict: 'E',
  529. replace: true,
  530. scope: {
  531. user: '=',
  532. size: '@'
  533. },
  534. templateUrl: 'gravatar.html',
  535. controller: 'GravatarDirectiveController'
  536. };
  537. });
  538. </code></pre>
  539. <p>Cela introduit cependant une petite subtilité pour les tests. Si vous relancez celui que vous aviez avant d’externaliser le template, vous allez avoir l’erreur suivante :</p>
  540. <pre><code>Error: Unexpected request: GET gravatar.html
  541. No more request expected
  542. </code></pre>
  543. <p>Et oui, si on externalise le template, AngularJS va faire une requête pour le récupérer auprès du serveur. D’où une requête GET inattendue…
  544. Mais on peut charger le template dans le test pour éviter ce problème. Il suffit pour cela d’utiliser <a href="https://github.com/karma-runner/karma-ng-html2js-preprocessor">karma-ng-html2js</a> (ou le module grunt/gulp équivalent). Le principe est de charger les templates dans un module à part et d’inclure ce module dans notre test.</p>
  545. <p>Il suffit alors de charger le template dans le test :</p>
  546. <pre><code>beforeEach(module('gravatar.html'));
  547. </code></pre>
  548. <p>Et le tour est joué !</p>
  549. <h1 id="rcursivit">4. Récursivité</h1>
  550. <p>Si vous faites des directives un peu avancées, un jour ou l’autre, vous allez tomber sur une directive qui s’appelle elle-même. Bizarrement, ce n’est pas supporté par défaut par AngularJS. Vous pouvez cependant ajouter un module, RecursionHelper, qui offre un service permettant de compiler manuellement des directives récursives :</p>
  551. <pre><code>angular.module('myProject.directives')
  552. .directive('container', function(RecursionHelper) {
  553. return {
  554. restrict: 'E',
  555. templateUrl: 'partials/container.html',
  556. controller: 'ContainerDirectiveCtrl',
  557. compile: function(element) {
  558. return RecursionHelper.compile(element, function() {
  559. });
  560. }
  561. };
  562. });
  563. </code></pre>
  564. <h1 id="apprendre-des-meilleurs">5. Apprendre des meilleurs</h1>
  565. <p>Le meilleur moyen de progresser en écriture de directives est de vous inspirer des projets open-source. Le projet AngularUI contient un grand nombre de directives, notamment les directives de <a href="http://angular-ui.github.io/bootstrap/">UIBootstrap</a> qui peuvent vous inspirer. L’un des principaux contributeurs au projet, <a href="https://github.com/pkozlowski-opensource">Pawel</a>, a fait un talk avec <a href="http://pkozlowski-opensource.github.io/ng-europe-2014/presentation/#/">quelques idées</a> complémentaires à cet article.</p>
  566. <p>Et si vous voulez mettre tout ça en pratique, <a href="http://ninja-squad.fr/training/angularjs">notre prochaine formation</a> a lieu à Paris les 9-11 Février, et la suivante à Lyon les 9-11 Mars !</p>
  567. </article>
  568. </section>
  569. <nav id="jumpto">
  570. <p>
  571. <a href="/david/blog/">Accueil du blog</a> |
  572. <a href="http://blog.ninja-squad.com/2015/01/27/5-astuces-sur-les-directives-et-leurs-tests/">Source originale</a> |
  573. <a href="/david/stream/2019/">Accueil du flux</a>
  574. </p>
  575. </nav>
  576. <footer>
  577. <div>
  578. <img src="/static/david/david-larlet-avatar.jpg" loading="lazy" class="avatar" width="200" height="200">
  579. <p>
  580. Bonjour/Hi!
  581. Je suis <a href="/david/" title="Profil public">David&nbsp;Larlet</a>, je vis actuellement à Montréal et j’alimente cet espace depuis 15 ans. <br>
  582. Si tu as apprécié cette lecture, n’hésite pas à poursuivre ton exploration. Par exemple via les <a href="/david/blog/" title="Expériences bienveillantes">réflexions bimestrielles</a>, la <a href="/david/stream/2019/" title="Pensées (dés)articulées">veille hebdomadaire</a> ou en t’abonnant au <a href="/david/log/" title="S’abonner aux publications via RSS">flux RSS</a> (<a href="/david/blog/2019/flux-rss/" title="Tiens c’est quoi un flux RSS ?">so 2005</a>).
  583. </p>
  584. <p>
  585. Je m’intéresse à la place que je peux avoir dans ce monde. En tant qu’humain, en tant que membre d’une famille et en tant qu’associé d’une coopérative. De temps en temps, je fais aussi des <a href="https://github.com/davidbgk" title="Principalement sur Github mais aussi ailleurs">trucs techniques</a>. Et encore plus rarement, <a href="/david/talks/" title="En ce moment je laisse plutôt la place aux autres">j’en parle</a>.
  586. </p>
  587. <p>
  588. Voici quelques articles choisis :
  589. <a href="/david/blog/2019/faire-equipe/" title="Accéder à l’article complet">Faire équipe</a>,
  590. <a href="/david/blog/2018/bivouac-automnal/" title="Accéder à l’article complet">Bivouac automnal</a>,
  591. <a href="/david/blog/2018/commodite-effondrement/" title="Accéder à l’article complet">Commodité et effondrement</a>,
  592. <a href="/david/blog/2017/donnees-communs/" title="Accéder à l’article complet">Des données aux communs</a>,
  593. <a href="/david/blog/2016/accompagner-enfant/" title="Accéder à l’article complet">Accompagner un enfant</a>,
  594. <a href="/david/blog/2016/senior-developer/" title="Accéder à l’article complet">Senior developer</a>,
  595. <a href="/david/blog/2016/illusion-sociale/" title="Accéder à l’article complet">L’illusion sociale</a>,
  596. <a href="/david/blog/2016/instantane-scopyleft/" title="Accéder à l’article complet">Instantané Scopyleft</a>,
  597. <a href="/david/blog/2016/enseigner-web/" title="Accéder à l’article complet">Enseigner le Web</a>,
  598. <a href="/david/blog/2016/simplicite-defaut/" title="Accéder à l’article complet">Simplicité par défaut</a>,
  599. <a href="/david/blog/2016/minimalisme-esthetique/" title="Accéder à l’article complet">Minimalisme et esthétique</a>,
  600. <a href="/david/blog/2014/un-web-omni-present/" title="Accéder à l’article complet">Un web omni-présent</a>,
  601. <a href="/david/blog/2014/manifeste-developpeur/" title="Accéder à l’article complet">Manifeste de développeur</a>,
  602. <a href="/david/blog/2013/confort-convivialite/" title="Accéder à l’article complet">Confort et convivialité</a>,
  603. <a href="/david/blog/2013/testament-numerique/" title="Accéder à l’article complet">Testament numérique</a>,
  604. et <a href="/david/blog/" title="Accéder aux archives">bien d’autres…</a>
  605. </p>
  606. <p>
  607. On peut <a href="mailto:david%40larlet.fr" title="Envoyer un courriel">échanger par courriel</a>. Si éventuellement tu souhaites que l’on travaille ensemble, tu devrais commencer par consulter le <a href="http://larlet.com">profil dédié à mon activité professionnelle</a> et/ou contacter directement <a href="http://scopyleft.fr/">scopyleft</a>, la <abbr title="Société coopérative et participative">SCOP</abbr> dont je fais partie depuis six ans. Je recommande au préalable de lire <a href="/david/blog/2018/cout-site/" title="Attention ce qui va suivre peut vous choquer">combien coûte un site</a> et pourquoi je suis plutôt favorable à une <a href="/david/pro/devis/" title="Discutons-en !">non-demande de devis</a>.
  608. </p>
  609. <p>
  610. Je ne traque pas ta navigation mais mon
  611. <abbr title="Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33.184162340">hébergeur</abbr>
  612. conserve des logs d’accès.
  613. </p>
  614. </div>
  615. </footer>
  616. <script type="text/javascript">
  617. ;(_ => {
  618. const jumper = document.getElementById('jumper')
  619. jumper.addEventListener('click', e => {
  620. e.preventDefault()
  621. const anchor = e.target.getAttribute('href')
  622. const targetEl = document.getElementById(anchor.substring(1))
  623. targetEl.scrollIntoView({behavior: 'smooth'})
  624. })
  625. })()
  626. </script>