📜 ⬆️ ⬇️

Ractive.js - the diamond age of web development

According to the development team itself, Ractive.js is a library for developing dynamic web interfaces, thanks to which the world of web development will flourish: everyone will be given bonuses of 100%, holivars who will be cooler will step aside, and developers who write interactive , dynamic sites finally cease to be covered with gray hair and foul.

In short, the diamond age of web development will come.

Starting another project, before starting to write Backbone code (fu-fu-fu), I decided to use this miracle in the project (diamonds!). And since I’ve chewed up Google, I realized that there’s only one article about Ractive.js on Habré, I need to eliminate this injustice and at the same time write about whether the car of happiness will fall to all of us and whether anyone will be happy at all. After all, to promise the "diamond age" is one thing (every 4 years we hear from the telephones), and to do it is quite another.
')
Under the cut, I’ll review what Ractive.js is and how it works, and I will write out in detail the production task with full implementation and description of what all this threatens to us all.

What is this beast at first?


Briefly (in fact, very briefly, but catch the idea, and the details will come with the code) and in fact: Ractive.js is terribly simple and consists of two halves:



Accordingly, after you have described the template, the data and where it will all be rendered (usually the id DOM of the element on the page),
Ractive.js provides (and absolutely disinterestedly and without your participation) a two-way connection between this data and the template.
That is, as soon as the data is changed, your view that corresponds to this data is immediately reactively changed, the necessary DOM elements are deleted and deleted. Well, in the opposite direction:
as soon as a user pokes something up in your application, the data changes.
And so in a circle. And all very reactive.

You ask: "And what's the joy?"

And the joy is that you write a short, clear, declarative code without millions of events (which, as usual, fly back and forth, and we are trying to observit, bandage, etc.).
And most importantly, there is no manipulation of the DOM! We are not creating new elements ... we are not deleting them from the DOM ...
we never actually look for any element with $ (selector) - everything is always at hand.
Ractive builds a parallel DOM in the same way as React does and produces only pointwise manipulations with the DOM.
No unnecessary movements, and therefore it works very quickly. By the way, the Ractive is better / worse. React is a topic for a separate article.

It's simple: changed the data - changed the display. This is the general idea of ​​the library.

Now we will look at the implementation, the actual code and the details of how it works. But first we will form a TOR for what we want to write.

Practice


It was necessary to write comments to the blog. You ask why not to use Disqus or something like that. The bottom line is that
Our project is engaged in education in the field of healthy lifestyle and in a public blog we used just Disqus.
But within the framework of the project, online trainings are held, and comments in them should be private, as they contain private information and all sorts of valuable answers to questions.
So I had to make my component.

So, the requirements:



Actually, to immediately understand what and how we will build here on the link to jsfiddle with the final result .

Templates

Ractive.js for UI description uses {{mustache}}. Only they podhachili him slightly and expanded.
I will not describe here mustache, you can read about it here who is not familiar.

I will describe only the features that made Ractive:


There are still differences, but now it’s not up to them.
Then everything is simple: each Ractive instance has data on it that needs to be displayed in the template.
It puts data into the template and builds the HTML structure we need, and each object from data has a html block from the template.
Therefore, when changing data, Ractive knows exactly what needs to be added and what to remove from the real DOM, and therefore it works very quickly, producing point replacements in the DOM.
Also, since each piece of DOM now corresponds to a piece of data in data, all events that are proxied via on-eventName (for example, on-click) are accompanied by a link to the data from data as context. For example, if I have such a template:

 {{#user}} <--  this=user --> <div on-click='alert_username'>{{this.username}}</div> {{/user}} 

then the alert_username event handler might look like this:
 function(e){ var user = e.context; alert(user.username); } 


I hope that the real example will become clearer.

Next is our implementation of the template for our task, but first we will describe the structure of our data (comments) in the data:

 ractive.data = {'replys':[ { md_text:"  ", date:10240912834091,//  nix // user:{ username:' ', image:'http://link-roga-serega-avatar.ru' }, //   replys:[{ md_text:"    ", date:10240912834091, replys:[...]}, {...}] }, { md_text:"   ", date:10240912834091,//  nix user:{ username:' ' }, ]}; 

Template with comments:
 {{#top_reply}} <!--       .    ,  --> {{#if replys.length > 0}} <!--   -  --> {{#if this.reply_draft}} <!--        comment_form -    .--> {{>comment_form}} {{else}} <!-- ,    ' '    --> <!--    reply  e.context = this; this = top_reply --> <button class="add_comment" on-click="reply"> </button> {{/if}} {{/if}} {{/top_reply}} <!--      partial   --> {{#replys}} {{>comment}} {{/replys}} <!--  --> {{#bottom_reply}} {{>comment_form}} {{/bottom_reply}} <!-- {{>comment}} --> <!--  --> <!--     partial    replys - this    --> <article id="{{this.id}}" class="comment" intro="scroll_to:{go:{{this.is_new}}}"> <header> <!--            --> <img class="avatar" src=""/> <span class="author">{{this.user.username}}</span> <!--   .    moment    .        data--> <time pubdate datetime={{moment(parseInt(date)).format()}}> {{moment(parseInt(date)).fromNow()}} </time> <!--    -       --> {{#if user.id === current_user.id}} <button class="delete" on-click="delete" disabled="{{deleting}}"></button> {{/if}} </header> <!--    --> {{{marked(md_text)}}} <footer> <!--       --> {{^this.reply_draft}} <button on-click="reply"></button> {{/this.reply_draft}} </footer> <!--   --> {{#if this.reply_draft}} {{>comment_form}} {{/if}} <!--     --> {{#this.replys}} {{>comment}} {{/this.replys}} </article> <!-- {{/comment}} --> <!-- {{>comment_form}} --> <article class="comment"> <header> <img class="avatar" src=""/> <span class="author">{{current_user.username}}</span> </header> <!--      save--> <!--  this    .context     .--> <form on-submit="save" on-reset="cancel_reply"> <!--      input    . --> <!--       textarea     this.reply_draft.text--> <textarea value="{{this.reply_draft.text}}" placeholder=" "></textarea> <!--     --> <button type="submit" disabled="{{this.loading}}"></button> <button type="reset"></button> </form> </article> <!-- {{/comment_form}} --> 

Data

Now we describe how the template is related to the data.
The model here is very simple, so I will not stop for a long time: JS class Comments with text type, publication date, user, as well as the ability to save comments to the server and delete from it.
There is also a method for downloading comments from the server, as promised nothing interesting.
 function Comment(id, md_text, user, domain, date, reply_to) { if (!md_text || !user || !domain || !date) { throw new Error('md_text, user, domain and date is required'); } this.id = id; this.md_text = md_text; this.user = user; this.date = date; this.reply_to = reply_to ? reply_to.id || reply_to : null; this.domain = domain; this.replys = []; }; Comment.prototype.destroy = function () { //ajax     }; Comment.prototype.save = function () { //ajax     }; Comment.fetch = function (domain, options) { //    . }; 

Now the implementation of the Ractive components:
 var Comments = Ractive.extend({ //$(selector)   Ractive     el: '#comments', //id tag <script>        (    ) template: '#comments_template', init : function (options) { var self = this; //     . var domain = options.comments_domain; // . var current_user = options.current_user; if (!current_user) { throw new Error('options.domain and options.current_user is required'); } //       . Comment .fetch(domain) .then(function (comments) { //          . var comments_by_id = _.indexBy(comments, 'id'); //   comments = _.sortBy(comments, 'date'); //      //    replys  ,          . comments = _.filter(comments, function (comment) { if (comment.reply_to && comments_by_id[comment.reply_to]) { var parent = comments_by_id[comment.reply_to]; parent.replys.push(comment); } return comment.reply_to === null }); //reative.set(prop_name,value)    data, ractive.get(prop_name)    data. //           . //   self.data.replys = comments     . self.set('replys', comments); }); //Ractive    . //Events     ractive.on('event_name',callback);  .on({prop_name:callback}); //Events       ...         . self.on({ //   . //   .    ractive     //e.node - html DOM   . //e.original -   DOM (  e.original.preventDefault()); //e.keypath -     .    ractive.data   .      data  : //data:{ comments:['text1','text2'] };     'text2'     'comments.1' //Ractive    DOM    ,  DOM     keypath . //  e.context -       e.keypath ( ractive.get(e.keypath) ); save : function (e) { e.original.preventDefault(); //save    . ,      ,    -  -      . var reply_to = e.context.id ? e.context.id : null; //   . var reply = e.context.reply_draft; //  var new_comment = new Comment(void 0, reply.text, current_user, domain, moment().valueOf(), reply_to); //   self.set(e.keypath + '.loading', true); //  new_comment.save() .then(function (comment) { //   setTimeout(function(){ // ,  . self.set(e.keypath + '.loading', false); self.set(e.keypath + '.reply_draft', false); var comments = reply_to ? self.get(e.keypath + '.replys') : self.get('replys'); //     comments.push(comment); //.   ,     . //     Ractive      . // -             ,      . },600); }) .fail(function (err) { self.set(e.keypath + '.loading', false); }); }, delete : function (e) { // . //       . //       . self.set(e.keypath + '.deleting', true); e .context .destroy() .then(function (comment) { self.set(e.keypath + '.md_text', comment.md_text); self.set(e.keypath + '.deleting', false); }) .fail(function () { self.set(e.keypath + '.deleting', false); //TODO: show erorr; }); }, reply : function (e) { //e.keypath      . //       . //   .        Comments...     v2.0 self.set(e.keypath + '.reply_draft', {}); }, cancel_reply: function (e) { //  ... //   self.set(e.keypath + '.reply_draft', false); } }); }, //data         . data: { bottom_reply: {}, // top&bottom_reply  ,   . top_reply: {}, marked: marked, //data       ,        . moment: moment //     . } }); 

Animations

For any element, you can add an intro \ outro attribute for the DOM element:, and each time you add / remove an element from the ROM DOM, it will trigger the specified animation.

Animations are described by a function that takes an object as a parameter:

t.node - DOM element to animate.
t.isIntro - element is inserted or removed.
t.complete - a function that needs to be called after you have all zanimirovali and everything is over. Ractive will set all element styles to initial state.

The actual function of the animation of our comments looks like this:

 //Ractive          \ DOM . transitions: { scroll_to: function (t, param) { //        (param.go === true) if (t.isIntro && param.go) { //      . //    $ var element = $(t.node); var offsetTop = element.offset().top - element.outerHeight(); $('html, body').animate({scrollTop: offsetTop}, 500, function () { //    t.setStyle('background-color', 'yellow'); t.animateStyle('background-color', 'transparent', { duration: 1500, easing : 'easing' }, t.complete); //   Ractive t.complete     . Ractive           . }); } else { //     DOM,      (param.go) t.complete(); } } } 

That's all, 230 lines with comments (sooo detailed), templates and js code. Virtually no DOM search - beauty.
Well, just once the working code can be found here jsfiddle.net/maxtern/e2mk0tn3

findings


Happiness lies in the fact that in this example, in fact, never once had to look for something in the DOM, look for references to objects, check the status of the component.
All declaratively and reactively, as promised. No one throws himself at each other with millions of events.
Very few "gluing" views with code models. Anyway, very little code.

Then Ractive works very quickly. Since you do not need to dig constantly into the DOM and he knows exactly where to change something,
There is virtually no redrawing of the view, so it works quickly. For example, in Backbone we sometimes call render and redraw a bunch of views, even those that have not changed.

It can also be used absolutely with any other libraries, for example, with the same Backbone - we take Model and Route from it.

Also on the website Ractive there is an interactive tutorial that can be completed in 30-50 minutes you know Ractive. those. 30-50 minutes and you are a pro.
The learning process is incredibly simple and straightforward.

What do you think? Personally, I think that the diamond age may not have come yet, but once again it has become much easier to live.

Epilogue


We are currently working on the next release on the Fifteen project site, and we plan to launch the power planner for the web.
The scheduler itself is designed as a tag game (which is evident from the name of the project) and a calendar. On mobile it looks like this:
image
With all this, the web still draws graphics, and users upload and edit images. In general, full interactive.

If this article is interesting, I will publish after the release how easy it is in Ractive to break a complex interface into components, and how they interact with each other using the example of this very scheduler.

There is also an idea to write “Ractive vs. React. Such reactive frameworks. ”

Official website: www.ractivejs.org
Especially good and they and the tutorial: learn.ractivejs.org/hello-world/1

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


All Articles