📜 ⬆️ ⬇️

Writing a complex application on knockout.js - 2

I am writing here one epic megahrenia, which I want to promote in Habré. This thing is a type of distributed social network. There are cores with api that communicate by some standard and front end. The peculiarity of the network is that the frontend lives “separately” from the kernel, that is, the network does not have its own domain - we take html, put a link to any kernel and get a network that lives on top of the site. Outwardly, this is similar to Facebook social plugins - comments and likes can be put on any page from there - only instead of fb-like tags powerful knockout.js + links are used; the user is not limited to snaps from comments and likes - almost any block from the network can be imported to the site and do almost any action. The frontend is written on the same technologies that the user can use and add on his page.

The result was a technique that may be of interest to the front-line man. I want to disassemble this technique in this article.

I'll tell you about the system that is embedded on the html-page through the knockout binding. The code lives in plug-in widgets, which consist of html-templates with knockout binding. Widgets can be nested in each other. All this uses require.js and lives in amd form. Dependencies on the external page are minimized, all libraries (jquery, knockout and plugins) are used only in their own local space with namespaces. To build the code, use r.js. Even as cool peppers we will write a full-fledged window manager on the basis of the bootstrap dialogue - with a knockout, it’s like two fingers on asphalt ...

Demo and source


I advise you to look through my first knockout article - http://habrahabr.ru/post/154003/ . We will develop the ideas started there.
')
The prototype of the frontend network is here - https://github.com/Kasheftin/uncrd .

How it all looks, you can see here - http://www.photovision.ru . There is a site with photos that is not intended to be altered. One script is connected from the uncrd.com domain, which provides network functionality. When clicking on photos, the photo viewer windows should pop up, you can go to the authors' profiles, register, post photos and comments. My goal was to write a standard network with the minimum allowable functionality.

The repository also contains a piece of network documentation that lists most of the blocks and describes their parameters - http://uncrd.com/docs/1.html .

It should be noted that the frontend consists of a universal core and network-specific widgets. View photos and profiles of the authors belong to the second, the window manager and the mechanism for connecting the widget to the first. We will consider the basis, but it is poorly allocated from the system code, since it is being developed at the same time (for example, authorization is in the kernel, although it should be in a network-specific place), so I’ll tell you something from the theory, and then I will collect in the demo-branch bare core, and analyze in detail a couple of demovidzhetov on it.

Disadvantages and css


The standard logic is when modules from some social network are connected to an external site - using an iframe. This is safe because the external site does not catch the session, and the css is correct, because the css of the external site does not affect the styles inside the iframe. This logic is broken here. The network code is embedded directly on the page of the site, and due to this we get much more opportunities, and not just a dumb insertion of predefined square blocks. Security is largely solved by the registration of sites on the network and tokens. But css is not solved. I don’t know how to insert your element on a page with arbitrary css-rules, don’t break everything around and know 100% what it looks like (except for listing absolutely all css-properties in each tag). Therefore, in the prototype it is assumed that the site works on the bootstrap (the network does not drag css with its bootstrap, as this may break the design of the source site).

Core system


The frontend consists of a system kernel and dynamically loaded widgets. Each widget consists of a js-object, which is located in a separate file in the amd-form (example messageForm.js ) and html-template with code (example messageForm.html ). Recently, I have encountered several applications on knockout.js with a modular approach, and they all used this kind of binding:

ko.bindingHandlers.widget = { update: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { var widget = ko.utils.unwrapObservable(valueAccessor().data); require([widget.templateName],function(html) { ko.renderTemplate(element,bindingContext.extend({$data:widget}),{html:html},element); if (widget.domInit) widget.domInit(elem,valueAccessor()); }); return { controlsDescendantBindings: true}; } 


Then in core.js or in any widget (since they support nesting each other), an object of the required widget is created: this.w = SomeWidget (); and in the right place of the template the <! - ko widget: {data: w} -> <! - / ko -> is called. I note that knockout natively supports only "named" and "internal unnamed" templates, and therefore the renderTemplate method in this case uses the stringTemplateEngine from this article .

The advantage of this approach is simplicity. At explicit creation of the widget it is known that where it lives, and the binding stupidly renders the template in the context of the newly created object. However, in uncrd it is necessary that widgets can be created from html, without programming. You need to work, for example, such code:

 ...   ... <!-- uncrd if: user --> , <!-- uncrd text: user.name --><!-- /uncrd --> <!-- uncrd widget: 'userMenu' --><!-- /uncrd --> <!-- /uncrd --> <!-- uncrd ifnot: user --> <!-- uncrd widget: 'loginForm' --><!-- /uncrd --> <!-- /uncrd --> ...   ... 


This means that widgets must be created inside widget-binding. In this case, there is a problem with access to the created object - inside the model code we do not know when the objects of the internal subwidgets will be loaded and created, and where they will live. Therefore, when creating a widget inside widget-binding, you need to register the created object in its parent within some childrenWidgets variable, and if you want to delete the widget, recursively delete the entire subtree. This is due to the rather voluminous code of the resulting binding, widgetBinding.js .

Window manager


I like the way the bootstrap dialog pops up. I even tried to use its window jquery plugin, however glitches with fade prevented me from creating multiple windows and incorrect scrolling. I'm lying. Mainly prevented by the realization that all the power of a knockout is at hand, and he is busy with jquery and DOM. I hate DOM manipulations if there is a knockout. In uncrd, DOM th manipulations are found only in two cases in specially designed domInit methods. Therefore, its own window manager windowManager.js was written on the bootstrap styles, which supports multiple open windows and drag-and-drop, and all window parameters are observable variables.

I'll tell you about one elegant trick there. windowManager has an open method through which a window opens with any widget. The opened window itself is also a widget, and therefore registers itself in the childrenWidgets array with the guy, which is the windowManager. When we initialize windowManager, we explicitly set this.childrenWidgets = ko.observableArray ([]), and therefore it is not overridden by a regular array in the widget binding. The convenience here is that observableArray has the same push, pop, splice methods as a regular array. Therefore, inside a binding binding widget it doesn’t matter whether it’s a regular array or observable. Each opened window registers itself now in observableArray. And this means that you can subscribe to the changes of the latter, which is done. And now - if childrenWidgets is not empty, you need to darken the site page, “nail down” the content with position: fixed and show the finished windows, and if not, return everything as it was.

The modalWindow3.js modal window itself is html from bootstrap + calculation of positioning depending on the size of the content, position and dragging, nothing supernatural. The only feature is that the name, title and footer of the internal widget can be sub-widgets defined via observable variables, and when parameters are changed, the widgets are automatically re-created. And for this you did not need to write a single line - everything is provided by the update method in the widget binding.

Boot indication


Load indication during ajax requests is one of the unloading things in the site's UI. Every time you need to think about where the icon will appear with “please wait” when submitting the next form, where the result will be drawn (especially if there is an error), and what to do with the other elements on the screen (whether to block the input and how to respond when trying to close the window with the form being sent) . In the network engine, the following mechanism is implemented to indicate loading.

The eventEmitter methods are mixed into the prototype of each widget. When the widget is initialized, you can set the special variable this.requiresLoading = true, and then widgetBinding will consider the widget asynchronous and expect a ready event from it. However, the widget is rendered immediately, regardless of its state, and therefore must internally take care of what it shows until it prepares its data. Simple widgets usually have the variable this.loading = ko.observable (true), and their templates look like this:

 <!-- uncrd if: loading --> <div class="uncrd-loading-with-icon">...</div> <!-- /uncrd --> <!-- uncrd ifnot: loading --> ...   ... <!-- /uncrd --> 


Let's presuppose that on the page there is a link, when clicked, you need to open a modal window with a user profile,

 <a href="#" data-uncrd="click:open.bind($data,{name:'profile',id:123})">  #123</a>. 


When you click a modal window opens (the modal window widget does not require its download), a profile widget is displayed inside it, which shows the download icon while prompting for user data. It works clumsily, it turns out that when surfing within the network, with each click, the same empty modal window with a download icon is shown each time. To avoid this, it is here that the ready event from the widget is used. You can write this:

 <a href="#" data-uncrd="click:open.bind($data,{name:'profile',id:123,loading:'after'})">  #123</a> <!--   --> <a class="uncrd-loading-after" href="#" data-uncrd="click:open.bind($data,{name:'profile',id:123})">  #123</a>. 


And then when you click a modal window does not open until an asynchronous internal widget emits a ready event. Instead, we have an event of the event click, the event.currentTarget element, we check for the presence of the loading property or the uncrd-loading css class of something and draw an absolutely positioned loading icon next to the currentTarget element. After means after the element, before - before, over - over the middle of the link (in photos and avatars). If it was not possible to draw the download icon to the element that caused the opening of the window, force the window to open in a clumsy way. The last case occurs, for example, when the page is reloaded - the router during initialization, depending on location.hash, opens the corresponding window but does not know to which element on the page to draw an icon while the data is being loaded.

Connecting libraries and compiling with r.js


I repeat once again that an important requirement for the network is maximum autonomy. All libraries, including jquery and knockout, live locally and are loaded from the system kernel. That is why the use of tags and attributes data-uncrd = "..." instead of native and data-bind = "..." - this is not foppery. All libraries and kernel scripts are assembled into a single main.js file using bare r.js. Config here - build.js . However, in the future I will advise everyone to use soil as a basis. In r.js, for some reason, you can either process one file or the entire directory, but you can't put main.js from one of the kernel files + in one action next to the subdirectory of the widgets. Also, if you have a project on require.js with require.config ({...}) in the main main.js file, you should be aware that this config is not taken into account when building with r.js - all paths should Re-specified in the build.js file, which is specified during the build (node ​​r.js -o build.js).

In r.js, errors ( # 341 ) are possible when working with namespaces, this is due to the fact that at the moment the namespace is substituted into the define and require variables through regular regular expressions. In the code of the knockout.js library, the syntax in the amd environment check is slightly different, and the regularspace for the namespace does not work there. There is no solution yet, you need to check the output and add regulars or edit the library code.

Practice writing your widgets


In the repository in the demo branch threw all unnecessary. In the /demo/1.html folder there is a file on which primitive widgets are displayed, which we will now write.

Let's start with the top bar widget, which does not require downloads, but simply shows the bar with the top menu. The widget's logic is in source / widgets / models / topBar.js, it is empty:

 define(function() { var TopBar = function(options) { } return TopBar; }); 


The widget's HTML template is in source / widgets / templates / topBar.html, the usual html type:
 <div class="navbar navbar-fixed-top uncrd-topbar"> <div class="navbar-inner"> <div class="container"> <a class="brand" href="#">UnCRD</a> <ul class="nav"> <li><a href="#" data-uncrd="click:o('page1')">  </a></li> <li><a href="#" data-uncrd="click:o({name:'page2',param1:'value1',loading:'after'})">   </a></li> <li><a href="#" data-uncrd="click:o({modalWindow:{header:'',content:''}})">   </a></li> </ul> <div class="loginForm" data-uncrd="widget:{name:'loginForm',template:'loginFormInline'}"></div> </div> </div> </div> 


Despite the fact that TopBar contains nothing, it should be remembered that widgetBinding adds eventEmitter methods to it, the widget has an array of this.childrenWidgets from one element - the loginForm internal widget. It also has methods common for all widgets: this.destroy (deletes the subwidget tree), this.open (opens the window) and this.o (short for this.open, which returns this.open.bind (...)).

Let's complicate topBar. Suppose that at initialization it should beautifully jquery-slow go to the top. There are two ways to do this. It is more correct to write customBinding to leave and pryndit it to the root element of the template, however sometimes you want to have access to the DOM from the model from inside the model, which obviously appears after creating the widget object and using renderTemplate. The domInit method is provided for this - it is called after the renderTemplate if specified and has self parameters (the widget itself), element (the DOM element that caused the widget to be created) and firstDomChild (the first found DOM element of the nodeType = 1 type within the template):

 define(["jquery"],function($) { var TopBar = function(options) { } TopBar.prototype.domInit = function(self,element,firstDomChild) { $(firstDomChild).hide().slideDown(); } return TopBar; }); 


When clicking on the first link of the top bar, the method o ('page1') is called. This means that a modal window will be opened, into the content of which the widget with the model /widgets/models/page1.js and the template /widgets/templates/page1.html will be loaded. Despite the fact that the modalWindow widget formally contains page1, in fact, page1 is the main one, and modalWindow is only a binding, so page1 has access to its window in the property this.modalWindow.

 define(function() { var Page1 = function(o) { var modalWindow = o.options.modalWindow; if (modalWindow) { modalWindow.width(700); modalWindow.cssPosition("absolute"); this.close = function() { modalWindow.destroy(); //      modalWindow,    modalWindow      } } else { this.close = function() { this.destroy(); } } } return Page1; }); 


 <div> ...   ... <a href="#" data-uncrd="click:close">    page1</a> </div> 


By default, a modal window has position = fixed. When the window is opened, the site content is dimmed by the fade div and “nailed”, i.e. it is placed position = fixed, marginTop = scrollTop, then the darkened content remains in place and the scroll disappears. If the modal window has position = absolute and is higher than the screen height, the resulting scroll begins to control the window offset instead of the content offset. When all windows are closed, the original properties are put down. This behavior seems to me more correct than bootstrap, when a dialogue with position: fixed appears, and the content under it continues to scroll (and ahtung happens, if suddenly the modal window is higher in height than the screen).

Let's proceed to asynchronous loading. Let page2 need to load some data before displaying. We set this.requiresLoading, emit the ready event, do not forget that the widget can be shown immediately with no data loaded:

 define(["knockout"],function(ko) { var Page2 = function(o) { this.requiresLoading = true; this.loading = ko.observable(true); this.stringFromServer = ko.observable(null); } Page2.prototype.domInit = function(self,element,firstDomChild) { setTimeout(function() { //     self.stringFromServer("   "); self.loading(false); self.emit("ready"); },1000); } return Page2; }); 


 <!-- uncrd if: loading --> <div class="uncrd-loading-with-icon">...</div> <!-- /uncrd --> <!-- uncrd ifnot: loading --> <div data-uncrd="text:stringFromServer"></div> <!-- /uncrd --> 


Paste this widget directly to the / demo/1.html page next to the top bar and reload the page. We see that the widget here has nowhere to ascribe its download icon, so it is drawn immediately and shows the download icon inside. However, when clicking on the page2 link in the top menu, the download icon will appear next to the link, and the modal window with the page will appear ready.

The properties of the modal window can be set from the internal widget, but with a lower priority, you can specify them directly in the open method. For example, you can not specify the name of the internal widget at all, but instead specify the content property - then you get a normal modal window with the text:

 <a href="#" data-uncrd="click:o({modalWindow:{header:'',content:''}})">    </a> 


Total


Simple widgets written together. Complex widgets can be seen in the repository, https://github.com/Kasheftin/uncrd . These are the same simple widgets, only more voluminous. The system is in development at the prototype level. How the prototype works on a live site - see here: http://www.photovision.ru . A demo page with all the widgets and pieces of documentation is available here: http://uncrd.com/docs/1.html . A demo from the bare core and three stupid example widgets can be downloaded from the demo branch, everything has already been assembled (and relative paths are set), you just need to open /demo/1.html in the browser.

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


All Articles