Skip to content

Instantly share code, notes, and snippets.

@benjamingr
Created July 10, 2017 19:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save benjamingr/ef6533784aa0c1d5ed0d98973da3c381 to your computer and use it in GitHub Desktop.
Save benjamingr/ef6533784aa0c1d5ed0d98973da3c381 to your computer and use it in GitHub Desktop.
/*
* Mustache like logic less templating with reverse data binding
*/
//HACK, do not use, EVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVER
//EVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVER
//EVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVEREVER
// in production code
//templates a given view based on the given viewmodel, for more detail on how this works please check the unit tests
window.Mustache = {};
var superSecretIndex = 0;
window.Mustache.render = function template(view, viewmodel) {
$("*").off("change", "[id*=bindAT" + superSecretIndex + "]");
$("*").off("keyup", "[id*=bindAT" + superSecretIndex + "]");
var deps = [];
superSecretIndex++;
function ec(str) {
return (str + "").replace(/[&<>"'\/]/g, function (backRef) {
return escapedCharacters[backRef];
});
};
var escapedCharacters = {
'<': '&gt;',
'>': '&lt;',
'&': '&amp;',
'"': "&quot",
'\'': "&apos",
"/": '&#x2F;'
};
if (typeof view !== "string") {
throw "Only strings supported for templating atm";
}
//regular expressions that match {{}}, {{#}} and {{{}}}
var tag = /\{\{.+?\}\}/g;
var funcTag = /\{\{&.+?\}\}/g;
var selectTag = /\{\{#(.+?)\}\}((.|\r|\n)*?)\{\{\/\1\}\}/g;//capturing groups backref ftw
var unescapedTag = /\{\{\{.+?\}\}\}/g;
var editorFor = /\{\{%.+?\}\}/g;
//checks the truthy value of an expression for templating, does not do nesting really
var ifTag = /\{\{if\((.+?)\)\}\}.*?\{\{endif\}\}/g;
//do all templating sessions, first if tags, then functions, then unescaped tags, then normal tags
view = view.replace(selectTag, function (match, offset, str) {
var nestedAttempt = match.substring(3, match.indexOf("}}"));
nestedAttempt = nestedAttempt.split(".");
var current = walkTheDotNotation(viewmodel, nestedAttempt);
if ((current === "" || !current)) {
return "";//means current is falsy and I should not render
}
var inner = match.substring(match.indexOf("}}") + 2, match.lastIndexOf("{{"));
if (current instanceof Array) {
var sb = [];
for (var elem in current) {
sb.push(template(inner, current[elem]));
}
return sb.join("");
}
return inner;
}).replace(ifTag, function (match, cond, idx) {
var nestedAttempt = cond.split(".");
var current = walkTheDotNotation(viewmodel, nestedAttempt);
if (current) {//truthy
return match;
} else {
return "";
}
}).replace(funcTag, function (match) {
var nestedAttempt = match.substring(7, match.length - 2).split(".");
var current = walkTheDotNotation(viewmodel, nestedAttempt);
return (typeof current === "function") ? current() : ((current) || "");
}).replace(editorFor, function (match) {
var nestedAttempt = match.substring(3, match.length - 2).split(".");
var current = walkTheDotNotation(viewmodel, nestedAttempt);
nestedAttempt = "SEPERATOR" + superSecretIndex + "SEPERATOR" + nestedAttempt.join("SEPERATOR");
deps.push("bind" + nestedAttempt);
if (typeof current === "function") {
var res = current();
current = res;
}
if (typeof current === "object" && current instanceof Array) {
//drop down
var ret = "<select>";
for (var i in current) {
ret += "<option value='" + current[i] + "'>" + current[i] + "</option>";
}
return ret + "</select>";
}
if (typeof current === "object" && current instanceof Date) {
//date picker
return "<input type='text' id='bind" + nestedAttempt + "' value='" + current + "' />";
}
if (typeof current === "boolean") {
return "<input type='checkbox' id='bind" + nestedAttempt + "' " + (current ? "checked" : "") + "'/>";
}
if (nestedAttempt.toLowerCase().indexOf("password") !== -1) {
return "<input type='password' id='bind" + nestedAttempt + "' value='" + current + "' />";
}
return "<input type='text' id='bind" + nestedAttempt + "' value='" + current + "' />";
}).replace(unescapedTag, function (match) {
var nestedAttempt = match.substring(3, match.length - 3).split(".");
var current = walkTheDotNotation(viewmodel, nestedAttempt);
return (typeof current === "function") ? "" : (current || "");
}).replace(tag, function (match) {
var nestedAttempt = match.substring(2, match.length - 2).split(".");
var current = walkTheDotNotation(viewmodel, nestedAttempt);
return (typeof current === "function") ? "" : (ec(current) || "");
});
//return the view after templating the viewmodel in
for (var sel in deps) {
function dep() {
var idSep = $(this).attr("id").split("SEPERATOR");
var idLast = idSep[idSep.length - 1];
viewmodel[idLast] = $(this).val();
$(viewmodel).trigger("change");
}
$("body").on("change", "#" + deps[sel], dep).on("keyup", "#" + deps[sel], dep);
}
return view;
//helper function to walk the dot notation
function walkTheDotNotation(current, nestedAttempt) {
if (nestedAttempt.length === 2 && nestedAttempt[0] === "" && "" === nestedAttempt[1]) {
return current;
}
for (var i = 0; i < nestedAttempt.length; i++) {
if (!current.hasOwnProperty(nestedAttempt[i])) {
return "";
}
current = (current[nestedAttempt[i]]);
}
return current;
}
};
@benjamingr
Copy link
Author

benjamingr commented Jul 10, 2017

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <script src="jquery.js"></script>
    <script src="Template.js"></script>
    <script src="util.js"></script>
    <script>
        $(function () {
            var inbox = [];
            var sentItems = [];
            var inboxBind = m.bind("/emails/", inbox, "#inbox");
            var sentBind = m.bind("/sent/", sentItems, "#sent");
            $("#inboxbutton").click(function () {
                $("#inbox").show();
                $("#sent").hide();
            });
            $("#sentbutton").click(function () {
                $("#sent").show();
                $("#inbox").hide();
            });
            inboxBind.crud.retrieve().done(function (data) {
                console.log(data[0]);
            });
            var handleOpenMessage = function () {
                //this should actually be /emails/?id= but I didn't want to change our api
                //this can be a lot cleaner
                var id = $(this).attr("messageid");
                var fullView = $(this).find(".fullView");
                fullView.show();
                var mBind = m.bind("/emails/" + id, {}, fullView, $("#MessageInboxView").html());
                mBind.el.click(function (e) {
                    e.stopPropagation();
                    $(this).hide();
                });
                mBind.crud.retrieve(id);
            };
            $("#inbox").on("click", ".message", handleOpenMessage);
            $("#sent").on("click", ".message", handleOpenMessage);

            sentBind.crud.retrieve();
        });
    </script>
    <title>Mail</title>
</head>
<body>
    <ul>
        <li><a href="#" id="inboxbutton">Inbox</a></li>
        <li><a href="#" id='sentbutton'>Sent</a></li>
        <li><a href="logout">Logout</a></li>
    </ul>

    <div id="inbox">
        {{#.}}
        <div messageid="{{id}}" class="message">
            {{from}} {{title}} {{date}}<div class="fullView" style='display: none'></div>
        </div>
        {{/.}}
    </div>

    <div id="sent" style="display: none" >
        {{#.}}
        <div  messageid="{{id}}" class="message">{{to}} {{title}} {{date}}<div class="fullView" style='display: none'></div></div>
        {{/.}}
    </div >

    <div id="MessageInboxView" style="display: none">
        From: {{from}}
        <br />
        Title {{title}}
        <br />
        Message: {{message}}
        <br />

    </div>
</body>
</html>

@benjamingr
Copy link
Author

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <script src="jquery.js"></script>
        <script src="Template.js"></script>
        <script src="util.js"></script>
        <script>
            var a;
            $(function () {
                a = {
                    text: "",
                    repeater:[1,2,3,4,5],
                    hasText:function() {
                        return !!(a.text && a.text.length && a.text.length > 0);
                    },
                    textLength:function() {
                        return (a.text && a.text.length) ? a.text.length : 0;
                    }
                };
                
                var aBind = m.bind("/test/", a, "#stuff");
                aBind.watchedBy("#stuff2").watchedBy("#stuff3").crud.retrieve()
                    .done(aBind.keepSyncedToServer.bind(aBind, 2000, 1));

                $("#updateToServer").click(aBind.crud.update);
                $("#updateFromServer").click(aBind.crud.retrieve);
            });
        </script>
        <title>Test </title>
    </head>
<body>
    <div id="stuff">
        Hello {{%text}}
        {{#repeater}}
        Hello!
        {{/repeater}}
    </div>
    <div id="stuff2">
        Hello {{text}}
    </div>
    <div id="stuff3">
        has text : {{&hasText}} <br />
        text length: {{&textLength}}
    </div>
    <button id="updateToServer">Update Server</button>
    <button id="updateFromServer">Update Local</button>

</body>
</html>

@benjamingr
Copy link
Author

//util.js :D
/*
*******************************************************************************
* Author: Benjamin Gruenbaum
* Date: 25/01/2013
* Version: 0.1.0
*******************************************************************************
*/// deps: mustache, jQuery
(function ($, mustache) {
    var mvc,
        viewUpdateQueue = [];
    mvc = {
        viewBind: viewBinder,
        modelBind: modelBinder,
        update: updateViewBindings,
        bind: bindTogether
    };


    // MVC functions
    function bindTogether(url, object, selector,template) {
        var updateFunction;
        var crud = modelBinder(url, object);
        var elem = $(selector);
        if (typeof template === "string") {
            updateFunction = virtualViewBiner(elem, object,template);
        } else {
            updateFunction = viewBinder(elem, object);
        }
        
        $(crud).on("r", function () {
            updateFunction();
        });
        var retObj = {
            crud: crud,
            el: elem,
            watchedBy: function (sel) {
                var viewFun = mvc.viewBind(sel, object);
                $(crud).on("r", function () {
                    console.log("YOYO");
                    viewFun();
                });
                $(object).on("change", function () {
                    viewFun();
                });
                return retObj;
            },
            keepSyncedFromServer: function (frequency, id) {
                //only syncs when idle
                var time;
                $("body").on("mousedown keydown", function () {
                    clearTimeout(time);
                    time = setTimeout(upd, frequency);
                });

                function upd() {
                    crud.retrieve(id).done(function () {
                        time = setTimeout(upd, frequency);
                    });
                }
                upd();
            },
            keepSyncedToServer: function (frequency, id) {
                //there should be some logic here that only pushes when has to (check object changed)
                var time;
                $("body").on("mousedown keydown", function () {
                    clearTimeout(time);
                    time = setTimeout(upd, frequency);
                });

                function upd() {
                    crud.update(id).done(function () {
                        setTimeout(upd, frequency);
                    });
                }
                upd();
            }
        };
        return retObj;
    }

    //binds an object to a url
    function modelBinder(url, object) {
        var crud = {
            model: object,
            create: function () {
                return $.post(url, object).done(function () {
                    $(crud).trigger("c");
                }).fail(function () {
                    $(crud).trigger("createfail");
                });
            },
            retrieve: function (id) {
                return $.getJSON(url, id || object.id || object).done(function (data) {
                    $.extend(object, data);
                    $(crud).trigger("r");
                }).fail(function () {
                    $(crud).trigger("retrievefail");
                });
            },
            update: function () {
                return $.ajax({ url: url, data: JSON.stringify(object), type: "PUT",contentType:"application/json" }).done(function () {
                    $(crud).trigger("u");
                }).fail(function () {
                    $(crud).trigger("updatefail");
                });
            },
            del: function () {
                $(crud).trigger("d");
                return $.ajax({ type: "DELETE", url: url, data: object }).done(function () {
                    object = {};
                }).fail(function () {
                    $(crud).trigger("deletefail");
                });
            }
        };
        return crud;
    }
    //binds a selector to an object
    function viewBinder(domElem, object) {
        var elem = $(domElem);
        var template = $(elem).html();

        var fun = function () {
            elem.html(mustache.render(template, object));
        };
        viewUpdateQueue.push(fun);
        return fun;
    }
    
    function virtualViewBiner(domElem, object,template) {
        var fun = function () {
            
            $(domElem).html(mustache.render(template, object));
            console.log(domElem);
        };
        viewUpdateQueue.push(fun);
        return fun;
    }


    function updateViewBindings() {
        for (var item in viewUpdateQueue) {
            viewUpdateQueue[item]();
        }
    }
    //export result
    if (typeof module === "object") {
        module.exports = mvc;
    } else {
        window.m = mvc;
    }
}($, Mustache));

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment