Documentation
A detailed walkthrough of NakedJSX features.
For a high-level overview, please visit nakedjsx.org.
This page was built using NakedJSX, and you can look its source.
Topics
Hello World
NakedJSX searches a directory for filenames that match *-page.jsx. Each matching file is compiled and then executed to produce a HTML file in an output directory.
Here is a near-minimal NakedJSX project. It consists of one file in an otherwise empty directory:
src/index-page.jsx
import { Page } from '@nakedjsx/core/page'
const BodyContent =
() =>
<>
<h1>Hello World</h1>
<p>A near-minimal NakedJSX example.</p>
</>
Page.Create('en');
Page.AppendHead(<title>Hello World</title>);
Page.AppendBody(<BodyContent />);
Page.Render();
Building this requires running an npx command. If you have Node.js installed, you can create the file above and try it now:
# build command
$ npx nakedjsx src --out out --pretty
This tells NakedJSX to look for pages to build in the 'src' directory, build them into a 'out' directory, and to format the generated files nicely.
The result in this case is a single new file:
out/index.html (199 bytes) (open in new tab)
<!doctype html>
<html lang="en">
<head>
<title>Hello World</title>
</head>
<body>
<h1>Hello World</h1>
<p>A near-minimal NakedJSX example.</p>
</body>
</html>
If you build it without the --pretty
flag, the result is tightly packed and suitable for distribution:
# build command
$ npx nakedjsx src --out out
out/index.html (149 bytes) (open in new tab)
<!DOCTYPE html><html lang="en"><head><title>Hello World</title></head><body><h1>Hello World</h1><p>A near-minimal NakedJSX example.</p></body></html>
Adding CSS and Client JavaScript
CSS and client JavaScript features are covered in detail later. However it's worth looking at a second example that touches on these function areas:
src/index-page.jsx
import { Page } from '@nakedjsx/core/page'
const BodyContent =
({ title }) =>
<>
<h1 css="color: fuchsia">{title}</h1>
</>
const ClientJsx =
() =>
<p css="color: #ff00ff">
This paragraph was added by browser JavaScript!
</p>
// Prepare to produce a HTML file
Page.Create('en');
// Provide some static content
Page.AppendCss('body { font-family: sans-serif }');
Page.AppendHead(<title>Hello NakedJSX 2!</title>);
Page.AppendBody(<BodyContent title="Hello NakedJSX 2!" />);
// Make a JSX function available to browser JavaScript
Page.AppendJs(ClientJsx);
// Add some JavaScript that will run in the browser.
Page.AppendJs(document.body.appendChild(<ClientJsx />));
// Output the HTML file
Page.Render();
The resulting HTML now has embedded CSS and JavaScript:
out/index.html (1972 bytes) (open in new tab)
<!doctype html>
<html lang="en">
<head>
<title>Hello NakedJSX 2!</title>
<style>
body {
font-family: sans-serif;
}
.a {
color: #f0f;
}
</style>
</head>
<body>
<h1 class="a">Hello NakedJSX 2!</h1>
<script>
'use strict'
const t = Element.prototype.appendChild
function e(t, e, ...n) {
if (((e = e || {}), 'function' == typeof t))
return (e.children = n), t(e)
const o = document.createElement(t)
for (const [t, n] of Object.entries(e)) {
if (t.startsWith('on')) {
const e = t.toLowerCase()
if (e in window) {
o.addEventListener(e.substring(2), n)
continue
}
}
o.setAttribute(t, n)
}
for (const t of n) o.appendChild(t)
return o
}
;(Element.prototype.appendChild = function (e) {
if (Array.isArray(e)) {
for (const t of e) this.appendChild(t)
return e
}
return 'string' == typeof e
? t.call(this, document.createTextNode(e))
: e
? t.call(this, e)
: void 0
}),
document.body.appendChild(
e(
() =>
e(
'p',
{ class: 'a' },
'This paragraph was added by browser JavaScript!',
),
null,
),
)
</script>
</body>
</html>
Note that:
- The scoped CSS is extracted from the JSX, minified, with the resulting class shared by both the page JSX and client JSX.
- Some functions were automatically added to allow the compiled client JSX to create DOM nodes in the browser.
The same example built without --pretty
is less than a kilobyte in size:
out/index.html (887 bytes) (open in new tab)
<!DOCTYPE html><html lang="en"><head><title>Hello NakedJSX 2!</title><style>body{font-family:sans-serif}.a{color:#f0f}</style></head><body><h1 class="a">Hello NakedJSX 2!</h1><script>"use strict";const t=Element.prototype.appendChild;function e(t,e,...n){if(e=e||{},"function"==typeof t)return e.children=n,t(e);const o=document.createElement(t);for(const[t,n]of Object.entries(e)){if(t.startsWith("on")){const e=t.toLowerCase();if(e in window){o.addEventListener(e.substring(2),n);continue}}o.setAttribute(t,n)}for(const t of n)o.appendChild(t);return o}Element.prototype.appendChild=function(e){if(Array.isArray(e)){for(const t of e)this.appendChild(t);return e}return"string"==typeof e?t.call(this,document.createTextNode(e)):e?t.call(this,e):void 0},document.body.appendChild(e((()=>e("p",{class:"a"},"This paragraph was added by browser JavaScript!")),null));</script></body></html>
Development Tools
Development Server
NakedJSX includes a development server. You can start it by passing the --dev
flag on the command line:
$ npx nakedjsx src --out out --dev
This time, instead of exiting after the build, this is displayed:
Development server: http://localhost:8999, Press (x) to exit
You can now open http://localhost:8999 and you will see the rendered index page. If you edit and save src/index-page.jsx, the server will rebuild out/index.html and the browser will refresh automatically.
Using a Config File
As projects start to make use of more NakedJSX features, the required command line options can stack up. A config file can be used to avoid this.
Building with --config-save
will save the build configuration into a .nakedjsx.json
config file in your source directory.
This config file will be automatically read by future builds, removing the need to specify anything other than the source directory when invoking npx nakedjsx
.
Config file settings can be overriden by arguments supplied on the command line.
Use of a config file is entirely optional.
Build-time Defined Values
Values can be passed from the command line into the generated code. This is useful for switching between multiple client ids for third party APIs.
To use this feature, pass --define <key> <value>
on the build command, and import from the key in page JavaScript:
src/index-page.jsx
import { Page } from '@nakedjsx/core/page'
import buildValue from 'BUILD_KEY'
const BodyContent =
() =>
<>
<h1>Definition Injection</h1>
<p>The following was passed from the build command: {buildValue}</p>
</>
Page.Create('en');
Page.AppendBody(<BodyContent />);
Page.Render();
# build command
$ npx nakedjsx src --out out --define BUILD_KEY BUILD_VALUE --pretty
out/index.html (196 bytes) (open in new tab)
<!doctype html>
<html lang="en">
<head></head>
<body>
<h1>Definition Injection</h1>
<p>The following was passed from the build command: BUILD_VALUE</p>
</body>
</html>
Import Source Path Aliases
NakedJSX supports import source path aliases, making it easier to manage larger projects.
To use this feature, pass --path-alias <alias> <path>
on the build command, and then use the alias at the start of your import string:
lib/something.mjs
export const something = 'a string imported via a path alias.';
src/index-page.jsx
import { Page } from '@nakedjsx/core/page'
import { something } from '$LIB/something.mjs'
Page.Create('en');
Page.AppendBody(
<>
<h1>Path Alias</h1>
<p>Here is something: {something}</p>
</>
);
Page.Render();
# build command
$ npx nakedjsx src --out out --path-alias $LIB lib --pretty
out/index.html (180 bytes) (open in new tab)
<!doctype html>
<html lang="en">
<head></head>
<body>
<h1>Path Alias</h1>
<p>Here is something: a string imported via a path alias.</p>
</body>
</html>
JSX
JSX is a HTML-like extension to the JavaScript language, and all due respect must be paid to the people at Meta who designed it. It's a fabulous way to create reusable HTML templates.
For those not familiar, Meta's legacy Introducing JSX page serves as a good introduction to JSX.
Props and Children
If you have used JSX before, props and children work as you would expect. For example:
src/index-page.jsx
import { Page } from '@nakedjsx/core/page'
const Section =
({ title, children }) =>
<>
<h2>{title}</h2>
{children}
</>
Page.Create('en');
Page.AppendBody(
<>
<h1>Using JSX</h1>
<Section title="Properties" />
<Section title="and Children">
<p>Work,</p>
<p>as expected.</p>
</Section>
</>
);
Page.Render();
is rendered as follows:
# build command
$ npx nakedjsx src --out out --pretty
out/index.html (216 bytes) (open in new tab)
<!doctype html>
<html lang="en">
<head></head>
<body>
<h1>Using JSX</h1>
<h2>Properties</h2>
<h2>and Children</h2>
<p>Work,</p>
<p>as expected.</p>
</body>
</html>
Note how the <Section>
implementation was able to dynamically set the heading title and selectively place its children.
Fragments
Note the <>
... </>
JSX 'fragment' syntax. Fragments allow a flat list of JSX elements to be passed around in code in the same way that a single element can be.
Essentially, a tag implementation needs to return either a single top level element like a <div>
, or a fragment. The returned element / frament can itself have as many children as you like.
Exporting and Importing
JSX functions compile down to functions, and you can export and import them like any other function. A refactored version of the previous example might be split into two files like this:
src/common.jsx
export const Section =
({ title, children }) =>
<>
<h2>{title}</h2>
{children}
</>
src/index-page.jsx
import { Page } from '@nakedjsx/core/page'
import { Section } from './common.jsx'
Page.Create('en');
Page.AppendBody(
<>
<h1>Using JSX</h1>
<Section title="Properties" />
<Section title="and Children">
<p>Work,</p>
<p>as expected.</p>
</Section>
</>
);
Page.Render();
Note that the Section
tag has been imported from common.jsx.
In this way, libraries of reusable components can be easily shared by multiple pages, or even published in an npm package.
Conditional Rendering
The usual JSX conditional rendering tricks work. In the following example, the <h2>
title will only render if a title prop is supplied on the <Section>
tag:
src/common.jsx
export const Section =
({ title, children }) =>
<>
{title && <h2>{title}</h2>}
{children}
</>
Refs
There are times when it is helpful to capture a reference to a created element, and add more children to it later. One use case is to build a table of contents as content is added below:
src/index-page.jsx
import { Page } from '@nakedjsx/core/page'
// Create an empty ref - we'll bind it to an element later
const tocList = Page.RefCreate();
const TocEntry =
({ title, path }) =>
<li>
<a href={'#' + path}>{title}</a>
</li>;
const Section =
({ title, path, children }) =>
{
// A section is being added, so create a link in the table of contents.
tocList.appendJsx(<TocEntry title={title} path={path} />);
return <div id={path}>
<h2>{title}</h2>
{children}
</div>
}
Page.Create('en');
Page.AppendBody(
<>
<h1>Refs</h1>
{/* Capture a reference to the nav ul element using the magic 'ref' prop. */}
<nav><ul ref={tocList} /></nav>
<hr/>
<Section title="Section One" path="section-one">
This is section one.
</Section>
<Section title="Section Two" path="section-two">
This is section two.
</Section>
</>
);
Page.Render();
A Ref is created by calling Page.RefCreate()
, and it is later bound to the <ul>
element by passing it as a ref
prop.
Then, as each Section tag is created, a new TocEntry is added to the <ul>
using tocList.appendJsx()
.
The result:
out/index.html (535 bytes) (open in new tab)
<!doctype html>
<html lang="en">
<head></head>
<body>
<h1>Refs</h1>
<nav>
<ul>
<li><a href="#section-one">Section One</a></li>
<li><a href="#section-two">Section Two</a></li>
</ul>
</nav>
<hr />
<div id="section-one">
<h2>Section One</h2>
This is section one.
</div>
<div id="section-two">
<h2>Section Two</h2>
This is section two.
</div>
</body>
</html>
This documentation uses a similar approach to build the topic list.
Context - Parent to Child
Another useful feature allows parent elements to pass data to child elements using the built-in context
prop.
In this example, a Section
tag uses context to determine which heading tag to use:
src/tags.jsx
const Heading =
({ depth, children, ...props }) =>
{
if (depth == 1)
return <h1 {...props}>{children}</h1>
if (depth == 2)
return <h2 {...props}>{children}</h2>
if (depth == 3)
return <h3 {...props}>{children}</h3>
if (depth == 4)
return <h4 {...props}>{children}</h4>
if (depth == 5)
return <h5 {...props}>{children}</h5>
else
return <h6 {...props}>{children}</h6>
}
export const Section =
({ title, path, context, children }) =>
{
// Start a depth of 2, and increment for each nested section.
context.depth = context.depth ? context.depth + 1 : 2;
return <div css="margin-left: 32px">
<Heading depth={context.depth}>{title}</Heading>
{children}
</div>
}
src/index-page.jsx
import { Page } from '@nakedjsx/core/page'
import { Section } from './tags.jsx'
Page.Create('en');
Page.AppendBody(
<>
<h1>Context</h1>
<Section title="h2: Section 1">
<Section title="h3: Section 1.1">
<Section title="h4: Section 1.1.1">
</Section>
</Section>
<Section title="h3: Section 1.2">
</Section>
</Section>
<Section title="h2: Section 2">
</Section>
</>
);
Page.Render();
out/index.html (547 bytes) (open in new tab)
<!doctype html>
<html lang="en">
<head>
<style>
.a {
margin-left: 32px;
}
</style>
</head>
<body>
<h1>Context</h1>
<div class="a">
<h2>h2: Section 1</h2>
<div class="a">
<h3>h3: Section 1.1</h3>
<div class="a"><h4>h4: Section 1.1.1</h4></div>
</div>
<div class="a"><h3>h3: Section 1.2</h3></div>
</div>
<div class="a"><h2>h2: Section 2</h2></div>
</body>
</html>
Note that context.depth
is never directly decremented, and yet the correct heading tag is used in each case. This is because changes made to the context object itself are never visible to parent elements.
Context - Child to Parent
There are cases in which it is useful for children to make data available to their parents. Achieving this requires two things:
- A mutable object or array in the parent context
- A way to control when children are evaluated
Here is an example of a <Section>
tag that adds a 'Back to top' link at the end of each section that has no subsections. This avoids, for example, a top level section with one child section adding two consequative 'Return to Top' links to the page.
src/tags.jsx
import { Page } from '@nakedjsx/core/page'
export const Section =
({ title, context, children }) =>
{
// If a parent section provided context,
// let it know it has a subsection.
if (context.has)
context.has.subsection = true;
// Provide a context to child sections
context.has = { subsection: false };
// Evaluate children tags immediately.
// After this, context.has.children is valid.
children = Page.EvaluateNow(children);
const result =
<div css={'margin-left: 48px'}>
<strong>{title}</strong>
<p>
This section has {
context.has.subsection
? 'at least one subsection.'
: 'no subsections.'
}
</p>
{children}
{!context.has.subsection &&
<p><a href="#top">Back to Top.</a></p>
}
</div>
return result;
}
src/index-page.jsx
import { Page } from '@nakedjsx/core/page'
import { Section } from './tags.jsx'
Page.Create('en');
Page.AppendBody(
<>
<h1 id="top">Context (Evaluate Now)</h1>
<Section title="Section 1">
<Section title="Section 1.1">
<Section title="Section 1.1.1" />
<Section title="Section 1.1.2">
<Section title="Section 1.1.2.1" />
</Section>
</Section>
<Section title="Section 1.2">
</Section>
</Section>
<Section title="Section 2">
</Section>
</>
);
Page.Render();
out/index.html (1562 bytes) (open in new tab)
<!doctype html>
<html lang="en">
<head>
<style>
.a {
margin-left: 48px;
}
</style>
</head>
<body>
<h1 id="top">Context (Evaluate Now)</h1>
<div class="a">
<strong>Section 1</strong>
<p>This section has at least one subsection.</p>
<div class="a">
<strong>Section 1.1</strong>
<p>This section has at least one subsection.</p>
<div class="a">
<strong>Section 1.1.1</strong>
<p>This section has no subsections.</p>
<p><a href="#top">Back to Top.</a></p>
</div>
<div class="a">
<strong>Section 1.1.2</strong>
<p>This section has at least one subsection.</p>
<div class="a">
<strong>Section 1.1.2.1</strong>
<p>This section has no subsections.</p>
<p><a href="#top">Back to Top.</a></p>
</div>
</div>
</div>
<div class="a">
<strong>Section 1.2</strong>
<p>This section has no subsections.</p>
<p><a href="#top">Back to Top.</a></p>
</div>
</div>
<div class="a">
<strong>Section 2</strong>
<p>This section has no subsections.</p>
<p><a href="#top">Back to Top.</a></p>
</div>
</body>
</html>
A another use of this feature can be seen in the <Example> tag used to compile the examples in this documentation.
CSS
NakedJSX is CSS aware, and injects an inline <style>
tag in the document head that contains classes generated by NakedJSX features.
Scoped CSS
NakedJSX will extract CSS classes based on a css
JSX prop.
If two different JSX functions contain equivalent CSS, they will share a class in the final output:
src/index-page.jsx
import { Page } from '@nakedjsx/core/page'
const Section =
({ title, children }) =>
<>
<h2 css="color: fuchsia">{title}</h2>
{children}
</>
Page.Create('en');
Page.AppendBody(
<>
<Section title="Fuchsia Title">
<p css="color: #ff00ff">Fuchsia Content.</p>
</Section>
</>
);
Page.Render();
out/index.html (263 bytes) (open in new tab)
<!doctype html>
<html lang="en">
<head>
<style>
.a {
color: #f0f;
}
</style>
</head>
<body>
<h2 class="a">Fuchsia Title</h2>
<p class="a">Fuchsia Content.</p>
</body>
</html>
In this case the CSS compiler produces color: #f0f
from both color: fuchsia
and color: #ff00ff
, so a single CSS class is shared by both elements.
Nested CSS
NakedJSX supports CSS nesting syntax, with automatic conversion to browser compatible CSS:
src/index-page.jsx
import { Page } from '@nakedjsx/core/page'
Page.Create('en');
Page.AppendBody(
<>
<h1>Nested CSS</h1>
<ul css={`
list-style-type: upper-roman;
& li {
line-height: 1.5
}
`}>
<li>Item one</li>
<li>Item two</li>
<li>Item three</li>
<li>Item four</li>
</ul>
</>
);
Page.Render();
out/index.html (453 bytes) (open in new tab)
<!doctype html>
<html lang="en">
<head>
<style>
.a {
list-style-type: upper-roman;
}
.a li {
line-height: 1.5;
}
</style>
</head>
<body>
<h1>Nested CSS</h1>
<ul class="a">
<li>Item one</li>
<li>Item two</li>
<li>Item three</li>
<li>Item four</li>
</ul>
</body>
</html>
Page.AppendCss()
Raw CSS can be added by passing a string containing CSS to the Page.AppendCss()
function.
This is particularly useful for incorporating CSS imported from a file as a raw asset. When NakedJSX knows what CSS us being used, it can avoid generating CSS classes with names that are already in use.
src/index-page.jsx
import { Page } from '@nakedjsx/core/page'
Page.Create('en');
Page.AppendCss(`
html {
font-family: sans-serif;
}
`);
Page.AppendBody(
<>
<h1>Phew.</h1>
<p>Those yucky serifs are gone.</p>
</>
);
Page.Render();
out/index.html (261 bytes) (open in new tab)
<!doctype html>
<html lang="en">
<head>
<style>
html {
font-family: sans-serif;
}
</style>
</head>
<body>
<h1>Phew.</h1>
<p>Those yucky serifs are gone.</p>
</body>
</html>
Common CSS File
Document default CSS and common utility classes can be placed in a common CSS file that is added to all pages. This requires building with an additional flag (but don't forget that build flags can be saved into a config file):
src/style.css
html {
font-family: sans-serif;
}
body {
font-size: 1.125rem;
}
src/index-page.jsx
import { Page } from '@nakedjsx/core/page'
Page.Create('en');
Page.AppendBody(
<>
<h1>Hello again, NakedJSX</h1>
<p>
A minimal NakedJSX example,
with a common CSS file.
</p>
</>
);
Page.Render();
built with the following command:
# build command
$ npx nakedjsx src --out out --css-common src/style.css --pretty
out/index.html (370 bytes) (open in new tab)
<!doctype html>
<html lang="en">
<head>
<style>
html {
font-family: sans-serif;
}
body {
font-size: 1.125rem;
}
</style>
</head>
<body>
<h1>Hello again, NakedJSX</h1>
<p>A minimal NakedJSX example, with a common CSS file.</p>
</body>
</html>
Assets
Assets files such as images, JSON, CSS can be added to a build via a JavaScript import statment.
Optionally, the asset file can be processed during import by a NakedJSX plugin and it's simple to make your own plugins.
Arbitrary Files
NakedJSX allows you to publish and link to arbitrary files via the asset system.
To import a file as a generic asset, use a regular JavaScript import statement but prefix the import path with ::
, like this:
import circleHref from '::./circle.svg'
This will cause NakedJSX to place a copy of ./circle.svg
in the asset directory in the build output folder, and circleHref
will be a relative URL to it.
For example:
src/circle.svg
<svg width="64" height="64" xmlns="http://www.w3.org/2000/svg">
<circle cx="32" cy="32" r="30" fill="#fff" stroke="#000" stroke-width="2" />
</svg>
src/index-page.jsx
import { Page } from '@nakedjsx/core/page'
import circleHref from '::./circle.svg'
Page.Create('en');
Page.AppendBody(
<>
<h1>Title</h1>
<p><img src={circleHref} /></p>
</>
);
Page.Render();
produces the following:
out/asset/circle.oMMrymZnZTYGC05OM9Rrf5H5Yj4.svg (151 bytes) (open in new tab)
<svg width="64" height="64" xmlns="http://www.w3.org/2000/svg">
<circle cx="32" cy="32" r="30" fill="#fff" stroke="#000" stroke-width="2" />
</svg>
out/index.html (179 bytes) (open in new tab)
<!doctype html>
<html lang="en">
<head></head>
<body>
<h1>Title</h1>
<p><img src="asset/circle.oMMrymZnZTYGC05OM9Rrf5H5Yj4.svg" /></p>
</body>
</html>
JSON Data
If you have JSON data in a file, you can import it using a :json:
asset import like this:
src/data.json
{
"Australia":
{
"population": 26357171,
"updated": "Monday, May 29, 2023"
}
}
src/index-page.jsx
import { Page } from '@nakedjsx/core/page'
import data from ':json:./data.json'
Page.Create('en');
Page.AppendBody(
<>
<h1>Population of Australia</h1>
<p>
{data.Australia.population} as of {data.Australia.updated}.
</p>
</>
);
Page.Render();
the result:
out/index.html (175 bytes) (open in new tab)
<!doctype html>
<html lang="en">
<head></head>
<body>
<h1>Population of Australia</h1>
<p>26357171 as of Monday, May 29, 2023.</p>
</body>
</html>
Raw Asset String
Sometimes it is desireable to embed the content of an asset itself in your page JS, as this can save a round trip to the server. NakedJSX includes a :raw:
import plugin that returns a string containing the contents of a specified asset file.
That content can then be placed directly into the document without further processing, using the built-in <raw-content>
tag.
src/circle.svg
<svg width="64" height="64" xmlns="http://www.w3.org/2000/svg">
<circle cx="32" cy="32" r="30" fill="#fff" stroke="#000" stroke-width="2" />
</svg>
src/index-page.jsx
import { Page } from '@nakedjsx/core/page'
import circleRaw from ':raw:./circle.svg'
Page.Create('en');
Page.AppendBody(
<>
<h1>Title</h1>
<raw-content content={circleRaw} />
</>
);
Page.Render();
with the result:
out/index.html (389 bytes) (open in new tab)
<!doctype html>
<html lang="en">
<head></head>
<body>
<h1>Title</h1>
<svg width="64" height="64" xmlns="http://www.w3.org/2000/svg">
<circle
cx="32"
cy="32"
r="30"
fill="#fff"
stroke="#000"
stroke-width="2"
/>
</svg>
</body>
</html>
Raw Asset Buffer
The :raw:
plugin also supports importing an asset as a Buffer object rather than as a utf-8 string. To do this, add a query string of ?as=Buffer
:
src/circle.svg
<svg width="64" height="64" xmlns="http://www.w3.org/2000/svg">
<circle cx="32" cy="32" r="30" fill="#fff" stroke="#000" stroke-width="2" />
</svg>
src/index-page.jsx
import { Page } from '@nakedjsx/core/page'
import circleRawBuffer from ':raw:./circle.svg?as=Buffer'
Page.Create('en');
Page.AppendBody(
<>
<h1>Raw Buffer</h1>
<pre><code>{
JSON.stringify(circleRawBuffer.toJSON())
}</code></pre>
<pre><code>{
circleRawBuffer.toString()
}</code></pre>
</>
);
Page.Render();
with the result (best viewed via 'open in new tab'):
out/index.html (1011 bytes) (open in new tab)
<!doctype html>
<html lang="en">
<head></head>
<body>
<h1>Raw Buffer</h1>
<pre><code>{"type":"Buffer","data":[60,115,118,103,32,119,105,100,116,104,61,34,54,52,34,32,104,101,105,103,104,116,61,34,54,52,34,32,120,109,108,110,115,61,34,104,116,116,112,58,47,47,119,119,119,46,119,51,46,111,114,103,47,50,48,48,48,47,115,118,103,34,62,10,32,32,32,32,60,99,105,114,99,108,101,32,99,120,61,34,51,50,34,32,99,121,61,34,51,50,34,32,114,61,34,51,48,34,32,102,105,108,108,61,34,35,102,102,102,34,32,115,116,114,111,107,101,61,34,35,48,48,48,34,32,115,116,114,111,107,101,45,119,105,100,116,104,61,34,50,34,32,47,62,10,60,47,115,118,103,62]}</code></pre>
<pre><code><svg width="64" height="64" xmlns="http://www.w3.org/2000/svg">
<circle cx="32" cy="32" r="30" fill="#fff" stroke="#000" stroke-width="2" />
</svg></code></pre>
</body>
</html>
Client Javascript
NakedJSX can compile client JavaScript to execute in the browser, which has two distinct advantages over linking to externally built JavaScript:
- NakedJSX compiled client JavaScript can use inline JSX
- The scoped CSS deduplication system allows client and page JSX to share generated classes
By default, compiled client JavaScript is placed in a <script>
tag at the end of the <body>
. It can also be configured to place it in an asset file, with a <script>
linking to it automatically added to the document <head>
.
There are several ways to get NakedJSX to compile client JavaScript:
- A dedicated
*-client.mjs
file - The
Page.AppendJs()
function - The
Page.AppendJsIfNew()
function - Adding inline event handlers to JSX elements
- The
Page.AppendJsCall()
function
A dedicated file is usually the right choice for large amounts of code, with the other methods used for event handlers, support functions needed by custom JSX tags, and other snippets.
Adding via Dedicated File
Files matching the pattern *-client.mjs
are automatically compiled and placed into a script tag in the HTML generated by the corresponing *-page.jsx
.
Modern JavaScript can be used, with the result being transpiled to a browser-compatible format when necessary.
src/index-client.js
var p = document.getElementById('click-me');
var clickCounter = 0;
p.onclick =
() =>
{
p.appendChild(document.createElement('br'));
p.appendChild(document.createTextNode(`Click ${++clickCounter}: This content was dynamically added to the DOM.`));
};
src/index-page.jsx
import { Page } from '@nakedjsx/core/page'
Page.Create('en');
Page.AppendBody(
<>
<h1>Title</h1>
<p id="click-me">Click Me!</p>
</>
);
Page.Render();
As you can see, the JavaScript output is minified (although it has been formatted a little because we built with --pretty
):
out/index.html (615 bytes) (open in new tab)
<!doctype html>
<html lang="en">
<head></head>
<body>
<h1>Title</h1>
<p id="click-me">Click Me!</p>
<script>
'use strict'
var e = document.getElementById('click-me'),
t = 0
e.onclick = () => {
e.appendChild(document.createElement('br')),
e.appendChild(
document.createTextNode(
`Click ${++t}: This content was dynamically added to the DOM.`,
),
)
}
</script>
</body>
</html>
Minification is great for production builds, but not during development. In development mode (--dev
) minification is disabled and sourcemaps are generated, providing a comfortable debugging experience.
Adding via Page.AppendJs()
Page.AppendJs(...code)
adds JavaScript to the page for the browser to execute. If multiple arguments are supplied, each will be added to the client JavaScript in turn.
A string argument containing JavaScript code is supported, however it is usually a better developer experience to pass source directly to Page.AppendJs()
. The page JavaScript compilation process will automatically convert code passed this way to strings of code. For example:
Page.AppendJs(
alert('magic!'),
document.write('code')
);
Page.AppendJs(console.log('transformation'))
Becomes:
Page.AppendJs("alert('magic!')", "document.write('code')");
Page.AppendJs("console.log('transformation')");
The code has been converted to strings of code, which are later compiled as client JavaScript. This ultimately results in this <script>
being placed the HTML file:
<script>
alert('magic!');
document.write('code');
console.log('transformation');
</script>
Appending blocks of code
If you directly add an anonymous or arrow function, then each statement in the body of that function will be added to the top level scope of the client JavaScript. If an unnamed function was added to the client JavaScript there would be no way to invoke it, so NakedJSX repurposes the syntax:
src/index-page.jsx
import { Page } from '@nakedjsx/core/page'
Page.Create('en');
Page.AppendJs(
function()
{
document.write('zero');
});
Page.AppendJs(
() =>
{
document.write(' one');
document.write(' two');
});
Page.AppendJs(() => document.write(' three'));
Page.AppendBody(<h1>Anon / Arrow Function</h1>);
Page.Render();
out/index.html (338 bytes) (open in new tab)
<!doctype html>
<html lang="en">
<head></head>
<body>
<h1>Anon / Arrow Function</h1>
<script>
'use strict'
document.write('zero'),
document.write(' one'),
document.write(' two'),
document.write(' three')
</script>
</body>
</html>
Adding via Inline Event Handlers
It's also possible to add client JavaScript using classic inline event handlers on JSX elements:
src/index-page.jsx
import { Page } from '@nakedjsx/core/page'
Page.Create('en');
Page.AppendJs(
function clicked()
{
alert('You clicked!');
});
Page.AppendBody(
<>
<h1>Event Handler</h1>
<p onClick="clicked()">Click Me!</p>
</>
);
Page.Render();
out/index.html (313 bytes) (open in new tab)
<!doctype html>
<html lang="en">
<head></head>
<body>
<h1>Event Handler</h1>
<p onclick="clicked()">Click Me!</p>
<script>
'use strict'
window.clicked = function () {
alert('You clicked!')
}
</script>
</body>
</html>
The minified output is a little different here, as clicked()
has not been renamed to something shorter. Currently, NakedJSX will prevent the renaming of an identifier that is used in an inline event handler. The toplevel function has also been set as a window property to prevent the usual tree shaking process from removing the seemingly unused function. These inefficiencies will be addressed in future updates.
Adding via Page.AppendJsCall()
A common pattern is to pass a named function to Page.AppendJs()
, and then generate a series of calls to that function with differing arguments.
While it is possible to manually generate a JavaScript string containg the calls and then pass that string to Page.AppendJs()
, NakedJSX provides an easier way.
Page.AppendJsCall(functionName, ...args)
accepts the name of a function followed by a series of arguments. It generates and then appends client JavaScript that will call that function with those arguments. Here is a contrived example:
src/index-page.jsx
import { Page } from '@nakedjsx/core/page'
Page.Create('en');
Page.AppendJs(
function type(thing)
{
const t = typeof thing;
if (t !== 'object')
return t;
return Array.isArray(thing) ? 'array' : t;
});
Page.AppendJs(
function clientLog(...args)
{
const pre = document.getElementById('parent');
for (const arg of args)
pre.innerText += `${type(arg)} arg: ${JSON.stringify(arg)}
`;
});
Page.AppendJsCall('clientLog', 'one', 2, ['three'], { four: 4 });
Page.AppendJsCall('clientLog', 5.5);
Page.AppendBody(
<>
<h1>AppendJsCall</h1>
<pre id="parent" />
</>
);
Page.Render();
out/index.html (625 bytes) (open in new tab)
<!doctype html>
<html lang="en">
<head></head>
<body>
<h1>AppendJsCall</h1>
<pre id="parent"></pre>
<script>
'use strict'
function t(t) {
const n = typeof t
return 'object' !== n ? n : Array.isArray(t) ? 'array' : n
}
function n(...n) {
const r = document.getElementById('parent')
for (const e of n)
r.innerText += `${t(e)} arg: ${JSON.stringify(e)}\n`
}
n('one', 2, ['three'], { four: 4 }), n(5.5)
</script>
</body>
</html>
Using JSX in Client JavaScript
Client JavaScript can also use JSX. Props are supported, as are scoped and nested CSS. Extracted CSS classes are deduplicated with those used by the HTML.
Refs and context are not currently supported in client JavaScript.
Here is a version of an earlier example converted to use JSX:
src/index-client.js
const JsxTag =
({ count }) =>
<>
<br/>
Click {`${count}`}: This
<span css="color: fuchsia"> JSX </span>
content was dynamically added to the DOM.
</>
var clickCounter = 0;
src/index-page.jsx
import { Page } from '@nakedjsx/core/page'
Page.Create('en');
Page.AppendBody(
<>
<h1 css="color: fuchsia">Title</h1>
<p onClick="this.appendChild(<JsxTag count={++clickCounter}/>); console.log(this); console.log({ hello: world, self: this })">Click Me!</p>
</>
);
Page.Render();
The client JSX is compiled down to JavaScript that creates the necessary DOM elements and sets their attributes.
About half a kilobyte is added for the DOM element construction runtime.
The browser Element.prototype.appendChild()
implementation is patched to add support for adding an array of elements. Without this, this example would have needed to iterate over the JSX fragment returned by <JsxTag />
.
out/index.html (2055 bytes) (open in new tab)
<!doctype html>
<html lang="en">
<head>
<style>
.a {
color: #f0f;
}
</style>
</head>
<body>
<h1 class="a">Title</h1>
<p onClick="__nakedjsx_event_handler_a.call(this,event)">Click Me!</p>
<script>
'use strict'
const t = Element.prototype.appendChild
function n(t, n, ...e) {
if (((n = n || {}), 'function' == typeof t))
return (n.children = e), t(n)
const o = document.createElement(t)
for (const [t, e] of Object.entries(n)) {
if (t.startsWith('on')) {
const n = t.toLowerCase()
if (n in window) {
o.addEventListener(n.substring(2), e)
continue
}
}
o.setAttribute(t, e)
}
for (const t of e) o.appendChild(t)
return o
}
Element.prototype.appendChild = function (n) {
if (Array.isArray(n)) {
for (const t of n) this.appendChild(t)
return n
}
return 'string' == typeof n
? t.call(this, document.createTextNode(n))
: n
? t.call(this, n)
: void 0
}
const e = ({ count: t }) => [
n('br', null),
'Click ',
`${t}`,
': This',
n('span', { class: 'a' }, ' JSX '),
'content was dynamically added to the DOM.',
]
var o = 0
window.__nakedjsx_event_handler_a = function (t) {
this.appendChild(n(e, { count: ++o })),
console.log(this),
console.log({ hello: world, self: this })
}
</script>
</body>
</html>
Multiple Pages
Used as it has been so far in this guide, NakedJSX will generate a single HTML page for each *-page.jsx
within the project source directory.
However, it is perfectly valid for a single *-page.jsx
file to generate multiple pages, or even no page at all.
Important: while a single *-page.jsx
file can produce multiple output pages, the Page API can only work on a single page between each pair of Page.Create() and Page.Render() calls.
Hardcoded
The simplest way to create multiple pages from a single file is to simply add more calls to the Page API and override the output filename, like this:
src/index-page.jsx
import { Page } from '@nakedjsx/core/page'
const BodyContent =
({ title, children }) =>
<>
<h1>{title}</h1>
{children}
</>
// Make a HTML page, and override the default filename
Page.Create('en');
Page.AppendBody(
<BodyContent title="Output File One">
<p>This is the content for output file one.</p>
</BodyContent>
);
Page.Render('index-one.html');
// Now make another page!
Page.Create('en');
Page.AppendBody(
<BodyContent title="Output File Two">
<p>This is the content for output file two.</p>
</BodyContent>
);
Page.Render('index-two.html');
Note that a filename is being passed to Page.Render()
to override the default filename.
out/index-one.html (171 bytes) (open in new tab)
<!doctype html>
<html lang="en">
<head></head>
<body>
<h1>Output File One</h1>
<p>This is the content for output file one.</p>
</body>
</html>
out/index-two.html (171 bytes) (open in new tab)
<!doctype html>
<html lang="en">
<head></head>
<body>
<h1>Output File Two</h1>
<p>This is the content for output file two.</p>
</body>
</html>
From Data at Build-Time
It is also possible fetch data at build time and use it to generate an arbitrary number of pages.
Simply fetch the data at the top level scope in the page JavaScript, awaiting as needed, then generate your Page.* API calls in a loop.
TODO: example
From Source Generated at Compile-Time
It is possible to fetch or construct sources (JSX / MDX / JavaScript / etc.) at compile time. These sources are then compiled by NakedJSX, with full support for scoped CSS, asset import plugins and anything else that static sources can do.
This requires the use of the dyanmic
built-in plugin. This plugin requires a source file that is executed at compile time, and which returns JavaScript source for NakedJSX to compile.
This example generates a NakedJSX page for each of an arbitray number of MDX files. The dynamic JavaScript file scans a folder for MDX files, generates a JavaScript import statement of each via the @nakedjsx/plugin-asset-mdx
plugin, and returns an array of JSX:
TODO: example
Plugins
Plugins can be enabled by passing --plugin <alias> <plugin-package-name-or-path>
on the command line.
Each plugin loaded must be given a unique alias. For an asset import plugin, the alias correspondes to the label between ::
characters in an asset import string.
The npx nakedjsx
command bundles in @nakedjsx/plugin-asset-image and @nakedjsx/plugin-asset-prism, but other plugins need to be installed either globally (npm install -g
) or locally (npm install
) in the project source directory or a parent of it. You can also pass a path to a JavaScript file that implements the plugin interface.
@nakedjsx/plugin-asset-image
Generate a <picture>
HTML tag with multi-resolution webp & jpeg sourcesets from an image file.
Documentation for @nakedjsx/plugin-asset-image.
@nakedjsx/plugin-asset-prism
Provides a <PrismCode>
tag that uses Prism to render source code to HTML with syntax highlighting, and an optional CSS theme that handles both light and dark modes. Handy for creating technical documentation sites.
Documentation for @nakedjsx/plugin-asset-prism.
@nakedjsx/plugin-asset-mdx
This plugin allows MDX files to be imported and used as JSX tags within NakedJSX Pages.
Documentation for @nakedjsx/plugin-asset-mdx.
Plugin Development Guide
Development of asset plugins is straightforward. Create project specific plugins, or publish them in npm packages for others to use.
Cookbook
Using JSX with jQuery
With NakedJSX, you can pass inline JSX directly to jQuery functions. See the NakedJSX jQuery cookbook entry for an example.