We are writing a complex application on knockout.js - 2
I’m writing one epic mega-crap that I want to promote on a hub This thing is like a distributed social network. There are kernels with api that communicate by some standard and frontend. The network’s peculiarity is that the frontend lives “separately” from the kernel, that is, the network does not have its own domain - take html, put a link to any core and get a network that lives on top of the site. Outwardly, it looks like Facebook social plugins - comments and likes from there can be put on any page - only powerful knockout.js binders are used instead of fb-like tags+ the user is not limited to bits of comments and likes - almost any block from the network can be imported to the site and almost any action can be taken. The frontend is written on the same technologies that the user can use and add on his page.
As a result, a technique was formed that may be of interest to the front-end vendor. I want to make out this technique in this article.
I’ll tell you about a system that is embedded in an html page through knockout binders. The code lives in plug-in widgets that consist of html templates with a knockout binding. Widgets can be nested within each other. All this uses require.js and lives in amdform. Dependencies on the external page are minimized; all libraries (jquery, knockout, and plugins) use only their own in local space with namespaces. To build the code, r.js is used . We will also write a full-fledged window manager on the basis of the bootstrap dialogue as cool peppers - with a knockout it's like two fingers on the asphalt ...
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 of the network lies 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 intentionally was not redone. One script is connected from the uncrd.com domain, which provides network functionality. When clicking on photos, the windows of the photo viewer should pop up, you can go to the profiles of the authors, register, post photos and comments. My goal was to write a standard network with minimal functionality.
The repository also contains a piece of documentation on working with the network, where most of the blocks are listed and their parameters are described - http://uncrd.com/docs/1.html .
It should be noted that the frontend consists of a universal core and network-specific widgets. Viewing photos and profiles of authors belong to the second, the window manager and the widget connection mechanism to the first. We will consider the basis, but it is poorly isolated from the system code, because it is developed at a time (for example, authorization is in the kernel, although it should be in a specific place for the network), so I will tell you something from the theory, and then collect it in the demo-branch bare core, and we will analyze in detail a couple of demo widgets on it.
The standard logic when modules from some social network are connected to an external site is the use of iframe. This is safe because the external site does not drag out the session, and 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 site page, and due to this we get much more features, and not just a dumb insert of predefined square blocks. Security is largely decided by registering sites on the network and tokens. But css is not solved. I don’t know how to insert my 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, the prototype assumes that the site is running on bootstrap (the network does not drag css with its bootstrap,
The frontend consists of the core of the system and dynamically loaded widgets. Each widget consists of a js-object, which is located in a separate file in an amd-form (example messageForm.js ) and an html-template with code (example messageForm.html ). Recently, I have seen several applications on knockout.js with a modular approach, and they all used this kind of binding:
Then, in core.js or in any widget (since they support attachments to each other), the object of the required widget is created: this.w = SomeWidget (); and in the right place on the template, binding is called . I note that knockout natively only supports “named” and “internal nameless” templates, and therefore the renderTemplate method in this case uses the stringTemplateEngine from this article .
The plus of this approach is simplicity. When the widget is explicitly created, 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. For example, the following code needs to work:
This means that widgets must be created inside widget binders. In this case, there is a problem with access to the created object - inside the model code we do not know when the internal sub-widget objects will be loaded and created and where they will live. Therefore, when creating a widget inside widget-binding, it is necessary to register the created object in its parent within some childrenWidgets variable, and if you want to remove the widget, delete the entire subtree recursively. This is due to the rather voluminous code of the resulting binding, widgetBinding.js .
I love how the bootstrap popup dialog looks like. I even tried to use its window jquery plugin, but glitches with fade prevented when creating several windows and the wrong scroll. I'm lying. The main obstacle was the realization that all the power of the knockout was at hand, and he was busy with jquery and the DOM. I hate DOM manipulations if there is a knockout. In uncrd, manipulations with the DOM are found in only two cases in the specially assigned domInit methods. Therefore, a window manager, windowManager.js , was written on 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 with any widget opens. The opened window itself is also a widget, and therefore registers itself in the childrenWidgets array of the parent, which is windowManager. When initializing the windowManager, we explicitly set this.childrenWidgets = ko.observableArray ([]), and therefore it is not overridden by an ordinary array in widget binding. The convenience of the nakout here is that observableArray has the same push, pop, splice methods as a regular array. Therefore, inside the widget, the binding is not important, the usual one is an 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 page of the site,
The modalWindow3.js modal window itself is html from the bootstrap + positioning calculation depending on the size of the content, position and drag, nothing supernatural. The only feature is that the name, title and footer of the internal widget can be sub-widgets defined through observable variables, and when the parameters change, widgets are automatically recreated. And for this, it was not necessary to write a single line - everything is provided by the update method in widget binding.
Indication of loading with ajax requests is one of the pairing things in the site UI. Each time you need to think about where the “please wait” icon will pop up when submitting the next form, where the result will be drawn (especially if there is an error), and what to do with the rest of the elements on the screen (whether to block inputs and how to react to trying to close the window with the sending form) . The following engine is implemented in the network engine to indicate load. EventEmitter
methods are mixed into the prototype of each widget. When initializing the widget, you can put a 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, it must take care of itself that it will not show it until it prepares its data. Simple widgets usually have the variable this.loading = ko.observable (true), and their templates look like this:
Suppose that there is a link on the page, when you click on it you need to open a modal window with a user profile,
When you click, a modal window opens (the modal window widget does not need to be loaded), a profile widget is displayed inside it, which shows the download icon while requesting user data. This works clumsily, it turns out that when surfing inside the network, every time you click, the same empty modal window with a download icon is shown every time. To avoid this, it is here that the ready event from the widget is used. You can write like this:
And then, when clicked, the modal window does not open until the asynchronous internal widget emits the ready event. Instead, we have the event click event, the event.currentTarget element, check for the presence of the loading property or the uncss-loading-css class, and draw an absolutely positioned loading icon next to the currentTarget element. After means after the element, before - before, over - on top of the middle of the link (in photos and avatars). If you couldn’t attach the loading icon to the element that caused the window to open, force the window to open in a clumsy way. The latter case occurs, for example, when the page is reloaded - the router, upon 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 loading.
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 core of the system. That is why using tags and attributes data-uncrd = "..." instead of native ones and data-bind = "..." is not fopping. All libraries and kernel scripts are collected in one main.js file with the naked r.js . The config here is build.js . However, in the future I will and I advise everyone to use soil as a basis. For some reason, in r.js, you can process either a single file or the entire directory, but you cannot collect main.js from kernel files in one step + put a subdirectory of widgets next to it. Also, if you have a project on require.js with require.config ({...}) in the main.js file, you should be prepared for the fact that this configuration is not taken into account when building with r.js - all paths should be re-specified in the build.js file that is specified during assembly (node r.js -o build.js).
Errors are possible in r.js ( # 341) when working with namespace, this is due to the fact that at the moment namespace is substituted into the variables define and require through regular regular expressions. In the code of the knockout.js library, the syntax in checking for amd environments is slightly different, and the regularity for namespace does not work there. There is no solution yet, you need to check the output and add regulars or edit the library code.
In the repository in the demo branch, I threw everything unnecessary. In the /demo/1.html folder there is a file that shows the primitive widgets that we will write now.
Let's start with the widget of the top bar, which does not require downloads, but simply shows a bar with the top menu. The widget logic is in source / widgets / models / topBar.js, it is empty:
The widget's html template is located in source / widgets / templates / topBar.html, a regular html type:
Despite the fact that TopBar does not contain anything, it should be remembered that widgetBinding mixes eventEmitter methods into it, the widget has an this.childrenWidgets array of one element - the loginForm internal widget. It also has methods common to all widgets this.destroy (removes the sub-widget tree), this.open (opens a window) and this.o (short for this.open, which returns this.open.bind (...)).
Let's complicate topBar. Suppose that during initialization it should nicely jquery-slow go top. There are two ways to do this. It’s more correct to write customBinding for driving out and attach it to the root element of the template, but sometimes you want inside the model to have access to the DOM of the template, which, obviously, appears after creating the widget object and applying renderTemplate. The domInit method is provided for this - it is called after renderTemplate if specified and has parameters self (the widget itself), element (the DOM element that caused the widget to be created) and firstDomChild (the first DOM element found of type nodeType = 1 inside the template):
When you click on the first link of the top bar, the o ('page1') method is called. This means that a modal window will be opened, into the content of which a widget with the model /widgets/models/page1.js and the template /widgets/templates/page1.html will be loaded. Despite the fact that formally the modalWindow widget contains page1, in fact page1 is the main one, and modalWindow is just a binding, so page1 has access to its window in the this.modalWindow property.
By default, the modal window has position = fixed. When the window is opened, the content of the site is darkened by the fade-div and “beaten,” it is affixed position = fixed, marginTop = scrollTop, then the darkened content remains in place and the scroll disappears. If the modal window has position = absolute and is larger than the screen in height, the resulting scroll will control the window shift instead of the content shift. When all windows are closed, the original properties are affixed to the content. This behavior seems to me more correct than bootstrap, when a dialog with position: fixed appears, and the content under it continues to scroll (and an ahtung happens if suddenly the modal window is larger than the screen in height).
Let's move on to asynchronous loading. Let page2 need to load some data before displaying it. We put down this.requiresLoading, we emit the ready event, do not forget that the widget can be displayed immediately with unloaded data:
Insert this widget directly on the /demo/1.html page next to the top bar and reload the page. We see that the widget has nowhere to attribute its download icon to, so it is drawn immediately and shows the download icon inside. However, when you click on the page2 link in the top menu, the download icon appears next to the link, and the modal window with the page appears ready.
The properties of a modal window can be set from the internal widget, however with a lower priority they can be specified directly in the open method. For example, you don’t have to specify the name of the internal widget at all, but instead specify the content property - then you get a regular modal window with text:
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 under development at the prototype level. How the prototype works on a live site - see here: http://www.photovision.ru . See the demo page with all widgets and pieces of documentation here: http://uncrd.com/docs/1.html . The demo from the bare kernel and three stupid widgets-examples can be downloaded from the demo branch , everything is already assembled there (and relative paths are put down), you just need to open /demo/1.html in the browser.
As a result, a technique was formed that may be of interest to the front-end vendor. I want to make out this technique in this article.
I’ll tell you about a system that is embedded in an html page through knockout binders. The code lives in plug-in widgets that consist of html templates with a knockout binding. Widgets can be nested within each other. All this uses require.js and lives in amdform. Dependencies on the external page are minimized; all libraries (jquery, knockout, and plugins) use only their own in local space with namespaces. To build the code, r.js is used . We will also write a full-fledged window manager on the basis of the bootstrap dialogue as cool peppers - with a knockout it's like two fingers on the 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 of the network lies 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 intentionally was not redone. One script is connected from the uncrd.com domain, which provides network functionality. When clicking on photos, the windows of the photo viewer should pop up, you can go to the profiles of the authors, register, post photos and comments. My goal was to write a standard network with minimal functionality.
The repository also contains a piece of documentation on working with the network, where most of the blocks are listed and their parameters are described - http://uncrd.com/docs/1.html .
It should be noted that the frontend consists of a universal core and network-specific widgets. Viewing photos and profiles of authors belong to the second, the window manager and the widget connection mechanism to the first. We will consider the basis, but it is poorly isolated from the system code, because it is developed at a time (for example, authorization is in the kernel, although it should be in a specific place for the network), so I will tell you something from the theory, and then collect it in the demo-branch bare core, and we will analyze in detail a couple of demo widgets on it.
Cons and css
The standard logic when modules from some social network are connected to an external site is the use of iframe. This is safe because the external site does not drag out the session, and 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 site page, and due to this we get much more features, and not just a dumb insert of predefined square blocks. Security is largely decided by registering sites on the network and tokens. But css is not solved. I don’t know how to insert my 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, the prototype assumes that the site is running on bootstrap (the network does not drag css with its bootstrap,
System core
The frontend consists of the core of the system and dynamically loaded widgets. Each widget consists of a js-object, which is located in a separate file in an amd-form (example messageForm.js ) and an html-template with code (example messageForm.html ). Recently, I have seen 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 attachments to each other), the object of the required widget is created: this.w = SomeWidget (); and in the right place on the template, binding is called . I note that knockout natively only supports “named” and “internal nameless” templates, and therefore the renderTemplate method in this case uses the stringTemplateEngine from this article .
The plus of this approach is simplicity. When the widget is explicitly created, 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. For example, the following code needs to work:
... код сайта ...
Привет,
... код сайта ...
This means that widgets must be created inside widget binders. In this case, there is a problem with access to the created object - inside the model code we do not know when the internal sub-widget objects will be loaded and created and where they will live. Therefore, when creating a widget inside widget-binding, it is necessary to register the created object in its parent within some childrenWidgets variable, and if you want to remove the widget, delete the entire subtree recursively. This is due to the rather voluminous code of the resulting binding, widgetBinding.js .
Window manager
I love how the bootstrap popup dialog looks like. I even tried to use its window jquery plugin, but glitches with fade prevented when creating several windows and the wrong scroll. I'm lying. The main obstacle was the realization that all the power of the knockout was at hand, and he was busy with jquery and the DOM. I hate DOM manipulations if there is a knockout. In uncrd, manipulations with the DOM are found in only two cases in the specially assigned domInit methods. Therefore, a window manager, windowManager.js , was written on 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 with any widget opens. The opened window itself is also a widget, and therefore registers itself in the childrenWidgets array of the parent, which is windowManager. When initializing the windowManager, we explicitly set this.childrenWidgets = ko.observableArray ([]), and therefore it is not overridden by an ordinary array in widget binding. The convenience of the nakout here is that observableArray has the same push, pop, splice methods as a regular array. Therefore, inside the widget, the binding is not important, the usual one is an 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 page of the site,
The modalWindow3.js modal window itself is html from the bootstrap + positioning calculation depending on the size of the content, position and drag, nothing supernatural. The only feature is that the name, title and footer of the internal widget can be sub-widgets defined through observable variables, and when the parameters change, widgets are automatically recreated. And for this, it was not necessary to write a single line - everything is provided by the update method in widget binding.
Loading indication
Indication of loading with ajax requests is one of the pairing things in the site UI. Each time you need to think about where the “please wait” icon will pop up when submitting the next form, where the result will be drawn (especially if there is an error), and what to do with the rest of the elements on the screen (whether to block inputs and how to react to trying to close the window with the sending form) . The following engine is implemented in the network engine to indicate load. EventEmitter
methods are mixed into the prototype of each widget. When initializing the widget, you can put a 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, it must take care of itself that it will not show it until it prepares its data. Simple widgets usually have the variable this.loading = ko.observable (true), and their templates look like this:
... здесь разметка виджета...
Suppose that there is a link on the page, when you click on it you need to open a modal window with a user profile,
Профиль юзера #123.
When you click, a modal window opens (the modal window widget does not need to be loaded), a profile widget is displayed inside it, which shows the download icon while requesting user data. This works clumsily, it turns out that when surfing inside the network, every time you click, the same empty modal window with a download icon is shown every time. To avoid this, it is here that the ready event from the widget is used. You can write like this:
Профиль юзера #123Профиль юзера #123.
And then, when clicked, the modal window does not open until the asynchronous internal widget emits the ready event. Instead, we have the event click event, the event.currentTarget element, check for the presence of the loading property or the uncss-loading-css class, and draw an absolutely positioned loading icon next to the currentTarget element. After means after the element, before - before, over - on top of the middle of the link (in photos and avatars). If you couldn’t attach the loading icon to the element that caused the window to open, force the window to open in a clumsy way. The latter case occurs, for example, when the page is reloaded - the router, upon 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 loading.
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 core of the system. That is why using tags and attributes data-uncrd = "..." instead of native ones and data-bind = "..." is not fopping. All libraries and kernel scripts are collected in one main.js file with the naked r.js . The config here is build.js . However, in the future I will and I advise everyone to use soil as a basis. For some reason, in r.js, you can process either a single file or the entire directory, but you cannot collect main.js from kernel files in one step + put a subdirectory of widgets next to it. Also, if you have a project on require.js with require.config ({...}) in the main.js file, you should be prepared for the fact that this configuration is not taken into account when building with r.js - all paths should be re-specified in the build.js file that is specified during assembly (node r.js -o build.js).
Errors are possible in r.js ( # 341) when working with namespace, this is due to the fact that at the moment namespace is substituted into the variables define and require through regular regular expressions. In the code of the knockout.js library, the syntax in checking for amd environments is slightly different, and the regularity for 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, write your widgets
In the repository in the demo branch, I threw everything unnecessary. In the /demo/1.html folder there is a file that shows the primitive widgets that we will write now.
Let's start with the widget of the top bar, which does not require downloads, but simply shows a bar with the top menu. The widget 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 located in source / widgets / templates / topBar.html, a regular html type:
Despite the fact that TopBar does not contain anything, it should be remembered that widgetBinding mixes eventEmitter methods into it, the widget has an this.childrenWidgets array of one element - the loginForm internal widget. It also has methods common to all widgets this.destroy (removes the sub-widget tree), this.open (opens a window) and this.o (short for this.open, which returns this.open.bind (...)).
Let's complicate topBar. Suppose that during initialization it should nicely jquery-slow go top. There are two ways to do this. It’s more correct to write customBinding for driving out and attach it to the root element of the template, but sometimes you want inside the model to have access to the DOM of the template, which, obviously, appears after creating the widget object and applying renderTemplate. The domInit method is provided for this - it is called after renderTemplate if specified and has parameters self (the widget itself), element (the DOM element that caused the widget to be created) and firstDomChild (the first DOM element found of type nodeType = 1 inside the template):
define(["jquery"],function($) {
var TopBar = function(options) { }
TopBar.prototype.domInit = function(self,element,firstDomChild) {
$(firstDomChild).hide().slideDown();
}
return TopBar;
});
When you click on the first link of the top bar, the o ('page1') method is called. This means that a modal window will be opened, into the content of which a widget with the model /widgets/models/page1.js and the template /widgets/templates/page1.html will be loaded. Despite the fact that formally the modalWindow widget contains page1, in fact page1 is the main one, and modalWindow is just a binding, so page1 has access to its window in the this.modalWindow property.
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;
});
... много контента ...
Закрыть модальное окно с page1
By default, the modal window has position = fixed. When the window is opened, the content of the site is darkened by the fade-div and “beaten,” it is affixed position = fixed, marginTop = scrollTop, then the darkened content remains in place and the scroll disappears. If the modal window has position = absolute and is larger than the screen in height, the resulting scroll will control the window shift instead of the content shift. When all windows are closed, the original properties are affixed to the content. This behavior seems to me more correct than bootstrap, when a dialog with position: fixed appears, and the content under it continues to scroll (and an ahtung happens if suddenly the modal window is larger than the screen in height).
Let's move on to asynchronous loading. Let page2 need to load some data before displaying it. We put down this.requiresLoading, we emit the ready event, do not forget that the widget can be displayed immediately with unloaded data:
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;
});
Insert this widget directly on the /demo/1.html page next to the top bar and reload the page. We see that the widget has nowhere to attribute its download icon to, so it is drawn immediately and shows the download icon inside. However, when you click on the page2 link in the top menu, the download icon appears next to the link, and the modal window with the page appears ready.
The properties of a modal window can be set from the internal widget, however with a lower priority they can be specified directly in the open method. For example, you don’t have to specify the name of the internal widget at all, but instead specify the content property - then you get a regular modal window with text:
Модальное окно без внутреннего виджета
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 under development at the prototype level. How the prototype works on a live site - see here: http://www.photovision.ru . See the demo page with all widgets and pieces of documentation here: http://uncrd.com/docs/1.html . The demo from the bare kernel and three stupid widgets-examples can be downloaded from the demo branch , everything is already assembled there (and relative paths are put down), you just need to open /demo/1.html in the browser.