all repos — h3 @ 0c0e83cd257e58656aaabc4e95840e55a2e1ffe9

A tiny, extremely minimalist JavaScript microframework.

docs/md/tutorial.md

 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
## Tutorial

As a (meta) explanation of how to use H3, let's have a look at how the [H3 web site](https://h3.js.org) itself was created.

The idea was to build a simple web site to display the documentation of the H3 microframework, so it must be able to:

- Provide a simple way to navigate through page.
- Render markdown content (via [marked.js](https://marked.js.org/#/README.md#README.md))
- Apply syntax highlighting (via [Prism.js](https://prismjs.com/))

As far as look and feel is concerned, I wanted something minimal but functional, so [mini.css](https://minicss.org/) was more than enough.

The full source of the site is available [here](https://github.com/h3rald/h3/tree/master/docs).

### Create a simple HTML file

Start by creating a simple HTML file. Place a script loading the entry point of your application within the `body` and set its type to `module`.

This will let you load an ES6 file containing imports to other files... it works in all major browsers, but it doesn't work in IE (but we don't care about that, do we?).

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>H3</title>
    <meta
      name="description"
      content="A bare-bones client-side web microframework"
    />
    <meta name="author" content="Fabio Cevasco" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="shortcut icon" href="favicon.png" type="image/png" />
    <link rel="stylesheet" href="css/mini-default.css" />
    <link rel="stylesheet" href="css/prism.css" />
    <link rel="stylesheet" href="css/style.css" />
  </head>
  <body>
    <script type="module" src="js/app.js"></script>
  </body>
</html>
```

### Create a single-page application

In this case the code for the SPA is not very complex, you can have a look at it [here](https://github.com/h3rald/h3/blob/master/docs/js/app.js).

Normally you'd have several components, at least one file containing modules to manage the application state, etc. (see the [todo list example](https://github.com/h3rald/h3/tree/master/docs/example)), but in this case a single component is sufficient.

Start by importing all the JavaScript modules you need:

```js
import { h3, h } from "./h3.js";
import marked from "./vendor/marked.js";
import Prism from "./vendor/prism.js";
```

Easy enough. Then we want to store the mapping between the different page fragments and their titles:

```js
const labels = {
  overview: "Overview",
  "quick-start": "Quick Start",
  "key-concepts": "Key Concepts",
  "best-practices": "Best Practices",
  tutorial: "Tutorial",
  api: "API",
  about: "About",
};
```

We are going to store the HTML contents of each page in an Object, and we're going to need a simple function to [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) the Markdown file and render it as HTML:

```js
const fetchPage = async ({ pages, id, md }) => {
  if (!pages[id]) {
    const response = await fetch(md);
    const text = await response.text();
    pages[id] = marked(text);
  }
};
```

Basically this function is going to be called when you navigate to each page, and it:

1. fetches the content of the requested file (`md`))
2. renders the Markdown code into HTML using the _marked_ library, and stores it in the `pages` object

We are gonna use our `fetchPage` function inside the `setup` of the main (and only) screen of our app, `Page`:

```js
const Page = h3.screen({
  setup: async (state) => {
    state.pages = {};
    state.id = h3.route.path.slice(1);
    state.ids = Object.keys(labels);
    state.md = state.ids.includes(state.id)
      ? `md/${state.id}.md`
      : `md/overview.md`;
    await fetchPage(state);
  },
  display: (state) => {
    return h("div.page", [
      Header,
      h("div.row", [
        h("input#drawer-control.drawer", { type: "checkbox" }),
        Navigation(state.id, state.ids),
        Content(state.pages[state.id]),
        Footer,
      ]),
    ]);
  },
  teardown: (state) => state,
});
```

Note that this screen has a `setup`, a `display` and a `teardown` method, both taking `state` as parameter. In H3, screens are nothing but stateful components that are used to render the whole page of the application, and are therefore typically redered when navigating to a new route.

The `state` parameter is nothing but an empty object that can be used to store data that will be accessible to the `setup`, `display` and `teardown` methods, and (typically) will be destroyed when another screen is rendered.

The `setup` function allows you to perform some operations that should take place _before_ the screen is rendered. In this case, we want to fetch the page contents (if necessary) beforehand to avoid displaying a spinner while the content is being loaded. Note that the `setup` method can be asynchronous, and in this case the `display` method will not be called until all asynchronous operations have been completed (assuming you are `await`ing them).

The `teardown` function in this case only makes sure that the existing screen state (in particular any loaded markdown page) will be passed on to the next screen during navigation (which, in this case, is still the `Page` screen), so that existing pages will not be fetched again.

The main responsibility of this screen is to fetch the Markdown content and render the whole page, but note how the rendering different portions of the page are delegated to different components: `Header`, `Navigation`, `Content`, and `Footer`.

The `Header` and `Footer` components are very simple: their only job is to render static content:

```js
const Header = () => {
  return h("header.row.sticky", [
    h("a.logo.col-sm-1", { href: "#/" }, [
      h("img", { alt: "H3", src: "images/h3.svg" }),
    ]),
    h("div.version.col-sm.col-md", [
      h("div.version-number", "v0.11.0"),
      h("div.version-label", "“Keen Klingon“"),
    ]),
    h("label.drawer-toggle.button.col-sm-last", { for: "drawer-control" }),
  ]);
};

const Footer = () => {
  return h("footer", [h("div", "© 2020 Fabio Cevasco")]);
};
```

The `Navigation` component is more interesting, as it takes two parameters:

- The ID of the current page
- The list of page IDs

...and it uses this information to create the site navigation menu dynamically:

```js
const Navigation = (id, ids) => {
  const menu = ids.map((p) =>
    h(`a${p === id ? ".active" : ""}`, { href: `#/${p}` }, labels[p])
  );
  return h("nav#navigation.col-md-3", [
    h("label.drawer-close", { for: "drawer-control" }),
    ...menu,
  ]);
};
```

Finally, the `Content` component takes a string containing the HTML of the page content to render using the special `$html` attribute that can be used to essentially set the `innerHTML` property of an element:

```js
const Content = (html) => {
  const content = h("div.content", { $html: html });
  return h("main.col-sm-12.col-md-9", [
    h(
      "div.card.fluid",
      h("div.section", { $onrender: () => Prism.highlightAll() }, content)
    ),
  ]);
};
```

Now, the key here is that we are only ever going to render "known" pages that are listed in the `labels` object.

Suppose for example that the `#/overview` page is loaded. The `h3.route.path` in this case is going to be set to `/overview`, which in turns corresponds to an ID of a well-known page (`overview`).

In a similar way, other well-known pages can easily be mapped to IDs, but it is also important to handle _unknown_ pages (technically I could even pass an URL to a different site containing a malicious markdown page and have it rendered!), and if a page passed in the URL fragment is not present in the `labels` Object, the Overview page will be rendered instead.

This feature is also handy to automatically load the Overview when no fragment is specified.

What is that weird `$onrender` property you ask? Well, that's a H3-specific callback that will be executed whenever the corresponding DOM node is rendered... that's essentially the perfect place to for executing operations that must be perform when the DOM is fully available, like highlighting our code snippets using _Prism_ in this case.

### Initialization

Done? Not quite. We need to initialize the SPA by passing the `Page` component to the `h3.init()` method to trigger the first rendering:

```js
h3.init(Page);
```

And that's it. Now, keep in mind that this is the _short_ version of initialization using a single component and a single route, but still, that's good enough for our use case.

### Next steps

Made it this far? Good. Wanna know more? Have a look at the code of the [todo list example](https://github.com/h3rald/h3/tree/master/docs/example) and try it out [here](https://h3.js.org/example/index.html).

Once you feel more comfortable and you are ready to dive into a more complex application, featuring different routes, screens, forms, validation, confirmation messages, plenty of third-party components etc., have a look at [LitePad](https://github.com/h3rald/litepad). You can see it in action here: [litepad.h3rald.com](https://litepad.h3rald.com/).

Note: the LitePad online demo will store all its data in localStorage.