📜 ⬆️ ⬇️

An invoice for payment. Working application on sails.js, ractive.js, Backbone.js



Good day, over the weekend from boredom and lack of work, I decided to entertain myself by writing a small application that will fit in as a training material for exploring the possibilities of two wonderful libraries - ractive.js and sails.js

Formulation of the problem

At work, it is often necessary, after completing the next task (I am a freelancer), to bill the customer for payment of services. Especially if you are dealing with legal entities. For this, I used a simple html-template, in which I put the data in my hands, correcting the next ...
')
It looks like this

image

I admit, styles and layouts are stolen from freshbooks.com, which I used in my time. Unfortunately, it didn’t suit me for Russian clients, and a simple html template was enough for me.

Choice of technology

In the current trend of popularity of js-frameworks of all stripes and server-side js development, I wanted to use something tasty and reactive for this task in order to spend a little time in this stream of happiness js ... And try these toys in parallel.

After some studies, comparisons and intuitive insights, I stopped at sails.js as a server. I chose between derby and sails - in the end I chose a sailboat, mainly because of its simplicity (the dock is easy and pleasant to read), it also has a very cool rest api generator out of the box. In terms of learning, Derby seemed harder and more monstrous (for this example, a clear overhead).

On the client I decided to play with ractive.js . And later it was decided to connect backbone.js - mainly because of the convenient work with the models.

I didn’t have sails.js and ractive.js experience before this example. I used only backbone.
Let's get started

Server


For our example, we will use sails v0.10 - it is still in beta, but compared to the current stable version 0.9.x there are several buns in it that will be useful. In particular, model assocoations , which allow one-to-many, many-to-many to be set (and other relationships between models), the grunt task system is also modified in 0.10. In the dock at 0.10, everything is pretty clearly written

sails v0.10 can be delivered via npm (I set it globally)
sudo npm install -g "git://github.com/balderdashy/sails.git#v0.10" 

check
 sails -v 

0.10.0 - excellent

Creating a skeleton of the sailsjs application

Create a new application, for example, invoicer and set dependencies
 sails new invoicer cd invoicer npm install 

Next, by running the sails lift command, you can start the embedded express.js server localhost:1337

Creating an entity (models) API

We need 3 models for the application:

Create with the sails generate api <api_name> command sails generate api <api_name>
API generation
 zaebee@zaeboo$ sails generate api user debug: Generated a new model `User` at api/models/User.js! debug: Generated a new controller `user` at api/controllers/UserController.js! info: REST API generated @ http://localhost:1337/user info: and will be available the next time you run `sails lift`. zaebee@zaeboo$ sails generate api invoice debug: Generated a new model `Invoice` at api/models/Invoice.js! debug: Generated a new controller `invoice` at api/controllers/InvoiceController.js! info: REST API generated @ http://localhost:1337/invoice info: and will be available the next time you run `sails lift`. zaebee@zaeboo$ sails generate api task debug: Generated a new controller `task` at api/controllers/TaskController.js! debug: Generated a new model `Task` at api/models/Task.js! info: REST API generated @ http://localhost:1337/task info: and will be available the next time you run `sails lift`. 


after that 3 files will appear in the api / controllers folder
 -rw-r--r-- 1 146  28 17:15 InvoiceController.js -rw-r--r-- 1 143  28 17:15 TaskController.js -rw-r--r-- 1 143  28 17:15 UserController.js 

also api / models
 -rw-r--r-- 1 146  28 17:15 Invoice.js -rw-r--r-- 1 143  28 17:15 Task.js -rw-r--r-- 1 143  28 17:15 User.js 


Easy and simple sails created 3 methods for us,
localhost:1337/user
localhost:1337/invoice
localhost:1337/task
which support CRUD operations. There are also aliases for them, for example, localhost:1337/user/create?name=Andrey&address=Russia localhost:1337/user/create?name=Andrey&address=Russia - will create a new user instance. You can play through postman

I also advise you to read the documentation for the controllers.

Storage Configuration (DB)

Where are the generated data stored? By default, the disk is used as storage, which is specified in the config/connections.js settings and config/models.js
config / connections.js code
 module.exports.connections = { localDiskDb: { adapter: 'sails-disk' }, someMysqlServer: { adapter : 'sails-mysql', host : 'YOUR_MYSQL_SERVER_HOSTNAME_OR_IP_ADDRESS', user : 'YOUR_MYSQL_USER', password: 'YOUR_MYSQL_PASSWORD', database: 'YOUR_MYSQL_DB' }, someMongodbServer: { adapter : 'sails-mongo', host : 'localhost', port : 27017, //user : 'username', //password : 'password', database : 'invoicer' }, somePostgresqlServer: { adapter : 'sails-postgresql', host : 'YOUR_POSTGRES_SERVER_HOSTNAME_OR_IP_ADDRESS', user : 'YOUR_POSTGRES_USER', password : 'YOUR_POSTGRES_PASSWORD', database : 'YOUR_POSTGRES_DB' } }; 


We will use mongo to store the records, for this we will slightly change the config / models.js:
code config / models.js
 /** * Models * (sails.config.models) * * Unless you override them, the following properties will be included * in each of your models. */ module.exports.models = { // Your app's default connection. // ie the name of one of your app's connections (see `config/connections.js`) // // (defaults to localDiskDb) connection: 'someMongodbServer' }; 


We describe the fields we need for the User, Invoice, and Task models.
api / models / User.js
 module.exports = { attributes: { name: 'string', email: 'string', avatar: 'string', address: 'text', account: 'text', invoices: { collection: 'invoice', via: 'owner', } }, }; 


api / models / Invoice.js
 module.exports = { attributes: { total_amount: 'float', name: 'string', address: 'text', owner: { required: false, model: 'user', }, tasks: { required: false, collection: 'task', via: 'invoice', } }, }; 


api / models / Task.js
 module.exports = { attributes: { name: 'string', description: 'text', hours: 'float', rate: 'float', invoice: { required: false, model: 'invoice', via: 'tasks', } }, }; 


to use mongo adapter you need to put the package sails-mongo
 npm install sails-mongo@0.10 


Adding `action` for the controller, and a template (view) for it

We need to create a controller that will generate a page for our main task (creating an invoice):
 sails generate controller main generate 

We have created a new MainController.js , in which one generate function is created, the so-called action
if you go to urla localhost:1337/main/generate localhost:1337/main/generate we will see what the generate function returned to us
By default, it will return json
 return res.json({ todo: 'Not implemented yet!' }); 

We want to see in the browser html-page. To do this, replace the above code with
 return res.view() 

update the page in the browser and see the error
 { "view": { "name": "main/generate", "root": "/home/zaebee/projects/invoicer/views", "defaultEngine": "ejs", "ext": ".ejs" } } 

This means that we have not created a chalon for the view. All html-templates for controllers are in the views folder and have the following structure views/<controller_name>/<action_name>

create an empty views / main / generate template
 zaebee@zaeboo$ mkdir views/main zaebee@zaeboo$ touch views/main/generate.ejs 

By default, ejs is used as a template engine. Sails supports many template engines and you can change it in the config / views.js file to your favorite:
ejs, jade, handlebars, mustache
underscore, hogan, haml, haml-coffee, dust
atpl, eco, ect, jazz, jqtpl, JUST, liquor, QEJS,
swig, templayed, toffee, walrus, & whiskers


ATTENTION! in sails version 0.10, support for layouts only works with ejs. In short, there is a base layout, views/layout.ejs , from which all other views/layout.ejs inherit. And when using a template engine, inheritance other than ejs will not be. Sails makes this clear if you change the engine option in the config/views.js
 warn: Sails' built-in layout support only works with the `ejs` view engine. warn: You're using `hogan`. warn: Ignoring `sails.config.views.layout`... 


Customer


The server is ready, let's start writing the client part of our invoice creation application.

Connecting statics

All static (or public client code) lies in the assets folder. in order to include new files to your template, simply place them in the appropriate folder (scripts in assets / js, styles in assets / styles, client templates in assets / templates) and sails using your grunt task to write them to your index / layout .ejs - in special sections:
Listing the source file /views/layout.ejs
  <!DOCTYPE html> <html> <head> <title>New Sails App</title> <!-- Viewport mobile tag for sensible mobile support --> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <!--STYLES--> <link rel="stylesheet" href="/styles/importer.css"> <!--STYLES END--> </head> <body> <%- body %> <!--TEMPLATES--> <!--TEMPLATES END--> <!--SCRIPTS--> <script src="/js/dependencies/sails.io.js"></script> <!--SCRIPTS END--> </body> </html> 


Let's connect the necessary libraries to our layout (Jquery, Underscore, Backbone, Ractive) via cdn, also put bootstrap.min.css and the finished app.css file in the assets/styles folder. Also, we will place additional js that will be needed ( bootstrap.min.css , moment.ru.js and moment.min.js - a library for working with dates) in the folder assets/js/vendor and empty file app.js in the assets/js folder assets/js . Run sails lift and see what we now have in the views/layout.ejs
Listing the source file /views/layout.ejs
  <!DOCTYPE html> <html> <head> <title>New Sails App</title> <!-- Viewport mobile tag for sensible mobile support --> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <!--STYLES--> <link rel="stylesheet" href="/styles/app.css"> <link rel="stylesheet" href="/styles/bootstrap.min.css"> <link rel="stylesheet" href="/styles/importer.css"> <!--STYLES END--> <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.4.4/underscore-min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/backbone.js/1.0.0/backbone-min.js"></script> <script src="//cdn.ractivejs.org/latest/ractive.min.js"></script> <script src="//api.filepicker.io/v1/filepicker.js"></script </head> <body> <%- body %> <!--TEMPLATES--> <!--TEMPLATES END--> <!--SCRIPTS--> <script src="/js/dependencies/sails.io.js"></script> <script src="/js/app.js"></script> <script src="/js/vendor/bootstrap.min.js"></script> <script src="/js/vendor/moment.min.js"></script> <script src="/js/vendor/moment.ru.js"></script> <!--SCRIPTS END--> </body> </html> 


Great, sails did everything for us. True, there is one drawback - vendor scripts are connected below our app.js. We will fix the tasks/pipeline.js file tasks/pipeline.js indicate to grunt that the vendor folder needs to be connected before:
Listing of the tasks / pipeline.js file
 ...... // CSS files to inject in order // // (if you're using LESS with the built-in default config, you'll want // to change `assets/styles/importer.less` instead.) var cssFilesToInject = [ 'styles/**/*.css' ]; // Client-side javascript files to inject in order // (uses Grunt-style wildcard/glob/splat expressions) var jsFilesToInject = [ // Dependencies like sails.io.js, jQuery, or Angular // are brought in here 'js/dependencies/**/*.js', 'js/vendor/**/*.js', //   vendor // All of the rest of your client-side js files // will be injected here in no particular order. 'js/**/*.js' ]; ........ 


The preparation of the client part is completed - we can proceed directly to writing the business logic of the application.

Create a page layout skeleton. Ractive.js templates

Take another look at our layout. On it, I highlighted the blocks that we will bind to our dynamic data.

Create a base markup in the views / main / generate.ejs file that our client templates will be included in
listing file views / main / generate.ejs
  <div class="main_bg"> <div class="container primary-content"> <div class="invoice-container rounded-container peel-shadows col-sm-8 col-sm-offset-2"> <h2 style="text-align:center;margin-bottom:30px;">  </h2> <div class="invheader"> <div class="invheader-upper"> <!-- User .       : , ,  --> </div> <div class="invheader-lower"> <!-- Invoice .            --> </div> </div> <div class="invbody"> <div class="invbody-tasks"> <!-- Task .        --> </div> <div class="clearb" style="height: 1px; overflow: hidden;"></div> <div class="invbody-account"> <!-- User  .        --> </div> </div> </div> </div> </div> 



So, the basic markup is ready - it's time for ractive.js templates
Create a template for each of our blocks (there will be a total of four) and place them in assets / templates
listing of assets / templates / invheader-upper.html file
  <div class="invheader-address-account" on-hover="toggleBtn"> <a role="button" title="{{ .editing ? '' : ' ' }}" class="hidden-print btn btn-primary btn-sm hide" on-click="edit"><i class="glyphicon glyphicon-{{ .editing ? 'ok' : 'pencil' }}"></i> </a> <b>:</b> <div class="user-name {{ .editing ? 'editing' : '' }}"> <span>{{^name}} {{/name}}{{name}}</span> {{#.editing}} <div class='edit-container'> <input intro="select" value="{{name}}" class="form-control" placeholder="  "> </div> {{/.editing}} </div> <div class="user-address {{ .editing ? 'editing' : '' }}"> <span>{{^address}}  {{/address}}{{{address}}}</span> {{#.editing}} <div class='edit-container'> <textarea value="{{address}}" class='edit form-control' placeholder="  ">{{address}}</textarea> </div> {{/.editing}} </div> </div> <div on-hover="togglePicker" class="invheader-logo-container"> <div class="invheader-logo"> {{#avatar}} <img src="{{avatar}}/convert?h=110&w=250" alt="{{name}}"> {{/avatar}} <div class="hidden-print BoardCreateRep {{ avatar ? 'hide' : '' }}"> <input type="filepicker-dragdrop" data-fp-mimetype="image/png" data-fp-apikey="A3lXl09sRSejY4e0pOOSQz" data-fp-button-class="btn btn-primary hidden-print" data-fp-button-text=" " data-fp-drag-text="  " data-fp-drag-class="hidden-print drop-avatar" onchange="app.user.fire('setAvatar', event)"> </div> </div> </div> 


listing of assets / templates / invheader-lower.html
  <div class="invheader-address-client" on-hover="toggleBtn"> <a role="button" title="{{ .editing ? '' : ' ' }}" class="hidden-print btn btn-primary btn-sm hide" on-click="edit"><i class="glyphicon glyphicon-{{ .editing ? 'ok' : 'pencil' }}"></i> </a> <b>:</b> <div class="cleint-name {{ .editing ? 'editing' : '' }}"> <span>{{^invoice.name}} {{/invoice.name}}{{invoice.name}}</span> {{#.editing}} <div class='edit-container'> <input intro="select" value="{{invoice.name}}" class="form-control" placeholder="   "> </div> {{/.editing}} </div> <div class="client-address {{ .editing ? 'editing' : '' }}"> <span>{{^invoice.address}}  {{/invoice.address}}{{{invoice.address}}}</span> {{#.editing}} <div class='edit-container'> <textarea value="{{invoice.address}}" class="edit form-control" placeholder="   ">{{invoice.address}}</textarea> </div> {{/.editing}} </div> </div> <div class="invheader-invoicedetails"> <table cellspacing="0"> <tbody> <tr> <th> </th> <td>#{{ lastFour(invoice.id) }}</td> </tr> <tr> <th>           </th> <td>{{ date(invoice.createdAt) }}</td> </tr> <tr class="invheader-invoicedetails-balance"> <th><div>             </div></th> <td><div> {{^invoice.total_amount}}0.00{{/invoice.total_amount}}{{ invoice.total_amount }}       </div></td> </tr> </tbody> </table> </div> 


listing of assets / templates / invbody-tasks.html
  <table class="invbody-items" cellspacing="0"> <thead> <tr> <th class="first"><div class="item">      </div></th> <th><div class="description">,        </div></th> <th><div class="unitcost"> ()</div></th> <th><div class="quantity">      </div></th> <th class="last"><div class="linetotal"> ()</div></th> </tr> </thead> <tbody> {{#tasks}} <tr> <td style="width: 160px;"> <a on-tap="destroy:{{this}}" role="button" class='hidden-print destroy'></a> <div on-click="edit" class="item">{{name}}</div> <input intro="select" class="form-control hide" value="{{name}}" on-blur-enter="hide:{{this}}"> </td> <td> <div on-click="edit" class="description">{{description}}</div> <textarea class="form-control hide" value="{{description}}" on-blur-enter="hide:{{this}}">{{description}}</textarea> </td> <td style="width: 85px;"> <div on-click="edit" class="unitcost">{{ format(rate) }}</div> <input class="form-control hide" value="{{rate}}" on-blur-enter="hide:{{this}}"> </td> <td style="width: 80px;"> <div on-click="edit" class="quantity">{{ format(hours) }}</div> <input class="form-control hide" value="{{hours}}" on-blur-enter="hide:{{this}}"> </td> <td style="width: 90px;"> <div class="linetotal">{{ format(rate * hours) }}</div> </td> </tr> {{/tasks}} <tr> <td class="hidden-print text-center" colspan="5"> <button on-click="add" class="btn btn-primary btn-sm"><i class="glyphicon glyphicon-plus "></i> </button> </td> </tr> </tbody> </table> <table class="invbody-summary" cellspacing="0"> <tbody> <tr> <td class="invbody-summary-clean"> </td> <td style="width: 150px;"><strong> :    </strong></td> <td style="width: 120px;"><strong> {{ total(tasks) }} </strong></td> </tr> <tr class="invbody-summary-paid"> <td class="invbody-summary-clean"> </td> <td style="width: 150px;">    </td> <td style="width: 120px;">-0.00</td> </tr> <tr class="invbody-summary-total"> <td class="invbody-summary-clean"> </td> <td style="width: 150px;"><div><strong>  :    </strong></div></td> <td style="width: 120px;"><div><strong> {{ total(tasks) }} </strong></div></td> </tr> </tbody> </table> 


listing of assets / templates / invbody-account.html
  <div class="invbody-terms" on-hover="toggleBtn"> <a role="button" title="{{ .editing ? '' : ' ' }}" class="hidden-print btn btn-primary btn-sm hide" on-click="edit"><i class="glyphicon glyphicon-{{ .editing ? 'ok' : 'pencil' }}"></i> </a> <b>:</b> <div class="user-account {{ .editing ? 'editing' : '' }}"> <span>{{^account}}  {{/account}}{{{account}}}</span> {{#.editing}} <div class='edit-container'> <textarea value="{{account}}" class='edit form-control' placeholder="    ">{{account}}</textarea> </div> {{/.editing}} </div> </div> 


In general, this is the usual html, interspersed with mustache-like tags {{}} , in which ractive.js inserts its data. You may also notice some directives on-click="edit" - executes the edit method on click; on-hover="toggleBtn" , on-tap="destroy:{{this}}" illuminate this moment later, you can still explore the dock for events ractive.js

Events are connected to ractive in the form of plug-ins - the so-called proxy-events. To make the events work, you need to download the ones we need (I downloaded all the event plugins) and put them in the assets/js/vendor folder
Place the adapter for Backbone in the same folder so that ractive.js can use the backbone model as a data source.

Initialization of data. Binding data and templates

Let's summarize what is at the moment and what we want to get in the end


First, create the Backbone models we need in our empty assets/js/app.js :
Listing assets / js / app.js
 var app = app || {}; (function (app) { app.User = Backbone.Model.extend({ urlRoot: '/user', }); app.Invoice = Backbone.Model.extend({ urlRoot: '/invoice', }); app.Task = Backbone.Model.extend({ urlRoot: '/task', }); app.Tasks = Backbone.Collection.extend({ url: '/task', model: app.Task }); })(app); 


Ok, now let's create a ractive instance that will be tied to our app.User model and will render our assets/templates/invheader-upper.html and assets/templates/invbody-account.html
Create a file assets / js / user.js
Listing assets / js / user.js
 var app = app || {}; (function (app) { var backboneUser = new app.User; //    ractive   Ractive.extend //  new Ractive({}),      2   var RactiveUser = Ractive.extend({ init: function (options) { this.data = options.data; this.on({ //      //   `on-click="edit"` edit: function (event) { var editing = this.get('editing'); this.set( 'editing', !editing ); if (editing) { this.data.save(); //     } }, //       //  https://www.inkfilepicker.com //   `onchange="app.user.fire('setAvatar', event)"` setAvatar: function (event) { if (event.fpfile) { var url = event.fpfile.url; this.set('avatar', url); } else { this.set('avatar', null); } this.data.save(); //     }, //        //   `on-hover="togglePicker"` togglePicker: function (event) { if (!this.get('avatar')) return; if ( event.hover ) { $(event.node).find('.BoardCreateRep').removeClass('hide'); } else { $(event.node).find('.BoardCreateRep').addClass('hide'); } }, //        //   `on-hover="toggleBtn"` toggleBtn: function (event) { if ( event.hover ) { $(event.node).find('[role=button]').removeClass('hide'); } else { $(event.node).find('[role=button]').addClass('hide'); } } }); } }); //  RactiveUser    //      `.invheader-upper` app.user = new RactiveUser({ el: '.invheader-upper', template: JST['assets/templates/invheader-upper.html'](), data: backboneUser, adaptors: [ 'Backbone' ], }); //  RactiveUser    //      `.invheader-account` app.account = new RactiveUser({ el: '.invbody-account', template: JST['assets/templates/invbody-account.html'](), data: backboneUser, adaptors: [ 'Backbone' ], }); //    Id  //  id  (   ) //      app.user.observe('id', function(id){ if (id && app.invoice) { app.invoice.data.invoice.set('owner', id); app.invoice.data.invoice.save(); } }); })(app); 


The code is quite simple. Here we create the base class RactiveUser . You can usually create an instance through new Ractive({}) , but in particular, here we need 2 elements for the user, which are tied to one model and which follow almost the same events. The events themselves are specified in the body of the init function.

Let's go further, create by analogy assets/js/invoice.js and assets/js/task.js
Listing assets / js / invoice.js
 var app = app || {}; (function (app) { app.invoice = new Ractive({ el: '.invheader-lower', template: JST['assets/templates/invheader-lower.html'](), data: { invoice: new app.Invoice, //  Backbone  //        {{ date(createdAt) }} date: function (date) { return moment(date).format('D MMMM YYYY'); }, //    {{ lastFour(id) }} lastFour: function (str) { return str.slice(-4); } }, adaptors: [ 'Backbone' ], transitions: { select: function ( t ) { setTimeout( function () { t.node.select(); t.complete(); }, 200 ); } } }); app.invoice.on({ //      //   `on-click="edit"` edit: function (event) { console.log(event); var editing = this.get('editing'); this.set( 'editing', !editing ); if (editing) { this.data.invoice.save({owner: app.user.data.id}); } }, //        //   `on-hover="toggleBtn"` toggleBtn: function (event) { if ( event.hover ) { $(event.node).find('[role=button]').removeClass('hide'); } else { $(event.node).find('[role=button]').addClass('hide'); } } }); //      app.invoice.data.invoice.save(); })(app); 


Listing assets / js / task.js
 var app = app || {}; (function (app) { app.tasks = new Ractive({ el: '.invbody-tasks', template: JST['assets/templates/invbody-tasks.html'](), data: { tasks: new app.Tasks, //  Backbone  //     {{ format(price) }} format: function ( num ) { return num.toFixed( 2 ); }, //     {{ total(tasks) }} total: function ( collection ) { var total = collection.reduce(function( sum, el ) { return el.get('rate') * el.get('hours') + sum; }, 0 ); return total.toFixed( 2 ); }, }, adaptors: [ 'Backbone' ], transitions: { select: function ( t ) { setTimeout( function () { t.node.select(); t.complete(); }, 200 ); } } }); app.tasks.on({ //       //   `on-click="add"` add: function ( event ) { var tasks = this.get('tasks'); var task = new app.Task({ name: ' ', description: ' ', hours: 0, rate: 0, }); tasks.add(task); task.save(null, { // ,         success: function() { task.set('invoice', app.invoice.data.invoice.id); task.save(); } }); }, //      //   `on-tap="destroy:{{this}}"` destroy: function ( event, task ) { task.destroy(); }, //       //   `on-click="edit"` edit: function ( event ) { $(event.node).hide(); $(event.node).next().removeClass('hide').focus().select(); }, //     -  //   `on-blur-enter="hide"` hide: function ( event, task ) { $(event.node).addClass('hide'); $(event.node).prev().show(); task.save({invoice: app.invoice.data.invoice.id}); }, }); //     `hours`  `rate`   //    //      // TODO       app.tasks.observe('tasks.*.hours tasks.*.rate', function(tasks, old, keypath){ var total = this.data.total(this.data.tasks); app.invoice.data.invoice.set('total_amount', total); }); })(app); 


Here, too, the code is quite understandable, for comments added comments. In essence, this is all client code. I also planned to fasten the method for generating a static invoice based on id (for example, localhost:1337/main/generate/535ea7aa6113230d773fd160 localhost:1337/main/generate/535ea7aa6113230d773fd160 ) or use api pdfcrowd.com, since they have a module for node, which allows you to create pdf by urla ... However, I didn’t manage to do it over the weekend. Now I create pdf via ctrp + P (send to print) -> “Print to file”. And in order not to get out unnecessary html elements (for example, buttons) - added for them the class hidden-print .

Deploy to server

This is almost all - the application is ready. This example is on a githab

On the server we clone the repository, set dependencies and run sails in production mode:
 node app.js --port=8000 --prod 


Launched a working demo in production mode

Summary


The result of working with both sailsjs and ractive is very pleased.
Sailsjs - pluses:
+ I liked how simple api is created in sails
+ Very cool configuration options, starting from the template engine, database, and used by ORM (I plan to fasten bookshelfjs.org on sails)
+ I liked it very much that there are ready-made grunt hauls, which do a good job of generating the task of both prod and vir bundles.
+ there is a command ( sails www ) which collects only the client code - convenient for separating the work of the front and the server.
+ ( , , )

Minuses:
— model assocoations (, v0.10 — , v0.9.x — )
— ejs

Ractivejs — :
+ backbone
+ ( )
+ mustache ( ejs — )
+ ,

Ractive — .

Thank you for attention.

Source: https://habr.com/ru/post/221171/


All Articles