24 May 2017 Ampersand.js
My main application development experience is with Ruby on Rails. In the JS frameworks space it was Angular. To me, Angular felt a bit like Java/Spring. It felt heavy, with a lot of code overhead to do the most simple of tasks. I think Ampersand aligns more with Rails, where I was able to get productive early, and results were more immediate. I could learn as I went and be productive at the same time. The short project turnaround times (~2 weeks per widget) meant that productivity was essential.
As a side note, I was also learning Coffeescript at the same time. Although there was an initial learning curve, it’s productivity benefits were soon realised.
The widgets being created generally had very simple requirements. Mostly it was just displaying data in a table, sometimes from multiple sources, often with filtering, and continually updating from a live feed. There was no posting back to the server involved. We used a Rails backend since we already had code for interfacing to external APIs. The backend service was to provide data in a simple API to the frontend, some storage of configuration settings and historical data, and handle caching. NPM was used for packaging the js modules for runtime.
With Ampersand, you can have your Models and Collections connected to your data API very easily, and then wire your Models to your Views for presentation with minimal fuss. Here is an example to demonstrate a very simple application showing articles with comments:
A Model Example:
Model = require("ampersand-model") Collection = require("ampersand-rest-collection") Comments = require("./comment").CommentCollection Article = Model.extend( props: title: ['string'] author: ['string'] rating: ['number'] collections: comments: Comments ) ArticleCollection = Collection.extend( url: -> '/api/articles.json' model: Article mainIndex: 'title' comparator: 'rating' ) module.exports = Article: Article ArticleCollection: ArticleCollection
This defines an Article which extends ampersand-model. It has several properties with their data types, and a collection of Comments (code not shown).
The ArticleCollection extends ampersand-rest-collection. It specifies the url for the source data, which can be updated with the fetch method (which you will see later) on the collection. The mainIndex can be used to reference a member of the collection. The comparator is for sorting the items in the collection.
A Model View Example:
View = require("ampersand-view") module.exports = View.extend( template: " <div class='article'> <div class='title'></div> <div class='author'></div> <div class='rating'></div> </div> " render: -> @renderWithTemplate() @ bindings: 'model.title': '.article > .title' 'model.author': '.article > .author' 'model.rating': '.article > .rating' )
This view code, extending ampersand-view, defines how an object is rendered. I am using just a few lines of html for a template – all the styling/layout is done with css as it should be. The interesting part is the bindings section. Here the model’s properties are mapped to dom elements using familiar css selectors. There are other, more advanced, binding options available (for example, toggling a class based on a boolean property value). As a model property changes, the dom is updated automatically.
A Collection View Example:
View = require("ampersand-view") ArticleView = require("./article") module.exports = View.extend( template: " <div class='articles'></div> " render: -> @renderWithTemplate() @renderCollection(@collection, ArticleView, @el) @ )
When it comes to the collection view, again we extend ampersand-view, but with a different render function. We use the renderCollection function, passing in the collection data, and also provide the ArticleView object (defined previously) to say how each item in the collection should be rendered.
The Main View example:
View = require("ampersand-view") AmpersandCollection = require("ampersand-rest-collection") ArticlesView = require("./articles") Articles = require("../models/article").ArticleCollection Article = require("../models/article").Article module.exports = View.extend( template: " <div class='widget-container'> <div class='articles'></div> </div> " collections: articles: Articles initialize: -> @articles.fetch() render: -> @renderWithTemplate() @ subviews: articlesView: prepareView: (el)-> new ArticlesView(el:el, collection: @articles) selector: '.articles' )
I am defining a ‘main’ view here as a top-level container for the app. This is the same type of object as the other views, but I’m making use of more features from ampersand-view to tie the application together.
I reference the ArticleCollection defined earlier, and use an initialize function to fetch the articles dataset when this view is instantiated. I have also added the ArticlesView in subviews, so that the articles collection will be rendered.
Main Application Object example:
MainView = require("./views/main") domReady = require("domready") widget = require("ampersand-app") widget.extend( init: -> window.myWidget = this start: (appElementId)-> self = window.myWidget # wait for document ready to render our main view # this ensures the document has a body, etc. domReady -> # init our main view self.mainView = self.view = new MainView( el: document.getElementById(appElementId) ) # ...and render it self.mainView.render() ) # load it widget.init()
and the app will fetch and render the content within the container on the web page.
This is a very basic example, but it should give you a feel for how clear and compact the code can be. If you have your own API, you could modify the example code above, provide some CSS, and effectively have a stand-alone widget to display your API’s information on any web page.
Readability and Maintainability
The thing that appeals to me here is that the code is neatly laid out and so easy to follow. Each object has well named properties and there is no bloated code. The structure makes putting your application together almost as easy as filling in the blanks.
There is obviously more depth available to you:
- Derived Properties (properties that depend on one or more of the base properties) will be frequently used for displaying your data in different ways.
- Collection Filters can be used to show a subset of your collection based on different property values.
- A range of Bindings to modify attributes and classes of your dom elements based on your data.
- A nice Event trigger system to handle how your data reacts to changes.
Is Ampersand The Answer?
I can say that it has worked quite well for us, but our projects have a specific and limited scope that made Ampersand a good fit. You will have to decide for yourself if it is worth a punt. However, if you have something small on which to give it a try, it is at least worth a look. If nothing else, I think it shows that you can have a lightweight and effective framework, which serves the purpose for which it was designed.