Skip to main content Accessibility Feedback

Reef v7

A lightweight library for creating reactive, state-based components and UI. Reef is a simpler alternative to React, Vue, and other large frameworks.

Features

  • Weighs just 2.5kb minified and gzipped, with zero dependencies.
  • Simple templating with JavaScript strings or template literals.
  • Load it with a <script> element or ES module import—no command line or transpiling required.
  • Uses DOM diffing to update only the things that have changed.
  • Has Redux/Vuex-like data stores, setters and getters baked right in.
  • Automatically encodes markup in your data to protect you from cross-site scripting (XSS) attacks.
  • Work with native JavaScript methods and browser APIs instead of custom methods and pseudo-languages.
  • Supported all the way back to IE9.

Ditch that bloated framework, and make web development fun and simple again!

Why use Reef?

Reef is an anti-framework.

It does a lot less than the big guys like React and Vue. It doesn’t have a Virtual DOM. It doesn’t require you to learn a custom templating syntax. It doesn’t provide a bunch of custom methods.

Reef does just one thing: render UI.

Couldn’t you just use some template strings and innerHTML? Sure. But Reef only updates things that have changed instead of clobbering the DOM and removing focus from your form fields. It also automatically renders a new UI when your data updates, and helps protect you from XSS attacks.

If you’re craving a simpler, back-to-basics web development experience, Reef is for you.

(And if not, that’s cool too! Carry on.)

Getting Started

1. Include Reef on your site

Reef comes in two flavors: standalone and polyfilled.

The polyfilled build uses the .polyfill suffix, and includes the polyfills for Proxies and Custom Events that are required for IE support.

CDN

The fastest way to get started is with the CDN from jsDelivr.

<script src="https://cdn.jsdelivr.net/npm/reefjs/dist/reef.min.js"></script>

Reef uses semantic versioning. You can grab a major, minor, or patch version from the CDN with the @1.2.3 syntax. You can find all available versions under releases.

<!-- Use the latest major version -->
<script src="https://cdn.jsdelivr.net/npm/reefjs@7/dist/reef.min.js"></script>

ES Modules

Reef also supports modern browsers and module bundlers (like Rollup, Webpack, Snowpack, and so on) using the ES modules import syntax. Use the .es version.

import Reef from 'https://cdn.jsdelivr.net/npm/reefjs/dist/reef.es.min.js';

More ways to install Reef

Direct Download

You can download the files directly from GitHub.

Compiled and production-ready code can be found in the dist directory. The src directory contains development code.

<script src="path/to/reef.min.js"></script>

NPM

You can also use NPM (or your favorite package manager). First, install with NPM.

npm install reefjs --save

Then import the package.

import Reef from 'reefjs';

CommonJS

If you use NodeJS, you can import Reef using the require() method with the .cjs version.

var Reef = require('https://cdn.jsdelivr.net/npm/reefjs/dist/reef.cjs.min.js');

AMD

If you use RequireJS, SystemJS, and other AMD formats, you can import Reef with the .amd version.

requirejs(['https://cdn.jsdelivr.net/npm/reefjs/dist/reef.amd.min.js'], function (Reef) {
  //...
});

2. Add an element to render your component/UI into

This is typically an empty div with a targetable selector.

<div id="app"></div>

3. Create your component

Create a new Reef() instance, passing in two arguments: your selector, and your options.

Provide a selector

The first argument is the selector for the element you want to render the UI into. Alternatively, you can pass in the element itself.

// This works
var app = new Reef('#app');

// This does too
var elem = document.querySelector('#app');
var app = new Reef(elem);

Provide a Template

The second argument is an object of options. It requires a template property, as either a string or a function that returns a string, to render into the DOM.

You can use old-school strings or ES6 template literals.

// Your template can be a string
var app = new Reef('#app', {
	template: '<h1>Hello, world!</h1>'
});

// It can also be a function that returns a string
var app = new Reef('#app', {
	template: function () {
		return '<h1>Hello, world!</h1>';
	}
});

[Optional] Add State/Data

As an optional property of the options argument, you can include state for your component with the data property.

The data object is automatically encoded and passed into your template function, so that you can use it to customize your template.

// Some data
var app = new Reef('#app', {
	data: {
		greeting: 'Hello',
		name: 'world'
	},
	template: function (props) {
		return `<h1>${props.greeting}, ${props.name}!</h1>`;
	}
});

Template literals give you a simple, JSX-like templating experience. If you want, you can use old-school concatenated strings for more backwards compatibility.

4. Render your component

Render your component by calling the render() method on it.

app.render();

Try the demo on CodePen →

State Management

Reef uses data reactivity to update your UI.

Data reactivity means that the UI “reacts” to changes in your data. Update your data, and the UI automatically renders any required updates based on the new state.

// Create a component and render it
var app = new Reef('#app', {
	data: {
		greeting: 'Hello',
		name: 'world'
	},
	template: function (props) {
		return `<h1>${props.greeting}, ${props.name}!</h1>`;
	}
});
app.render();

// This causes component to update with "Hi, universe"
app.data.greeting = 'Hi';
app.data.name = 'Universe';

You can also update the entire data object.

// This will also update the UI
app.data = {
	greeting: 'Hi',
	name: 'Universe'
};

Try data reactivity on CodePen →

For better performance, multiple property updates may be batched into a single, asynchronous render. You can detect when a render has been completed using the render event hook.

Non-Reactive Data

Sometimes, you want to update data without updating the UI.

You can get an immutable copy of your data by passing it into the Reef.clone() method. This creates a non-reactive copy of your data that won’t affect the state of your component.

// Create an immutable copy of the app.data
var data = Reef.clone(app.data);

// Update the copy
// This does NOT update the app.data or render a new UI
data.name = 'Universe';

When you’re ready to update your component data, you can set the component’s data property to your cloned copy.

// Reactively update the component data
app.data = data;

Try non-reactive data on CodePen →

Note: You can use the Reef.clone() method to create an immutable copy of any array or object, not just your component data.

Advanced Components

HTML Templates

Default and state-based HTML attributes

You can use component data to conditionally include or change the value of HTML attributes in your template.

In the example below, the checkbox is checked when agreeToTOS is true.

var app = new Reef('#app', {
	data: {
		agreeToTOS: true
	},
	template: function (props) {
		return `
			<label for="tos">
				<input type="checkbox" id="tos" ${props.agreeToTOS ? 'checked' : ''}>
			</label>`;
	}
});

You might also want to use a default value for an attribute, but not change it based on your component’s state. You can do that by prefixing any attribute with default in your template.

In this example, option[value="hermione"] has the [selected] attribute on it when first rendered, but will defer to whatever changes the user makes when diffing and updating the UI.

var app = new Reef('#app', {
	data: {},
	template: function () {
		return `
			<label for="wizards">Who is the best wizard?</label>
			<select>
				<option value="harry">Harry</option>
				<option value="hermione" defaultSelected>Hermione</option>
				<option value="neville">Neville</option>
			</select>`;
	}
});

Preventing Cross-Site Scripting (XSS) Attacks

To reduce your risk of cross-site scripting (XSS) attacks, Reef automatically encodes any markup in your data before passing it into your template.

You can disable this feature by setting the allowHTML option to true.

Important! Do NOT do this with third-party or user-provided data. This exposes you to the risk of cross-site scripting (XSS) attacks.

var app = new Reef('#app', {
	data: {
		greeting: '<strong>Hello</strong>',
		name: 'world'
	},
	template: function (props) {
		return `<h1>${props.greeting}, ${props.name}!</h1>`;
	},
	allowHTML: true // Do NOT use with third-party/user-supplied data
});

Try allowing HTML in your data on CodePen →

Getting the element the template is being rendered into

An optional second argument is passed into the template() function: the element the template is being rendered into.

This is particularly handy if you have data attributes on your element that affect what’s rendered into the template.

Requires Reef 7.5 or higher.

<div id="app" data-greeting="Hello"></div>
var app = new Reef('#app', {
	data: {
		name: 'world'
	},
	template: function (props, elem) {
		return `<h1>${elem.getAttribute('data-greeting')}, ${props.name}!</h1>`;
	}
});

Try getting the HTML element that the template was rendered into on CodePen →

Nested Components

If you’re managing a bigger app, you may have components nested inside other components.

Reef provides you with a way to attach nested components to their parent components. When the parent component is updated, it will automatically update the UI of its nested components if needed.

Associate a nested component with its parent using the attachTo key in your options. You can provide a component or array of components for a value.

You only need to render the parent component. It’s nested components will render automatically.

// Parent component
var app = new Reef('#app', {
	data: {
		greeting: 'Hello, world!'
	},
	template: function (props) {
		return `
			<h1>${props.greeting}</h1>
			<div id="todos"></div>`;
	}
});

// Nested component
var todos = new Reef('#todos', {
	data: {
		todos: ['Swim', 'Climb', 'Jump', 'Play']
	},
	template: function (props) {
		return `
			<ul>
				${props.todos.map(function (todo) {
					return `<li>${todo}</li>`;
				}).join('')}
			</ul>`;
	},
	attachTo: app
});

app.render();

Try nested components on CodePen →

Attaching and Detaching Nested Components

You can attach or detach nested components at any time using the attach() and detach() methods on the parent component.

Provide an individual component or array of components as an argument.

// Attach components
app.attach(todos);
app.attach([todos]);

// Detach components
app.detach(todos);
app.detach([todos]);

Try attaching nested components on CodePen →

Shared State with Data Stores

A Data Store is a special Reef object that holds reactive data you can share with multiple components.

Any time you update the data in your Data Store, any components that use the data will also be updated, and will render again if there are any UI changes.

Create a Data Store using the new Reef.Store() constructor.

var store = new Reef.Store({
	data: {
		heading: 'My Todos',
		todos: ['Swim', 'Climb', 'Jump', 'Play']
	}
});

To use your Data Store with a component, pass it in with the store property instead of providing a data object.

var app = new Reef('#app', {
	store: store,
	template: function (props) {
		return `
			<h1>${props.heading}</h1>
			<ul>
				${props.todos.map(function (todo) {
					return `<li>${todo}</li>`;
				}).join('')}
			</ul>`;

	}
});

When using a Data Store, a component will have no data of its own. All state/data updates must happen by updating the store.

// Add a todo item
store.data.todos.push('Take a nap... zzzz');

Try creating a Data Store on CodePen →

Setters & Getters

Reef’s reactive data makes updating your UI as simple updating an object property.

But as your app scales, you may find that keeping track of what’s updating state and causing changes to the UI becomes harder to track and maintain.

Setters and getters provide you with a way to control how data flows in and out of your component.

Setters

Setters are functions that update your component or store data.

Create setters by passing in an object of setter functions with the setters property in your Reef options. The first argument on a setter function is the store or component data. You can pass in as many other arguments as you’d like.

var store = new Reef.Store({
	data: {
		heading: 'My Todos',
		todos: ['Swim', 'Climb', 'Jump', 'Play']
	},
	setters: {
		// Add a new todo item to the component
		addTodo: function (props, todo) {
			props.todos.push(todo);
		}
	}
});

Use setter functions by calling the do() method on your component or store. Pass in the name of setter, along with any required arguments (except for props).

// Add a new todo item
store.do('addTodo', 'Take a nap... zzzz');

When a component/store has setter functions, you cannot update data directly.

Setter functions are the only way to make updates. This protects your component or store data from unwanted changes. The data property always returns an immutable copy of the data.

// This will NOT update the store.data or the UI
store.data.todos.push('Take a nap... zzzz');

Try working with setter functions on CodePen →

Getters

Getters are functions that parse data from your component or store and return a value.

They’re useful if you need to manipulate and retrieve the same data across multiple views of components. Rather than having to import helper functions, you can attach them directly to the component or store.

Create getters by passing in an object of getter functions with the getters property in your Reef options. They accept the store or component data as their only argument.

var store = new Reef.Store({
	data: {
		heading: 'My Todos',
		todos: ['Swim', 'Climb', 'Jump', 'Play']
	},
	getters: {
		total: function (props) {
			return props.todos.length;
		}
	}
});

Use getter functions by calling the get() method on your component or store. Pass in the name of getter as an argument.

// Get the number of todo items
store.get('total');

Try working with getter functions on CodePen →

Asynchronous Data

You can use asynchronous data (such as content from an API) in your templates.

Set an initial default value, make your API call, and update the data property once you get data back. This will automatically trigger a render.

// Create an app
var app = new Reef('#app', {
	data: {
		articles: []
	},
	template: function (props) {

    // If there are no articles
    if (!props.articles.length) {
      return `<p>There are no articles.</p>`;
    }

    // Otherwise, show the articles
	return `
		<ul>
			${props.articles.map(function (article) {
				return `<li>
					<strong><a href="#">${article.title}.</a></strong>
					${article.body}
				</li>`;
			}).join('')}
		</ul>`;
	}
});

// Fetch API data
// Then, update the app data
fetch('https://jsonplaceholder.typicode.com/posts').then(function (response) {
	return response.json();
}).then(function (data) {
	app.data.articles = data;
});

Try create a template from asynchronous data on CodePen →

You might also choose to hard-code a loading message in your markup.

<div id="app">Loading...</div>

Event Hooks

Whenever Reef updates the DOM, it emits a custom render event that you can listen for with addEventListener().

The render event is emitted on the element that was updated, and bubbles, so you can also use event delegation. The event.detail property includes a copy of the data at the time that the component template was rendered.

document.addEventListener('render', function (event) {

	// Only run for elements with the #app ID
	if (!event.target.matches('#app')) return;

	// Log the data at the time of render
	console.log(event.detail);

}, false);

Try the render event hook on CodePen →

Emitting your own custom events

Reef includes a helper function, Reef.emit(), that you can use to emit your own custom events in your apps.

Pass in the element to emit the event on and the event name as arguments. You can optionally pass in an object with event details as a third argument.

// Emit the 'partyTime' event on the document element
Reef.emit(document, 'partyTime', {
	msg: `It's party time!`
});

Debugging

By default, Reef fails silently. You can put Reef into debugging mode to expose helpful error message in the Console tab of your browser’s Developer Tools.

Turn debugging mode on or off with the Reef.debug() method. Pass in true to turn it on, and false to turn it off.

// Turns debugging mode on
Reef.debug(true);

// Turns debugging mode off
Reef.debug(false);

Routing

Reef includes an optional router you can use to handle URL/route management with your single-page apps (SPA’s).

Features

  • Automatically renders your Reef components whenever the route changes.
  • Works with any link element. Unlike bigger frameworks, you don’t need custom routing components.
  • Baked-in accessibility. Reef’s router automatically handles focus management and title updates.
  • Supports real URL paths, with an optional hashbang pattern (#!) fallback.
  • Weighs just 2kb minified and gzipped.

Installation

Reef Router is just as easy to install as Reef itself. Reef must also be installed as a dependency.

Reef Router requires Reef v7.1.0 or higher.

CDN

<script src="https://cdn.jsdelivr.net/npm/reefjs/dist/reef.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/reefjs/dist/router.min.js"></script>

ES Modules

import Reef from 'https://cdn.jsdelivr.net/npm/reefjs/dist/reef.es.min.js';
import 'https://cdn.jsdelivr.net/npm/reefjs/dist/router.es.min.js';

More ways to install Reef

Direct Download

<script src="path/to/reef.min.js"></script>
<script src="path/to/router.min.js"></script>

NPM

import Reef from 'reefjs';
import 'reefjs/router';

CommonJS

var Reef = require('https://cdn.jsdelivr.net/npm/reefjs/dist/reef.cjs.min.js');
var Router = require('https://cdn.jsdelivr.net/npm/reefjs/dist/router.cjs.min.js');

AMD

requirejs(['https://cdn.jsdelivr.net/npm/reefjs/dist/reef.amd.min.js', 'https://cdn.jsdelivr.net/npm/reefjs/dist/router.amd.min.js'], function (Reef) {
  //...
});

Getting Started

Step 1: Create your links

No custom components required. Any link element with an href will work.

<ul>
	<li><a href="/">Home</a></li>
	<li><a href="/about">About</a></li>
	<li><a href="/contact">Contact</a></li>
</ul>

Step 2: Define your routes

Create a new Reef.Router() to define your routes.

Every route requires a title and url. You can add any additional properties that you want (for example, an id for the route). You can use * as a url to catch any unmatched URLs.

var router = new Reef.Router({
	routes: [
		{
			id: 'home',
			title: 'Home',
			url: '/'
		},
		{
			id: 'about',
			title: 'About',
			url: '/about'
		},
		{
			id: 'contact',
			title: 'Contact Us',
			url: '/contact'
		}
	]
});

Step 3: Associate your router with one or more components

For any Reef component that should be updated when the route changes, add a router property and associate your router component with it.

Details about the current route are automatically passed into the template function as a second argument. By default, for unmatched routes the route argument will have a value of null.

var app = new Reef('#app', {
	router: router,
	data: {
		greeting: 'hello!'
	},
	template: function (props, route) {
		return `
			<h1>${route.title}</h1>
			<p>${props.greeting}</p>`;
	}
});

Note: when using a router, the element that the template was rendered into becomes the third argument on the template() function.

Accessibility

Any time the route changes, any associated components automatically re-render.

The document.title is also updated, and focus is shifted to the primary heading on the page (or an anchor element if scrolling to an anchored location).

Focus rings on headings

Headings and anchor locations will appear with a focus ring around them, which you may find visually unappealing.

Elements that don’t normally receive focus are given a tabindex if -1 to make them focusable with JS. You can remove the focus ring by styling [tabindex="-1"].

[tabindex="-1"] {
	outline: 0;
}

Note: you should NOT remove focus styles from elements that are normally focusable.

Advanced Routing

Getting parameters from routes

You can include variable parameters in your URLs, either in the path itself or as query or search parameters.

Reef Router will add them to the route object that gets passed into your template(). Path parameters are included under the params property, and query or search parameters are included under the search property.

<ul>
	<li><a href="/">Home</a></li>
	<li><a href="/account/tom?photo=true">My Account</a></li>
</ul>
var router = new Reef.Router({
	routes: [
		{
			id: 'home',
			title: 'Home',
			url: '/'
		},
		{
			id: 'user-account',
			title: 'User Account',
			url: '/account/:user'
		}
	]
});

// In this example:
// route.params.user will be "tom"
// route.search.photo will be "true"
var app = new Reef('#app', {
	router: router,
	data: {
		greeting: 'hello!'
	},
	template: function (props, route) {
		return `
			<h1>${route.title}</h1>
			<p>${props.greeting} ${route.params.user}</p>
			${route.search.photo ? `<p>
				<img alt="A photo of ${route.params.user}" src="/img/${route.params.user}.jpg">
			</p>` : ''}`;
	}
});

Nested routes

Reef Router supports nested routes out-of-the-box.

The order does not matter. Reef Router will check deeper routes for matches first.

var router = new Reef.Router({
	routes: [
		{
			id: 'home',
			title: 'Home',
			url: '/'
		},
		{
			id: 'account',
			title: 'Account',
			url: '/account/'
		},
		{
			id: 'user-account',
			title: 'User Account',
			url: '/account/:user'
		},
		{
			id: 'user-password',
			title: 'Change Password',
			url: '/account/:user/password'
		}
	]
});

Redirects

As your app grows, routes may change. You can setup redirects from one route to another.

When creating the route, create the url property as normal, and add a redirect property with the route that the URL should point to.

var router = new Reef.Router({
	routes: [
		{
			id: 'contact',
			title: 'Contact',
			url: '/contact/'
		},
		{
			url: '/contact-us/',
			redirect: '/contact/'
		}
	]
});

The redirect property can also be a function that returns a string.

The function automatically receive the existing route object, with URL and search parameters, as an argument.

var router = new Reef.Router({
	routes: [
		{
			id: 'user-account',
			title: 'User Account',
			url: '/account/:user'
		},
		{
			url: '/my-account/:user',
			redirect: function (route) {
				return `/account/${route.params.user}/`
			}
		},
	]
});

Options & Settings

In addition to your routes, Reef Router accepts a few options you can use to customize how the router behaves.

var router = new Reef.Router({
	root: '', // The root URL for your app, if using a subdirectory
	title: '{{title}}', // The pattern to use for the page title. {{title}} will be replaced with the actual title
	useHash: false // If true, uses a hashbang (#!) pattern instead of true URL paths
});

The title property can be a string or a function that returns a string.

The useHash property is automatically set to true in browsers that don’t support the history.pushState() method, and local file: pages.

Examples

The app lives at my-site.com/my-app/.

var router = new Reef.Router({
	root: '/my-app'
});

The document.title will always have | My App after it.

var router = new Reef.Router({
	title: '{{title}} | My App'
});

The document.title will always have | My App after it except on the homepage, where it’s just My App.

var router = new Reef.Router({
	title: function (route) {
		if (route && route.id === 'home') {
			return 'My App';
		}
		return '{{title}} | My App';
	}
});

Always use the hashbang pattern.

var router = new Reef.Router({
	useHash: true
});

API

Reef.Router exposes a few public methods you can use in your scripts.

addRoutes()

Add routes to an existing route. Accepts an array of routes, or an individual route object.

// Add an individual route
router.addRoutes({
	id: 'sale',
	title: 'Holiday Sale!',
	url: '/sale'
});

// Add multiple routes
router.addRoutes([
	{
		id: 'about',
		title: 'About',
		url: '/about'
	},
	{
		id: 'contact',
		title: 'Contact Us',
		url: '/contact'
	}
]);

Programmatically navigate to a URL. Pass in the URL as an argument.

// Go to the /about page
router.navigate('/about');

addComponent()

Associate a component with the router for automatic rendering.

// Add the app component
router.addComponent(app);

current

The current property will return the details object for the current route.

// Get the current route details
router.current;

Routing Events

Reef Router emits two custom events on the window whenever a route change happens.

  • beforeRouteUpdated fires before the route is changed. It includes the current and next routes as properties under event.detail
  • routeUpdated fires after the route has been changed. It includes the current and previous routes as properties under event.detail.
// Run a callback before the route changes
// Useful for tearing down scripts that won't be needed on the next view
window.addEventListener('beforeRouteUpdated', function (event) {

	// The route that's about to the change
	var current = event.detail.current;

	// The new route
	var next = event.detail.next;

});

// Run a callback after the route changes
// Useful for loading or re-initializing scripts
window.addEventListener('routeUpdated', function (event) {

	// The new route
	var current = event.detail.current;

	// The previous route
	var previous = event.detail.previous;

});

Kudos

Reef Router’s URL path matching and parameter extraction is adapted from Navigo by Krasimir Tsonev.

Click handling is adapted from pages.js by Vision Media.

Demos

Want to see Reef in action? Here are some demos and examples you can play with.

Browser Compatibility

Reef works in all modern browsers, and IE 9 and above.

For IE support, you need to either...

  • Use the .polyfill build of Reef, or
  • Include your own polyfills for Proxies and the CustomEvent() object, or
  • Transpile your code into ES5 with BabelJS.

License

The code is available under the MIT License.