API Docs for: 0.1.6
Show:

File: temp/NgModel.js

  1. /*!
  2. * @license NgModel for AngularJS
  3. * (c) 2015 AyeCue
  4. * License: MIT
  5. * This is inspired by:
  6. * ExtJS modellayer (https://docs.sencha.com/extjs/5.1/5.1.0-apidocs/#!/api/Ext.data.Model),
  7. * ActiveRecord (http://github.com/bfanger/angular-activerecord)
  8. */
  9. s
  10. s
  11. s
  12. s
  13. s
  14. s
  15. s
  16. s
  17. /**
  18. * Batch for NgModel operations
  19. *
  20. * @class Batch
  21. * @constructor
  22. * @param {NgModel} model Model to use in batch.
  23. * @param {String} operation Operation type like 'read' or 'delete'.
  24. * @param {Object} [options] Extra options for batch.
  25. * @private
  26. */
  27. function Batch(model,operation,options){
  28. var me = this;
  29.  
  30. angular.extend(me,{
  31. model: model,
  32. operation: operation,
  33. options: options || {}
  34. });
  35.  
  36. me.initOptions();
  37. };
  38.  
  39. Batch.prototype = {
  40. /**
  41. * Initialize all options for batch.
  42. *
  43. * @method initOptions
  44. * @return {Object}
  45. * @private
  46. */
  47. initOptions: function(){
  48. var me = this,
  49. model = me.model,
  50. options = me.options;
  51.  
  52. options.data = options.data || {};
  53.  
  54. if (model.appendData[me.operation]) {
  55. angular.extend(options.data,model.getRequestData());
  56.  
  57. angular.forEach(model.notPersist,function(toExclude) {
  58. removeNamespace(options.data,toExclude,true);
  59. });
  60. }
  61.  
  62. if (!options.headers) {
  63. options.headers = model.headers;
  64. } else {
  65. angular.extend(options.headers,model.headers);
  66. }
  67.  
  68. if (!options.method) {
  69. options.method = model.actionMethods[me.operation];
  70. }
  71.  
  72. if (!options.url) {
  73. options.url = model.buildUrl(me.operation,options);
  74. }
  75.  
  76. return options;
  77. },
  78.  
  79. /**
  80. * Get if batch is reading.
  81. *
  82. * @method isReading
  83. * @return {Boolean}
  84. * @private
  85. */
  86. isReading: function(){
  87. return this.operation === 'read';
  88. },
  89.  
  90. /**
  91. * Get if batch is writing.
  92. *
  93. * @method isWriting
  94. * @return {Boolean}
  95. * @private
  96. */
  97. isWriting: function(){
  98. return this.operation === 'create' || this.operation === 'update';
  99. },
  100.  
  101. /**
  102. * Get options property of batch.
  103. *
  104. * @method get
  105. * @return {Object}
  106. * @private
  107. */
  108. get: function(){
  109. return this.options;
  110. }
  111. };
  112.  
  113. /**
  114. * Process filters through data object.
  115. *
  116. * @param {Object} [filters] Collection of filters you want to apply.
  117. * @param {Object} [properties] Object of the data.
  118. * @private
  119. */
  120. function applyFilters (filters, properties) {
  121. if (!filters) {
  122. return;
  123. }
  124.  
  125. angular.forEach(filters, function (filter,namespace) {
  126. var value = getNamespace(properties,namespace),
  127. newValue;
  128.  
  129. if (isDefined(value)) {
  130. newValue = angular.isFunction(filter) ? filter(value) : $parse(namespace + '|' + filter)(properties);
  131. setNamespace(properties,namespace,newValue);
  132. }
  133. });
  134. }
  135.  
  136. /**
  137. * Check if value is defined.
  138. *
  139. * @param {Mixed} obj Value to check if it's defined.
  140. * @return Boolean
  141. * @private
  142. */
  143. function isDefined (obj) {
  144. return obj !== undefined && obj !== null;
  145. }
  146.  
  147. /**
  148. * Extend properties to target class.
  149. *
  150. * @param {Object} properties Properties to extend.
  151. * @param {Mixed} to Target class.
  152. * @private
  153. */
  154. function extendTo(properties,to){
  155. angular.forEach(properties,function(value,key){
  156. if (!properties.hasOwnProperty(key)) {
  157. return;
  158. }
  159. if (angular.isFunction(value)) {
  160. value.$previous = to[key];
  161. }
  162. to[key] = value;
  163. });
  164. }
  165.  
  166. /**
  167. * Call parent class method
  168. *
  169. * @param {Mixed} [args] Arguments you need in the parent method.
  170. * @param {String} [method] If you use strict mode you may have to this argument to define the method you want to call.
  171. * @return Mixed
  172. * @private
  173. */
  174. function callParent(args,method){
  175. var caller = this[method] || this.callParent.caller || arguments.callee.caller || arguments.caller;
  176.  
  177. if (!caller) {
  178. if (isStrict()) {
  179. throw new Error('You are in strict mode use the second parameter in "callParent".');
  180. }
  181.  
  182. throw new Error('The method "callParent" failed because cannot identify caller.');
  183. }
  184.  
  185. return caller.$previous.apply(this,args);
  186. }
  187.  
  188. /**
  189. * Check if strict
  190. *
  191. * @return Boolean
  192. * @private
  193. */
  194. function isStrict() {
  195. return !this;
  196. }
  197.  
  198. /**
  199. * Set namespace in object.
  200. *
  201. * @param {Object} obj Object you want to manipulate.
  202. * @param {String} id Namespace for value you want to set.
  203. * @param {Mixed} value Value for namespace.
  204. * @private
  205. */
  206. function setNamespace(obj,id,value){
  207. $parse(id).assign(obj,value);
  208. }
  209.  
  210. /**
  211. * Get namespace in object.
  212. *
  213. * @param {Object} obj Object you want to search through.
  214. * @param {String} id Namespace for value you want to get.
  215. * @return {Mixed}
  216. * @private
  217. */
  218. function getNamespace(obj,id){
  219. return $parse(id)(obj);
  220. }
  221.  
  222. /**
  223. * Remove namespace in object.
  224. *
  225. * @param {Object} obj Object you want to manipulate.
  226. * @param {String} id Namespace for value you want to remove.
  227. * @param {Boolean} [autoDelete] Auto delete parent if object is empty.
  228. * @private
  229. */
  230. function removeNamespace(obj,id,autoDelete){
  231. var parts = id.split('.'),
  232. last = parts.pop(),
  233. pre = parts.join('.'),
  234. root = getNamespace(obj,pre);
  235.  
  236. root[last] = null;
  237. delete root[last];
  238.  
  239. if (autoDelete) {
  240. for (var key in root) {
  241. if (root.hasOwnProperty(key)) {
  242. return;
  243. }
  244. }
  245.  
  246. removeNamespace(obj,pre,autoDelete);
  247. }
  248. }
  249.  
  250. /**
  251. * Modellayer for AngularJS
  252. *
  253. * @class NgModel
  254. * @constructor
  255. * @param {Object} [properties] Start properties of record.
  256. */
  257. function NgModel (properties){
  258. var me = this;
  259.  
  260. me.response = null;
  261. me.data = angular.extend({},me.defaultData,properties);
  262. me.initialize.apply(me, arguments);
  263. }
  264.  
  265. /**
  266. * Create new child type of model.
  267. *
  268. * @method create
  269. * @static
  270. * @param {Object} [properties] prototypeProperties for child model prototype.
  271. * @param {Object} [statics] staticProperties for child model static properties.
  272. * @return {Function} Constructor
  273. */
  274. NgModel.create = function(properties,statics){
  275. var extend = this,
  276. sub;
  277.  
  278. properties = properties || {};
  279. statics = statics || {};
  280. if (properties.hasOwnProperty('constructor')) {
  281. sub = properties.constructor;
  282. properties.constructor = null;
  283. delete properties.constructor;
  284. } else {
  285. sub = function(){
  286. return extend.apply(this,arguments);
  287. };
  288. }
  289. sub.constructor.$previous = extend.constructor;
  290. sub.prototype = Object.create(extend.prototype);
  291. sub.$super = extend;
  292. angular.extend(sub,extend);
  293. extendTo(properties,sub.prototype);
  294. extendTo(statics,sub);
  295. return sub;
  296. };
  297.  
  298. /**
  299. * Fetch one record.
  300. *
  301. * @method fetchOne
  302. * @static
  303. * @param {Mixed} id
  304. * @param {Object} [options] Additional options for request.
  305. * @return $q.promise
  306. */
  307. NgModel.fetchOne = function (id,options){
  308. var model = new this();
  309. model.setId(id);
  310. return model.fetch(options);
  311. };
  312.  
  313. /**
  314. * Fetch collection of records.
  315. *
  316. * @method fetchAll
  317. * @static
  318. * @param {Object} [options] Additional options for request.
  319. * @return $q.promise
  320. */
  321. NgModel.fetchAll = function (options){
  322. var model = this,
  323. instance = new model(),
  324. deferred = $q.defer();
  325.  
  326. instance.sync('read',instance,options).then(function (response){
  327. var data = instance.parse(response.data, options),
  328. models, filters;
  329.  
  330. if (angular.isArray(data)) {
  331. models = [];
  332. filters = model.prototype.readFilters;
  333.  
  334. angular.forEach(data, function (item) {
  335. applyFilters(filters, item);
  336. models.push(new model(item));
  337. });
  338.  
  339. deferred.resolve(models);
  340. } else {
  341. deferred.reject('Not a valid response, expecting an array');
  342. }
  343. }, deferred.reject);
  344.  
  345. return deferred.promise;
  346. };
  347.  
  348. /**
  349. * Call parent class method
  350. *
  351. * @method callParent
  352. * @static
  353. * @private
  354. * @param {Mixed} [args] Arguments you need in the parent method.
  355. * @return {Mixed}
  356. */
  357. NgModel.callParent = callParent;
  358.  
  359. NgModel.prototype = {
  360. /**
  361. * The default namespace for the id property.
  362. *
  363. * @property idProperty
  364. * @type String
  365. * @default "id"
  366. */
  367. idProperty: 'id',
  368.  
  369. /**
  370. * The default propertiers of a record.
  371. *
  372. * @property defaultData
  373. * @type Object
  374. * @default {}
  375. */
  376. defaultData: {},
  377.  
  378. /**
  379. * The default headers for a request.
  380. *
  381. * @property headers
  382. * @type Object
  383. * @default {}
  384. */
  385. headers: {},
  386.  
  387. /**
  388. * The root namespace which contain the data.
  389. *
  390. * @property resultRoot
  391. * @type String
  392. * @default null
  393. */
  394. resultRoot: null,
  395.  
  396. /**
  397. * The main request url.
  398. *
  399. * @property url
  400. * @type String
  401. * @default null
  402. */
  403. url: null,
  404.  
  405. /**
  406. * Filter for data processing when reading.
  407. *
  408. * @property readFilters
  409. * @type Object
  410. * @default null
  411. */
  412. readFilters: null,
  413.  
  414. /**
  415. * Filter for data processing when writing.
  416. *
  417. * @property writeFilters
  418. * @type Object
  419. * @default null
  420. */
  421. writeFilters: null,
  422.  
  423. /**
  424. * Exclude non persit data from request.
  425. *
  426. * @property notPersist
  427. * @type Array<String>
  428. * @default []
  429. */
  430. notPersist: [],
  431.  
  432. /**
  433. * Define your model crud schema.
  434. *
  435. * @property actionMethods
  436. * @type Object
  437. * @default {'create': 'POST','read': 'GET','update': 'PUT','delete': 'DELETE'}
  438. */
  439. actionMethods: {
  440. 'create': 'POST',
  441. 'read': 'GET',
  442. 'update': 'PUT',
  443. 'delete': 'DELETE'
  444. },
  445.  
  446. /**
  447. * Specify certain url for certain operations (optional).
  448. *
  449. * @property api
  450. * @type Object
  451. * @default {'create': null,'read': null,'update': null,'delete': null}
  452. */
  453. api: {
  454. 'create': null,
  455. 'read': null,
  456. 'update': null,
  457. 'delete': null
  458. },
  459.  
  460. /**
  461. * Specify for which operation your model should append data to the request.
  462. *
  463. * @property appendData
  464. * @type Object
  465. * @default {'create': true,'read': false,'update': true,'delete': false}
  466. */
  467. appendData: {
  468. 'create': true,
  469. 'read': false,
  470. 'update': true,
  471. 'delete': false
  472. },
  473.  
  474. /**
  475. * Model initialization
  476. *
  477. * @method initialize
  478. * @private
  479. * @param {Object} [properties] Start properties of record.
  480. * @param {Object} [options] Additional options for model.
  481. */
  482. initialize: function (properties, options) {
  483. var me = this;
  484.  
  485. options = options || {};
  486.  
  487. if (properties) {
  488. if (options.parse) {
  489. properties = me.parse(properties);
  490. }
  491. if (options.readFilters) {
  492. applyFilters(me.readFilters, properties);
  493. }
  494.  
  495. me.extend(properties);
  496. me.previousProperties = function () {
  497. return properties;
  498. };
  499. }
  500.  
  501. if (options.url) {
  502. me.url = options.url;
  503. }
  504. },
  505.  
  506. /**
  507. * Check if model data has changed since the last sync.
  508. *
  509. * @method hasChanged
  510. * @param {String} [property] Property to control.
  511. * @return {Boolean}
  512. */
  513. hasChanged: function (property) {
  514. var me = this,
  515. changed = me.changedProperties();
  516.  
  517. if (property) {
  518. return property in changed;
  519. }
  520.  
  521. for (var i in changed) {
  522. return true;
  523. }
  524.  
  525. return false;
  526. },
  527.  
  528. /**
  529. * Create object which contains all changed properties.
  530. *
  531. * @method changedProperties
  532. * @param {Object} [diff] An object to diff against, determining if there would be a change.
  533. * @return {Object}
  534. */
  535. changedProperties: function (diff) {
  536. var me = this,
  537. current = diff || me.data,
  538. changed = {},
  539. previousProperties = me.previousProperties();
  540.  
  541. if (!isDefined(diff)) {
  542. angular.forEach(previousProperties,function(value,property){
  543. if (!isDefined(value)) {
  544. changed[property] = value;
  545. }
  546. });
  547. }
  548.  
  549. angular.forEach(current,function(value,property){
  550. if (current.hasOwnProperty(property) && !angular.equals(value, previousProperties[property]) === false) {
  551. changed[property] = value;
  552. }
  553. });
  554.  
  555. return changed;
  556. },
  557.  
  558. /**
  559. * Get the state of a property before the last change.
  560. *
  561. * @method previous
  562. * @param {String} property Property which you want to get.
  563. * @return {Mixed}
  564. */
  565. previous: function (property) {
  566. var me = this,
  567. previous = me.previousProperties();
  568.  
  569. return getNamespace(previous,property);
  570. },
  571.  
  572. /**
  573. * Get the state of data before the last change.
  574. *
  575. * @method previousProperties
  576. * @return {Object}
  577. */
  578. previousProperties: function () {
  579. return {};
  580. },
  581.  
  582. /**
  583. * Read a record.
  584. *
  585. * @method fetch
  586. * @param {Object} [options] Additonal request options.
  587. * @return $q.promise
  588. */
  589. fetch: function (options) {
  590. var me = this,
  591. deferred = $q.defer();
  592.  
  593. me.sync('read', me, options).then(function (response) {
  594. var data = me.parse(response.data, options),
  595. status = response.status,
  596. success = status < 400;
  597.  
  598. me.response = response;
  599.  
  600. if (success && angular.isObject(data)) {
  601. applyFilters(me.readFilters, data);
  602. me.extend(data);
  603. me.previousProperties = function () {
  604. return data;
  605. };
  606. deferred.resolve(me);
  607. } else if (success) {
  608. deferred.resolve(me);
  609. } else {
  610. deferred.reject(me);
  611. }
  612. },function(response){
  613. me.response = response;
  614. deferred.reject(me);
  615. });
  616.  
  617. return deferred.promise;
  618. },
  619.  
  620. /**
  621. * Save/Update a record.
  622. *
  623. * @method save
  624. * @param {Object} [options] Additonal request options.
  625. * @return $q.promise
  626. */
  627. save: function (options) {
  628. var me = this,
  629. operation = me.isNew() ? 'create' : 'update',
  630. deferred = $q.defer();
  631.  
  632. me.sync(operation, me, options).then(function (response) {
  633. var data = me.parse(response.data, options),
  634. status = response.status,
  635. success = status < 400;
  636.  
  637. me.response = response;
  638.  
  639. if (success && angular.isObject(data)) {
  640. applyFilters(me.readFilters, data);
  641. me.extend(data);
  642. me.previousProperties = function () {
  643. return data;
  644. };
  645. deferred.resolve(me);
  646. } else if (success) {
  647. deferred.resolve(me);
  648. } else {
  649. deferred.reject(me);
  650. }
  651. },function(response){
  652. me.response = response;
  653. deferred.reject(me);
  654. });
  655.  
  656. return deferred.promise;
  657. },
  658.  
  659. /**
  660. * Delete a record.
  661. *
  662. * @method destroy
  663. * @param {Object} [options] Additonal request options.
  664. * @return $q.promise
  665. */
  666. destroy: function (options) {
  667. var me = this,
  668. deferred = $q.defer();
  669.  
  670. if (me.isNew()) {
  671. deferred.resolve(me);
  672. return deferred.promise;
  673. }
  674.  
  675. me.sync('delete', me, options).then(function (response) {
  676. me.response = response;
  677. deferred.resolve(me);
  678. }, function(response){
  679. me.response = response;
  680. deferred.reject(me);
  681. });
  682.  
  683. return deferred.promise;
  684. },
  685.  
  686. /**
  687. * Sync a record.
  688. *
  689. * @method sync
  690. * @private
  691. * @param {String} operation Indicates which operation should be done.
  692. * @param {NgModel} model Record used for request.
  693. * @param {Object} [options] Additonal request options.
  694. * @return $q.promise
  695. */
  696. sync: function (operation, model, options) {
  697. var me = this,
  698. batch = new Batch(model,operation,options);
  699.  
  700. options = batch.get();
  701.  
  702. if (batch.isWriting()) {
  703. applyFilters(me.writeFilters,options.data);
  704. }
  705.  
  706. return $http(options);
  707. },
  708.  
  709. /**
  710. * Put together url for request.
  711. *
  712. * @method buildUrl
  713. * @private
  714. * @param {String} operation Indicates which operation should be done.
  715. * @param {Object} [options] Batch options.
  716. * @return {String}
  717. */
  718. buildUrl: function (operation,options){
  719. var me = this,
  720. url = me.api[operation] || me.url,
  721. data = angular.extend({},me.data,options.data),
  722. formatedUrl = $interpolate(url)(data);
  723.  
  724. if (!isDefined(data[me.idProperty])) {
  725. return formatedUrl;
  726. }
  727.  
  728. if (!isDefined(formatedUrl)) {
  729. throw 'Implement this.buildUrl() or specify this.url';
  730. }
  731.  
  732. return $interpolate('{{root}}{{end}}{{id}}')({
  733. root: formatedUrl,
  734. end: formatedUrl.charAt(formatedUrl.length - 1) === '/' ? '' : '/',
  735. id: encodeURIComponent(data[me.idProperty])
  736. });
  737. },
  738.  
  739. /**
  740. * Parse raw data from request response.
  741. *
  742. * @method parse
  743. * @private
  744. * @param {Object} data Raw request response data.
  745. * @param {Object} [options] Request options.
  746. * @return {Object}
  747. */
  748. parse: function (data, options) {
  749. if (!isDefined(this.resultRoot)) {
  750. return data;
  751. }
  752.  
  753. return getNamespace(data,this.resultRoot);
  754. },
  755.  
  756. /**
  757. * Indicates if record is new or not (important for save/update operation).
  758. *
  759. * @method isNew
  760. * @return {Boolean}
  761. */
  762. isNew: function () {
  763. return !isDefined(this.getId());
  764. },
  765.  
  766. /**
  767. * Getter for data property.
  768. *
  769. * @method toJSON
  770. * @return {Object}
  771. */
  772. toJSON: function (){
  773. return this.data;
  774. },
  775.  
  776. /**
  777. * Easy way to extend model to a scope.
  778. *
  779. * @method toScope
  780. * @param {Object} scope Scope you want to add the model.
  781. * @param {String} key Property where you want to add the model to.
  782. * @return {NgModel}
  783. */
  784. toScope: function(scope,key){
  785. scope[key] = this.data;
  786. return this;
  787. },
  788.  
  789. /**
  790. * Process data for batch (advised to extend as soon as you want extended data processing for requests).
  791. *
  792. * @method getRequestData
  793. * @private
  794. * @return {Object}
  795. */
  796. getRequestData: function (){
  797. return this.data;
  798. },
  799.  
  800. /**
  801. * Get id of record.
  802. *
  803. * @method getId
  804. * @return {Mixed}
  805. */
  806. getId: function (){
  807. return this.data[this.idProperty];
  808. },
  809.  
  810. /**
  811. * Change id of record.
  812. *
  813. * @method setId
  814. * @param {Mixed} id Id value for record.
  815. * @return {Mixed}
  816. */
  817. setId: function (id){
  818. this.data[this.idProperty] = id;
  819. return this;
  820. },
  821.  
  822. /**
  823. * Getter for certain data properties.
  824. *
  825. * @method get
  826. * @param {String} key Namespace for value you want to get.
  827. * @return {Mixed}
  828. */
  829. get: function (key){
  830. return getNamespace(this.data,key);
  831. },
  832.  
  833. /**
  834. * Setter for certain data properties.
  835. *
  836. * @method set
  837. * @param {String} key Namespace for value you want to set.
  838. * @param {Mixed} value Value you want to set.
  839. * @return {Mixed}
  840. */
  841. set: function (key,value){
  842. setNamespace(this.data,key,value);
  843. return this;
  844. },
  845.  
  846. /**
  847. * Create a new object which all filtered data wanted.
  848. *
  849. * @method range
  850. * @param {Mixed} [...] Property string you want to export or object with from/to information like {'myProp':'myNewProp'}.
  851. * @return {Object}
  852. */
  853. range: function (){
  854. var me = this,
  855. result = {};
  856.  
  857. angular.forEach(arguments,function(item){
  858. var from = item,
  859. to = item;
  860.  
  861. if (angular.isObject(item)) {
  862. from = item.from;
  863. to = item.to;
  864. }
  865.  
  866. result[from] = me.data[to];
  867. });
  868.  
  869. return result;
  870. },
  871.  
  872. /**
  873. * Easily extend the data object of your model.
  874. *
  875. * @method extend
  876. * @param {Object} data Data to extend.
  877. * @return {NgModel}
  878. */
  879. extend: function (data){
  880. angular.extend(this.data,data);
  881. return this;
  882. },
  883.  
  884. /**
  885. * Call parent class method
  886. *
  887. * @method callParent
  888. * @private
  889. * @param {Mixed} [args] Arguments you need in the parent method.
  890. * @return {Mixed}
  891. */
  892. callParent: callParent
  893. };
  894.  
  895. s
  896. s
  897. s