Custom components
There are four types of custom components in Nue:
Markdown extensions: These extend the Markdown syntax with custom tags, offering more flexibility in content formatting.
Layout modules: These components fill the slots in the page layout, helping create a structured design for your pages.
Layout components: These are custom server-side components used within layout modules, allowing for dynamic content rendering.
Interactive islands: These are rendered on the client side (CSR) and enhance user interaction with minimal JavaScript. Here’s an example:
Example island
Nue makes it easy to build interactive components like this:
Like React, but semantic
Nue components offer React-like functionality while focusing on semantic web design. They work seamlessly on both the server and client sides, allowing developers to enhance applications progressively without losing structure.
Unlike React, which relies heavily on JavaScript, Nue is based on HTML. Any valid HTML in Nue is also a valid component, making it simple and accessible.
<div class="{ type }">
<img src="{ img }">
<aside>
<h3>{ title }</h3>
<p :if="desc">{ desc }</p>
<slot/>
</aside>
</div>
Block assembly language
Nue components are named HTML fragments that can be looped and rendered conditionally, enabling nesting within other components. Assign a component name using the @name
attribute:
<div ="media-object" class="{ class }">
<img src="{ img }">
<aside>
<h3>{ title }</h3>
<p>{ desc }</p>
</aside>
</div>
Once named, components can be nested inside one another to form more complex applications and tree-like structures. For example:
<section ="image-gallery" class="gallery">
<header>
<h1>{ title }</h1>
<p>{ desc }</p>
</header>
<!-- media-object looped -->
<media-object :for="item in items" :bind="item"/>
</section>
Component libraries
Server-side components are saved with a .html
extension, while client-side components or islands use a .dhtml
or .htm
extension. You can group related components together in the same file to create a cohesive component library.
Components can be stored at three levels: globally, area-level, or page-level. These components are automatically aware of each other, allowing you to reuse them in different files without the need for explicit import statements.
To explicitly include components from a library folder, you can use the include statement. Components cascade similarly to CSS files, enabling a streamlined approach to component management.
Mounting
Once a component is available on the page, mounting it is straightforward:
In Markdown content
Custom components are mounted in Markdown content just like the built-in content tags, using square brackets:
[]
// with "heroic" styling
[.heroic]
In layout modules
In layout modules, components are mounted as custom HTML elements:
<image-gallery/>
<!-- with "heroic" styling -->
<image-gallery class="heroic"/>
Islands
If a component is not defined server-side in a .html
file, it is rendered directly as a custom element on the client side. For example:
<!-- this gets rendered on the client-side -->
<image-gallery custom="image-gallery"/>
In this case, Nue will first look for an implementation of the component or island defined in a .dhtml
file. If it is not found, a standard Web Component will be mounted as specified in a .js
or .ts
file.
Passing data
You can pass data to your components using attributes. These attributes can either be direct values or reference data from the unstructured data when the attribute name starts with a colon
Markdown example
In Markdown content, you can pass data as follows:
// application data (items) vs direct value (1)
[ :items="screenshots" index="1"]
In this example, :items
references the screenshots
array from the application data, while index
is a direct value set to 1
.
HTML component example
In layout modules and interactive islands, you can similarly pass data as arguments:
<image-gallery :items="products" index="2"/>
Here, :items
pulls from the products
data, and index
is set to 2
. This allows the component to dynamically render based on the provided data while keeping your templates clean and maintainable.
Islands
Client-side components, such as islands, receive data through nested JSON. This enables you to encapsulate data directly within the component:
<image-gallery custom="image-gallery">
<script type="application/json">
{
"items": [...],
"index": 2
}
</script>
</image-gallery>
In this structure, the JSON data is embedded within a <script>
tag of type application/json
, allowing the component to access it seamlessly on the client side.
Nested HTML and slots
Slots enable you to build highly reusable, multi-purpose components by allowing a parent component to inherit functionality from a child component. Here’s how it works:
Parent component
The parent component defines a structure that includes a slot for nested content:
<!-- parent component -->
<div class="{ type }">
<img src="{ img }">
<aside>
<h3>{ title }</h3>
<p>{ desc }</p>
<slot/>
</aside>
</div>
Passing custom content
You can pass custom content to the parent component through the slot:
<media-object>
<!-- the <slot/> is replaced with this nested markup -->
<h4>{ price }</h4>
<button>Add to cart</button>
</media-object>
In this example, the <slot/>
element in the media-object
is replaced with the nested markup. The nested content can include anything from text and HTML tags to other custom components, such as product ratings, comment sections, or product metadata.
Looping through nested content
You can also pass nested content within loops, allowing for dynamic rendering:
<media-object :for="item in items" :bind="item">
<h4>{ item.price }</h4>
<button>Add to cart</button>
</media-object>
Nested Markdown
Nue allows you to pass nested Markdown content to your components. For example, consider a custom background-video
component that includes nested table data:
[ :src="bgvideo"]
A brief overview of features and capabilities.
[]
Hot-reloading | Next.js | Nue
--------------------- | ------- | ---
Server components | × | ×
Client components | × | ×
Content | | ×
Styling | | ×
Data / metadata | | ×
Page changes | | ×
CSS errors | | ×
Simplified component implementation
Here’s a simplified implementation of the background-video
component:
<div ="background-video" class="bg-video">
<video :src/>
<!-- the Markdown generated HTML goes here -->
<slot/>
</div>
In this setup, the nested Markdown is processed and inserted into the <slot/>
within the background-video
component, allowing for rich content integration.
Instances
Both server-side and client-side components in Nue can be scripted before rendering on the page. The scripting API is inspired by ES6 classes, enabling each component to have properties and methods. You can reference these properties and call the methods directly from your template code.
Properties and methods
Properties and methods are defined inside a <script>
block that is a direct child of the component root. Here’s an example of a pretty-date
server-side component:
<time ="pretty-date" :datetime="toIso(date)">
{ pretty(date) }
<!-- Properties and methods -->
<script>
// Property to hold the locale for date formatting
locale = 'en-US';
// Constructor method runs when the component is created
constructor({ date, locale }) {
if (locale) this.locale = locale;
this.date = date;
}
// Method to format the date into a readable string
pretty(date) {
return date.toLocaleDateString(this.locale, {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
// Method to convert the date to ISO format
toIso(date) {
return date.toISOString().slice(0, 10);
}
</script>
</time>
How it works
In this example:
Properties allow you to set default values and dynamic values for rendering. For instance,
locale
is initialized to'en-US'
and can be overridden.The constructor method runs when the component is created, allowing you to manipulate user-provided data (like
date
andlocale
) before it is processed by the template.Methods are useful for formatting and transforming data. The
pretty
method formats the date for display, while thetoIso
method converts it to a standard format.
The scripting block helps to extract complex JavaScript logic from the HTML template block, resulting in cleaner and more readable markup. This is particularly important for interactive islands, where the amount of scripting is typically much higher.
Local scripts
You can set up local functions and variables using root-level script blocks:
<script>
let counter = 0; // Initialize a counter variable
</script>
<div ="comp-a">
<h3>A count: { counter }</h3>
<script>
constructor() {
this.counter = ++counter; // Increment counter and store it in this component
}
</script>
</div>
<div ="comp-b">
<h3>B count: { counter }</h3>
<script>
constructor() {
this.counter = ++counter; // Increment counter and store it in this component
}
</script>
</div>
Local variables and functions provide another way to decouple complex scripting logic from your components, allowing you to share functions and variables between components in the same file.
Note that currently, you can only use the import
statement in client-side components, but support for server-side imports is planned for the future.
Passthrough scripts
Sometimes you may want the script block to be executed directly by the browser. You can achieve this by using the type
, src
, or client
attributes, which instruct the compiler to pass the scripts directly to the client. For example:
<!-- Passed to the client directly -->
<script async src="https://www.googletagmanager.com/gtag/js?id=TAG_ID"></script>
<!-- Same here -->
<script type="text/javascript">
console.info({ hello: 'World' }); // Log a message to the console
</script>
You can also use a client
attribute in place of the traditional type="text/javascript"
:
<script client>
console.info('hey'); // Log a message to the console
</script>
The above will be rendered as:
<script>
console.info('hey'); // Log a message to the console
</script>
Interactive components
Interactive components in Nue are executed on the client side, directly within the user's browser. They are created and mounted using the same syntax as server-side components, but interactive components can respond to user input and re-render themselves to reflect new states. This functionality makes them ideal for a variety of applications, such as feedback forms, login forms, registration flows, account dropdowns, image galleries, or any other component that requires interactivity.
Example: Image Gallery
Let’s add a simple image gallery component to this page:
[]
images: [tomatoes.jpg, lemons.jpg, peas.jpg, popcorn.jpg]
basedir: /img
Here’s the source code for the gallery component:
<div ="image-gallery" class="image-gallery" translate="no">
<!-- Action to seek to the previous image -->
<a class="seek prev" ="index--" :if="index"></a>
<!-- The currently displayed image -->
<img src="{ basedir }/{ images[index] }">
<!-- Action to seek to the next image -->
<a class="seek next" ="index++" :if="index + 1 < images.length"></a>
<!-- The gray dots below the image -->
<nav>
<a :for="src, i in images" class="{ current: i == index }" ="index = i"></a>
</nav>
<!-- Scripting section -->
<script>
// Image index representing the component state
index = 0;
</script>
</div>
Inside the component, all control flow operations, such as loops and conditionals, are reactive — they respond to user events and re-render based on the new state. Here, we have a numeric state variable index
, which updates as the user clicks the navigational elements, automatically changing the displayed image accordingly.
Event handlers
In Nue, attributes starting with the @
symbol define event handlers. These handlers are JavaScript functions that respond to user interactions, such as clicks, keypresses, or mouse movements.
Inline handlers
Inline handlers are defined directly within the attribute:
<button ="count++">Increment</button>
Inline handlers are great for simple expressions that don’t require additional logic.
Method handlers
For more complex functionality, it's best to move the logic into an instance method:
<dialog>
<button ="close">Close</button>
<script>
close() {
this.root.close(); // Close the dialog
location.hash = ''; // Clear the URL hash
}
</script>
</dialog>
Method calls
You can pass arguments to method calls:
<div>
<button ="say('yo!')">Say yo!</button>
<script>
say(msg) {
console.log(msg); // Log the message to the console
}
</script>
</div>
Event argument
Method handlers always receive an Event object as the last argument, unless it is explicitly named $event
:
<div>
<button ="first">First</button>
<button ="second('Hello')">World</button>
<button ="third('Hello', $event, 'World')">Nue</button>
<script>
// prints "First"
first($event) {
console.info($event.target.textContent); // Log the button text
}
// prints "Hello World"
second(hey, $event) {
console.info(hey, $event.target.textContent); // Log hello and button text
}
// prints "Hello Nue World"
third(hey, $event, who) {
console.info(hey, $event.target.textContent, who); // Log all three
}
</script>
</div>
Event modifiers
Nue provides convenient shortcuts for common DOM event manipulation functions. For instance, @submit.prevent
is a shortcut to call event.preventDefault().
<!-- Prevent the default event from occurring -->
<form .prevent="onSubmit"></form>
<!-- Modifiers can be chained -->
<a .stop.prevent="doThat"></a>
<!-- Run the modifier only -->
<form .prevent></form>
The following modifiers are supported:
.prevent
: Prevents the default behavior of the event from occurring..stop
: Prevents further propagation of the event..self
: Only triggers the handler ifevent.target
is the element itself..once
: The event will be triggered at most once.
Key modifiers
Key modifiers bind the event handler to specific keyboard keys:
<!-- Only call `submit` when the `key` is `Enter` -->
<input .enter="submit">
You can use any valid key names from KeyboardEvent.key as modifiers, converting them to kebab-case. For example, the following handler is called only if event.key
is equal to PageDown
:
<input .page-down="onPageDown">
Nue provides aliases for commonly used keys:
.enter
: Captures bothEnter
andReturn.
.delete
: Captures bothDelete
andBackspace.
.esc
: Captures bothEsc
andEscape.
.space
: CapturesSpacebar
, " ",Space Bar.
.tab
: CapturesTab.
.up
: CapturesUp
andArrowUp.
.down
: CapturesDown
andArrowDown.
.left
: CapturesLeft
andArrowLeft.
.right
: CapturesRight
andArrowRight.
Dynamic arrays
When you define a loop with the :for
expression, Nue automatically detects if the looped array is mutated and triggers the necessary UI updates. The following array methods are supported:
push(item)
: Adds a new item to the end of the array.unshift(item)
: Adds a new item to the beginning of the array.sort(fn)
: Sorts items based on the given function.reverse()
: Reverses the order of items.splice(start, count)
: Removes items from the array.shift()
: Removes the first item from the array.pop()
: Removes the last item from the array.remove(item)
: A Nue-specific helper method to remove the given item from the array.
Replacing the array
Mutation methods modify the original array they are called on. Non-mutating methods, such as filter()
, concat()
, and slice()
, return a new array. In these cases, you should replace the old array with the new one, and Nue will render the updates accordingly:
search() {
this.items = this.items.filter(item => item.text.match(/Foo/));
}
Example: array.push
Here’s a simple demo of using an array:
[]
users:
- name: Alex Martinez
role: Lead frontend developer
img: /img/face-3.jpg
- name: Sarah Park
role: UI/UX Designer
img: /img/face-4.jpg
- name: Jamie Huang
role: JS/TS developer
img: /img/face-2.jpg
- name: Heidi Blum
role: UX developer
img: /img/face-1.jpg
- name: Adam Nattie
role: Backend developer
img: /img/face-5.jpg
- name: Mila Harrison
role: Senior frontend developer
img: /img/face-6.jpg
Here's the source code for the above demo:
<div ="array-demo" class="array-demo">
<button ="add" :disabled="items[5]">Add user</button>
<ul>
<li :for="el of items">
<img :src="el.img">
<h3>{ el.name }</h3>
<p>{ el.role }</p>
</li>
</ul>
<script>
// render first three users
constructor({ users }) {
this.items = users.slice(0, 2)
this.all = users
}
// insert a new item
add() {
const { items, all } = this
const item = all[items.length]
if (item) items.push(item)
}
</script>
</div>
Note that the transition effect is done with vanilla CSS using @starting-style
without specialized <transition>
elements or motion libraries. This keeps the implementation lean and clean.
Lifecycle methods
Each component instance goes through a series of steps during its lifetime: first, it is created, then mounted on the page, and finally, it gets updated one or more times. Sometimes the component is removed or unmounted
from the page.
You can hook custom functionality to these steps by creating instance methods with specific names:
<script>
// Called when the component is created. Data/args is provided as the
first argument.
constructor(data) {
// Initialization logic here
}
// Called after the component is mounted on the page.
mounted(data) {
// Logic to run after mounting here
}
// Called after the component is updated.
updated() {
// Logic to run after an update here
}
// Called after the component is removed from the page.
unmounted() {
// Cleanup logic here
}
</script>
Inside these callback functions, this
points to the instance API, allowing access to various properties and methods related to the component.
Instance API
The component API is accessible via the this
variable inside the lifecycle methods. It has the following attributes and methods:
root
: The root DOM node of the component instance.$el
: An alias for the root DOM node.$parent
: The root DOM node of the parent instance.$refs
: Access to named DOM nodes and inner components within the component.mount(root: DOMElement)
: Mounts the instance to the specified root element.unmount()
: Removes the component from the current component tree.update(data?: Object)
: Forces the component instance to re-render itself with optional data. This is useful after fetching data from a server or during any asynchronous event.mountChild(name, wrap, data)
: Mounts a new child component on a DOM element inside the current component.
The component re-renders itself automatically after calling an event handler, but you need to call this manually if there is no clear interaction to detect.
References
You can obtain a handle to nested DOM elements or components via the $refs
property:
<div ="my-component">
<!-- Name a DOM node with the "ref" attribute -->
<figure ref="image"></figure>
<!-- Or with the "name" attribute -->
<input name="email" placeholder="Hey, dude">
<!-- Custom elements are automatically named -->
<image-gallery/>
<!-- Refs work in templates too -->
<h3>{ $refs.email.placeholder }</h3>
<script>
// References are available after mount
mounted() {
// Get a handle to the image DOM node
const image = this.$refs.image;
// Get a handle to the image-gallery component API
const gallery = this.$refs['image-gallery'];
}
</script>
</div>
Sharing code between components
You can add and import shared code within a top-level <script>
tag. Here’s an example library that defines both a shopping cart and a button component that adds items to the cart. The cart itself is defined in cart.js
, which is a plain JavaScript file. This cart is used by both components.
<!-- Shared code -->
<script>
import { shopping_cart, addToCart } from './cart.js';
</script>
<!-- Shopping cart component -->
<article ="shopping-cart">
<div :for="item in items">
<h3>{ item.price }</h3>
<p>{ item.amount }</p>
</div>
<script>
constructor() {
this.items = shopping_cart.getItems(); // Load initial items
}
</script>
</article>
<!-- "Add to cart" component -->
<button ="add-to-cart" ="click">
<script>
click() {
addToCart(this.data); // Add item to the cart
}
</script>
</button>
Summary
Nue opens the door to a new way of building web applications, allowing you to harness the power of Markdown extensions, server-side components, and client-side components—all with a simple and intuitive syntax. By focusing on an HTML-based approach, you can prioritize layout, structure, semantics, and accessibility, freeing yourself from the frustration of tangled JavaScript stack traces.