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';
Direct Download You can download the files directly from GitHub. Compiled and production-ready code can be found in the 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 More ways to install Reef
dist
directory. The src
directory contains development code.<script src="path/to/reef.min.js"></script>
npm install reefjs --save
import Reef from 'reefjs';
require()
method with the .cjs
version.var Reef = require('https://cdn.jsdelivr.net/npm/reefjs/dist/reef.cjs.min.js');
.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();
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';
Direct Download NPM CommonJS AMDMore ways to install Reef
<script src="path/to/reef.min.js"></script>
<script src="path/to/router.min.js"></script>
import Reef from 'reefjs';
import 'reefjs/router';
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');
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'
}
]);
navigate()
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 thecurrent
andnext
routes as properties underevent.detail
routeUpdated
fires after the route has been changed. It includes thecurrent
andprevious
routes as properties underevent.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.