angoose
Version:
Angoose is a Remote Method Invocation module that comes with built-in mongoose/angular support. Now you can call server side module in browser just like you're in the server side!
306 lines (259 loc) • 13.3 kB
JavaScript
(function(){
angular.module('angoose.ui.directives').directive('deformField', angField).directive('angField', angField);
function angField($compile, $templateCache, $interpolate, $injector, $controller, $ui, angoose) {
// Find the "input" element in the template. It will be one of input, select or textarea.
// We need to ensure it is wrapped in jqLite\jQuery
function findInputElement(templateElement) {
return angular.element(templateElement.find('input')[0] || templateElement.find('select')[0] || templateElement.find('textarea')[0]);
}
function findLabelElement(templateElement) {
return templateElement.find('label');
}
// Search through the originalDirective's element for elements that contain information about how to map
// validation keys to messages
function getValidationMessageMap(originalElement) {
// Find all the <validator> child elements and extract their (key, message) info
var validationMessages = {};
angular.forEach(originalElement.find('validator'), function(element) {
// Wrap the element in jqLite/jQuery
element = angular.element(element);
// Store the message info to be provided to the scope later
// The content of the validation element may include interpolation {{}}
// so we will actually store a function created by the $interpolate service
// To get the interpolated message we will call this function with the scope. e.g.
// var messageString = getMessage(scope);
validationMessages[element.attr('key')] = $interpolate(element.text());
});
return validationMessages;
}
// Find the content that will go into the new label
// Label is provided as a <label> child element of the original element
function getLabelContent(element) {
var label = element.find('label');
return label[0] && label.html();
}
function getTemplateContent(element){
var em = element.find('template');
if(!em) return null;
if(!em.attr('name') && ! em.attr("url"))
return em[0] && em.html();
return em.attr('name') || em.attr('url');
}
return {
restrict:'E',
scope:{
instance:'=',
path:'=',
modelSchema:'=',
fieldSchema:'=',
readonly:'@',
itemIndex:'=' // for array index
},
//require:'?ngModel',
priority: 100, // We need this directive to happen before ng-model
terminal: true, // We are going to deal with this element
compile: function(element, attrs) {
if ( attrs.ngRepeat || attrs.ngSwitch || attrs.uiIf ) {
throw new Error('The ng-repeat, ng-switch and ui-if directives are not supported on the same element as the field directive.');
}
if ( !attrs.ngModel ) {
//throw new Error('The ng-model directive must appear on the field element');
}
// Extract the label and validation message info from the directive's original element
var validationMessages = getValidationMessageMap(element);
var labelContent = getLabelContent(element);
// Clear the directive's original element now that we have extracted what we need from it
element.html('');
return function postLink(scope, element, attrs, ngModelController) {
window.pscope = scope;
if(scope.path && scope.path.indexOf("_") == 0) return;
enterscope(scope, "deform-field "+ scope.path)
var customTemplate = attrs.template;
var customController = attrs.controller;
var customDirective = attrs.directive;
var modelClass = scope.instance && scope.instance.constructor;
//var schema = getSchema( scope.instance).paths[scope.path] ;
var modelSchema = scope.modelSchema || (modelClass && modelClass.schema);
//if(!modelSchema) console.error("Model schema lost in scope ", scope.$id, "parent is ", scope.$parent.$id)
var schema = scope.fieldSchema || ( modelClass && modelClass.schema.paths[scope.path]);
if(!schema) {
return console.error("Missing schema for path", scope.path);
}
schema.options = schema.options || {};
var directive = customDirective || mapDirective(scope.path, schema, modelSchema, scope.itemIndex)
var template = customTemplate || mapTemplate(scope.path, schema, modelSchema);
angoose.logger.trace("Field ", scope.path, "template", template );
var labelContent = attrs.label == undefined? ( schema.options.label ||scope.path) : attrs.label;
var childScope = scope.$new();
// Generate an id for the field from the ng-model expression and the current scope
// We replace dots with underscores to work with browsers and ngModel lookup on the FormController
// We couldn't do this in the compile function as we need to be able to calculate the unique id from the scope
childScope.$fieldId = (scope.path+ (scope.itemIndex || "") ).replace('.', '_').toLowerCase() + '_' + childScope.$id;
function handleDirective(directive, childScope){
var html = '<directive-name path="path" model-schema="model-schema" field-schema="fieldSchema" instance="instance"></directive-name>';
html = html.replace('directive-name', directive);
var directiveElement = angular.element(html);
var labelElement = directiveElement.find('label');
labelElement.attr('for', childScope.$fieldId);
// Update the label's contents
labelElement.html(labelContent );
element.append(directiveElement);
$compile(directiveElement)(childScope);
}
angoose.logger.trace("Created child scope for deform-field ", scope.path, childScope.$id, "using directive: ",directive)
if(directive){
handleDirective( directive, childScope);
}
else
{
// Load up the template for this kind of field, default to the simple input if none given
$ui.loadFieldTemplate(template ).then(function(templateElement) {
// Set up the scope - the template will have its own scope, which is a child of the directive's scope
// Attach a copy of the message map to the scope
childScope.$validationMessages = angular.copy(validationMessages);
childScope.$fieldLabel = labelContent;
// Update the $fieldErrors array when the validity of the field changes
childScope.$watch('$field.$dirty && $field.$error', function(errorList) {
//angoose.logger.debug("Got error", errorList);
childScope.$fieldErrors = [];
angular.forEach(errorList, function(invalid, key) {
if ( invalid ) {
childScope.$fieldErrors.push(key);
}
});
}, true);
// Copy over all left over attributes to the input element
// We can't use interpolation in the template for directives such as ng-model
var inputElement = findInputElement(templateElement);
angular.forEach(attrs.$attr, function (original, normalized) {
var value = element.attr(original);
inputElement.attr(original, value);
});
var ngModelVal = "instance."+ scope.path + (scope.itemIndex == undefined? "": "["+ scope.itemIndex+ "]" );
inputElement.attr("ng-model", ngModelVal);
//console.trace("NGMODEL #### ", inputElement.attr('ng-model'))
// Wire up the input (id and name) and its label (for).
// We need to set the input element's name here before we compile the template.
// If we leave it to be interpolated at the next $digest the formController doesn't pick it up
inputElement.attr('name', childScope.$fieldId);
inputElement.attr('id', childScope.$fieldId);
if(scope.readonly || schema.options.readonly === true) inputElement.attr('readonly', "true");
schema.options.required && inputElement.attr('required', 'true');
var labelElement = templateElement.find('label');
labelElement.attr('for', childScope.$fieldId);
// Update the label's contents
labelElement.html(labelContent);
// Place our template as a child of the original element.
// This needs to be done before compilation to ensure that it picks up any containing form.
element.append(templateElement);
// filed controller
//var fieldController = customController || "dfController"+ camelcase(template);
var fieldController = customController || "dfc-"+template;
try{
$controller(fieldController, {$scope: childScope, $schema: schema, inputElement: inputElement, templateElement: templateElement})
angoose.logger.trace("Invoked custom controller", fieldController);
}
catch(err){
if((err+"").indexOf("is not a function")<0)
angoose.logger.error("fieldControoler error", err)
}
// We now compile and link our template here in the postLink function
// This allows the ng-model directive on our template's <input> element to access the ngFormController
$compile(templateElement)(childScope);
// Now that our template has been compiled and linked we can access the <input> element's ngModelController
childScope.$field = inputElement.controller('ngModel');
});
} // end loadTemplate
}; // end postLink
}
};
}
// ** mapDirective **
//
// Default mapping based on schema definition:
// - list of sub schemas: deform-sublist
// - single subschema object: deform-subschema
function mapDirective(path, pathSchema, modelSchema, itemIndex){
if( itemIndex !== undefined) return null; // we're in a array
if(pathSchema && pathSchema.options && Array.isArray(pathSchema.options.type)){
if( pathSchema.schema)
return "deform-sublist"
if(pathSchema.caster && (! pathSchema.caster.options || !pathSchema.caster.options.ref)){ // !pathSchema.options.ref filters out CustomRef, ugly!
/** array of simple types */
//console.log("Simple array type!!!",pathSchema, pathSchema.options.ref, pathSchema.options.type);
return "ang-array"
}
}
else if( pathSchema.schema)
return "deform-subschema"
return null;
}
// ** mapTemplate **
//
// Default mapping for Mongoose schema type -> form field
//
// - ObjectID: selector
// - Boolean: Checkbox
// - String: input
// - Number: input
// - Date: ?
// - Array of simple types: selector multi
// - Array of ref objects: selector multi
// - String/Number with enum values: Select
// - Mixed: ??
//
function mapTemplate(path, pathSchema, modelSchema){
if(pathSchema.options.template) return pathSchema.options.template;
if(pathSchema.options.multiline) return "textarea"
var template = 'input';
var opts = pathSchema.options || {};
//@todo: refactor them into subclasses
switch(pathSchema.instance || (pathSchema.options && pathSchema.options.type)){
case 'ObjetcID':
if(opts.ref) template = 'selector';
break;
case 'Boolean':
template = 'checkbox';
break;
case 'Date':
case 'String':
case 'Number':
default:
break;
}
if(getRef(pathSchema))
template = "selector";
if(Array.isArray(opts['enum']) && opts['enum'].length>0){
template = "select";
}
if(Array.isArray(opts.type )){
if(pathSchema.caster){
/** array of simple types */
// console.log("Simple types", pathSchema)
}
}
angoose.logger.trace("Path ", path, " Type ", pathSchema , " template: ", template)
return template;
}
function getRef(pathSchema){
var opts = pathSchema.options;
/** Mongoose, array of ObjectIDs */
if( Array.isArray(opts.type) && opts.type.length>0 && opts.type[0] && opts.type[0].ref )
return opts.type[0].ref;
/** Single ObjectID Reference*/
if(pathSchema.instance == 'ObjectID' && opts.ref) return opts.ref;
if(pathSchema.options.ref && pathSchema.instance == 'CustomRef'){
/** deform custom ref*/
return pathSchema.options.ref;
}
/** deform rich reference, array */
if(Array.isArray(opts.type ) && pathSchema.caster &&
pathSchema.caster.instance == 'CustomRef' && pathSchema.caster.options.ref ){
return pathSchema.caster.options.ref;
}
return null;
}
function camelcase(str){
return str? str.substring(0,1).toUpperCase() + str.substring(1): str;
}
})(); // end enclosure