Skip to main content Accessibility Feedback

Reef v10

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 3kb 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 (though you can if you want).
  • Uses DOM diffing to update only the things that have changed.
  • Has Redux/Vuex-like data stores, with setters and getters baked right in.
  • Automatically sanitizes HTML before rendering to help protect you from cross-site scripting (XSS) attacks.
  • Work with native JavaScript methods and browser APIs instead of custom methods and pseudo-languages.
  • Compatible with all modern browsers.

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 works without any build step.

The CDN is the fastest and simplest way to get started, but you can use importable modules or a direct download if you’d prefer.

<!-- Get the latest major version -->
<script src="https://cdn.jsdelivr.net/npm/reefjs@10/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.

More ways to install Reef

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@10/dist/reef.es.min.js';

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.

let Reef = require('https://cdn.jsdelivr.net/npm/reefjs@10/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@10/dist/reef.amd.min.js'], function (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>

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
let app = new Reef('#app');

// This does too
let elem = document.querySelector('#app');
let 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.

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

// It can also be a function that returns a string
let 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
let 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.

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
let app = new Reef('#app', {
	data: {
		greeting: 'Hello',
		name: 'world'
	},
	template: function (props) {
		return `<h1>${props.greeting}, ${props.name}!</h1>`;
	}
});

// Render the initial UI
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 reef: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 with the Reef.prototype.dataCopy property. This returns an immutable, non-reactive copy of your data that won’t affect the state of your component or cause a UI update.

// Get an immutable copy of the app.data
let data = app.dataCopy;

// 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 →

Advanced Components

As your project gets bigger, the way you manage components and data may need to grow with it. Reef has some options to help make things a little easier.

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.

Use the reef-checked, reef-selected, and reef-value attributes to dynamically control the checked, selected, and value attributes, respectively. Use a falsy value when the item should not be checked or selected.

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

let app = new Reef('#app', {
	data: {
		agreeToTOS: true
	},
	template: function (props) {
		return `
			<label>
				<input type="checkbox" reef-checked="${agreeToTOS}">
			</label>`;
	}
});

You might also want to use a default value when an element initially renders, but defer to any changes the user makes after that.

You can do that with the reef-default-checked, reef-default-selected, and reef-default-value attributes.

In this example, 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.

let app = new Reef('#app', {
	template: function () {
		return `
			<label for="wizards">Who is the best wizard?</label>
			<select>
				<option>Harry</option>
				<option reef-default-selected>Hermione</option>
				<option>Neville</option>
			</select>`;
	}
});

Try controlling form attributes on CodePen →

Preventing Cross-Site Scripting (XSS) Attacks

To reduce your risk of cross-site scripting (XSS) attacks, Reef automatically sanitizes the HTML from your template before rendering it.

In the example below, the attempted XSS attack (the alert()) will not run. Safe HTML, like the bold in the greeting property, will be rendered as expected.

let app = new Reef('#app', {
	data: {
		greeting: '<strong>Hello</strong>',
		name: 'world',
		img: '<img src="x" onerror="alert(1)">'
	},
	template: function (props) {
		return `
			<h1>${props.greeting}, ${props.name}!</h1>
			${props.img}`;
	}
});

Try HTML sanitization 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.

<div id="app" data-greeting="Hello"></div>
let 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 on your options object. 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
let app = new Reef('#app', {
	data: {
		greeting: 'Hello, world!'
	},
	template: function (props) {
		return `
			<h1>${props.greeting}</h1>
			<div id="todos"></div>`;
	}
});

// Nested component
let 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.

let 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.

let 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, your component can still have its own local data as well.

The local component data is merged into the store and pass into the template() function as a single props object.

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

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

	}
});

Try creating a Data Store on CodePen →

Note: if any properties in your store and data that share the same name, the local component data gets priority.

Setters & Getters

Reef’s reactive data makes updating your UI as simple as 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 on your options object. The first parameter on a setter function must be the store or component data. You can add as many other parameters as you’d like.

let 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 the setter, along with any required arguments (except for props).

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

When a component or store has setter functions, they become the only way to update app or store data.

This protects your component or store data from unwanted changes. The data property always returns an immutable copy.

// 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 or 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 on your options object. The first parameter on a getter function must be the store or component data. You can add as many other parameters as you’d like.

let 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, along with any required arguments (except for props).

// 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
let 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>

Debugging

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

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

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

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

API Reference

Reef exposes a set of options, methods, and custom events that you can hook into.

Global Methods

Run these methods directly on the Reef object.

Reef.debug()

Turn debug mode on or off. Pass in true to turn debug mode on, and false to turn it off.

// Turn debug mode on
Reef.debug(true);

// Turn debug mode off
Reef.debug(false);

Reef.clone()

Create an immutable copy of an array, object, Map(), or Set().

Reef.clone({});

Reef.err()

Log a warning in the console conditionally only if debug mode is on.

Reef.err('You did something, silly!');

Reef.emit()

Emit a custom event. Pass in the element to emit the event on, the event name, and optionally, event details as arguments.

// Emits the "awesome" event on the document
Reef.emit(document, 'awesome');

// Emit the "awesome" event on the #app element, with some details
let app = document.querySelector('#app');
Reef.emit(app, 'awesome', {
	whoIs: 'You are'
});

You can listen for custom events with the Element.addEventListener() method.

Component Properties

Access these properties on individual Reef components.

Reef.prototype.data

Get a reactive copy of the app data.

let data = app.data;

Reef.prototype.dataCopy

Get a non-reactive, immutable copy of the app data.

let copy = app.dataCopy;

// This will not update the component data or cause a render
copy.todos.push('Zzzz... take a nap!');

Reef.prototype.elem

The element the component is associated with. Returns a string or Node.

let elem = app.elem;

Component Methods

Run these methods on individual Reef components.

Reef.prototype.render()

Render a Reef component in the UI.

let app = new Reef('#app', {
	template: 'Hello world!'
});

app.render();

Reef.prototype.attach()

Attach one or more components to a Reef component. Pass in the component or an array of components as an argument.

let app = new Reef('#app', {
	template: '<ul id="todos"></ul>'
});

let todos = new Reef('#todos', {
	template: '<li>Build something with Reef</li>'
});

// Attach one item
app.attach(todos);

// Attach an array of items
app.attach([todos]);

Reef.prototype.detach()

Detach an attached component. Pass in the component or an array of components as an argument.

// Detach one component
app.detach(todos);

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

Reef.prototype.do()

Run a setter function. Pass in the name of the setter, and a comma-separate list of any arguments.

let app = new Reef('#app', {
	data: {
		count: 0
	},
	template: function (props) {
		return count;
	},
	setters: {
		increase: function (props) {
			props.count++;
		}
	}
});

// Run the increase setter
app.do('increase');

Reef.prototype.get()

Run a getter function. Pass in the name of the getter, and a comma-separate list of any arguments.

let app = new Reef('#app', {
	data: {
		count: 0
	},
	template: function (props) {
		return count;
	},
	getters: {
		count: function (props) {
			return props.count;
		}
	}
});

// Run the count getter
app.get('count');

Events

Reef emits custom events throughout the lifecycle of a component or instance.

All Reef events follow a reef:{event-name} pattern. Unless otherwise specified, all events are emitted on the document element. Event details can be accessed on the event.details property.

// Listen for when Reef components are rendered into the DOM
document.addEventListener('reef:render', function (event) {
	console.log(event.target); // The element it was rendered into
	console.log(event.detail); // The data used for the render
});
  • reef:ready is emitted when Reef is loaded into the DOM and ready to use.
  • reef:initialized is emitted when a new Reef component is initialized.
    • event.detail - the instance
  • reef:before-render is emitted on an element before a new UI is rendered into it.
    • event.target - the element the component is being rendered into
    • event.detail - the current component data
    • event.preventDefault() - stop the component from rendering
  • reef:render is emitted on an element after a new UI is rendered into it.
    • event.target - the element the component was rendered into
    • event.detail - the component data at time of render
  • reef:attached is emitted when one or more components is attached to a component.
    • event.detail - an object with the component and an array of attached components
  • reef:detached is emitted when one or more components is detached from a component.
    • event.detail - an object with the component and an array of detached components

Options

All of the options for Reef.

// This can be a string or a element
let elem = '#app';

new Reef(elem, {

	// The component data
	data: {},

	// A component or array of components to attach to
	attachTo: [],

	// A data store to use
	// If used, the data option is ignored
	store: null,

	// An object of setter methods
	setters: {},

	// An object of getter methods
	getters: {}

});

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. That means:

  • The latest versions of Edge, Chrome, Firefox, and Safari.
  • Mobile Safari, Chrome, and Firefox on Safari.
  • WebView, Chrome, and Firefox for Android.

If you need to support older browsers, you’ll need to transpile your code into ES5 with BabelJS.

License

The code is available under the MIT License.