Skip to content

Instantly share code, notes, and snippets.

@miguelludert
Last active August 29, 2015 13:57
Show Gist options
  • Save miguelludert/9533997 to your computer and use it in GitHub Desktop.
Save miguelludert/9533997 to your computer and use it in GitHub Desktop.
Rockout (for knockout)
var utils = {
isValid: function (toValidate, deep, show) {
if (deep === undefined) {
deep = true;
}
var deepValid = true;
if (deep) {
var validateThis = ko.utils.unwrapObservable(toValidate);
var childrenToTest = _(validateThis).where(function (item) {
return ko.isObservable(item) || _.isObject(item);
});
if (show) {
// loop through each child explictly, allowing each child to display its error messages
childrenToTest.each(function (item) {
var isValidChild = ko.validation.utils.isValid(item, deep, true);
if (!isValidChild) {
deepValid = false;
}
});
} else {
// if show is false, we do not care about displaying error messages, leave after the first invalid item is found
deepValid = childrenToTest.all(function (item) {
return ko.validation.utils.isValid(item, deep, false);
});
}
}
if (show & ko.isObservable(toValidate) && _.isFunction(toValidate.isValid)) {
toValidate.isModified(true);
}
return deepValid && (_.isFunction(toValidate.isValid) ? toValidate.isValid() : true);
},
validate: function (toValidate, deep) {
return ko.validation.utils.isValid(toValidate, deep, true);
}
};
_.extend(ko.validation.utils, utils);
// Copyright Miguel Ludert 2014
// MIT License - http://opensource.org/licenses/MIT
// https://gist.github.com/thinkingsites/9533997
define(["knockout", "lodash"], function (ko, _) {
var isString = _.isString;
function round(newValueAsNum, precision) {
if (isNaN(newValueAsNum) || isNaN(precision)) {
return newValueAsNum;
} else {
var roundingMultiplier = Math.pow(10, precision);
return Math.round(newValueAsNum * roundingMultiplier) / roundingMultiplier;
}
}
var presets = {
'int' : function(val){
return presets['number'](val, { precision: 0 });
},
'number': function (val, options) {
var precision = options && options.precision ? options.precision : undefined;
val = parseFloat(val);
val = _.isNaN(val) ? undefined : val;
val = round(val, precision);
return val;
},
'date' : function (val) {
if (!_.isDate(val)) {
if (isString(val) || _.isNumber(val)) {
// take into account those messed up MS dates
if(isString(val) && val.match(/Date\(-?\d+\)/)){
// strip out the milliseconds and set the date
val = val.match(/-?\d+/);
}
val = new Date(val);
} else {
val = undefined;
}
}
return val;
},
'bool': function (val) {
return (val ? true : false);
}
};
ko.extenders.castAs = function (target, options) {
var callback;
if (_.isFunction(options)) {
// if the options is not a direct fu
callback = options;
} else if (isString(options)) {
// if the options is a string preset, look for it
if (_.has(presets, options)) {
callback = presets[options];
} else {
throw "Cannot find casting preset " + options;
}
} else if(_.isPlainObject(options)) {
// if the options are an object check to see if we're getting a callback or a constructor
if(_.has(options,'callback')){
//strip out the call back and pass the options into the callback on execute
callback = options['callback'];
} else if(_.has(options,'constructor')) {
// if the options have a constructor create an constructor callback
callback = function (val) {
if (_.isUndefined(val)) {
return undefined;
} else {
return new this(val);
}
}.bind(options.constructor); // do a bind so we don't lose the constructor later
}
//omit the callback and constructor properties from the options, we don't want to pass them into the callback once it executes
options = _.omit(options,'callback','constructor');
} else {
// don't know what to do with these options, throw an error
throw "Cannot use options passed into castAs extender: " + options;
}
var result = ko.computed({
read: target, //always return the original observables value
write: function (newValue) {
var current = target(),
valueToWrite = callback(newValue, options);
//only write if it changed
if (valueToWrite !== current) {
target(valueToWrite);
} else {
//if the rounded value is the same, but a different value was written, force a notification for the current field
if (newValue !== current) {
target.notifySubscribers(valueToWrite);
}
}
}
}).extend({ notify: 'always' });
//initialize with current value to make sure it is rounded appropriately
result(target());
//return the new computed observable
return result;
};
});
// Copyright Miguel Ludert 2014
// MIT License - http://opensource.org/licenses/MIT
// https://gist.github.com/thinkingsites/9533997
"use strict";
define(["knockout", "jquery", "lodash"], function (ko, $, _) {
var
defaultOptions = {
jQueryStyles: true,
showOverlay: true,
draggable: false,
resizable : false,
modalId: "rockout-modal-window",
parent: "body",
overlayId: "rockout-modal-overlay",
style: {
minHeight: "150px",
minWidth: "200px"
}
},
jqClasses = {
contentClass: '.ui-dialog-content',
closeClass: '.ui-dialog-titlebar-close',
titleClass : '.ui-dialog-titlebar'
},
rockoutClasses = {
contentClass: '.rockout-ui-modal-content',
closeClass : '.rockout-ui-modal-close',
titleClass: '.rockout-ui-modal-header'
},
currentModal;
function rockoutTempate(title) {
return [
"<div class='rockout-ui-modal-header'>",
"<span class='rockout-ui-modal-header-text'>",
title,
"</span>",
"<button class='rockout-ui-modal-close'>Close</button>",
"</div>",
"<div class='rockout-ui-modal-content'></div>",
].join('');
}
function jqueryTemplate(title) {
return [
// the dialog's absolute position must be overridden to static to work with our positioning
'<div class="ui-dialog ui-widget ui-widget-content ui-corner-all ui-front" tabindex="-1" role="dialog" aria-describedby="dialog" style="position:static">',
'<div class="ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix">',
'<span class="ui-dialog-title">',
title,
'</span>',
'<button type="button" class="ui-button ui-widget ui-state-default ui-corner-all ui-button-icon-only ui-dialog-titlebar-close" role="button" aria-disabled="false" title="close">',
'<span class="ui-button-icon-primary ui-icon ui-icon-closethick"></span>',
'<span class="ui-button-text">close</span>',
'</button>',
'</div>',
'<div class="ui-dialog-content ui-widget-content">',
'</div>',
'</div'
].join('');
}
function ensureElement(id, appendTo) {
var element = $("#" + id);
if (!element.length) {
element = $("<div></div>")
.attr("id", id)
.appendTo(appendTo);
}
return element;
};
function resizeModal() {
// make sure we are using jquery
var $this = $(this);
// setting margins has to happen in a different call to allow the prior css to set properly
$this.css({
top: "50%",
left: "50%",
marginTop: "-" + ($this.outerHeight() / 2).toString() + "px",
marginLeft: "-" + ($this.outerWidth() / 2).toString() + "px",
});
}
function closeModal() {
if (currentModal) {
currentModal.modal.hide();
currentModal.overlay.hide();
currentModal = undefined;
}
};
ko.bindingHandlers.modal = {
init: function (element, valueAccessor, allBindings) {
$(element).on("click", function () {
var bindings = allBindings() || {},
options = _.extend({}, defaultOptions, bindings.modalOptions),
modal = ensureElement(bindings.modalId || "rockout-modal-window", options.parent),
overlay = ensureElement(bindings.modalId || "rockout-modal-overlay", options.parent),
classes = options.jQueryStyles ? jqClasses : rockoutClasses,
templateFunction = options.jQueryStyles ? jqueryTemplate : rockoutTempate,
modalContent;
if (options.showOverlay) {
// set up overlay
overlay.css({
top: 0,
bottom: 0,
left: 0,
right: 0,
position: "fixed",
zIndex: 999999, // six digits
backgroundColor: "rgba(0,0,0,0.6)",
display: "block"
});
}
// set up the modal window, use html to clean the content and reset the modal
modal.html(templateFunction(options.title || "&nbsp;")).css(_.extend({
position: "fixed",
zIndex: 9999999, // seven digits
display: "block",
}, options.style));
// resize the current modal after initial setup
resizeModal.call(modal);
// set the current modal so we can close them easily
currentModal = {
modal: modal,
overlay : overlay
};
var modalBindings = valueAccessor();
if (_.isString(modalBindings)) {
// of the value is a string, convert it to an object with template bindings to access the afterRender event
modalBindings = ({
template: function(){
return this;
}.bind({
name: modalBindings,
afterRender : resizeModal.bind(modal)
})
})
} else {
// if the new bindings contain a template of a module, append the after render
if (_.has(modalBindings, "template") || _.has(modalBindings, "module")) {
var key = _.has(modalBindings, "template") ? "template" : "module";
// if template is a string then turn it into an object
if (_.isString(modalBindings[key])) {
modalBindings[key] = {
name: modalBindings[key],
afterRender : resizeModal.bind(modal)
};
} else {
// if the modal is an object retain the original afterRender and inject your own call
var originalAfterRender = modalBindings[key].afterRender;
modalBindings[key].afterRender = function (elements) {
if(_.isFunction(originalAfterRender)){
originalAfterRender(elements);
}
resizeModal.call(modal);
}
}
}
// to use applyBindingAccessorsToNode, all bindings must be wrapped in a function if not already a function
_.each(modalBindings, function (item, key) {
if(!_.isFunction(item)){
modalBindings[key] = function () {
// there is a strange bug with the bind function that changes the native string into a string object and breaks templates and modules
return _.isString(this) ? this.toString() : this;
}.bind(item);
}
});
}
// if the modal is to be draggable, set it up here
if (options.draggable) {
modal.draggable({
handle: classes.titleClass
});
}
// if the modal is to be resizable, set it up here
if (options.resizable) {
modal.resizable();
}
// grab the modal content so we can bind the content to it
modalContent = $(classes.contentClass, modal);
// toggle display the close modal button and bind the click event
$(classes.closeClass, modal).toggle(_.isUndefined(options.closeButton) ? true : false).on("click", function () {
ko.rockout.closeModal();
});
// don't forget that because we're using jquery for node construction, we have to access the jquery node collection
ko.applyBindingAccessorsToNode(modalContent[0], modalBindings);
});
return { controlsDescendantBindings: true };
},
options : defaultOptions
};
// if the rockout object doesn't exist
ko.rockout = ko.rockout || {};
ko.rockout.closeModal = closeModal;
// all rockout modules should return the rockout object
return ko.rockout;
});
// Copyright Miguel Ludert 2014
// MIT License - http://opensource.org/licenses/MIT
// https://gist.github.com/thinkingsites/9533997
"use sctrict";
define(["knockout", "_"], function (ko, _) {
// namespacing
ko.rockout = ko.rockout || {};
ko.rockout.utils = ko.rockout.utils || {};
function patch(target) {
var
data = _.extend.apply(undefined, [{}].concat(_.rest(_.toArray(arguments)))),
targetKeys = _.keys(target),
dataKeys = _.keys(data);
_(targetKeys).intersection(dataKeys).where(function (item) {
// get the intersection of the observable keys
return ko.isWriteableObservable(self[item]);
}).each(function (item) {
// set the observable
target[item](data[item]);
});
};
// stand alone method for patching
ko.rockout.utils.patch = patch;
// adds a "patch" method to the target object that merges all the data from a source object into the observables where the keys match
ko.rockout.utils.addPatching = function (target, initial) {
var inProgress = ko.observable(false);
target.patch = function (data) {
inProgress(true);
patch(target, data);
inProgress(false);
};
// patch.pause signals to the consuming function that patching is in progress
target.patch.inProgress = inProgress;
// if there are initial values, run the patch now
if (initial) {
target.patch(initial);
}
};
return ko.rockout;
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment