Why HtmlJs

Let the dynamic be the dynamic.

HtmlJs is the most powerful MVVM JavaScript framework. HtmlJs completely separates HTML vs JavaScript but keep them connected together.

Below is some reasons that you should check out HtmlJs.

  1. Performance
    HtmlJs performance is absolutely outstanding. Check out performance tests Htmljs, KnockoutJs and AngularJs .

  2. Full-fledged framework
    You've already had a lot of tools for your Single-page application in small 32KB gzip. There're too many JavaScript frameworks available. It could be a JS war. Decide to choose one of them costs time and effort as well as money. HtmlJs packages validation, dirty checking, Promise/Ajax, scripts loading/modules, routing.

  3. Fluent API
    All modules in HtmlJs are written in fluent API. You can render view, do ajax call fluently. This could make your code lighter, faster, easier to read and maintain.

  4. No template
    Surprisingly? Sure, no template at all, no more hard code, event meaningful hard code will make you stress in long term project. HtmlJs is designed to reduce as many as magic strings in the application. HtmlJs uses DOM API to render DOM elements, a straight forward way, that's why it's the fastest one. It's the old way of coding Js. Using templates is hard/impossible to attach events, working with recursion, working with loop inside loop, validation, manage variable's context/scope, debug. Anyway template must be parsed into DOM, this must be slower.
    Stop using template system, let the dynamic (dynamic HTML) belongs to the dynamic (JavaScript).

  5. Extensible custom control
    You can never get this feature anywhere else. Every template/string based Js framework has this problem. With HtmlJs, this feature is definitely native, scope of function, scope of variable are still there for you. Your own control will be always light, easy to read, easy to maintain. Again, stop using template system.

Click here to check out real-world example.

Nuget: PM> Install-Package HtmlJs Bower: $> bower install htmljs

Hello world

<div id="helloWorld"> <input id="hw-input" class="form-control input-w160" /> <br /> <span id="hw-span"></span> </div>
var helloWorld = html.data('Hello world!'); html('#hw-input').input(helloWorld); html('#hw-span').text(helloWorld);
Try it Yourself »
Explanation

Have you tried to edit text in the input? Amazing?
That's right!
If you're not familiar with any JavaScript data-binding framework. You'll be blown away.
If you're, then this framework solve many problems you've faced.
Please read this tutorial carefully line by line if you're not familiar with any JavaScript data-binding framework.
Try to modify as much as you can. For example: modify variable names, add more controls, remove some controls
If you can't get what the framework does, don't hesitate to contact me at: nhan.aswigs@gmail.com

Get started with html.data

var helloWorld = html.data('Hello world!');

html('#hw-input').input(helloWorld);
html('#hw-span').text(helloWorld);
                                          
html.data will give you an observed object regardless of object's data type.
Whenever you change its value inside, all of subscribed element will be update.
The change can be made by changing input value or change by code.
You can try to modify initial value inside it to see what happen. e.g:
var helloWorld = html.data('Hello your name!');
Line number 3 html('#hw-input').input(helloWorld) will query the DOM element and bind data to that element.
Line number 4 html('#hw-span').text(helloWorld) will query the span element and set the text Hello world! to that span tag.
Do they look really native compare to HTML element? Yes, my design is native control without adding nonsense DOM attribute.
Now, you can change the code a little bit to see what happen.
HTML code
<div id="helloWorld">
    <input class="form-control input-w160 firstname" placeholder="First name" />
    <input class="form-control input-w160 lastname" placeholder="Last name" />
    <br />
    Hello 
    <span class="firstname"></span>
    <span class="lastname"></span>
     !
</div>
And the JavaScript
var firstName = html.data('Jonny');
var lastName = html.data('Tri Nguyen');
html('#helloWorld input.firstname').input(firstName);
html('#helloWorld input.lastname').input(lastName);

html('#helloWorld span.firstname').text(firstName);
html('#helloWorld span.lastname').text(lastName);
Now you can test your result. Your first name and last name change immediately right after your changes.

Dependency Tracking

<div id="computed"> <input class="form-control input-w160 firstname" /> <input class="form-control input-w160 lastname" /> <br /> Hello <span class="fullname"></span> ! </div>
//Declare View-Model var firstName = html.data('Jonny'); var lastName = html.data('Tri Nguyen'); var fullName = html.data(function(){ return firstName() + ' ' + lastName(); }); //Bind View-Model to the View html('#computed input.firstname').input(firstName); html('#computed input.lastname').input(lastName); html('#computed span.fullname').text(fullName);
Try it Yourself »
Explanation

In the previous example, you've tried bind first name and last name.
What if you want to display full name. If you create another observer named fullName
It could lead to another headache whenever user change firstName or lastName value.
To solve this problem, just pass a function into html.data.

                                      var fullName = html.data(function(){
                                        return firstName() + ' ' + lastName();
                                      });
                                    
Now you're able to see the full name displayed in the end of paragraph.

Validation

Simple validation.


<input id="validations" placeholder="Enter your name" class="form-control input-w160" /> <br /> Hello <span id="validationSpan">Hello world!</span><span>!</span>
var helloWorld = html.data('Nhan Nguyen') .required('Name is required.') .maxLength(15, 'Max length for name is 15 characters.'); html('#validations').input(helloWorld); html('#validationSpan').text(helloWorld);
Try it Yourself »


More validation.


<input id="validations-username" placeholder="Enter your first name" class="form-control input-w160" /><br /> <input id="validations-password" type="password" placeholder="Enter your password" class="form-control input-w160" /><br /> <input id="validations-passwordConfirmed" type="password" placeholder="Confirm the password" class="form-control input-w160" /><br />
var username = html.data('') .required('Username is required.') .maxLength(15, 'Max length for username is 15 characters.'); var password = html.data('') .required('Password is required.') .minLength(8, 'Min length for password is 6 characters.') .maxLength(32, 'Max length for password is 32 characters.'); var passwordConfirmed = html.data('').equal(password, 'Not match the password!'); html('#validations-username').input(username); html('#validations-password').input(password); html('#validations-passwordConfirmed').input(passwordConfirmed);
Try it Yourself »


Custom error handler.


<input id="validationHandler" placeholder="Enter your first name" class="form-control input-w160" /><br />
//declare variable (html.data) var fullname = html.data('') .required('Username is required.') .maxLength(15, 'Max length for username is 15 characters.'); html('#validationHandler').input(fullname, function(e){ //callback when validation finish var error = e.validationResults.firstOrDefault(function(i){return i.isValid === false;}); if(error){ e.input.style.color = 'red'; e.input.title = error.message; } else { e.input.style.color = 'black'; e.input.title = ''; } });
Try it Yourself »


Custom validation rules.


<input id="validation-custom" placeholder="Enter your first name" class="form-control input-w160" /><br />
//Add more rules to html.data.validation html.data.validation.beginWith = function(char, message) { this.validate(function(newValue, oldvalue) { if(newValue.charAt(0).toLowerCase() !== char.toLowerCase()) { this.setValidationResult(false, message); } else { this.setValidationResult(true); } }); return this; }; var username = html.data('') .required('Username is required.') .beginWith('A', 'Username must begin with A or a character') .maxLength(15, 'Max length for username is 15 characters.'); html('#validation-custom').input(username);
Try it Yourself »
All built-in validation rules in html.data.validation object.
They are required, isNumber, isEmail, pattern(regular expression pattern), maxLength, minLength, stringLength, range, greaterThan, lessThan, greaterThanOrEqual, lessThanOrEqual, equal

Simple list

This feature is the most powerful of HtmlJs
<div id="SList"> <form class="form-inline" role="form"> <input id="newPhone" class="form-control input-w160" placeholder="New smart phone..." /> <button id="addMore" class="btn btnViolet">Add</button> </form> <span>Your smart phones:</span> <ul> <li>Sample smart phone.</li> <li>These samples will be removed when binding data.</li> </ul> </div>
/* Declare data for the view. * Declaration should be put in another file. */ var addSP = function(e){ e.preventDefault(); smartPhone() !== '' && smartPhoneList.add(smartPhone()); smartPhone(''); }; var smartPhone = html.data(''); var smartPhoneList = html.data([ 'Blackberry', 'iPhone', 'Lg g pro2' ]); /* End data declaration */ /* Binding data */ html('#SList ul').each(smartPhoneList, function(phone, index){ html.li(phone); }); html('#SList input').input(smartPhone); html('#SList button').click(addSP); /* End binding data */
Try it Yourself »
Explanation
Firstly, declare view model for the page
var smartPhone = html.data('');
var smartPhoneList = html.data([
  'Blackberry',
  'iPhone',
  'Lg g pro2'
]);
smartPhone variable will be bounded to the textbox
smartPhoneList will be bounded to the list of smart phones.
Secondly render View from View-Model.
html('#SList ul').each(smartPhoneList, function(phone, index){
    html.li(phone);
});
When you want to render a list, you take a parent element, in this example it is ul node
Then use each method. This method takes 2 parameters:
1. An array or an observed array (smartPhoneList in this case).
2. A callback function to render the list.
The callback function takes 2 parameters:
2.a. An item in the list
2.b. Item index
NOTE: You can't use each method without a parent.

Great! Now you will want to add more smart phone into the list. So bind the value of the input as below.
html('#SList input').input(smartPhone);
Ok now, but it doesn't work because you will need to add an behaviour for Add button.
var addSP = function(e){
  smartPhone() !== '' && smartPhoneList.add(smartPhone());
  smartPhone('');}
And bind the event click.
html('#SList button').click(addSP)
Done! You can add a new one into smart phone list.

Complex list

The list with some behaviours
<div id="TestComplexList"> <span>Jonny Tri Nguyen and his children.</span><br /> <button id="newChild" class="btnViolet">Add new child</button> <div class="children"> <span>Name: A</span> <span>Age: 20</span> <button class="btnViolet">Delete</button> </div> </div>
/* Declare data for the view. */ var ViewModel = function () { var self = this; this.FirstName ='Jonny'; this.LastName = 'Tri Nguyen'; this.Children = html.data([ {Name: 'Anderson', Age: 30}, {Name: 'Peter', Age: 20} ]); this.Counter = html.data(function () { return self.Children().length; }); this.newChild = function(e) { self.Children.push({Name: 'Unknown', Age: 0}); }; this.deletePerson = function (e, model) { self.Children.remove(model); }; }; var vm = new ViewModel(); /* End data declaration */ /* Binding data */ html('#TestComplexList div').each(vm.Children, function(child, index){ html.span('Name: ').$() .span(child.Name).$() .space(2) .span('Age: ').$() .span(child.Age).$() .button('Delete').click(vm.deletePerson, child).clss('btnViolet').$() .br(); }); html('#newChild').click(vm.newChild); /* End binding data */
Try it Yourself »
Explanation
Firstly, you declare view model for the page.
They are the name of a person and his children.
Here we define a class for a child, just for creating object convenience.
var vm = {
    FirstName: 'Jonny',
    LastName: 'Tri Nguyen',
    Children: html.data([
        {Name: 'Anderson', Age: 30},
        {Name: 'Peter', Age: 20},
    ])
};
Then you render the person and his children.
html('#TestComplexList div').each(vm.Children, function(child, index){
    html.span('Name: ').$()
        .span(child.Name).$()
        .space(2)
        .span('Age: ').$()
        .span(child.Age).$()
        .button('Delete').$()
        .br();
});
Great! Now you've rendered the children. Let's add behaviour for delete buttons.
Add a behaviour into view model like so
var vm = {
    //...previous code
    deletePerson: function(e, model){
        remove(model);
    }
};
Modify button code as below.
.button('Delete').click(vm.deletePerson, child).clss('btnBlue').$()
You may wonder why do need to pass extra child parameter to click event.
It's so strange and not like jQuery way (lol).
No, you're not forced to put that parameter in case you know what object you want to work with.
In the previous example, you know that is smartPhone variable
However in this case, you have no idea about which child you want delete.
Then you want to get that parameter, it right there for you in the callback function (say deletePerson).
All you need to do is just delete it from the list.
The framework will handle removing action for you.
As you can tell, after clicking on the button, magic happens, that person is removed without any effort of manipulating DOM element.

Complex List 2

This is a real world example. This example is one of Knockout tutorial lessons.
<div id="meal-reservation"> <span>Your seat reservations</span> <span id="seatNumber" class="red"></span><br /> <span>Total surcharge</span> <span id="surcharge" class="red"></span><br /> <span>Total discounted</span> <span id="discount" class="red"></span><br /> <button id="addSeat" class="btnNormal">Add seat</button><br /> <table> <thead><tr><th>Passenger name</th><th>Meal</th><th>Surchage</th></tr></thead> <tbody> <tr> <td> <input value="Bert" class="form-control input-w160"/> </td> <td> <select> <option value="0">Standard sandwich</option> <option value="0">Premium (lobster)</option> <option value="0">Ultimate (whole zebra)</option> </select> </td> <td> <span>$ 290.00</span> </td> <td> <button class="btnViolet">Remove</button> </td> </tr> </tbody> </table> </div>
// Class to represent a row in the seat reservations grid function SeatReservation(name, initialMeal) { var self = this; self.name = html.data(name); self.meal = html.data(initialMeal); self.formattedPrice = html.data(function() { var price = self.meal().price; return price ? "$" + price.toFixed(2) : "None"; }); } // Overall viewmodel for this screen, along with initial state function ReservationsViewModel() { var self = this; // Non-editable catalog data - would come from the server self.availableMeals = [ { mealName: "Standard (sandwich)", price: 0 }, { mealName: "Premium (lobster)", price: 34.95 }, { mealName: "Ultimate (whole zebra)", price: 290 } ]; // Editable data self.seats = html.data([ new SeatReservation("Steve", self.availableMeals[0]), new SeatReservation("Bert", self.availableMeals[1]) ]); // Computed data self.totalSurcharge = html.data(function() { var total = 0; for (var i = 0; i < self.seats().length; i++) total += self.seats()[i].meal().price; return total.toFixed(2); }); self.totalDiscount = html.data(function() { return (self.totalSurcharge()*90/100).toFixed(2); }); self.seatNum = html.data(function(){ return self.seats().length; }); // Operations self.addSeat = function() { self.seats.add(new SeatReservation("", self.availableMeals[0])); } self.removeSeat = function(e, seat) { self.seats.remove(seat) } } var vm = new ReservationsViewModel(); html('#meal-reservation table tbody').each(vm.seats, function(seat, index){ html.tr() .td().input(seat.name).clss('form-control input-w160').$().$() .td() .dropdown(vm.availableMeals, seat.meal, 'mealName').clss('form-control input-w210 margin5').refresh(seat, vm).$() .$() .td().span(seat.formattedPrice).$().$() .td().button('Remove').clss('btnViolet').click(vm.removeSeat, seat).refresh(vm).$().$() .$() }); //computed properties html('#seatNumber').text(vm.seatNum); html('#surcharge').text(vm.totalSurcharge); html('#discount').text(vm.totalDiscount); // event handling html('#addSeat').click(vm.addSeat).refresh(vm);
Try it Yourself »
Explanation
Is that an complex example? Don't worry, I will explain you step by step. It'll take you 5-10 minutes to get it.
First of all, add a list of seat reservations without any behaviours.
// Class to represent a row in the seat reservations grid
function SeatReservation(name, initialMeal) {
    var self = this;
    self.name = html.data(name);
    self.meal = html.data(initialMeal);
    self.formattedPrice = html.data(function() {
        var price = self.meal().price;
        return price ? "$" + price.toFixed(2) : "None";        
    });
}
//Viewmodel for the page, along with initial state
function ReservationsViewModel() {
    var self = this;
    // Non-editable catalog data - would come from the server
    self.availableMeals = [
        { mealName: "Standard (sandwich)", price: 0 },
        { mealName: "Premium (lobster)", price: 34.95 },
        { mealName: "Ultimate (whole zebra)", price: 290 }
    ];    
    // Editable data
    self.seats = html.data([
        new SeatReservation("Steve", self.availableMeals[0]),
        new SeatReservation("Bert", self.availableMeals[1])
    ]);
}
var vm = new ReservationsViewModel();
And here is the rendering code (aka the View in MVVM)
html('#meal-reservation table tbody').each(vm.seats, function(seat, index){
    html.tr()
        .td().input(seat.name).clss('form-control input-w160').$().$()
        .td()
            .dropdown(vm.availableMeals, seat.meal, 'mealName').clss('form-control input-w210 margin5').$()
        .$()
        .td().span(seat.formattedPrice).$().$()
        .td().button('Remove').clss('btnViolet').$().$()
        .$()
});
Well done! You got a meal reservation list with initial values.
And there are some parts of the code will make you confuse. You may ask what is the .$() and why use that. Is that sort of jQuery method? Of course, no jQuery here. This framework doesn't depend on any library. The answer is that is the function to indicate that you have nothing more to do in a DOM element. For example, you can add class, add event click on an input or event add a child element. After doing those things, you want to render another control. Then you must tell the framework in somehow append another control into the parent node. So the role of $ method is just exactly like the role of end tag in HTML code. And therefore, br() method wouldn't end up with $().

One more thing is dropdown method. It is built-in control for simple select tag. It takes 4 parameters as following.
1. An observed array (availableSeats in this case).
2. Current value of the list.
3. (Optional) Display field in each object representing an option.
4. (Optional) Value field in each object representing an option.


Now you will add some computed properties like total surcharge and total discount.
Add to ReservationsViewModel the following code:
function ReservationsViewModel() {
    //...previous code
    
    // Computed data
    self.totalSurcharge = html.data(function() {
       var total = 0;
       for (var i = 0; i < self.seats().length; i++)
           total += self.seats()[i].meal().price;
       return total.toFixed(2);
    });
    
    self.totalDiscount = html.data(function() {
       return (self.totalSurcharge()*90/100).toFixed(2);
    });
    
    self.seatNum = html.data(function(){
        return self.seats().length;
    });
}
                                      
And you bind those data to the View.
//...previous code
html('#seatNumber').text(vm.seatNum);
html('#surcharge').text(vm.totalSurcharge);
html('#discount').text(vm.totalDiscount);
Now you got some useful information numbers from the list.
If you want that these numbers to be live updated. Then you need to add some behaviours like "Add" or "Remove".
The code in View-Model.
function ReservationsViewModel() {
    //...previous code
    
    // Operations
    self.addSeat = function() {
        self.seats.add(new SeatReservation("", self.availableMeals[0]));
    };
    self.removeSeat = function(e, seat) {
        self.seats.remove(seat);
    };
}
You need to bind those events to the View.
//...previous code
html('#addSeat').click(vm.addSeat).refresh(vm);
//...previous code
//.td().span(seat.formattedPrice).$().$()
.td().button('Remove').clss('btnViolet').click(vm.removeSeat, seat).refresh(vm).$().$()
What does refresh mean in the code? It is a method for refresh any kind of computed properties in view-model after a change or click event.
Why do you need that method? Because you have too many dependencies need to be refreshed, and those dependencies maybe too weak for the framework to know. For example some of seats are added at runtime by user, how can you declare dependencies without its reference at coding time. Or you simply don't want to declare too many dependencies (you may have hundred of them in real world application). So all you need to do is add refresh method.

Just a little more. Add refresh method after dropdown control to refresh data when changing selected item.
//...previous code
//.td()
    .dropdown(vm.availableMeals, seat.meal, 'mealName').refresh(seat, vm).$()
//$()
//...

Filter

<div id="test-filter"> <button class="btnNormal">Add seat</button> <input id="searchStr" class="form-control input-w160" placeholder="Searching..." /> <br /> <table> <thead><tr><th>Passenger name</th><th>Meal</th><th>Surchage</th></tr></thead> <tbody> </tbody> </table> </div>
// Class to represent a row in the seat reservations grid function SeatReservation(name, initialMeal) { var self = this; self.name = html.data(name); self.meal = html.data(initialMeal); self.formattedPrice = html.data(function() { var price = self.meal().price; return price ? "$" + price.toFixed(2) : "None"; }); } function ReservationsViewModel() { var self = this; // Non-editable catalog data - would come from the server self.availableMeals = [ { mealName: "Standard (sandwich)", price: 0 }, { mealName: "Premium (lobster)", price: 34.95 }, { mealName: "Ultimate (whole zebra)", price: 290 } ]; // Editable data self.seats = html.data([ new SeatReservation("Steve", self.availableMeals[0]), new SeatReservation("Bert", self.availableMeals[1]), new SeatReservation("Beckham", self.availableMeals[2]), new SeatReservation("Tom Cruise", self.availableMeals[2]), new SeatReservation("Peter Parker", self.availableMeals[1]) ]); // Operations self.addSeat = function() { self.seats.add(new SeatReservation("", self.availableMeals[0])); } self.removeSeat = function(e, seat) { self.seats.remove(seat) } } var vm = new ReservationsViewModel(); html('#test-filter #searchStr').searchbox(vm.seats); html('#test-filter button').click(vm.addSeat); html('#test-filter table tbody').each(vm.seats, function(seat, index) { html.tr() .td().input(seat.name).clss('form-control input-w160').$('tr') .td() .dropdown(vm.availableMeals, seat.meal, 'mealName').clss('form-control input-w210 margin5').$('tr') .td().span(seat.formattedPrice).$('tr') .td().button('Remove').clss('btnViolet').click(vm.removeSeat, seat).$('tr'); });
Try it Yourself »

Reorder

<div id="test-Reorder"> <button class="btnNormal">Add seat</button> <input class="form-control input-w160 searchbox" placeholder="Searching..." /> <br /> <table> <thead> <tr> </tr> </thead> <tbody> </tbody> </table> </div>
// Class to represent a row in the seat reservations grid function SeatReservation(name, initialMeal) { var self = this; self.name = html.data(name); self.meal = html.data(initialMeal); self.formattedPrice = html.data(function() { var price = self.meal().price; return price ? "$" + price.toFixed(2) : "None"; }); } function ReservationsViewModel() { var self = this; // Non-editable catalog data - would come from the server self.availableMeals = [ { mealName: "Standard (sandwich)", price: 0 }, { mealName: "Premium (lobster)", price: 34.95 }, { mealName: "Ultimate (whole zebra)", price: 290 } ]; self.headers = [ { display: "Passenger", sortField: 'name' }, { display: "Meal", sortField: 'meal' }, { display: "Price", sortField: 'price' }, ]; // Editable data self.seats = html.data([ new SeatReservation("Steve", self.availableMeals[0]), new SeatReservation("Bert", self.availableMeals[1]), new SeatReservation("Beckham", self.availableMeals[2]), new SeatReservation("Tom Cruise", self.availableMeals[2]), new SeatReservation("Peter Parker", self.availableMeals[1]) ]); // Operations self.addSeat = function() { self.seats.add(new SeatReservation("", self.availableMeals[0])); } self.removeSeat = function(e, seat) { self.seats.remove(seat) } self.order = function(e, sortField) { //override the method to if(sortField === 'meal') { sortField = function(seat) { return seat.meal().mealName; }; sortField.isAsc = true; } if(sortField === 'price') { sortField = function(seat) { return seat.meal().price; }; sortField.isAsc = false; } self.seats.orderBy(sortField); }; self.up = function(e, seat) { var currentArray = self.seats.getFilterResult() || self.seats(); var index = currentArray.indexOf(seat); var moveTo = index - 1; if(index === 0) { moveTo = currentArray.length - 1; } self.seats.move(index, moveTo); } self.down = function(e, seat) { var currentArray = self.seats.getFilterResult() || self.seats(); var index = currentArray.indexOf(seat); var moveTo = index + 1; if(index === currentArray.length - 1) { moveTo = 0; } self.seats.move(index, moveTo); } } var vm = new ReservationsViewModel(); html('#test-Reorder button').click(vm.addSeat); html('#test-Reorder .searchbox').searchbox(vm.seats); html('#test-Reorder table thead tr').each(vm.headers, function(header, index) { html.th(header.display + ' ').click(vm.order, header.sortField).append(function() { if(header.sortField === 'price') { html.i().clss('fa fa-lg fa-sort-amount-desc').$(); } else { html.i().clss('fa fa-lg fa-sort-alpha-asc').$(); } }).$(); }); html('#test-Reorder table tbody').each(vm.seats, function(seat, index){ html.tr() .td().input(seat.name).clss('form-control input-w160').$('tr') .td() .dropdown(vm.availableMeals, seat.meal, 'mealName').clss('form-control input-w210 margin5').$('tr') .td().span(seat.formattedPrice).$('tr') .td() .button('Remove').clss('btnViolet').click(vm.removeSeat, seat).$('td').space(3) .i().clss('fa fa-lg fa-toggle-up').click(vm.up, seat).$('td').space(3) .i().clss('fa fa-lg fa-toggle-down').click(vm.down, seat) .$('tr') }); vm.seats.splice(2, 1, [ new SeatReservation("Nhan FU", vm.availableMeals[2]), new SeatReservation("Duy kute", vm.availableMeals[2]) ]);
Try it Yourself »

jQuery Integration

Integrate easily with any jQuery controls.
<input id="datepicker" class="form-control input-w160" placeholder="Select date" /> <br /> <span id="dateStr"></span>
/* CUSTOM CONTROL */ html.datepicker = function(observedDate) { //get the current element of HtmlJs. var currentElem = this.element(); //bind that element to bootstrap datepicker var datepicker = $(currentElem).datepicker({format:'dd/mm/yyyy'}) //register change event to update observedDate datepicker.on('changeDate', function(e){ observedDate($(currentElem).data('datepicker').date); }); //subscribe to observedDate //so that we can change the datepicker control by setting observerdDate's value observedDate.subscribe(function(val) { var widget = $(currentElem).data("datepicker"); if (widget) { widget.date = val; if (widget.date) { widget.setValue(widget.date); } } }); //return this (html object) for fluent API return this; }; /* END CUSTOM CONTROL */ var date = html.data(new Date()); html('#datepicker').datepicker(date); html('#dateStr').text(date); date(new Date(2014, 0, 1));
Try it Yourself »
Explanation Firstly, you need to create a new control - html.datepicker
Inside that function, you also need register an event to update observer's value.
In this case, that event is 'changeDate'.
NOTE: you need to return this at the end to facilitate fluent API.
html.datepicker = function(observedDate) {
    //get the current element of HtmlJs.
    var currentElem = this.element();  
    
    //bind that element to bootstrap datepicker
    var datepicker = $(currentElem).datepicker({format:'dd/mm/yyyy'})
    
    //register change event to update observedDate
    datepicker.on('changeDate', function(e){  
        observedDate($(currentElem).data('datepicker').date);
    });
    
    //return this (html object) for fluent API
    return this;
};
Secondly, you need to subscribe a function how to update your custom control.
    //register change event to update observedDate
    //datepicker.on('changeDate', function(e){  
    //    observedDate($(currentElem).data('datepicker').date);
    //});
    //...previous code
    
    //subscribe to observedDate
    //so that we can change the datepicker control by setting observerdDate's value
    observedDate.subscribe(function(val) {
        var widget = $(currentElem).data("datepicker");
        if (widget) {
            widget.date = val;
            if (widget.date) {
                widget.setValue(widget.date);
            }
        }
    });
After all declaration, you can use your custom control the same way as built-in controls.
Actually, all of built-in controls have the same structure like that.
Remember 4 steps:
First extend html object
Second get the DOM element and bind event to that element. This step will make change to observer
Third update DOM element whenever observer's value changed
Last (Optional) return this for fluent API

Utils

Try it Yourself »
I've provided you a lot of util functions in html-enigne, mainly to manipulate array with fluent API.
They are: select, where, reduce, reduceRight, find, first, firstOrDefault, indexOf, add, addRange, any, remove, removeAt, swap, orderBy . You can extend more functions by adding to html.array
These functions are really useful when working with array with clear expression. E.g you want to select student name from student list with grade is greater than 5. Then the expression would be:
var studentList = html.array([{name: 'Bob', grade: 2}, {name: 'Peter', grade : 5}, {name: 'Angelina', grade: 8}]);
studentList
    .where(function(s){ return s.grade > 5; })
    .select(function(s){ return s.name; });
                                    
It is a very clear expression, right?
If you want to get average grade of those student. You can use reduce method.
studentList.reduce(function(total, student) {
    return total + student.grade;
}, 0)/studentList.length;
                                    
If you want to find a student with highest grade. You can use find.
studentList.find(function(std1, std2) {
    return std1.grade > std2.grade ? std1: std2;
});
                                    
If you want to find first student that has his name begin with 'A' character, you can use first.
NOTE: this method throw exception when it can't find element that matches the condition. So please use firstOrDefault; this method won't throw exception but null value in case no item found.
studentList.first(function(std) {
    return std.name.charAt(0) === 'A';
});
                                    
studentList.firstOrDefault(function(std) {
    return std.name.charAt(0) === 'A';
});
                                    
If you want to confirm there is any student that has grade greater 7 or 9, you can use any method.
studentList.any(function(std) {
    return std.grade > 7;
});
//true
studentList.any(function(std) {
    return std.grade > 9;
});
//false
                                    
If you want to remove swap 2 elements in the list you can use swap method.
NOTE: this method uses 0 based index.
studentList.swap(0, 2);
If you want to order by name, just pass string "name" into orderBy method, or you can pass a function return property name.
studentList.orderBy('name');
studentList.orderBy(function(std){ return std.name; });
You can combine two way, passing a property string and a function like following code:
studentList.orderBy('grade', function(std){ return std.name; });
If you want to order grade descendant, you must pass an object with the structure {field: 'some field', isAsc: false}
studentList.orderBy({field: 'grade', isAsc: false}, function(std){ return std.name; });

Dynamic script loading

You can use html-engine to load script and css file dynamically. Usage is similar to ASP.NET MVC bundle (absolutely no minification feature, you can use minification during build process).
//declare this config to not load a script again when it has been loaded.
html.config.allowDuplicate = false;
//define styles and scripts path
html.scripts({
    apiTests: 'api-tests.js',
    htmlArrayTests: 'html.array-tests.js',
    integrationTests: 'integration-tests.js'
});
html.scripts({
    testlib: 'qunit-1.14.0.js',
    'all-tests': ['api-tests.js', 'extension-tests.js']
});
html.styles({
    'qunit': 'qunit-1.14.0.css'
});
//render scripts
html.scripts.render('testlib').done(function(){
    //Do something when test-library has been loaded
}).then('all-tests').then('htmlArrayTests').then('integrationTests');
//render css
html.styles.render('qunit');
HtmlJs can load module dynamically and here it comes with ability to export and import modules at run time. So that you can choose to bundle/minify JavaScript at build time without expose your object to global.
For example, you want to expose myObj for another module. In myModule.js you need to export it.
var myObj = {someModule: 'Just a plain string for demo'};
// export module with its keyword
html.module('myModule', myObj);
						
And then in anotherModule.js file, you can get that obj like following.
html.scripts.render('myModule.js').done(function (myObj) {
	console.log(myObj, 'Tah dah, got it');
}, ['myModule']); // declare keywords for modules as second parameter in done function
						
Or if you've already bundled/minified js files at build time. Just simply use import function.
var myModule = html.module('myModule');
						

Routing

//Register route
html.router('#user/:id/details', function(id){
    document.getElementById('content1').style.display = '';
    document.getElementById('content2').style.display = 'none';
    document.getElementById('content3').style.display = 'none';
});
html.router('#user/:id/edit', function(id){
    document.getElementById('content1').style.display = 'none';
    document.getElementById('content2').style.display = '';
    document.getElementById('content3').style.display = 'none';
});
html.router('#user/:id/delete', function(id){
    document.getElementById('content1').style.display = 'none';
    document.getElementById('content2').style.display = 'none';
    document.getElementById('content3').style.display = '';
});
                                    
Ignore a pattern.
html.ignoreRoute('#:section');
Navigate to a specific route that its pattern has been registered.
html.navigate('#user/100/edit');
Routing in html-engine uses history API of HTML5 fallback to hashtag HTML4. You can configure to enable or disable the history. Below is an example for history API.

Ajax

Html Ajax implements using Promise pattern. You can set done and fail functions to the promise. Refer to Ajax API for more details.
var ajax = html.ajax('someURL', {someParam: 'myData'}, 'GET', true);
                                    
Then you can use like this.
ajax.done(function (data) {
	console.log(data);
}).fail(function (failReason) {
	console.log(failReason);
});
Usually, you'll use html.getJSON or html.postJSON
They're shorthand for html.ajax. In fact they use html.ajax with default parameter (POST/GET)
var ajax = html.getJSON('myURL', {myParam: 'myData'});
	var ajax = html.getJSON('myURL', {myParam: 'myData'});
Another version for ajax is partial view. This method will load a view and append to a container.
html('#container').partial('childView.html').done(function () {
    console.log('Partial loaded');
};