UNPKG

backdulake

Version:

A convenient ALT super-store for React.js including validation facilities

305 lines (240 loc) 9.04 kB
import _ from 'underscore'; import amanda from 'amanda'; var Immutable = require('immutable'); class Backdulake { /* * The constructor of your store definition receives the alt instance as its first and only argument. All instance variables, * values assigned to `this`, in any part of the StoreModel will become part of state. */ constructor() { // Instance variables defined anywhere in the store will become the state. this.handleReset(); this.__jsonSchemaValidator = amanda('json'); //JSON schema intended to be used with Amanda //TO BE overriden this.modelSchema={ type: "object", properties: { "id": { required: false, type: 'number' } } }; //Ala backbone id attribute this.idAttribute="id"; //Aliases of model atrributes for nicer error messages //TO BE overriden this.aliases={}; //Sample: // this.aliases={ // pc: "Postal Code", // vat: "VAT Registration Number", // }; //Extender classes will have to use this.exportPublicMethods() function to add extra methods while keeping these ones this.publicMethods={ find: this.find.bind(this), validate: this.validate.bind(this) }; // (lifecycleMethod: string, handler: function): undefined // on: This method can be used to listen to Lifecycle events. Normally they would set up in the constructor } getImmutableState() { //return new Immutable.Record(this); let ObjectRecord = Immutable.Record(this); return new ObjectRecord(); } find(id){ var collection = this.collection || this.getState().collection; return _.find(collection, function(item){ return item[this.idAttribute] == id; },this); } //Declared static to be Public method on alt validate(model, jsonSchema){ if(!model){ console.console.warn("Backdulake.validate() needs a model argument, it doesn't take this model by default"); return; } jsonSchema = jsonSchema || this.modelSchema; // Validate the attributes against the schema. var that=this; this.__jsonSchemaValidator.validate(model, jsonSchema, this.__validationOptions(this.aliases), function(error) { that.errorMessage=error; }); //Returns the error object if any if(that.errorMessage) return that.errorMessage; } __validationOptions(aliases, getMessages) { //Keep the aliases on the closure var vowels = "aeiou"; var fieldAlias= function(s){ if(Array.isArray(s)) return _.map(s,function(e){ return aliases && aliases[e] || e; }).join(' '); return aliases && aliases[s] || s; } var defaultMessages={ required: function(property, value, attributeValue) { return '‘'+ fieldAlias(property) + '’ is required.'; }, number: function(property, value, attributeValue) { return '‘'+ fieldAlias(property) + '’ has to be valid number.'; }, minLength: function(property, propertyValue, attributeValue) { return '‘'+ fieldAlias(property) + '’ must be at least ' + attributeValue + ' characters long.'; }, maxLength: function(property, propertyValue, attributeValue) { return '‘'+ fieldAlias(property) + '’ must be no longer than ' + attributeValue + ' characters.'; }, length: function(property, propertyValue, attributeValue) { return '‘'+ fieldAlias(property) + '’ must be exactly ' + attributeValue + ' characters long.'; }, pattern: function(property, propertyValue, attributeValue) { return 'Well, ‘' + fieldAlias(property) + '’ doesn\'t look like to have the right format. '; }, format: function(property, propertyValue, attributeValue) { return 'Well, ‘' + fieldAlias(property) + '’ doesn\'t look like to have the right format. '; }, type: function(property, propertyValue, attributeValue) { return '‘' + fieldAlias(property) + '’ must be ' + (vowels.indexOf(attributeValue[0]) > -1 ? 'an' : 'a') + ' valid ‘' + attributeValue + '’.'; } }; return { singleError: false, //Nicer error messages messages: getMessages && getMessages(aliases) || defaultMessages }; } //To reset the all store data but aliases & modelSchema handleReset(){ //Represents the collection of all the models belonging to the parentId this.collection = []; //TO BE overriden //Intended to be a house of objects with sane default values rather //than spreading them into components default properties this.defaultModel={}; //Represents the parent of the collection this.parentId = null; //Stores the id of the selected model while we wait for asynch responses from the server this[this.idAttribute]=null; //Backdulake.model //--------- //Represents the current Model, if its new the id will be null. this.model = null; //True if we are waiting for an asynch response from the server this.loading = false; this.errorMessage = null; } //up to the client to call validate before saving or not! //Move into the action? handleValidateAndSave(model){ this.loading=true; if(model){ console.log("Validating external model at " + this.displayName + ": " + model ); } this.errorMessage=this.__validate(model, this.modelSchema, this.aliases); model=model || this.model; } //Take default values for the new model //Intended to be a house of objects with sane default values rather //than spreading them into components default properties handleCreate(){ this.loading=false; this.model=this.defaultModel; this[this.idAttribute]=null; this.emitChange(); } //Don't update the collection till we get the server ACK //Intended to be used for creation & update handleSave(model) { this.loading=true; this.model=model; if(model[this.idAttribute]) this[this.idAttribute]=model[this.idAttribute] } handleSaved(response) { this.loading=false; //Unwrap data just in case response=response.data || response; //Check for typical setup errors if( _.isEmpty(response) ){ console.error("Backdulake.handleSaved: the response is empty!"); return; } if( ! response[this.idAttribute] ){ console.error("Backdulake.handleSaved: No id present on the response, changes NOT persisted on the store"); return; } if( _.isEmpty(this.model) ){ console.warn("Backdulake.handleSaved: The model prior to the server response is empty. You might forgot to bind handleSave to the correspondant Action. Will continue assumming the Server response will provide it!"); this.model=response; } else { _.extend(this.model,response); } if (this[this.idAttribute] && (this[this.idAttribute] != response[this.idAttribute]) ){ console.warn("Backdulake.handleSaved: The id from the response + (" + response[this.idAttribute] + ") doesn't match the current model one (" + this.model[this.idAttribute] + "). You might forgot to bind Backdulake.handleSave"); } //Find it if it exists let index=_.findIndex(this.collection, function(m){ return m[this.idAttribute] == response[this.idAttribute]; },this); //Update the existing model if(index > -1){ this.collection[index]=this.model; //Add the new model and the id } else{ // this.model[this.idAttribute]=response[this.idAttribute]; this[this.idAttribute]=response[this.idAttribute]; this.collection.push(this.model); } this.emitChange(); } //Set the id. If the collection hasn't been populated from the server //the model will be selected based on this id as soon as it gets populated handleEdit(id){ this.loading=true; this[this.idAttribute]=id; this.model=this.find(id); } //Set the parent's modelId handleFetch(parentId) { this.loading=true; this.parentId=parentId; } handleFetched(response) { this.loading=false; //A collection fetched if(Array.isArray(response)){ this.collection=response; //Set the model as soon as the collection gets populated if(this[this.idAttribute]){ this.model=this.find(this[this.idAttribute]); } //A single element fetched } else if (typeof response == "object"){ this.model=response; } this.emitChange(); } handleRemove(model) { this.loading=true; } handleRemoved(model) { this.loading=false; this.collection=_.reject(this.collection, function(m){ return m[this.idAttribute] == model[this.idAttribute]; },this); if(this.model && (model[this.idAttribute] == this.model[this.idAttribute])){ this[this.idAttribute]=null; this.model=null; } this.emitChange(); } handleFailed(data) { this.loading=false; } } // Export our newly created Store module.exports= Backdulake;