# Interactive Tutorials The site has code to aid development of interactive tutorials that connect to the live XRP Ledger (Mainnet, Testnet, or Devnet). This document explains how to use that code to create an interactive tutorial. **Note: This doc has been mostly updated to describe how the existing interactive tutorials work under Redocly, but the next phase of interactive tutorials will be different.** ## Limitations Interactive Tutorials are intended to demonstrate interactivity with the XRP Ledger from in-browser code. They have some limitations and requirements, as follows: - The actual code that runs in the interactive tutorial is JavaScript, because web browsers don't run other programming languages natively. Also, you can't use JavaScript functionality that's native to Node.js but not web browsers, such as `require("module")`. (Sometimes there are "shims" that imitate the Node.js behavior in the browser for these sorts of things, but that's more work.) - You _can_ still provide examples of the equivalent code in other programming languages. The actual code that runs has some differences than your example JavaScript code anyway because the actual code has to interact with the interactive tutorial's user interface: buttons, HTML outputs, etc. - The functionality you want to demonstrate has to be available on a public ledger and from the public API, whether that's Mainnet, Testnet, or Devnet. You can't make interactives for features that aren't enabled on a test network, or that require admin permissions. - If the tutorial involves sending transactions, you need to use a test network and probably the faucet to get credentials. You should not try to send Mainnet transactions because that would require a secret key that holds actual, valuable XRP. - If the tutorial requires certain objects to exist in the ledger (other than an account you can get from the Testnet faucet) you should either set those objects up on Mainnet in such a way that they'll never get removed, or be prepared to re-create them on the test network. - As a best practice, don't call the faucet unless the user interacts with the page by clicking a button or something. Otherwise, web crawlers and things just loading the page end up draining the faucet pretty quickly. ## Memos Transactions sent from interactive tutorials automatically attach a memo indicating what button of which tutorial sent the memo. Anyone who is watching Testnet/Devnet transactions can look for these memos to see when people are using the tutorials. The memo is identified by a `MemoType` that decodes to the URL of this document: ``` MemoType (hex): 68747470733A2F2F6769746875622E636F6D2F5852504C462F7872706C2D6465762D706F7274616C2F626C6F622F6D61737465722F746F6F6C2F494E5445524143544956455F5455544F5249414C535F524541444D452E6D64 MemoType (ASCII-decoded): https://github.com/XRPLF/xrpl-dev-portal/blob/master/tool/INTERACTIVE_TUTORIALS_README.md ``` The memo has a `MemoFormat` value of `6170706C69636174696F6E2F6A736F6E` (hex), which represents the MIME type `application/json`. The memo has a `MemoData` field which is ASCII-encoded JSON containing the following data: | Field | Type | Contents | |---|---|---| | `path` | String | The `window.location.pathname` of the tutorial. For example, `/send-xrp.html`. | | `button` | String | The unique html ID of the button that triggered this transaction. For example, `submit-button`. | | `step` | String (Number) | The step number that contained the button to trigger this transaction. For example, `"1"`. The first interactive block is step 1 (they are not 0-indexed). | | `totalsteps` | String (Number) | The total number of interactive blocks in the tutorial that triggered this transaction. Not all steps of an interactive tutorial involve sending transactions, but all steps are counted. | For privacy reasons, the memo does not and MUST NOT include personally identifying information about the user or their browser. **Note:** The interactive tutorial code assumes that the path and ID are both possible to encode with plain ASCII, so please avoid using non-ASCII characters in the IDs and filenames. ## Recommended Process An interactive tutorial is a page, so you add it to the `dactyl-config.yml` page like any other page. However, you need to add the following pieces to make the interactive stuff work: 1. Set page properties, either in the config file or the page's frontmatter. You need to include an array of step names as the `steps` parameter in the frontmatter blurb: Use Tickets to send a transaction outside of normal Sequence order. steps: ['Generate', 'Connect', 'Configure Issuer', 'Wait (Issuer Setup)', 'Configure Hot Address', 'Wait (Hot Address Setup)', 'Make Trust Line', 'Wait (TrustSet)', 'Send Token', 'Wait (Payment)', 'Confirm Balances'] The `steps` list must exactly match the list of step labels in the tutorial. Start with some steps you know you'll have like 'Generate' and keep the frontmatter updated as you add more. The `interactive_steps` filter is no longer needed under Redocly. 2. For the tutorial, you're going to create (at least) two JavaScript files: - **Example Code:** The example code that you'll display on the page. You want to make sure it actually runs correctly as shown to the user, after all. You should start by making this one. You should save this file as `content/_code-samples/{YOUR TUTORIAL}/{YOUR TUTORIAL}.js`. - **Interactive Code:** A modified version of the same code that will actually run, and also interact with the user interface in the browser itself. You should adapt this one from the other version after you get the other one working. While working on this version, remember to backport any changes that are also applicable to the example code version. You should save this file as `static/js/tutorials/{YOUR_TUTORIAL}.js`. 3. Start working on the Example Code file first. Rather than starting with empty example code, you copy and adapt one of the existing code samples, such as [Send XRP](../content/_code-samples/send-xrp/). For this, you can just open the `demo.html` directly in your browser and use the developer console; you can see your changes immediately after refreshing the page. 4. When you have working sample code, break it down into logical steps. Then, write the actual prose of the tutorial, with descriptions of each step. Use excerpts of the example code to demonstrate each step. You can gloss over certain parts of the sample code if they're tangential to the goal of the tutorial, like the nitty-gritty of getting credentials from the Testnet faucet. This is where `{% code-snippet %}` comes in really handy. You can pull in just an excerpt of a code sample based on starting and following bits (it goes up to but does not include the section you specify with `before`). For example: ```markdoc {% tab label="JavaScript" %} {% code-snippet file="/_code-samples/trade-in-the-decentralized-exchange/js/trade-in-the-dex.js" from="// Get credentials" before="// Define the proposed trade" language="js" /%} {% /tab %} ``` Both `from` and `before` are optional; if you omit them, it'll all the way from the start of the file to the end. Tabs are also optional, but recommended if you have code in multiple languages. 5. Figure out what an interactive version of the tutorial looks like and where the user might interact with key steps. Understanding [Bootstrap forms code](https://getbootstrap.com/docs/4.0/components/forms/) and [jQuery](https://api.jquery.com/) will help you get a better sense of what's more or less feasible to do. For some tutorials, the only user interaction may be clicking a button and looking at some output in each step. Giving the user some choices and freedom adds a bit of "wow" factor and can also be more educational because users can try it again and see how things turn out differently. Just keep in mind, you're not building an entire app, you're just providing a demo. 6. Write the interactive code, embedding interactive blocks in each step of the tutorial near the code samples that do roughly the same thing. ## How to Use the Interactive Bits To run your custom interactive code on your tutorial page, add scripts tag to the Markdown content of the page you're writing. Conventionally, this can be under the "Prerequisites" heading of the tutorial, but it's mostly invisible to the user so it can go almost anywhere. The two files you should include are the shared interactive tutorials code, and the specific code for your tutorial. For example: ```html ``` ### Starting Snippets There are also snippets that handle some of the most generic and repeated parts of the tutorials. For many tutorials, the first two steps are going to be "Connect to the Testnet" and "Get Credentials". The snippets for these are: ```markdoc {% partial file="/_snippets/interactive-tutorials/generate-step.md" /%} ``` ``` {% partial file="/_snippets/interactive-tutorials/connect-step.md" /%} ``` ### Interactive Blocks In your Markdown file, you'll use a Markdoc component to encapsulate the interactive code sections. By convention, it looks like this, though all the HTML is optional: ```markdoc {% interactive-block label="Step Name" steps=$frontmatter.steps %} {% loading-icon message=" Doing stuff..." /%}
{% /interactive-block %} ``` **Warning:** It won't work right if you don't leave empty lines between the HTML tags and the Markdoc tags (`{% %}`). Things you'll want to customize for each section: - Each step name (`Step Name` in this example) must be unique within the page. It's used in the interactive tutorial breadcrumbs, so it's best if it's short (one or two words). The names `Connect` and `Generate` are taken by their respective snippets. - The button can have whatever message you want on it (like `Click this button`). - The class `previous-steps-required` causes the button to be disabled until the previous step in the tutorial has been marked as complete, and provides a tooltip on the disabled button while it's disabled. - You can instead use the class `connection-required`; this allows the user to do this step at any time as long as they're already connected to the ledger, so they can skip ahead to this step. Don't use both that and `previous-steps-required` on the same button. - Usually you want to give each button a unique ID (like `your-button-id`). This is how you refer to the button in your JavaScript code. - The loading message (`Doing stuff...`) is hidden by default (that's what the `collapse` class does) but you can show it when you start something that might take a while, like querying or sending a network request, and then hide it again when the thing is done. - The empty output-area div is where you'll write the results of doing whatever the step does. By convention, you shouldn't put anything in it since most of the example functions wipe its contents as the first thing they do. - You can put other custom interactive stuff in the block as needed, usually above the loader and output area. ### The Wait Snippet If you have a step in your tutorial where you wait for a transaction to get validated, the "Wait" snippet is there for you. The "Wait" snippet should be used like this: ```markdoc {% partial file="/_snippets/interactive-tutorials/wait-step.md" /%} ``` If you have multiple "Wait" steps, you need to give each one a unique name. To do that: ```markdoc {% partial file="/_snippets/interactive-tutorials/wait-step.md" variables={label: "Wait (again)"} %} ``` ### Step Code The custom JavaScript code for your interactive tutorial should be wrapped in a function that runs it when the whole page is loaded, like this: ```js window.onRouteChange(() => { // Your code here }) ``` **Warning:** Don't use `$(document).ready(() => {...})`. That callback doesn't work properly under Redocly/React. Inside that block, the code for each individual block tends to follow a pattern: you make event handlers for the ways users should interact with the buttons and things in each step. Within the handlers, you can use the event to identify the block so that most of the code is very familiar. The following example is pulled from the "Use Tickets" interactive tutorial, with extra comments added to clarify the various pieces. ```js // 7. Check Available Tickets -------------------------------------------------- // create and bind a handler to a mouse click on the button with ID "check-tickets" $("#check-tickets").click( async function(event) { // Use jQuery to find the interactive block that contains the button that // triggered this event, so we can do stuff within it. You can use this as-is // in basically every handler. const block = $(event.target).closest(".interactive-block") // Get the address from the "Generate" step snippet, or display an error and // quit out of this handler if it hasn't been run successfully yet. // There's also a get_wallet(event) function which works the exact same way, // but returns a Wallet instance. const address = get_address(event) if (!address) {return} // Wipe previous output in this interactive block. block.find(".output-area").html("") // Show the loader animation and text while we wait for the next commands to // finish. block.find(".loader").show() // Make a call using xrpl.js. The "api" is a Client instance provided by // the "Connect" step snippet. let response = await api.request({ "command": "account_objects", "account": address, "type": "ticket" }) // Hide the loader animation. If you called any commands that can error out, // don't forget to hide the loader in the error handling case, too. It's safe // to call this even if the loader is already hidden; if so, it does nothing. block.find(".loader").hide() // Display the output of the call in the interactive block for the user's // benefit. The pretty_print() helper function handles indentation and can // take either a JSON-like object or a string containing serialized JSON. // If you use .html() this will replace the contents of the output area; to // add to it without resetting it, use .append() instead. block.find(".output-area").html( `${pretty_print(response)}`)
// This particular step sets up a set of clickable "radio buttons" in the
// following step based on the results of the query. So ticket-selector is
// the ID of an element in the next interactive block's HTML bits.
// Wipe any previous output from the ticket selector area
$("#ticket-selector .form-area").html("")
// For each ticket from the response, add a radio button to the ticket
// selector area.
response.result.account_objects.forEach((ticket, i) => {
$("#ticket-selector .form-area").append(
`