Lightweight SPA framework based on Convention over Configuration
As its name implies, Dry.js is based around the idea of applying the Don't Repeat Yourself (DRY) principle to front end development as extensively as possible. It does so by following a Convention Over Configuration (CoC) approach.
Initial page load is one of the main concerns of Dry.js. The library weights less than 8kb minified (3kb gzipped) which makes it viable for any sort of web application.
Dry.js is intended to be a standard way to join external components and libraries. All of its components are designed so that they can be easily replaced.
For example, you can swap Dry.js's templating engine with Handlebars, integrate React to improve rendering performance, or manage all DOM interactions with jQuery/Zepto, etc.
Please read this article to further understand why this approach is necessary.
No one likes learning a new front-end framework every week, so that's why Dry.js was specifically designed to be super easy to use. Getting started will only take you some minutes, and there's not that much more to it. Complexity is the first sign of bad architecture, and your developers will thank you for it.
Have you ever wanted to change a certain feature of a given framework? Well, this is super easy in Dry.js because its code is brief and understandable, so re-writting a component is not complex at all. In fact I encourage you to do this if you want to replace any of the framework's core components with external libraries, you'll only have to run the tests afterwards to make sure that everything still works as expected.
Dry.js was inspired by what I consider to be the best features of the most well-known single page application frameworks:
- Provides only the minimum set of components necessary for web application development, like Backbone.js
- Follows an MVC, convention over configuration approach, like Ember.js (although much simpler)
- Comes with all you need right out of the box like Angular.js (a.k.a batteries included), alghough these features can be easily replaced by other external components
- Integrates an opinionated set of guidelines for the application's architecture and data flow, similar to what Facebook's Flux does
Setting up Dry.js is extremely easy. You only need to include the script anywhere on the page, and you are all set.
<html>
<head>
<title>Getting started</title>
</head>
<body>
<h1>Hello world</h1>
<!-- Include dry.min.js here -->
<script src="scripts/dry.min.js"></script>
</body>
</html>
You can download the minitied and development versions through any of these options.
- Through the links at the top of the page.
- Install with Bower: bower install dry-js
- Install with npm: npm install dry-js
A website is divided in apps. Their goal is to provide structure to the code, and can be seen as modules. Each app contains its own controllers, views and models, and handles a specific set of routes.
// Create new app
var app1 = dry.app('app1');
// Start the app
app1.init();
A controller contains methods. Each method handles a specific route or event, returning a view instance if the result of the method's execution triggers UI changes. In this sense, Dry.js controllers are very similar to Rails or ASP.NET MVC.
var app1 = dry.app('app1');
// Handles a list of products
app1.controller('products', {
'list': function() { // Triggered when navigating to /products/list
return new dry.View('ProductList')
}
});
The router is responsible for directing page navigation actions to controllers. As the previous example hints, you do not need to specify routes anywhere. They are automatically generated from controller names and methods.
var app1 = dry.app('app1');
// Home page
app1.controller('default', {
// Triggered on base url (root)
'default': function() {
return new dry.View('HomePage')
}
});
// Handles a list of products
app1.controller('products', {
// Triggered when navigating to /products/:id
':id': function(id) {
return new dry.View('ProductDetails')
}
// Triggered when navigating to /products/list
'list': function() {
return new dry.View('ProductList')
}
});
In a typical fashion, models concentrate the responsibility of handling data structure, storage and server communication. They are injected into views through controller actions, and come with a convenient API for handling Ajax communication. Expanding on the previous example:
var app1 = dry.app('app1');
// Product model definition
app1.model('Product', {
attributes: {
id: 1 // default
}
getAll: 'GET https://someUrl/products'
newProduct: 'POST https://someUrl/products/new',
updateProduct: 'PUT https://someUrl/products/{id}',
deleteProduct: 'DELETE https://someUrl/{id}'
});
// Handles a list of products
app1.controller('products', {
'list': function() {
// Creates a new model instance and calls the getAll method
var allProducts = app1.model('Product');
return allProducts
.getAll() // Promises are built-in
.then(function (data) {
allProducts.set(data);
// Pass the model to the view
return new dry.View('ProductList', {model: allProducts});
});
}
});
Views contain presentation logic. Each view contains a model instance which stores the data that is rendered on the template. They are also responsible for handling events, and do so by calling methods from the controller that created the view.
var app1 = dry.app('app1');
// Product model definition
app1.model('Product');
app1.view('ProductList', {
events: {
// When the button with class .btn-new' is pressed,
// call the 'new' method in the controller
'click .btn-new': 'new'
}
});
// Handles a list of products
app1.controller('products', {
'new': function() {
// Creates a new model instance
var product = app1.model('Product');
// Pass the model to the view
return app1.view('ProductList', {model: product});
}
});
Templates can be either strings or functinons which construct HTML code from the view data. Dry.js automatically finds template definitions in the dom looking for a script with a data-dry attribute, and then renders it to a DOM element with a data-dry attribute of the same value.
var app1 = dry.app('app1');
// Home page
app1.controller('main', {
'hello': function() {
return new dry.View('main/hello');
}
});
<!-- Template -->
<script data-dry="main/hello" type="text/template">
<% for(var i=0; i<10; i++) { %>
Lorem ipsum
<% } %>
</script>
<div data-dry="main/hello"><!-- Template will be rendered here --></div>
The convention here is very simple: by default, once a user navigates to a route (say www.someUrl.com/main/hello), then the main controller executes the hello method, which will create a new instance of the main/hello view.
Notice that the view instance is created on the fly, and since the only parameter specified when creating the view is its name, it will simply follow the default behavior for views. More specifically, it look for a script with a data-dry attribute equal to its name (the template) and render it into the first non-script DOM element with the same attribute value.
Filters are a simple and semantic way to reuse code inside your application (again, DRY principle). They check if a condition is fulfilled and execute a given action if it is. They can seve as annotations and make controller logic much easier to read.
var app1 = dry.app('app1');
function isLoggedIn() {
// some logic...
}
function redirectToHome() {
dry.navigate("/home");
}
app1.filter('IsLoggedIn', isLoggedIn, redirectToHome);
// Home page
app1.controller('main', {
'hello': function() {
app1.filter("IsLogedIn");
// Continue controller logic...
}
});
Note: filters are not required, so you can choose not to use them if you find them irrelevant or confusing.
- chrisdavies for his implementation of rlite (used as default router behind the secenes)
- John Resig for his micro templating engine
- Malko for his D.js implementation
- jQuery team for their implementation of jQuery.param
- srizon for his Drifolio template (used in the projects's site)