📜 ⬆️ ⬇️

(Archive) Matreshka.js 1.1: even more cool

image

Documentation in Russian
Github repository

Hello. Today, September 28 marks two years from the first commit to the Matryoshka repository. Coincidentally, by this time, a new release arrived with all sorts of goodies for any JavaScript developer (even for those who don’t want to use Matryoshka as a framework).

Matryoshka is a JavaScript framework (or, if you like, a library) based on accessors, and squeezing out of them incredible, at first glance, possibilities. Remember the time when the JavaScript getters and setters just appeared? How much noise was around them ... Articles, conversations ... Then, everything calmed down: many did not understand how to use these opportunities, except in simple examples. The nested doll is an excellent answer to the question of why we need accessors in JavaScript.
')


By tradition, let me remind you that this framework is able with the help of a small piece of code.

Previously, you could only do this:

// this -   //   "x"     this.bindNode('x', 'input.my-node'); //  ,  alert this.on('change:x', function() { alert(this.x); }); //  ,   //     "x"  this.x = 'Wow!'; 

Now you can also:

 var object = {}; //   "x"     MK.bindNode(object, 'x', 'input.my-node'); //  ,  alert MK.on(object, 'change:x', function() { alert(object.x); }); //  ,   //     "x"  object.x = 'Wow!'; 

Due to the fact that the latest versions of Chrome and NodeJS have finally begun to support most elements of the ES6 syntax, all the examples below in this post will be written on ES6. In such a simple way, I want to congratulate all those who consider these innovations to be incredibly cool and draw attention to those who are not familiar with them to ES.next.

Support for native objects


The most important innovation was the support Matryoshka arbitrary objects. Yes, in order to declare a binding or to do something else cool, it is not necessary to create a copy of the Matryoshka.

 let object = {}; //  "x”   '.my-node' MK.bindNode(object, 'x', '.my-node'); // "y”      x  z MK.linkProps(object, 'y', 'x z', (x, z) => x + z); //”z” -   ,   ,      MK.mediate(object, 'z', Number); // ... 

As you can see from the example, the new static methods completely repeat the dynamic ones with one small difference: they need to pass the original object as the first argument:

 // this.bindNode('x', '.my-node'); // MK.bindNode(object, 'x', '.my-node'); 

In addition, collections that are able to render themselves, now do not require specifying a model that previously had to be inherited from the Matreshka class.

 class MyArray extends MK.Array { itemRenderer() { return '<li>'; } constructor() { super().bindNode('sandbox', '.some-node'); } } let arr = new MyArr(); arr.push(someData); 

A complete list of new static methods is listed in the MatreshkaMagic section.

MatreshkaMagic library


Thanks to the support of native objects, it became possible to put all the "magic" functions into a separate, more compact library that does not include the Matreshka , Matreshka.Array , Matreshka.Object classes and the Class function. The developer has access to the object MatreshkaMagic or a shorter version of magic , which contains all the static methods of the Matreshka class.

The library is located in the magic / repository folder.

 magic.bindNode(object, 'x', '.my-node'); magic.linkProps(object, 'y', 'x z', (x, z) => x + z); //  . . 

More information about the library in the documentation .

"Deep binding"


Another cool feature that appeared due to the support of native objects is the so-called “deep binding”. Having an object of arbitrary nesting, you can associate a DOM node with any property somewhere in the depth of this object.

 this.a = {b: {c: {d: 41}}} this.bindNode('abcd', '.my-node'); 

The Matryoshka kernel keeps track of the entire branch of objects and reinstalls the binding if one of the branch objects is redefined

 this.ab = {c: {d: 42}}; 


“Deep Links”


The doll for a long time includes the linkProps method, which allows you to set the dependence of some properties on others.

You can set the dependence on your own properties:

 this.linkProps('a', 'bc d', (b, c, d) => b + c + d); this.b = 1; this.c = 2; this.d = 3; alert(this.a); // 6 

You can set the dependence on the properties of other objects:

 this.linkProps('a', [ externalObject, 'b', externalObject2, 'c', this, 'd' ]); externalObject.b = 1; externalObject2.c = 2; this.d = 3; alert(this.a); // 6 

Now linkProps supports specifying the path to the property:

 this.linkProps('a', 'bcd e.f', (d, f) => d + f); this.b = {c: {d: 40}}; this.e = {f: 2}; alert(this.a); // 42 

When something changes in the chain of the path to the property, the Matryoshka intercepts this change, breaks the link with the old chain and creates a dependence on the new chain.

 this.bc = {d: 1}; 

As before, you can create a dependency on the properties of other objects, while, as mentioned above, any object can act as a source object:

 let targetObject = {}, o1 = {b: {c: {d: 40}}}, o2 = {e: {f: 2}}; MK.linkProps(targetObject, 'a', [ o1, 'bcd', o2, 'ef' ], (d, f) => d + f); alert(targetObject.a); // 42 


Transparent syntax of delegated events


Let me remind you that in previous versions it was possible to hang an event not only on the current object ( this ), but also on an object of arbitrary nesting. But the syntax made you want something better. I will give a small example. Say, an instance of the Matryoshka has or should appear a certain property, which, in turn, is also an instance of the Matryoshka.

 this.a = new Matreshka(); 

You could, before or after assigning the property, create an event handler for any event that relates to this property. To do this, use the syntax with the "dog".

 this.on('a@someevent', handler); this.a.trigger('someevent'); 

For arrays and objects (which are collections of key-value type in Matryoshka), it was possible not to specify the target property, since the event is listened to in all elements included in the collection:

 this.on('@someevent', handler); this.push(new Matreshka()); this[0].trigger('someevent'); 

At first glance, it looks simple. But what if our object tree is a bit more complicated? For example, the property "a" contains a collection:

 this.a = new MK.Array(); this.a.push(new Matreshka()); 

How to catch an event inside such a collection? You can combine two dogs that say: “in the object "a" catch the event "@someevent" -> in the array element, catch the event "someevent" ”.

 this.on('a@@someevent', handler); this.a[0].trigger('someevent'); 

This can still be experienced (if you drink enough coffee). And what if we want to go deeper? Then the number of "dogs" will increase and coffee will no longer help ...

Agree, the potential of this feature is very large. We can listen to data events of any nesting, for example, to learn about changing the properties of an object contained in an array of arrays, etc. Therefore, it was decided to somewhat change the syntax of delegated events. “Dog” remained, but as the only separator of the path to the object and the name of the event. If an event concerns a nested object, the dogs are replaced with dots. If we want to learn about something in the collection, instead of a faceless dog we use an asterisk. Then I probably need to stop and give a couple of examples.
If we want to hang the handler on the property "" then the syntax remains the same:

 this.on('a@someevent', handler); 

If we want to catch an event from a collection item, then instead of this:

 this.on('@someevent', handler); 

We write this:

 this.on('*@someevent', handler); 

An asterisk means “any property responsible for the data in MK.Object ” or “any element of the MK.Array collection”.

We go deeper. We need to comb the following example described above:

 this.on('a@@someevent', handler); 

Now we write this:

 this.on('a.*@someevent', handler); 

The syntax has become much cleaner. You just need to specify the path to the object before @, and after it specify the name of the event.

Detailed article about the events.

setClassFor


setClassFor is another incredibly cool feature. It indicates which class should be the specified property. When you try to overwrite a property, the internal interceptor, instead of assignment, updates it with new data. We will understand by example.

 //     ( ) this.x = {a: 41}; //     this.setClassFor('x', MyClass); // ,      MyClass console.log(this.x instanceof MyClass); // true //   "a”  console.log(this.xa); // 41 //    //   "x”    var x = this.x; //    this.x = {a: 42}; // ,    console.log(xa); // 42 // ,       console.log(x === this.x); // true // Wow!    ,   ! 

If you have a deep structure of objects and setClassFor also running in nested objects, you can do interesting things. For example, save the presentation of multi-level data in the local storage.

 localStorage.x = JSON.stringify(this.x); 

And then restore them with a wave of a magic wand:

 this.x = JSON.parse(localStorage.x); 

Or, drive back and forth to the server.

Cases where such logic may need an incredible amount. As another example, here’s the code from the documentation (for brevity, the class properties from ECMAScript 7 are used):

 // app.js class App extends MK { constructor(appData) { this.appData = appData; this.setClassFor('appData', AppData); } } // app-data.js class AppData extends MK.Object { constructor(data) { super(data) .setClassFor({ friends: Friends, settins: Settings }); } } // friend.js class Friend extends MK.Object { constructor(data) { super(data); } } // friends.js class Friends extends MK.Array { Model = Friend; constructor(data) { super(...data); } } // settings.js class Settings extends MK.Object { constructor(data) { super(data) .setClassFor('credentials', Credentials); } } // credentials.js class Credentials extends MK.Object { constructor(data) { super(data); } } // app-init.js var app = new App({ settings: { name: 'Vasiliy Vasiliev', credentials: { email: 'vasia.vasia@gmail.com' } }, friends: [{ name: 'Yulia Zuyeva', id: 1 }, { name: 'Konstantin Konstantinopolsky', id: 2 }, { name: 'nagibator3000', id: 3 }] }); //        JSON.stringify(app.appData); //       appData //  ,     app.appData = { settings: { name: 'Petr Petrov', credentials: { email: 'petr.petrov@gmail.com' } }, friends: [{ name: 'Yulechka Zuyeva', id: 1 }, { name: 'Konstantin Konstantinopolsky', id: 2 }] }; 

More details in the documentation for the method .

DOM template engine


Matryoshka is a framework that professes the idea that logic should be contained in JS files, as opposed to frameworks that implement the MVVM pattern, forcing to describe logic in HTML code.

Implementing logic in JS files is really very convenient. But, sometimes, there are situations when the description of all bayding is too expensive in terms of the number of lines of code.

Therefore, it was decided to improve and speed up the DOM template engine, which was absent until recently in the official API. What is he doing? It takes a DOM node, a collection of DOM nodes, an HTML code, or a sandbox for the current object, parses it, finding angular-like constructions like {{KEY}} and creates bindings where these constructs are found.

 <a href="http://{{website.domain}}/{{category}}/{{page}}">Look at the {{info.title}}</a> 

 this.parseBindings(); this.website.domain = 'example.com'; this.category = 'foo'; this.page = 42; this.info.title = 'cool stuff'; 

The method is described in more detail in the documentation .

The method does not contradict the ideology of Matryoshka, since there can be no logic (cycles, conditions, handlers) in the pattern.

In addition to publishing the API of the method itself, the template engine for collections is now enabled by default (you no longer have to write useBindingsParser: true ).

 class MyArray extends MK.Array { itemRenderer = '<span>Hello, {{name}}</span>'; ... } 


More sugar for ECMAScript 2015



From the example to setClassFor can see that the methods are run immediately after super() . This possibility became real thanks to a very simple change: all three constructors ( Matreshka , Matreshka.Array , Matreshka.Object ) return this instead of undefined .

 class MyObject extends MK.Object { constructor(data) { super(data) .bindNode('x', '.my-node'); } } //   MyObject   a  b, //    myObject = new MyObject({a: 1, b: 2}); class MyCollection extends MK.Array { constructor(data) { super(...data) .bindNode('x', '.my-node'); } } //  ,   5  myCollection = new MyCollection([1,2,3,4,5]); 


Support for event-type objects in the on , once , onDebounce methods


In previous versions, the only syntax for declaring event handlers was available to the developer.

 this.on('eventname1', handler1); this.on('eventname2', handler2); 

Now you can declare several handlers by calling the corresponding method only once:

 this.on({ 'eventname1': handler1, 'eventname2': handler2 }); 

This news would not have been mentioned in this post if it were not for one thing: using ECMAScript 2015, you can greatly shorten the code in microtasks.

 this.on({ 'eventname1': evt => this.x = 42, 'eventname2': evt => doSomethingElse() }); 

Against the “old” syntax:

 this.on({ 'eventname1': function(evt) { this.x = 42 }, 'eventname2': function(evt) { doSomethingElse(); } }); 


Override itemRenderer


itemRenderer is a virtual property of the collection ( Matreshka.Array ), which tells how to draw the elements of the collection.

 //  ,  ES7 class MyCollection extends MK.Array { itemRenderer = "<div>Hi there!</div>"; constructor() { super() .bindNode('sandbox', '.array-sandbox') .push({a: 1}, {a: 2}); } } 

See the itemRenderer documentation for more details.

Starting with the new version, when overriding itemRenderer , the collection is automatically redrawn.

 //   - span this.itemRenderer = '<span>I'm a span</spam>'; //   - div this.itemRenderer = '<div>I'm a div</div>'; 

You can create several user cases: you want to change the design of the collection with one button, or your template resides on the server (in the example below, the Fetch API is used ).

 fetch('templates/my-template.html') .then(resp => resp.text()) .then(text => this.itemRenderer = text); 

You can work with the collection as usual: insert, delete, sort elements, while you do not need to wait until the server returns the template. Upon returning from the server, the collection itself will be displayed on the page

New binders


Let me remind you that a binder is an object that indicates how to associate a property with an element on a page. Binder is used by the bindNode method, which implements one or two-way binding.

 this.bindNode('x', '.my-node', binder); this.x = 42; //      

Detailed descriptions can be found in the documentation for the method .

MK.binders.progress - associates a property with the HTML5 state of the progress element. The binder does not need to be called manually, as it belongs to the standard binders collection.

 this.bindNode('x', '.my-progress'); this.x = 42; //     42 

MK.binders.innerText - associates a property with the text value of any element that has a textContent or innerText property.

 this.bindNode('x', '.my-node', MK.binders.innerText()); this.x = 'Some <i>Text</i>'; //    " ”,    

MK.binders.style - associates a property of an object with a property of an object's style element.

 this.bindNode('x', '.my-node', MK.binders.style('color')); this.x = 'red'; //      

And the most interesting: MK.binders.file . This new binder will not only catch the change in the user input[type=”file”] , but also read the file in the format you need:

 this.bindNode('x', '.my-file', MK.binders.file('dataURL')); //   ,    this.on('change:x', function() { console.log(this.x.readerResult); // "data:image/png;base64,iVBO..." }); 

For more information, see the documentation for the binder .

getValue for innerHTML , className , property and attribute binders


Now, when using the above listed binders, the Matryoshka kernel will check if there is a corresponding property in the current object and, if the answer is negative, extract the value from the element and assign it to the property.

 <div class="my-div">Some data</div> 

 // ,  this.x  . this.bindNode('x', '.my-div', MK.binders.innerHTML()); alert(this.x); //"Some data" 


onItemRender


Matreshka.Array has a new virtual method onItemRender . It is called when one of the elements of the collection has been drawn. The method makes the code more flat, allowing you to avoid listening to the "render" event.

The "render" event has always been a standard pattern that allows you to add the necessary bindings when rendering.

 class MyCollection extends MK.Array { Model: MyModel; itemRenderer = '<li>'; constructor() { super() .bindNode('sandbox', '.array-sandbox') .on('*@render', evt => { evt.self.bindNode(...); }); } } 

Now you can do this:

 class MyCollection extends MK.Array { Model: MyModel; itemRenderer = '<li>'; constructor() { super() .bindNode('sandbox', '.array-sandbox'); } onItemRender(item, evt) { item.bindNode(...); } } 

The “models” have a similar virtual method: onRender .
It used to be like this:

 class MyModel extends MK.Object { constructor() { super() .on('render', evt => { this.bindNode(...); }); } } 

Now you can write like this:

 class MyModel extends MK.Object { constructor() { super() } onRender() { this.bindNode(...); } } 


Properties nodes and $ nodes


After declaring a data binding and a DOM node, the developer could access the linked nodes using the bound and $ bound methods. bound returns the first bound element, $bound - all elements as a jQuery or Balalaika collection.

 this.bindNode('x', '.my-node'); var boundNode = this.bound('x'); var allBoundNodes = this.$bound('x'); 

The nodes and $nodes properties allow you to do the same, but practically for free, in terms of performance, since these properties are ordinary objects.

 this.bindNode('x', '.my-node'); var boundNode = this.nodes.x; var allBoundNodes = this.$nodes.x; 


Some more new methods


Matreshka.to converts an arbitrary object into instances of MK.Object and MK.Array .
MK.Array.of , which works in the same way as Array.of , but returns an instance of MK.Array .
MK.Array.from , which works the same way as Array.from , but returns an instance of MK.Array .
MK.trim for browsers that do not support String.prototype.trim .
MK.toArray , which converts an array-like array to a native Array is twice as fast as Array.prototype.slice does.

Increase in productivity


Using micro-optimizations (for example, using the for..in loop instead of the each function) and larger changes, it turned out to achieve excellent results. For example, in a benchmark with small collections (10 items), the Matryoshka fell behind React by 10-20 percent in Chrome and Firefox (although it overtook in benchmarks with a large number of collection items). Now in the same test, Matryoshka is 50% faster than React in Chrome, and 3 times faster in Firefox.

Here is a list of benchmarks to see for yourself: 10 elements , 50 elements , 100 elements , 500 elements , 1000 elements .

Bug work: tests


Matryoshka finally tested automatically. At the time of this writing, 148 tests have been implemented that test the performance of methods before they develop. The delegated events are especially meticulously tested, which are obliged to work in a variety of different circumstances and do not break anything.

Browser Support


A warning appeared on the documentation site that the use of Matryoshka in Internet Explorer 8 is not recommended due to the mass of methods that cannot be implemented for this version of the donkey. In fact, this is only a disclaimer on cases when a developer tries to use such methods without thinking. One thing to be remembered: static methods that add "magic" to native objects do not work in IE8.

Such code will work in IE8, in case this is an instance of Matryoshka.

 this.bindNode('key', '.node'); 

And this will work:

 var mk = new Matreshka(); mk.bindNode('key', '.node'); 

And this code will work only in IE9 + and in other browsers (including the ancient WebKit and Opera Mini):

 var object = {}; MK.bindNode(object, 'key', '.node'); 

If your hands are itching to use static methods in the eighth donkey, you can convert the object into an instance of the Nested dolls:

 var object = MK.to({}); MK.bindNode(object, 'key', '.node'); 

Thus, semantic versioning is respected.

Other changes


- The bindNode syntax has expanded a bit.
- Errors about missing nodes using the bindNode method bindNode more informative: now in the text, besides the key, the selector is indicated (if a selector is transmitted).
- The source code is broken into small components.
- Unnecessary spaces ( f(x) instead of f( x ) ) are removed from the code and from the examples on the site.
- As already stated above, Matryoshka supports Opera Mini and old WebKit.

For other changes and a list of corrected errors, you can find out in the corresponding section on the site .

And further


You can look at what has been done and is planned in Trello . In the same place, you can also request it by raising the priority of the cards.

In the Gitter chat , discussions of new features occur quite often. Because of this, the questions that users ask, and the answers to them are lost somewhere in the dark basements of the Internet. Therefore, it was decided to launch, as an experiment, a forum based on Muut ( in Russian and in English ). If a question arises, feel free to ask it there (even if you think the question is stupid).

For an example, here’s one of the great Rendol questions with an upside-down answer to it:
Greetings
Again, the same question that I previously had, but the answer I did not find it beautiful.
For example, the correspondence as in VK:
 User = { id: 5, name: 'Fedor', online: true } 

We place this user in different rooms: room1, room2, room3.
If User.online = false, then in all 3 rooms, for example, the color should change.
So 3 collections that contain one object and at the same time this object is displayed in 3 places.
Note: it is not necessary that these collections will be of the same type (not only rooms), they can be of different types and representations.
Is it possible to bind one object to multiple views?

Answer:
Yes, collections containing an object can be different. bindRenderedAsSandbox: false , , (, , ). . render , , , .
: jsbin.com/cidise/12/edit . user ( ul), -. tableUsers[0].name = 'xxx' , , . , .
Shl. IE 8, , , .instanceOf
 object.instanceOf( MyClass ); //  object instanceof MyClass 


, . .

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


All Articles