xkcd_extension_tutorial.rst 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866
  1. .. _xkcd_extension_tutorial:
  2. Let's Make an xkcd JupyterLab Extension
  3. ---------------------------------------
  4. .. warning::
  5. The extension developer API is not stable and will evolve in JupyterLab
  6. releases in the near future.
  7. JupyterLab extensions add features to the user experience. This page
  8. describes how to create one type of extension, an *application plugin*,
  9. that:
  10. - Adds a "Random `xkcd <https://xkcd.com>`__ comic" command to the
  11. *command palette* sidebar
  12. - Fetches the comic image and metadata when activated
  13. - Shows the image and metadata in a tab panel
  14. By working through this tutorial, you'll learn:
  15. - How to setup an extension development environment from scratch on a
  16. Linux or OSX machine.
  17. - Windows users: You'll need to modify the commands slightly.
  18. - How to start an extension project from
  19. `jupyterlab/extension-cookiecutter-ts <https://github.com/jupyterlab/extension-cookiecutter-ts>`__
  20. - How to iteratively code, build, and load your extension in JupyterLab
  21. - How to version control your work with git
  22. - How to release your extension for others to enjoy
  23. |Completed xkcd extension screenshot|
  24. Sound like fun? Excellent. Here we go!
  25. Setup a development environment
  26. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  27. Install conda using miniconda
  28. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  29. Start by installing miniconda, following
  30. `Conda's installation documentation <https://conda.io/docs/user-guide/install/index.html>`__.
  31. .. _install-nodejs-jupyterlab-etc-in-a-conda-environment:
  32. Install NodeJS, JupyterLab, etc. in a conda environment
  33. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  34. Next create a conda environment that includes:
  35. 1. the latest release of JupyterLab
  36. 2. `cookiecutter <https://github.com/audreyr/cookiecutter>`__, the tool
  37. you'll use to bootstrap your extension project structure
  38. 3. `NodeJS <https://nodejs.org>`__, the JavaScript runtime you'll use to
  39. compile the web assets (e.g., TypeScript, CSS) for your extension
  40. 4. `git <https://git-scm.com>`__, a version control system you'll use to
  41. take snapshots of your work as you progress through this tutorial
  42. It's best practice to leave the root conda environment, the one created
  43. by the miniconda installer, untouched and install your project specific
  44. dependencies in a named conda environment. Run this command to create a
  45. new environment named ``jupyterlab-ext``.
  46. .. code:: bash
  47. conda create -n jupyterlab-ext -c conda-forge --override-channels nodejs jupyterlab cookiecutter git
  48. Now activate the new environment so that all further commands you run
  49. work out of that environment.
  50. .. code:: bash
  51. conda activate jupyterlab-ext
  52. Note: You'll need to run the command above in each new terminal you open
  53. before you can work with the tools you installed in the
  54. ``jupyterlab-ext`` environment.
  55. Create a repository
  56. ~~~~~~~~~~~~~~~~~~~
  57. Create a new repository for your extension. For example, on
  58. `GitHub <https://help.github.com/articles/create-a-repo/>`__. This is an
  59. optional step but highly recommended if you want to share your
  60. extension.
  61. Create an extension project
  62. ~~~~~~~~~~~~~~~~~~~~~~~~~~~
  63. Initialize the project from a cookiecutter
  64. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  65. Next use cookiecutter to create a new project for your extension.
  66. This will create a new folder for your extension in your current directory.
  67. .. code:: bash
  68. cookiecutter https://github.com/jupyterlab/extension-cookiecutter-ts
  69. When prompted, enter values like the following for all of the
  70. cookiecutter prompts.
  71. ::
  72. author_name []: Your Name
  73. extension_name [myextension]: jupyterlab_xkcd
  74. project_short_description [A JupyterLab extension.]: Show a random xkcd.com comic in a JupyterLab panel
  75. repository [https://github.com/my_name/jupyterlab_myextension]: https://github.com/my_name/jupyterlab_xkcd
  76. Note: if not using a repository, leave the field blank. You can come
  77. back and edit the repository links in the ``package.json`` file later.
  78. Change to the directory the cookiecutter created and list the files.
  79. .. code:: bash
  80. cd jupyterlab_xkcd
  81. ls
  82. You should see a list like the following.
  83. ::
  84. README.md package.json src style tsconfig.json
  85. Build and install the extension for development
  86. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  87. Your new extension project has enough code in it to see it working in
  88. your JupyterLab. Run the following commands to install the initial
  89. project dependencies and install it in the JupyterLab environment. We
  90. defer building since it will be built in the next step.
  91. .. note::
  92. This tutorial uses ``jlpm`` to install Javascript packages and
  93. run build commands, which is JupyterLab's bundled
  94. version of ``yarn``. If you prefer, you can use another Javascript
  95. package manager like ``npm`` or ``yarn`` itself.
  96. .. code:: bash
  97. jlpm install
  98. jupyter labextension install . --no-build
  99. After the install completes, open a second terminal. Run these commands
  100. to activate the ``jupyterlab-ext`` environment and to start a JupyterLab
  101. instance in watch mode so that it will keep up with our changes as we
  102. make them.
  103. .. code:: bash
  104. conda activate jupyterlab-ext
  105. jupyter lab --watch
  106. See the initial extension in action
  107. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  108. After building with your extension, JupyterLab should open in your
  109. default web browser.
  110. In that window open the JavaScript console
  111. by following the instructions for your browser:
  112. - `Accessing the DevTools in Google
  113. Chrome <https://developer.chrome.com/devtools#access>`__
  114. - `Opening the Web Console in
  115. Firefox <https://developer.mozilla.org/en-US/docs/Tools/Web_Console/Opening_the_Web_Console>`__
  116. After you reload the page with the console open, you should see a message that says
  117. ``JupyterLab extension jupyterlab_xkcd is activated!`` in the console.
  118. If you do, congrats, you're ready to start modifying the the extension!
  119. If not, go back, make sure you didn't miss a step, and `reach
  120. out <https://github.com/jupyterlab/jupyterlab/blob/master/README.md#getting-help>`__ if you're stuck.
  121. Note: Leave the terminal running the ``jupyter lab --watch`` command
  122. open.
  123. Commit what you have to git
  124. ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  125. Run the following commands in your ``jupyterlab_xkcd`` folder to
  126. initialize it as a git repository and commit the current code.
  127. .. code:: bash
  128. git init
  129. git add .
  130. git commit -m 'Seed xkcd project from cookiecutter'
  131. Note: This step is not technically necessary, but it is good practice to
  132. track changes in version control system in case you need to rollback to
  133. an earlier version or want to collaborate with others. For example, you
  134. can compare your work throughout this tutorial with the commits in a
  135. reference version of ``jupyterlab_xkcd`` on GitHub at
  136. https://github.com/jupyterlab/jupyterlab_xkcd.
  137. Add an xkcd widget
  138. ~~~~~~~~~~~~~~~~~~
  139. Show an empty panel
  140. ^^^^^^^^^^^^^^^^^^^
  141. The *command palette* is the primary view of all commands available to
  142. you in JupyterLab. For your first addition, you're going to add a
  143. *Random xkcd comic* command to the palette and get it to show an *xkcd*
  144. tab panel when invoked.
  145. Fire up your favorite text editor and open the ``src/index.ts`` file in
  146. your extension project. Add the following import at the top of the file
  147. to get a reference to the command palette interface.
  148. .. code:: typescript
  149. import {
  150. ICommandPalette
  151. } from '@jupyterlab/apputils';
  152. You will also need to install this dependency. Run the following command in the
  153. repository root folder install the dependency and save it to your
  154. `package.json`:
  155. .. code:: bash
  156. jlpm add @jupyterlab/apputils
  157. Locate the ``extension`` object of type ``JupyterLabPlugin``. Change the
  158. definition so that it reads like so:
  159. .. code:: typescript
  160. /**
  161. * Initialization data for the jupyterlab_xkcd extension.
  162. */
  163. const extension: JupyterLabPlugin<void> = {
  164. id: 'jupyterlab_xkcd',
  165. autoStart: true,
  166. requires: [ICommandPalette],
  167. activate: (app: JupyterLab, palette: ICommandPalette) => {
  168. console.log('JupyterLab extension jupyterlab_xkcd is activated!');
  169. console.log('ICommandPalette:', palette);
  170. }
  171. };
  172. The ``requires`` attribute states that your plugin needs an object that
  173. implements the ``ICommandPalette`` interface when it starts. JupyterLab
  174. will pass an instance of ``ICommandPalette`` as the second parameter of
  175. ``activate`` in order to satisfy this requirement. Defining
  176. ``palette: ICommandPalette`` makes this instance available to your code
  177. in that function. The second ``console.log`` line exists only so that
  178. you can immediately check that your changes work.
  179. Run the following to rebuild your extension.
  180. .. code:: bash
  181. jlpm run build
  182. JupyterLab will rebuild after the extension does. You can
  183. see it's progress in the ``jupyter lab --watch`` window. After that
  184. finishes, return to the browser tab that opened when you
  185. started JupyterLab. Refresh it and look in the console. You should see
  186. the same activation message as before, plus the new message about the
  187. ICommandPalette instance you just added. If you don't, check the output
  188. of the build command for errors and correct your code.
  189. ::
  190. JupyterLab extension jupyterlab_xkcd is activated!
  191. ICommandPalette: Palette {_palette: CommandPalette}
  192. Note that we had to run ``jlpm run build`` in order for the bundle to
  193. update, because it is using the compiled JavaScript files in ``/lib``.
  194. If you wish to avoid running ``jlpm run build`` after each change, you
  195. can open a third terminal, and run the ``jlpm run watch`` command from
  196. your extension directory, which will automatically compile the
  197. TypeScript files as they change.
  198. Now return to your editor. Add the following additional import to the
  199. top of the file.
  200. .. code:: typescript
  201. import {
  202. Widget
  203. } from '@phosphor/widgets';
  204. Install this dependency as well:
  205. .. code:: bash
  206. jlpm add @phosphor/widgets
  207. Then modify the ``activate`` function again so that it has the following
  208. code:
  209. .. code-block:: typescript
  210. activate: (app: JupyterLab, palette: ICommandPalette) => {
  211. console.log('JupyterLab extension jupyterlab_xkcd is activated!');
  212. // Create a single widget
  213. let widget: Widget = new Widget();
  214. widget.id = 'xkcd-jupyterlab';
  215. widget.title.label = 'xkcd.com';
  216. widget.title.closable = true;
  217. // Add an application command
  218. const command: string = 'xkcd:open';
  219. app.commands.addCommand(command, {
  220. label: 'Random xkcd comic',
  221. execute: () => {
  222. if (!widget.isAttached) {
  223. // Attach the widget to the main work area if it's not there
  224. app.shell.addToMainArea(widget);
  225. }
  226. // Activate the widget
  227. app.shell.activateById(widget.id);
  228. }
  229. });
  230. // Add the command to the palette.
  231. palette.addItem({command, category: 'Tutorial'});
  232. }
  233. The first new block of code creates a ``Widget`` instance, assigns it a
  234. unique ID, gives it a label that will appear as its tab title, and makes
  235. the tab closable by the user. The second block of code add a new command
  236. labeled *Random xkcd comic* to JupyterLab. When the command executes,
  237. it attaches the widget to the main display area if it is not already
  238. present and then makes it the active tab. The last new line of code adds
  239. the command to the command palette in a section called *Tutorial*.
  240. Build your extension again using ``jlpm run build`` (unless you are using
  241. ``jlpm run watch`` already) and refresh the browser tab. Open the command
  242. palette on the left side by clicking on *Commands* and type *xkcd* in
  243. the search box. Your *Random xkcd comic*
  244. command should appear. Click it or select it with the keyboard and press
  245. *Enter*. You should see a new, blank panel appear with the tab title
  246. *xkcd.com*. Click the *x* on the tab to close it and activate the
  247. command again. The tab should reappear. Finally, click one of the
  248. launcher tabs so that the *xkcd.com* panel is still open but no longer
  249. active. Now run the *Random xkcd comic* command one more time. The
  250. single *xkcd.com* tab should come to the foreground.
  251. |Empty xkcd extension panel|
  252. If your widget is not behaving, compare your code with the reference
  253. project state at the `01-show-a-panel
  254. tag <https://github.com/jupyterlab/jupyterlab_xkcd/tree/0.34-01-show-a-panel>`__.
  255. Once you've got everything working properly, git commit your changes and
  256. carry on.
  257. .. code-block:: bash
  258. git add .
  259. git commit -m 'Show xkcd command on panel'
  260. Show a comic in the panel
  261. ^^^^^^^^^^^^^^^^^^^^^^^^^
  262. You've got an empty panel. It's time to add a comic to it. Go back to
  263. your code editor. Add the following code below the lines that create a
  264. ``Widget`` instance and above the lines that define the command.
  265. .. code-block:: typescript
  266. // Add an image element to the panel
  267. let img = document.createElement('img');
  268. widget.node.appendChild(img);
  269. // Fetch info about a random comic
  270. fetch('https:////egszlpbmle.execute-api.us-east-1.amazonaws.com/prod').then(response => {
  271. return response.json();
  272. }).then(data => {
  273. img.src = data.img;
  274. img.alt = data.title;
  275. img.title = data.alt;
  276. });
  277. The first two lines create a new HTML ``<img>`` element and add it to
  278. the widget DOM node. The next lines make a request using the HTML
  279. `fetch <https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch>`__
  280. API that returns information about a random xkcd comic, and set the
  281. image source, alternate text, and title attributes based on the
  282. response.
  283. Rebuild your extension if necessary (``jlpm run build``), refresh your
  284. browser tab, and run the *Random xkcd comic* command again. You should
  285. now see a comic in the xkcd.com panel when it opens.
  286. |Single xkcd extension panel|
  287. Note that the comic is not centered in the panel nor does the panel
  288. scroll if the comic is larger than the panel area. Also note that the
  289. comic does not update no matter how many times you close and reopen the
  290. panel. You'll address both of these problems in the upcoming sections.
  291. If you don't see a comic at all, compare your code with the
  292. `02-show-a-comic
  293. tag <https://github.com/jupyterlab/jupyterlab_xkcd/tree/0.34-02-show-a-comic>`__
  294. in the reference project. When it's working, make another git commit.
  295. .. code:: bash
  296. git add .
  297. git commit -m 'Show a comic in the panel'
  298. Improve the widget behavior
  299. ~~~~~~~~~~~~~~~~~~~~~~~~~~~
  300. Center the comic and add attribution
  301. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  302. Open ``style/index.css`` in our extension project directory for editing.
  303. Add the following lines to it.
  304. .. code-block:: css
  305. .jp-xkcdWidget {
  306. display: flex;
  307. flex-direction: column;
  308. overflow: auto;
  309. }
  310. .jp-xkcdCartoon {
  311. margin: auto;
  312. }
  313. .jp-xkcdAttribution {
  314. margin: 20px auto;
  315. }
  316. The first rule stacks content vertically within the widget panel and
  317. lets the panel scroll when the content overflows. The other rules center
  318. the cartoon and attribution badge horizontally and space them out
  319. vertically.
  320. Return to the ``index.ts`` file. Note that there is already an import of
  321. the CSS file in the ``index.ts`` file. Modify the the ``activate``
  322. function to apply the CSS classes and add the attribution badge markup.
  323. The beginning of the function should read like the following:
  324. .. code-block:: typescript
  325. :emphasize-lines: 9,13,16-22
  326. activate: (app: JupyterLab, palette: ICommandPalette) => {
  327. console.log('JupyterLab extension jupyterlab_xkcd is activated!');
  328. // Create a single widget
  329. let widget: Widget = new Widget();
  330. widget.id = 'xkcd-jupyterlab';
  331. widget.title.label = 'xkcd.com';
  332. widget.title.closable = true;
  333. widget.addClass('jp-xkcdWidget'); // new line
  334. // Add an image element to the panel
  335. let img = document.createElement('img');
  336. img.className = 'jp-xkcdCartoon'; // new line
  337. widget.node.appendChild(img);
  338. // New: add an attribution badge
  339. img.insertAdjacentHTML('afterend',
  340. `<div class="jp-xkcdAttribution">
  341. <a href="https://creativecommons.org/licenses/by-nc/2.5/" class="jp-xkcdAttribution" target="_blank">
  342. <img src="https://licensebuttons.net/l/by-nc/2.5/80x15.png" />
  343. </a>
  344. </div>`
  345. );
  346. // Keep all the remaining fetch and command lines the same
  347. // as before from here down ...
  348. Build your extension if necessary (``jlpm run build``) and refresh your
  349. JupyterLab browser tab. Invoke the *Random xkcd comic* command and
  350. confirm the comic is centered with an attribution badge below it. Resize
  351. the browser window or the panel so that the comic is larger than the
  352. available area. Make sure you can scroll the panel over the entire area
  353. of the comic.
  354. |Styled xkcd panel with attribution|
  355. If anything is misbehaving, compare your code with the reference project
  356. `03-style-and-attribute
  357. tag <https://github.com/jupyterlab/jupyterlab_xkcd/tree/0.34-03-style-and-attribute>`__.
  358. When everything is working as expected, make another commit.
  359. .. code:: bash
  360. git add .
  361. git commit -m 'Add styling, attribution'
  362. Show a new comic on demand
  363. ^^^^^^^^^^^^^^^^^^^^^^^^^^
  364. The ``activate`` function has grown quite long, and there's still more
  365. functionality to add. You should refactor the code into two separate
  366. parts:
  367. 1. An ``XkcdWidget`` that encapsulates the xkcd panel elements,
  368. configuration, and soon-to-be-added update behavior
  369. 2. An ``activate`` function that adds the widget instance to the UI and
  370. decide when the comic should refresh
  371. Start by refactoring the widget code into the new ``XkcdWidget`` class.
  372. Add the following additional import to the top of the file.
  373. .. code-block:: typescript
  374. import {
  375. Message
  376. } from '@phosphor/messaging';
  377. Install this dependency:
  378. .. code:: bash
  379. jlpm add @phosphor/messaging
  380. Then add the class just below the import statements in the ``index.ts``
  381. file.
  382. .. code-block:: typescript
  383. /**
  384. * An xckd comic viewer.
  385. */
  386. class XkcdWidget extends Widget {
  387. /**
  388. * Construct a new xkcd widget.
  389. */
  390. constructor() {
  391. super();
  392. this.id = 'xkcd-jupyterlab';
  393. this.title.label = 'xkcd.com';
  394. this.title.closable = true;
  395. this.addClass('jp-xkcdWidget');
  396. this.img = document.createElement('img');
  397. this.img.className = 'jp-xkcdCartoon';
  398. this.node.appendChild(this.img);
  399. this.img.insertAdjacentHTML('afterend',
  400. `<div class="jp-xkcdAttribution">
  401. <a href="https://creativecommons.org/licenses/by-nc/2.5/" class="jp-xkcdAttribution" target="_blank">
  402. <img src="https://licensebuttons.net/l/by-nc/2.5/80x15.png" />
  403. </a>
  404. </div>`
  405. );
  406. }
  407. /**
  408. * The image element associated with the widget.
  409. */
  410. readonly img: HTMLImageElement;
  411. /**
  412. * Handle update requests for the widget.
  413. */
  414. onUpdateRequest(msg: Message): void {
  415. fetch('https://egszlpbmle.execute-api.us-east-1.amazonaws.com/prod').then(response => {
  416. return response.json();
  417. }).then(data => {
  418. this.img.src = data.img;
  419. this.img.alt = data.title;
  420. this.img.title = data.alt;
  421. });
  422. }
  423. };
  424. You've written all of the code before. All you've done is restructure it
  425. to use instance variables and move the comic request to its own
  426. function.
  427. Next move the remaining logic in ``activate`` to a new, top-level
  428. function just below the ``XkcdWidget`` class definition. Modify the code
  429. to create a widget when one does not exist in the main JupyterLab area
  430. or to refresh the comic in the exist widget when the command runs again.
  431. The code for the ``activate`` function should read as follows after
  432. these changes:
  433. .. code-block:: typescript
  434. /**
  435. * Activate the xckd widget extension.
  436. */
  437. function activate(app: JupyterLab, palette: ICommandPalette) {
  438. console.log('JupyterLab extension jupyterlab_xkcd is activated!');
  439. // Create a single widget
  440. let widget: XkcdWidget = new XkcdWidget();
  441. // Add an application command
  442. const command: string = 'xkcd:open';
  443. app.commands.addCommand(command, {
  444. label: 'Random xkcd comic',
  445. execute: () => {
  446. if (!widget.isAttached) {
  447. // Attach the widget to the main work area if it's not there
  448. app.shell.addToMainArea(widget);
  449. }
  450. // Refresh the comic in the widget
  451. widget.update();
  452. // Activate the widget
  453. app.shell.activateById(widget.id);
  454. }
  455. });
  456. // Add the command to the palette.
  457. palette.addItem({ command, category: 'Tutorial' });
  458. };
  459. Remove the ``activate`` function definition from the
  460. ``JupyterLabPlugin`` object and refer instead to the top-level function
  461. like so:
  462. .. code-block:: typescript
  463. const extension: JupyterLabPlugin<void> = {
  464. id: 'jupyterlab_xkcd',
  465. autoStart: true,
  466. requires: [ICommandPalette],
  467. activate: activate
  468. };
  469. Make sure you retain the ``export default extension;`` line in the file.
  470. Now build the extension again and refresh the JupyterLab browser tab.
  471. Run the *Random xkcd comic* command more than once without closing the
  472. panel. The comic should update each time you execute the command. Close
  473. the panel, run the command, and it should both reappear and show a new
  474. comic.
  475. If anything is amiss, compare your code with the
  476. `04-refactor-and-refresh
  477. tag <https://github.com/jupyterlab/jupyterlab_xkcd/tree/0.34-04-refactor-and-refresh>`__
  478. to debug. Once it's working properly, commit it.
  479. .. code:: bash
  480. git add .
  481. git commit -m 'Refactor, refresh comic'
  482. Restore panel state when the browser refreshes
  483. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  484. You may notice that every time you refresh your browser tab, the xkcd
  485. panel disappears, even if it was open before you refreshed. Other open
  486. panels, like notebooks, terminals, and text editors, all reappear and
  487. return to where you left them in the panel layout. You can make your
  488. extension behave this way too.
  489. Update the imports at the top of your ``index.ts`` file so that the
  490. entire list of import statements looks like the following:
  491. .. code-block:: typescript
  492. :emphasize-lines: 2,6,9-11
  493. import {
  494. JupyterLab, JupyterLabPlugin, ILayoutRestorer // new
  495. } from '@jupyterlab/application';
  496. import {
  497. ICommandPalette, InstanceTracker // new
  498. } from '@jupyterlab/apputils';
  499. import {
  500. JSONExt // new
  501. } from '@phosphor/coreutils';
  502. import {
  503. Message
  504. } from '@phosphor/messaging';
  505. import {
  506. Widget
  507. } from '@phosphor/widgets';
  508. import '../style/index.css';
  509. Install this dependency:
  510. .. code:: bash
  511. jlpm add @phosphor/coreutils
  512. Then, add the ``ILayoutRestorer`` interface to the ``JupyterLabPlugin``
  513. definition. This addition passes the global ``LayoutRestorer`` to the
  514. third parameter of the ``activate``.
  515. .. code:: typescript
  516. const extension: JupyterLabPlugin<void> = {
  517. id: 'jupyterlab_xkcd',
  518. autoStart: true,
  519. requires: [ICommandPalette, ILayoutRestorer],
  520. activate: activate
  521. };
  522. Finally, rewrite the ``activate`` function so that it:
  523. 1. Declares a widget variable, but does not create an instance
  524. immediately
  525. 2. Constructs an ``InstanceTracker`` and tells the ``ILayoutRestorer``
  526. to use it to save/restore panel state
  527. 3. Creates, tracks, shows, and refreshes the widget panel appropriately
  528. .. code-block:: typescript
  529. function activate(app: JupyterLab, palette: ICommandPalette, restorer: ILayoutRestorer) {
  530. console.log('JupyterLab extension jupyterlab_xkcd is activated!');
  531. // Declare a widget variable
  532. let widget: XkcdWidget;
  533. // Add an application command
  534. const command: string = 'xkcd:open';
  535. app.commands.addCommand(command, {
  536. label: 'Random xkcd comic',
  537. execute: () => {
  538. if (!widget) {
  539. // Create a new widget if one does not exist
  540. widget = new XkcdWidget();
  541. widget.update();
  542. }
  543. if (!tracker.has(widget)) {
  544. // Track the state of the widget for later restoration
  545. tracker.add(widget);
  546. }
  547. if (!widget.isAttached) {
  548. // Attach the widget to the main work area if it's not there
  549. app.shell.addToMainArea(widget);
  550. } else {
  551. // Refresh the comic in the widget
  552. widget.update();
  553. }
  554. // Activate the widget
  555. app.shell.activateById(widget.id);
  556. }
  557. });
  558. // Add the command to the palette.
  559. palette.addItem({ command, category: 'Tutorial' });
  560. // Track and restore the widget state
  561. let tracker = new InstanceTracker<Widget>({ namespace: 'xkcd' });
  562. restorer.restore(tracker, {
  563. command,
  564. args: () => JSONExt.emptyObject,
  565. name: () => 'xkcd'
  566. });
  567. };
  568. Rebuild your extension one last time and refresh your browser tab.
  569. Execute the *Random xkcd comic* command and validate that the panel
  570. appears with a comic in it. Refresh the browser tab again. You should
  571. see an xkcd panel appear immediately without running the command. Close
  572. the panel and refresh the browser tab. You should not see an xkcd tab
  573. after the refresh.
  574. Refer to the `05-restore-panel-state
  575. tag <https://github.com/jupyterlab/jupyterlab_xkcd/tree/0.34-05-restore-panel-state>`__
  576. if your extension is misbehaving. Make a commit when the state of your
  577. extension persists properly.
  578. .. code:: bash
  579. git add .
  580. git commit -m 'Restore panel state'
  581. Congrats! You've implemented all of the behaviors laid out at the start
  582. of this tutorial. Now how about sharing it with the world?
  583. .. _publish-your-extension-to-npmjsorg:
  584. Publish your extension to npmjs.org
  585. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  586. npm is both a JavaScript package manager and the de facto registry for
  587. JavaScript software. You can `sign up for an account on the npmjs.com
  588. site <https://www.npmjs.com/signup>`__ or create an account from the
  589. command line by running ``npm adduser`` and entering values when
  590. prompted. Create an account now if you do not already have one. If you
  591. already have an account, login by running ``npm login`` and answering
  592. the prompts.
  593. Next, open the project ``package.json`` file in your text editor. Prefix
  594. the ``name`` field value with ``@your-npm-username>/`` so that the
  595. entire field reads ``"name": "@your-npm-username/jupyterlab_xkcd"`` where
  596. you've replaced the string ``your-npm-username`` with your real
  597. username. Review the homepage, repository, license, and `other supported
  598. package.json <https://docs.npmjs.com/files/package.json>`__ fields while
  599. you have the file open. Then open the ``README.md`` file and adjust the
  600. command in the *Installation* section so that it includes the full,
  601. username-prefixed package name you just included in the ``package.json``
  602. file. For example:
  603. .. code:: bash
  604. jupyter labextension install @your-npm-username/jupyterlab_xkcd
  605. Return to your terminal window and make one more git commit:
  606. .. code:: bash
  607. git add .
  608. git commit -m 'Prepare to publish package'
  609. Now run the following command to publish your package:
  610. .. code:: bash
  611. npm publish --access=public
  612. Check that your package appears on the npm website. You can either
  613. search for it from the homepage or visit
  614. ``https://www.npmjs.com/package/@your-username/jupyterlab_xkcd``
  615. directly. If it doesn't appear, make sure you've updated the package
  616. name properly in the ``package.json`` and run the npm command correctly.
  617. Compare your work with the state of the reference project at the
  618. `06-prepare-to-publish
  619. tag <https://github.com/jupyterlab/jupyterlab_xkcd/tree/0.34-06-prepare-to-publish>`__
  620. for further debugging.
  621. |Extension page on npmjs.com|
  622. You can now try installing your extension as a user would. Open a new
  623. terminal and run the following commands, again substituting your npm
  624. username where appropriate
  625. (make sure to stop the existing ``jupyter lab --watch`` command first):
  626. .. code:: bash
  627. conda create -n jupyterlab-xkcd jupyterlab nodejs
  628. conda activate jupyterlab-xkcd
  629. jupyter labextension install @your-npm-username/jupyterlab_xkcd
  630. jupyter lab
  631. You should see a fresh JupyterLab browser tab appear. When it does,
  632. execute the *Random xkcd comic* command to prove that your extension
  633. works when installed from npm.
  634. Learn more
  635. ~~~~~~~~~~
  636. You've completed the tutorial. Nicely done! If you want to keep
  637. learning, here are some suggestions about what to try next:
  638. - Assign a hotkey to the *Random xkcd comic* command.
  639. - Make the image a link to the comic on https://xkcd.com.
  640. - Push your extension git repository to GitHub.
  641. - Give users the ability to pin comics in separate, permanent panels.
  642. - Learn how to write :ref:`other kinds of
  643. extensions <developer_extensions>`.
  644. .. |Completed xkcd extension screenshot| image:: xkcd_tutorial_complete.png
  645. .. |Empty xkcd extension panel| image:: xkcd_tutorial_empty.png
  646. .. |Single xkcd extension panel| image:: xkcd_tutorial_single.png
  647. .. |Styled xkcd panel with attribution| image:: xkcd_tutorial_complete.png
  648. .. |Extension page on npmjs.com| image:: xkcd_tutorial_npm.png