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.
ES Modules Reef also supports modern browsers and module bundlers (like Rollup, Webpack, Snowpack, and so on) using the ES modules NPM You can also use NPM (or your favorite package manager). First, install with NPM. Then import the package. CommonJS If you use NodeJS, you can import Reef using the AMD If you use RequireJS, SystemJS, and other AMD formats, you can import Reef with the Direct Download You can download the files directly from GitHub. Compiled and production-ready code can be found in the More ways to install Reef
import
syntax. Use the .es
version.import Reef from 'https://cdn.jsdelivr.net/npm/reefjs@10/dist/reef.es.min.js';
npm install reefjs --save
import Reef from 'reefjs';
require()
method with the .cjs
version.let Reef = require('https://cdn.jsdelivr.net/npm/reefjs@10/dist/reef.cjs.min.js');
.amd
version.requirejs(['https://cdn.jsdelivr.net/npm/reefjs@10/dist/reef.amd.min.js'], function (Reef) {
//...
});
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();
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 intoevent.detail
- the current componentdata
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 intoevent.detail
- the componentdata
at time of render
reef:attached
is emitted when one or more components is attached to a component.event.detail
- an object with thecomponent
and an array ofattached
components
reef:detached
is emitted when one or more components is detached from a component.event.detail
- an object with thecomponent
and an array ofdetached
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.
- Clock
- Mirror Typing
- Pomodoro Timer
- Stopwatch
- Progress Bar
- Todo List
- Whack-a-Mole Game
- Articles from an API
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.