📜 ⬆️ ⬇️

Writing a complex application on knockoutjs

There is such a library knockout.js . It differs from other good tutorials for beginners and a bunch of clear working examples . There is also a slim MVVM model, declarative communication, and so on.

In short, if you, like me, played with this library, wrote beautiful shapers, and you liked it, then you wanted to apply this whole thing on a real project. And here is the problem - there is more than one mold in a real project. And since such tools, you want a single web page application and nothing else. And to make one controller and all templates to wrap up on one page is also stupid and slow.

Under the cut, I will give the basis of my complex application. It is not complicated at all, but modular and allows extensions, and templates and models are dynamically loaded. The idea was spied on in this presentation - http://www.knockmeout.net/2012/08/thatconference-2012-session.html , the presentation code was laid out on github - https://github.com/rniemeyer/SamplePresentation - based on this code will write your.
')
Retreat

At first I mentioned about wrapping up several forms on one page - this is not our method, but quite a normal solution. An example of a small single page application of two replaceable templates can be found in the tutorial directly on the home site - http://learn.knockoutjs.com/#/?tutorial=webmail .

Another retreat

Some blame knockout for its syntax, for the idea of ​​declaring connections directly in the html template in the data-bind = "..." attributes. Like it looks like a return to the 90s with javascript code inserts in onclick = "..". Yes, everything works through eval. Claims are justified - you can be tired of debugging the binding type
<div data-bind=”value: name, event: { focus: function() { viewModel.selectItem($data); }, blur: function() { viewModel.selectItem(null); }”></div> 
The struggle for html-code purity is extensively discussed in this article - http://www.knockmeout.net/2011/08/simplifying-and-cleaning-up-views-in.html . You need to use dependentObservable, make custom-bindings, avoid anonymous functions. You can write your bindingProvider or use this https://github.com/rniemeyer/knockout-classBindingProvider .

purpose

If you write a real application, taking the knockout examples as a base, you get huge monolithic models, and it may not be clear how to develop and debug them. The main goal of my example is to show one of the ways to break the code into visible pieces.

I will describe what we will have in the end. We will have templates in html files in the templates folder and knockout-js binding in the corresponding files in the modules folder. Under certain actions, the method will be run, which, using the required.js, will load the template and the code into the desired div. The final example code is here: https://github.com/Kasheftin/ko-test .

StringTemplateEngine

Knockoutjs out of the box supports two ways of working with templates - unnamed and named . Examples:
 //  <div data-bind="foreach: items"> //   <li> <span data-bind="name"></span> <span data-bind="price"></span> </li> </div> //  <div data-bind="template: {name:'person-template',data:person}"></div> <script type="text/html" id="person-template"> //   <h3 data-bind="text: name"></h3> <p>Credits: <span data-bind="text: credits"></span></p> </script> 

In all cases, patterns are pieces of an existing dom tree. In our case, the code will come from the server as a string, and the most organic solution is to write your template engine. You can learn the theory from this article, http://www.knockmeout.net/2011/10/ko-13-preview-part-3-template-sources.html . There is probably a good ready-made solution https://github.com/ifandelse/Knockout.js-External-Template-Engine , but we will write ours based on the presentation that we wrote about at the beginning.

Here, the code stringTemplateEngine from the presentation is https://github.com/rniemeyer/SamplePresentation/blob/master/js/stringTemplateEngine.js . What I don’t like: the global array ko.templates is used, in which the loaded templates are written, and the templates need to come up with the names by which they are called. We will not use this array, since require.js deals with caching. Our stringTemplateEngine will be called something like this:
 <div data-bind="with: currentState"> <div data-bind="template: {html:html,data:data}"></div> </div> 
That is, if the html property is specified, then our stringTemplateEngine is called, otherwise we give it to the standard knockout. currentState is an object that must have html properties with html code and possibly data with a module object.

So, make a new templateSource:
 ko.templateSources.stringTemplate = function(element,html) { this.domElement = element; this.html = ko.utils.unwrapObservable(html); } ko.templateSources.stringTemplate.prototype.text = function() { if (arguments.length == 0) return this.html; this.html = ko.utils.unwrapObservable(arguments[0]); } 

And override the makeTemplateSource method from the nativeTemplateEngine object. So far, no bikes - the redefinition of makeTemplateSource is written in the documentation. However, the built-in makeTemplateSource entry only accepts template and templateDocument, where template is the name of the template, if any, and a link to the current dom otherwise. A mess with type mixing is not a good solution. In addition, to connect our StringTemplateEngine, we need to check not the name attribute, but the html attribute. This data is not present, but it comes into the renderTemplate method, so we redefine it too:
 var engine = new ko.nativeTemplateEngine(); //   renderTemplate -   makeTemplateSource    engine.renderTemplate = function(template,bindingContext,options,templateDocument) { var templateSource = this.makeTemplateSource(template, templateDocument, bindingContext, options); return this.renderTemplateSource(templateSource, bindingContext, options); } //  ,   2  engine.makeTemplateSource = function(template, templateDocument, bindingContext, options) { //  engine  knockout- if (typeof template == "string") { templateDocument = templateDocument || document; var elem = templateDocument.getElementById(template); if (!elem) throw new Error("Cannot find template with ID " + template); return new ko.templateSources.domElement(elem); } //  stringTemplateEngine,  options else if (options && options.html) { return new ko.templateSources.stringTemplate(template,options.html); } else if ((template.nodeType == 1) || (template.nodeType == 8)) { //  engine   knockout- return new ko.templateSources.anonymousTemplate(template); } else throw new Error("Unknown template type: " + template); } ko.setTemplateEngine(engine); 

Overriding renderTemplate does not break knockout, because makeTemplateSource is called only in it and in another rewriteTemplate method described here: https://github.com/SteveSanderson/knockout/blob/master/src/templating/templateEngine.js . However, the latter is not called, since nativeTemplateEngine is set to allowTemplateRewriting = false.

The full code of our stringTemplateEngine can be found here: https://github.com/Kasheftin/ko-test/blob/master/js/stringTemplateEngine.js .

State.js

Now we will write state.js - this is an object that, during initialization, will load the specified template and module. Our state will be nested in each other, so the application itself will also be a state, a state will be nested in it from the menu that will load other state forms and data.

 define(["knockout","text"],function(ko) { return function(file,callback) { var s = this; s.callback = callback; s.data = ko.observable(null); s.html = ko.observable(null); require(["/js/modules/" + file + ".js","text!/js/templates/" + file + ".html"],function(Module,html) { s.data(typeof Module === "function" ? new Module(s) : Module); s.html(html); if (s.callback && typeof s.callback === "function") s.callback(s); }); s.setVar = function(i,v) { var data = s.data(); data[i] = v; s.data(data); } } }); 

This is all code. AMD script, use knockout and require.js text plugin to load html templates. The input is a file name and a callback method, inside there are two observable variables, data and html, the same ones that are required in our stringTemplateEngine. The setVar method is also rendered - several states live on the page at the same time, they must exchange data. As a rule, the link to the root state will be passed to setVar, and everything will go from there.

Main.js
The main page HTML code consists of a couple of lines:
 <body> <div class="container" data-bind="template:{html:html,data:data}"></div> <script type="text/javascript" data-main="/js/main" src="/lib/require/require.js"></script> </body> 

We write the main file that will be executed after the page loads and require.js:
 require(["knockout","state","stringTemplateEngine"], function(ko,State) { var sm = new State("app",function(state) { ko.applyBindings(state); }); }); 


App.js, App.html

I already wrote that the application itself is also a state. All in each other nested. A page consists of a menu and content. So the html-code of the page markup is in templates / app.html, and the initialization of the menu and content is in modules / app.js:
 // templates/app.html: <div class="row"> <div data-bind="with:menu"><div class="span3 menu" data-bind="template:{html:html,data:data}">Menu</div></div> <div data-bind="with:currentState"><div class="span9 content" data-bind="template:{html:html,data:data}"></div></div> </div> 
 // modules/app.js: define(["knockout","state"],function(ko,State) { return function() { var app = this; this.menu = new State("menu",function(state) { // ,  callback-,    app,  app       state- state.setVar("app",app); }); this.currentState = ko.observable(null); } }); 


Menu.js, Menu.html

I will give another example of the menu When you click on the links, the contents of another state are changed, the currentState variable that lies in the state-app. Access to it is available because the app was sent to setVar when the menu was initialized.
 // menu.html <ul class="nav nav-list"> <li><a href="javascript:void(0);" data-bind="click:gotoSample" data-id="1">Hello World</a></li> <li><a href="javascript:void(0);" data-bind="click:gotoSample" data-id="2">Click counter</a></li> <li><a href="javascript:void(0);" data-bind="click:gotoSample" data-id="3">Simple list</a></li> ... 
 // menu.js: define(["jquery","knockout","state"],function($,ko,State) { return function() { var menu = this; this.gotoSample = function(obj,e) { var sampleId = $(e.target).attr("data-id"); var newState = new State("samples/sample" + sampleId,function(state) { state.setVar("app",menu.app); //     app.currentState, ..   observable- currentState,     menu.app.currentState(state); }); } } }); 

That's all. The code for the modules is already broken. Pages of examples with different forms are copied from live examples , only decorated in amd-form. Then it is all loaded with initializations, ajax, but these are already “local” details that lie in the state ah.

Once again I will give a link to the final code of the example - https://github.com/Kasheftin/ko-test .

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


All Articles