Mozart.js

Learn more on Github


Why Mozart will be your new indispensible tool

Front end coding is getting out of hand. Projects that could easily robust and lightweight implementations fall into the pit of throwing in complex single page app frameworks to solve their needs.

Yet JavaScript already supports many of the basic parts of these frameworks on its own. The author of Mozart believes it's the organization of code we're really running after with gargantuan frameworks.

Mozart is not a framework. In fact, it's a small file that actually does very little more than give you some guide rails to adhering to a pattern.

Mozart uses the Scoped Component pattern to logically organize the entire front end into "components". Each of which has an API, routes, and events. Read more on the Readme

But isn't it more code?

Maybe. But better code is the goal, not necessarily less code.

m$.invoice_reporter = new Mozart;

m$.invoice_reporter.events(function(_$) {
  _$(".send").click(function() {
    var invoice = {
      name: _$(".name").val(),
      date: _$(".date").val(),
      amount: _$(".amount").val(),
      user: _$(".user").val()
    }
    _$.api.send({ invoice: invoice });
  });
  // ...
});

m$.invoice_reporter.api({
  send: function(_$, options) {
    m$.notification.api.info({
      message: "Sending invoice...", dismiss: false
    });
    m$.progress_bar.api.start();

    $.ajax(_$.routes.create({ data: options.invoice }))
      .done(function(data) {
        m$.notification.api.success({ message: "Invoice created" });
        m$.progress_bar.api.stop();
        _$.api.get_index();
      })
      .fail(function(data) {
        m$.notification.api.success({
          message: "Could not create invoice. Error: " + data
        });
        m$.progress_bar.api.stop();
      });
  },
  // ...
});

m$.invoice_reporter.routes({
  create: {
    method: "post",
    url: "/invoices"
  }
});

More Examples


          

Create a component

Before anything else, you'll need to load the Mozart script and declare your first component.

The variable m$ is provided by Mozart to organize your components at a high level without cluttering the window namespace.

Every component should follow the pattern m$.[component name]

m$.user_table = new Mozart;
          

Mozart will know to look for HTML elements with a data-component attribute that includes user_table when referencing m$.user_table

It's good practice to put in some fallback content for the component if you plan on dynamically filling from an external data source.

In this case, we've put "Loading users" as our first rung of the user table because we intend for there to be some delay before the user list comes in from the server. Once it is, we'll wipe away the contents of the data-render field.

We'll get to that later when we talk about templates.

User Table

  • Loading users...

Define an API

The API of your component will define named entry points into discreet functionality.

For this example, we'll assume we need to perform a basic database query to populate the table

m$.user_table.api({
  get_users: function(_$, options) {
    // ...
  }
});

Scoped selector

_$ will automatically be scoped to your component.

If you're using jQuery, this means this code woudl be the equivalent of calling $("[data-component~='user_table'] h1").html("test");

If you're using plain JavaScript, this would be the equivalent of calling document.querySelectorAll("[data-component~='user_table'] h1") so be mindful that this will always return an array. You'll need to tack on a [0] at the end if you expect and want to work with only one element.

m$.user_table.api({
  get_users: function(_$, options) {
    _$("h1").html("test");

    // OR

    _$("h1")[0].innerHTML = "test"; // If not using jQuery
  }
});

Calling api commands

If calling the component's own api command, you can call from _$.api which will persist this and can also be used in your events configuration.

m$.user_table.api({
  ask_bar_to_say_foo: function(_$, options) {
    _$.api.bar_says_foo();
  },

  bar_says_foo: function(_$, options) {
    alert("foo");
  }
});

To call another component's api, just replace the _$ with m$

m$.component_a.api({
  tell_b_to_say_foo: function(_$, options) {
    m$.component_b.say_foo();
  }
});

m$.component_b.api({
  say_foo: function(_$, options) {
    alert("foo");
  }
});

Arguments

Notice that each api function has two arguments, _$ and options

Callers need not worry about passing in _$, Mozart does this for you. Instead treat the api function as if it took only the argument options

m$.person_reporter.api({
  display_name_and_age: function(_$, options) {
    alert("Name is " + options.name + " and age is " + options.age);
  }
});

m$.person_reporter.events(function(_$) {
  _$("button").click(function() {
    var name = _$(".name").val(),
        age  = _$(".age").val();

    // Only one {} argument needed.
    _$.api.display_name_and_age({ name: name, age: age })
  });
});

Define routes

Routes allow you to easily categorize the urls and REST methods to call for different purposes

.
m$.user_table.routes({
  index: {
    method: "GET",
    route: "/users"
  }
});

m$.user_table.api({
  get_users: function(_$, options) {
    $.ajax(_$.routes.index)
      .done(function() { alert("yeah!") })
      .fail(function() { alert("aww.") });
  }
});

You can also interpolate strings for id-specific routing

m$.user_table.routes({
  show: {
    method: "GET",
    route: "/users/#{user_id}"
  }
});

m$.user_table.api({
  show_user: function(_$, options) {
    // Will GET "/users/5"
    $.ajax(_$.routes.index({ user_id: options.id }))
      .done(function() { alert("yeah!") })
      .fail(function() { alert("aww.") });
  }
});

m$.user_table.events(function(_$) {
  _$(".user-row").click(function() {
    // Assuming id = 5
    _$.api.show_user({ id: $(this).data().id });
  })
});

Define events

Selecting

Events allow you to scope selectors to your component and easily route behaviors to your api.

Remember to pass in a function that takes _$.

m$.user_table.events(function(_$) {
  _$("button").click(function() {
    alert("You clicked on the button that was in the user_table component")
  });

  $("button").click(function() {
    // Notice this one uses $ and not _$
    alert("The button you clicked on may or may not be in user_table component");
  });
});

Calling the API

Use the same syntax to call to the api as you would in an API function.

Ideally, event definitions do nothing more than route actions to apis.

m$.user_table.events(function(_$) {
  _$("button").click(function() {
    m$.notification.show({
      message: "Loading users...",
      type: "info"
    });
    _$.api.load_users();
    m$.notification.hide();

    // Notice the first and last calls are to
    // another component's api while the middle
    // line calls to its own.
  });
});

Special Topics

Dealing with collections

Collections are an important function of a framework like Mozart. I wanted to spend my time I had remaining on this project focusing on telling the story of this exploration but do not plan to regularly maintain this project.

However, I encourage anyone who finds something special in this way of organizing code to continue driving this forward.

Reuse HTML

Single page app frameworks have a few different ways of letting you define an html template to use for multiplying and filling in from a dataset.

It bears repeating that native HTML and JavaScript already has this functionality. Mozart has just added a thin wrapper around it to make it easier to use and compatible with the Mozart pattern.

Create a template

We can simply put our HTML template right in the HTML itself, using the <template> tag.

Note, it is highly recommended not to nest template tags within each other. You can still create nested HTML without doing this and this will be explained later.

Duplicate template

In your api, you can clone the template in one line with Mozart.clone

This will make a copy of and instantiate from your template element's contents.

m$.user_table = new Mozart;

m$.user_table.api({
  populate: function(_$, options) {
    var $link_template = $("#user_row");
    $.ajax(_$.router.get)
      .done(function(users) {
        var $tableHTML = $("<div></div>");
        users.forEach(function(user) {
          // We're wrapping in $() just so we can treat it
          // as a jQuery object.
          var $user_link = $(Mozart.clone($link_template));
          if (user.offline) {
            $user_link.find(".status").addClass("status-offline")
          }
          $user_link.find(".name").html(user.name);
          $user_link.find(".email").html(user.email);
          $tableHTML.append($user_link);
        });

        // Only talk to the DOM once, when we're done.
        _$(this).html($tableHTML.html());
      })
  }
});

Check back later this week. Documentation is underway.

This page was built with Kickstart. Table of Contents generated by Smooth ToCer