Lit tutorials are a way to provide a guided, interactive learning experience to Lit users. Under the hood they leverage playground elements as well as our markdown renderer.
Create a directory in /packages/lit-dev-content/samples/tutorials/
- This directory name is also going to be a part of the URL e.g.
https://lit.dev/tutorials/tutorial-name
In that directory create a tutorial.json
- See format in following section.
Create a description.md
in that directory that describes the tutorial
- This will show up in the card catalog
- This file can be empty, but must exist
Create a directory for each step in your tutorial
- Directory names must start from
00/
and increase numerically as a 2 digit number e.g.01/
will follow00/
If hasAfter: true
in tutorial.json the step must have before/
and after/
subdirectories
before/
holds the playground project for what is first presented to the userafter/
holds the playground project for when the user clicks thesolve
button.- If
noSolve
istrue
for this step's metadata intutorial.json
, then thesolve
will not be shown for the step, and theafter/
folder is not required. - If
hasAfter
isfalse
orundefined
for this step's metadata intutorial.json
, then theafter/
directory is optional and the solving the step will load the next step'sbefore/
directory
- If
If checkable: true
in tutorial.json add code checking
- create a
_check-code.js
file in thebefore/
directory - In your
project.json
- set your
"extends"
field to"/samples/checkable-tutorial-base.json"
- e.g.
"extends": "/samples/checkable-tutorial-base.json"
- e.g.
- add the code-checking file as a
hidden
file to the step'sproject.json
.- e.g.
"_check-code.js": { "hidden": true }
- e.g.
- set your
- import the new file in
index.html
- e.g.
<head>
<!-- playground-fold --><script type="module" src="./_check-code.js"></script><!-- playground-fold-end -->
<script type="module" src="./my-element.js"></script>
</head>
- Install the code checker from
'./_check-code-helpers.js'
and pass it an async callback that returns an object of type{passed: boolean, message?: string}
- See Code Checking for more details
Create a playground project for that step's before and after sections
- See playground-elements docs for playground project authoring.
Create a directory with the same tutorial name in /packages/lit-dev-content/site/tutorials/content/
- This will hold the step instructions markdown files.
For step xy/
create a markdown file named xy.md
in the content/
directory
- These are the instructinos for each step. See the Step Instruction Authoring section for more info.
Add your tutorial to the catalog by including the tutorial directory name in /packages/lit-dev-content/site/tutorials/tutorials.11tydata.js
- For example, if you want to add the tutorial directory
tutorial-name
to the catalog, invoke theloadTutorialData
function. e.g.
const tutorials = await Promise.all([
...
loadTutorialData('tutorial-name'),
...
]);
interface TutorialManifest {
// The title of the tutorial. This shows up as the title in
// the catalog card and in the tutorial itself
header: string;
// The difficulty of the tutorial
difficulty: '' | 'Beginner' | 'Intermediate' | 'Advanced';
// The size of the card in the catalog NOTE: you may have to
// trigger `npm run dev` to make this change reflect
size: 'tiny'|'small'|'medium'|'large';
// Approximate duration in minutes of the tutorial to be
// displayed on the catalog card. Set to 0 if you don't
// want to display a duration
duration: number;
// Category to display it in the catalog. Can be any string
// actually, but 'Learn' and 'Build' will actually make it
// display in the appropriate sections in the catalog
category: 'Learn' | 'Build' | 'Draft';
// Location of the image to display relative to site root.
// e.g. "images/animations.gif"
imgSrc?: string;
// Alt text for the image
imgAlt?: string;
// Array of tutorial step titles to display in the tutorial
// itself. Also used to calculate number of steps in tutorial
steps: {
// Title of the current step
title: string;
// If false or omitted, the "after" code will be set to the "before" code of
// the next step. This is useful for reducing code duplication when the
// next step is the "solved" code for the previous step.
//
// Set to true if there is an "after" directory for this step or if it is
// the last step in the tutorial.
hasAfter?: boolean;
// Set to true if there should be no "solve" button for this step; in this
// case no "after" folder is required.
noSolve?: boolean;
// Whether or not the step is code checkable. see the `Code Checking`
// section below for more details.
checkable?: boolean;
}[]
}
When authoring user instructions for a tutorial step, create a markdown file with the step number in its name in the /packages/lit-dev-content/site/tutorials/content/
directory. For example, the instructions for step my-tutorial/05/
should be named content/my-tutorial/05.md
.
To display code in the instructions you can use the switchable-sample
macro
{% switchable-sample %} ```ts @customElement('my-element') class MyElement extends LitElement { @property({attribute: false}) items = [1,2,3]; render() { html` <ul> ${this.items.map(item => html`<li>${item}</li>`)} </ul>`; } } ``` ```js class MyElement extends LitElement { render() { static properties = {items: {attribute: false}}; constructor() { super(); this.items = [1,2,3]; } html` <ul> ${this.items.map(item => html`<li>${item}</li>`)} </ul>`; } } customElements.define('my-element', MyElement); ``` {% endswitchable-sample %}
You can also insert an aside in your instructions by using the following format:
{% aside "positive" %}
The first line is always a bolded header.
Make sure to do `this`!
{% endaside %}
{% aside "warn" "no-header" %}
The `no-header` will make sure that this line is not bolded.
Beware of `this`!
{% endaside %}
{% aside "negative" %}
Make sure NOT to do `this`!
The following non-header lines here make sense to explain the assertion in
the header line above.
{% endaside %}
{% aside "info" %}
Check out more info [in this docs section](/docs/templates/expressions/#well-formed-html).
{% endaside %}
Note: markdown will only be parsed as markdown if there is an empty line between the text and the HTML tag.
The available asides are:
positive
warn
negative
info
labs
To enable code checking for a step, add the checkable: true
flag to the step in tutorial.json
.
Next create a file which will run your code checking. In this example we will call it _check-code.js
.
⚠️ Note: you MUST use a.js
file extension or else this repo will not be able to TS build because we use playground-elements to inject the hidden_check-code-helpers.js
file.
Then add this code check file as a hidden
file to the project.json
of the before
directory.
Additionally make sure your project.json
extends from /samples/checkable-tutorial-base.json
to include the hidden _check-code-helpers.js
communication file.
example: /before/project.json
{
"extends": "/samples/checkable-tutorial-base.json",
"files": {
"index.html": {},
"my-element.ts": {},
"_check-code.js": {"hidden": true}
}
}
Next, import this new file into your index.html
file. And feel free to add the <!-- playground-hide(-end) -->
comments to hide the import.
example: index.html
<head>
<!-- playground-hide --><script type="module" src="./_check-code.js"></script><!-- playground-hide-end -->
<script type="module" src="./my-element.js"></script>
</head>
<body>
<my-element name="User"></my-element>
</body>
In your _check-code.js
file, import and call installCodeChecker
from the hidden './_check-code-helpers.js'
file which is injected by playground elements in checkable-tutorial-base.json
.
installCodeChecker
will set up communications between the tutorial page and the playground and call an async callback when the user requests code checking.
The return type of the callback should be:
{passed: boolean, message?: string}
Where passed
is whether the code has passed the checks and message
is the optional error message to display.
example: check-code.js
import {installCodeChecker} from './_check-code-helpers.js';
installCodeChecker(async () => {
let passed = true;
let message = '';
const element = document.body.querySelector('my-element');
const nameAttribute = element.getAttribute('name');
if (element.name === undefined) {
passed = false;
message = `Define the 'name' property on the element.`;
} else if (element.name !== nameAttribute) {
passed = false;
message = `The element's name property is not a reactive property.`;
}
return {passed, message};
});
The first step should describe to the user what they are going to learn in the tutorial
- Code can be an empty
index.html
or a quick view at the final product.
Last step should have an action for the user to follow up what they just learned
- links to other tutorials
- links to next section of docs
You're on the docs site, link out to relevant documentation
- Teach the user how to empower themselves if they get stuck!
Determine if you're making a good use of a user's time
- Take the tutorial yourself
- Time each step
- Add about 30 seconds to a minute to you time for the final time approximation.
- After you've added it all up, look at the total time and ask yourself if a user would like to go through that time commitment for what they learn
- Cut extraneous content