
Developing MaskJS for more than half a year, we managed to turn the DOM template into a very powerful, but at the same time productive web framework. The article will acquaint you with perhaps interesting approaches to development. I'm sure it will be interesting to read about using signals and slots instead of DOM events. And how components make our lives easier. The mask can be easily integrated into an already finished project, and can even be used with any other framework. The main difference is probably the
render flow
, where in the process a Document Fragment / controllers / "binders" are created in stages. Actually all the flexibility is even difficult to convey, but I will try, and invite under the cat.
MaskJS @ GitHubA small todo
example for warming up.
mask-fiddle works best on webkit.View
')
Markup
In case someone does not know, the templates use syntax similar to css / less / sass. Recently, various bugs have been fixed, so the work of the engine should now be stable.
p { div#info.dark > 'Single Child' button data-user='123' style='cursor:pointer;' > span > 'Submit' input type=hidden value=x; }
As you can see, we removed the "<>" from the tags and removed the closing tags, but instead blocks are allocated with the usual "{}" brackets.
(For simplicity, a block with one child does not look like a selector with a ">" transition, but without children at all - closed with a semicolon). The text is placed in literals, as in javascript. This is not a tricky transformation, we easily concentrated the markup on the
structure - which is actually more in demand in the application architecture than html redundancy, which is aimed at text markup. This is surprising to many html supporters, but let's face it - we, and probably as well as you, are developing applications with multiple localization. We don’t have text in the views - only the keys to json with localization, then why is the hypertext markup syntax being asked?
And unlike templates based on indents, the mask is easily minified and takes up minimal space.
Speed
This is the main priority in the development of MaskJS - to offer maximum performance on mobile devices. It was possible to achieve a speed more or less comparable to html, and in the case of the webkit engine - even to increase, this especially applies to mobile platforms. And not because
html parsing in webkit is slow, but because
str.charCodeAt and
document.createElement are very fast). And most importantly, the overhead of controller / interpolation / dom events / data bindings in the MaskJS architecture is minimal. As a result, we no longer need to compile the templates, and this is already a big plus to the enjoyment of the development. If interested, several links to jsperf.com can be found in the readme on the
github .
Flexibility
MaskJS is a fairly extensible system. We can define controllers for any tags and create new ones; in fact, the entire hierarchy of controllers (
H MVC) is built on this feature. We can define handlers for any attributes. We can also define utilities that, when interpolating a model in a template, will transform or redefine data. And let me remind you that the mask on the client is rendered directly in the DocumentFragment, so we always work with DOM elements.
All controllers are created through the similarity of IoC containers, and if you are “in the subject line”, then you yourself understand how easy it will be to redefine or imitate them
(“mock”) .
Do you have a jQuery widget (or equivalent) and are you tired of initializing it every time after inserting it into a house? and initialized
widgets . With MaskJS, you create a Tag wrapper on your widget, and the mask will do everything for you:
mask.registerHandler(':timer', Compo({
Now, you can use this tag in the template as much as you like, but you no longer need to initialize it. A small example is the creation of timers:
$.getJSON(url).done(function(collection){ jmask("ul > % each=timers > li > :timer timespan='~[timespan]'; ", collection).appendTo(document.body); });
Design Patterns
There are many different architectural solutions, but everyone has a common goal - to reduce connections and dependencies. In MaskJS, the main focus is on
V (
View ) from MVC, and we are trying to abstract from the Model. The mask doesn't care what your Business Layer looks like and where it comes from. And this means that all classes, data and any business logic are independent of the view and controllers - and not only architecturally, but also from the MaskJS library as a whole. A model can be either a Data Centric (note - json service response) or a complex Domain Model. But in any case, it is separate and thus easy to develop and test.
Next I will give small examples of different MVC scenarios, something will be exaggerated - so do not judge strictly, I do it only for the sake of better clarity.
- Here we have a View:
mask.render(" div > 'A' ");
- Add dynamism by displaying a letter from the model (
var model = { letter: 'A' }
): mask.render(" div > '~[letter]' ", model)
- we get (Data) Model / View
- We associate the model with the view so that if the letter is changed in the model, the view is updated:
mask.render(" div > '~[bind: letter]' ", model);
- here is Model / View / ViewModel
- If we need to change or supplement the data for the view - we get the Model / View / Adapter:
mask.registerHandler(':myModelAdapter', { renderStart: function(model){ _extendModelFromLocalStorage(model); } }); mask.render(" :myModelAdapter > div > '~[letter]' ", model);
- If we need to separate the view from the model, we get Model / View / Presenter
mask.registerHandler(':myPresenter', Compo({ onRenderStart: function(model){ this.letter = _handle(model); this.model = this; } }); mask.render(" :myPresenter > div > '~[letter]' ", model);
- If necessary, so that the letter would change to “B” when clicked - hello Model / View / Controller:
mask.registerHandler(':letterChanger', Compo({ events: { 'click: div' : function(event){ this.model.letter = 'B';
- If you need to add a model + react to a click - and now the hierarchy is HMVC
mask.render(':myAdapter > :letterChanger > div > “~[bind: letter]" ');
- If you need to hide the view in the controller, we get the usual encapsulation - (this is certainly not an architectural design pattern, but a very important point in MaskJS)
mask.registerHandler(':letter', Compo({ template: ":letterChanger > div > '~[bind: letter]' " }); mask.render(" :myAdapter > :letter; " , model);
And you knew that for the full power of encapsulation, it is not bad to use different boot loaders, thereby taking controllers, their representations and styles into separate files. A simple example of the composition of the component:
header { #logo; :menu; :userInfo; } :viewManager { :userView; :aboutView; } :pageActivity; :notifier; :footer;
The names of components begin with a colon only for the best semantics of representations.
But the main thing in all these MV * - not their names, especially here everything is far-fetched (I hope no one offended?). And the essence itself, how we create controllers for different purposes. And as you can see, we point the dependencies directly from the view — by unloading the controllers themselves, and leave them to deal only with their immediate tasks.
Component / (Controller) / (Widget)
AST
MaskDOM
Parser transforms the View into a node tree. A “builder” is then traversed along it and interpolates the model - creates HTMLElements and Controllers (arbitrary tags). The standard assembly MaskJS includes another good library -
jmask @ github . It helps to work with the maskDOM tree, it uses the jQuery syntax and it is convenient to use it wherever you need to dynamically create a maskdom tree or modify it, for example, in
onRenderStart components:
If somewhere you use jQuery to create a DOM, then the mask will cope with this in the same way, and
much more quickly , a small example
$('<div><span></span></div>').addClass('container').data('foo','bar').children('span').text('2013').appendTo('body')
Controllers Tree
The builder also creates a tree from components, so you can find other controllers through selectors.
mask.registerHandler(':page', Compo({
Signals / Slots
mask-compo @ githubA component can have a hash object with a list of all the events it wants to process -
var _myCompo = Compo({ constructor: function(){ this.name = 'C'; }, events: { 'touchstart: .pane': function(event){ this instanceof _myCompo
But in this way, we bind to the markup (css classes) - which means binding to the
implementation of the view, which makes it difficult for us to replace the View. And this is not good. In many frameworks, you can call controller methods directly from the view, but this is also not the case, although MaskJS supports
expressions in templates -
div > '~[: controllerMethod("test") ]'
(I note that the mask has its expression parser and evaluator implemented without with / new Function / eval ). It is much better when the view sends signals up the tree of controllers, starting with the "owner" of the element - and there already, who wants, implements the logic.
mask.registerHandler(':myCompo', Compo({ constructor: function(){ this.name = 'C'; }, slots : { greet: function(sender){
Notice the declarative declaration of slots in the
slots
object — by this we clearly share the logic of the controller, and the mask itself will call these handlers when the corresponding signals are triggered.
Additionally, we can deactivate a slot or a signal at any time, and at the same time, all elements that send this signal in a given “scope” of controllers will receive the status
: disabled .
Pipes
Normal signals walk only up or down the component tree, and in order to connect two components that lie outside the hierarchy, you need to use tubes.
mask.registerHandler(':userInfo', Compo({ pipes: {
In this example, I tried to complicate the presentation a little.All signals can also be sent from the controllers themselves:
this.emitIn('name', args...);
Bindings
mask-binding @ githubHow is a web framework without “bindings”? Here everything is in the best traditions of the genre: One- / Two-way Bindings, Custom Binding Providers, Array Mutators, Validators.
An example can be viewed at
mask-try | bindings . Bindings are by their nature very productive, since in render time they only save the links to the house elements, and are attached to the model via
defineProperty/ __defineSetter__
. And yes - you rightly noticed, old browsers are not supported - but by redefining the standard provider, you can bind to functions like
setX/getX
, or other templates like
.get("x")/.set("x")
. Actually, if necessary, you can bypass restrictions.
Interesting moments:
- we can use expressions, example:
div style='height: ~[bind: item.age * index + 10]px'
Depending on how the age or index will change - this will be the height of the div
panel.
- two-way binding based on
dom events / (jquery) custom events
. - in order for the model to be complete, two-directional bindings can have component (s)
:validate
. Before assigning a model value, the provider will first check it, and in case of an error, inform the user about it and offer to return to the last correct value. So our model remains holistic.
input #device-type type=value > :dualbind value="age" { :validate match="^[az]{2}-[\d]{4}$" message=" ... pattern: xx-1234" }
- As you may have already noticed, the mask has a standard
%
component, which implements if/else/each/repeat/use
logic and so on. So, the bindings module also implements one-way binding for these things:
%% if="state == true" { %% each=userList {
Development
It is important not only to write large and productive applications, but also to get maximum pleasure from it. Dividing the development into components, ranging from the smallest (
:customCheckBox
) to the largest (
:inbox
), we always concentrate on what is necessary. To catch bugs, there are
debugger
and
log
attributes in the system controller:
.user { % debugger; .user-status > '~[bind:info.status]' %% log="info.status"; }
debugger
- during the render flow we stop and we can see the stack components, the current model and the html element.
log
- output data to the console. We can access both the model and the controller.
Hot Reload Plugin
... for IncludeJSA “hot” update of resources without page reload will not surprise anyone now - and the implementation is rather trivial, but not everything is so simple with scripts. This should include closures, dom events, and so on. We use the
IncludeJS
library to load all modules, and each script file can export the reload method. Also,
IncludeJS.Builder
includes a server that monitors changes to the requested files and notifies
IncludeJS
via
IncludeJS
. Actually the script is quite simple. But MaskJS, in turn, in the
reload plugin redefines
mask.registerHandler
- in it it records all the components that are registered and relates them to the path of the current script. The plugin also subscribes to the controller creation event, and saves the current model and a link to the controller. Thus, when through socket.io we receive a notification about a file change, we have a list of the names of the components that this file creates, as well as a list of instances
(instances)
. And then the matter of technology is to call
remove/dispose
each controller, and initialize the updated components into their places. Using signals and slots, parents do not need to subscribe to the dom events of the updated components - the signal will come. If the component loads
mask markup separately and we have changed something in it, then
IncludeJS
will regard this as a change in the
customCompo.js
file itself. The topic IncludeJS is quite capacious and about all its possibilities some other time. But the fact that the MaskJS architecture allows you to replace components on the fly greatly simplifies development, especially if the component is hidden behind N clicks (note somewhere in the dialog).
Node.js and TODO
Currently, MaskJS also works in node.js. The principles of operation are the same, only after creating mask dom, the html dom is created, which in turn turns into a
string buffer . And on the client, all components will be initialized and to complete, they will receive the necessary DOM elements in the
onRenderEnd method.
Routing is tied not to controllers, as in many frameworks, but to views. Remember? The view itself initializes the necessary controllers. Here you can use Master Pages technology and so on.
Work in this area is not finished yet, it will be necessary to deal with some nuances. But the goal is that the components / controllers / widgets work as on the client (first of all), but also with the ability to render on the
backend with termination on the client.
I also want to create a wrapper from a component over some kind of css framework. The mask will simplify the markup at times - hide the css classes and
div wrappers
). And simplify the creation of menus / calendars / dialogs, etc.
FIN
This is probably all the highlights. Much has not yet been told, but I hope it was clear at least what I was talking about, because the narrator is not from me, but there is a lot of material, so it is difficult to concentrate.
In the comments, please do not write - “look at the X-framework!” - we follow the mainstream, but I will be
very happy for deeper comments.
I would also like to thank rma4ok
habrauyazar for many good advice. Tried to take into account much. Also I will be glad to any other advice or suggestions. If you know interesting techniques from other languages ​​/ frameworks - please share your knowledge). And you can also join the development.
Good luck.