Read the tutorial below or view the full documentation
Imagine you're building this application
Just a little app to show users on the left, details on the right, and a form above to add another user.
The Mozart pattern defines each logical unit as a "component"
<header data-component="form">...</header>
<main>
<aside data-component="list">...</aside>
<div data-component="details">...</aside>
</main>
This makes a hard distinction between all the classes and ids we'll use and top-level components.
This also allows us to scope behaviors and styles. We can get away with simple
classes like .title
and .name
instead of .user_form--title
and
.user_form--name
, or not rely on classes at all.
<header data-component="form">
<h1>Create User</h1>
<form>
<label>Name</label>
<input type="text" />
<label>Email</label>
<input type="email" />
<button>Create</button>
</form>
</header>
[data-component="form"] {
label { width: 20%; }
input { width: 80%; }
button {
background: blue;
color: white;
}
}
But the behavior-driven patterns are where this pattern really shines.
Each component needs to talk to each other, make API calls to the backend, and update the page without refresh. Typically this is where a developer will install Angular, React, Ember, etc...despite their application being simple enough to lean on native JavaScript or jQuery.
Let's make the user form save data, clear itself out, and update the user list.
Looking at the form component, we'll divide this out into Mozart's three
concerns: api
, events
, and routes
m$.form = new Mozart();
m$.form.routes(
{
create: {
method: "POST",
url: "/users/"
},
index: {
method: "GET",
url: "/users/"
},
show: {
method: "GET",
url: "/users/#{user_id}"
}
}
);
Notice the #{user_id}
. That's not native javascript. Mozart will automatically
replace that part of the string (interpolation) based on the options
passed to
the route when it is called.
// This example uses jQuery, but you don't have to.
// More on that later.
m$.form.api(
{
new_user: function(_$, options) {
$.ajax(_$.routes.create({
data: {
name: options.name,
email: options.email
}
}))
.done(function(user) {
m$.alert.api({ msg: "User created.", type: "success" });
m$.list.api.add_row({ user: user });
_$.api.clear_form();
})
.fail(function() {
m$.alert.api({ msg: "Could not create user.", type: "error" });
});
},
clear_form: function(_$, options) { _$("input").val(""); }
}
);
When m$.form.api.new_user
is called, it will post the data to our pre-defined
route.
When it's finished, we'll call to another component (not outlined here) called
alert
. We can call to any component from any component.
Below that, we call to the list component's api which will handle modifying its dom to include the new user.
On the last line of done
, we use _$
instead of m$
to call the component's
own api function, clear_form
.
We'll look at _$
in more detail coming up.
m$.form.events(function(_$) {
_$("button").click(function() {
var name = _$("input[type='text']"),
email = _$("input[type='email']");
_$.api.new_user({
name: name,
email: email
});
});
});
Now that we have our routes and api set up, we just need to bind this behavior to an event.
Inside components, Mozart gives us _$
which is really just your app's jQuery
library but scoped to the component.
So _$("button")
is the same as $("[data-component~='form'] button")
If you're not using jQuery, $
will instead become a thin wrapper around
JavaScript's native document.querySelectorAll
and converted to a real Array
type. _$
will work the same way, but be mindful that $
and _$
will always
return arrays even if one element is found.
_$
is also decorated with _$.api
and _$.routes
which just call to the
component's own .api
and .routes
without having to hold on to your this
within functions.
So _$.api.create_user
is the same as m$.form.api.create_user
But if we're not using Angular, React, Ember, etc. How do we manage reusable and configurable chunks of HTML like React's Components or Angular's Directives?
Again, JavaScript has native functionality already that emulates this well. Enter Document Fragments.
Let's look at our list
component, which will show a user's row with their full
name.
<aside data-component="list">
<ul>
<li>
<i class="available"></i>
John Smith
</li>
<li>
<i class="busy"></i>
Jane Doe
</li>
...
</ul>
</aside>
To use a more interesting example, we'll assume each user has a status icon next to their name, shown above.
We want to populate this list from a dynamic list. So let's store the template of a user row and generate new DOM elements from it whenever needed.
<aside data-component="list">
<ul></ul>
</aside>
<template id="row">
<li>
<i></i>
<span></span>
</li>
</template>
Now, in our api, we can define a function to populate this list from JSON.
m$.list = new Mozart();
m$.list.api({
...
map_users: function(_$, options) {
var list_html = ""
options.users.forEach(function(user) {
var row_template = $("template#row")
var $row = Mozart.clone(row_template);
$row.find("i").addClass(user.status);
$row.find("span").html(user.name);
list_html += $row.html();
});
_$("ul").html(list_html);
}
...
});
Or we could implement our "add one row" call from form_api.js above
...
map_users: function(_$, options) { ... }
...
add_row: function(_$, options) {
var row_template = $("template#row")
var $row = Mozart.clone(row_template);
$row.find("i").addClass(user.status);
$row.find("span").html(user.name);
_$("ul").append($row);
}
...
All that's left to do now is to tell Mozart our implementation is ready. Run this after all your code.
Mozart.init();
- The above is just a tour. View the full documentation
- You only need one JS file. It's Just under 3kb unminified and barely over 1kb minified.
- Zero dependencies but works seamlessly with jQuery.
- ECMAScript 5, 6, and 7 compatible.
- Works in all modern browsers and probably slightly older ones too.
- Used in production on paste.lol