|
1 YUI.add('model-sync-local', function (Y, NAME) { |
|
2 |
|
3 /* |
|
4 An extension which provides a sync implementation through locally stored |
|
5 key value pairs, either through the HTML localStorage API or falling back |
|
6 onto an in-memory cache, that can be mixed into a Model or ModelList subclass. |
|
7 |
|
8 @module app |
|
9 @submodule model-sync-local |
|
10 @since 3.13.0 |
|
11 **/ |
|
12 |
|
13 /** |
|
14 An extension which provides a sync implementation through locally stored |
|
15 key value pairs, either through the HTML localStorage API or falling back |
|
16 onto an in-memory cache, that can be mixed into a Model or ModelList subclass. |
|
17 |
|
18 A group of Models/ModelLists is serialized in localStorage by either its |
|
19 class name, or a specified 'root' that is provided. |
|
20 |
|
21 var User = Y.Base.create('user', Y.Model, [Y.ModelSync.Local], { |
|
22 root: 'user' |
|
23 }); |
|
24 |
|
25 var Users = Y.Base.create('users', Y.ModelList, [Y.ModelSync.Local], { |
|
26 model: User, |
|
27 }); |
|
28 |
|
29 @class ModelSync.Local |
|
30 @extensionfor Model |
|
31 @extensionfor ModelList |
|
32 @since 3.13.0 |
|
33 **/ |
|
34 function LocalSync() {} |
|
35 |
|
36 /** |
|
37 Properties that shouldn't be turned into ad-hoc attributes when passed to a |
|
38 Model or ModelList constructor. |
|
39 |
|
40 @property _NON_ATTRS_CFG |
|
41 @type Array |
|
42 @default ['root'] |
|
43 @static |
|
44 @protected |
|
45 @since 3.13.0 |
|
46 **/ |
|
47 LocalSync._NON_ATTRS_CFG = ['root']; |
|
48 |
|
49 /** |
|
50 Feature testing for `localStorage` availability. |
|
51 Will return falsey for browsers with `localStorage`, but that don't |
|
52 actually work, such as iOS Safari in private browsing mode. |
|
53 |
|
54 @property _hasLocalStorage |
|
55 @type Boolean |
|
56 @private |
|
57 **/ |
|
58 LocalSync._hasLocalStorage = (function () { |
|
59 var LS = Y.config.win.localStorage, |
|
60 test = Y.guid(); |
|
61 |
|
62 try { |
|
63 LS.setItem(test, test); |
|
64 LS.removeItem(test); |
|
65 return true; |
|
66 } catch (e) { |
|
67 return false; |
|
68 } |
|
69 })(), |
|
70 |
|
71 /** |
|
72 Object of key/value pairs to fall back on when localStorage is not available. |
|
73 |
|
74 @property _data |
|
75 @type Object |
|
76 @private |
|
77 **/ |
|
78 |
|
79 LocalSync._data = LocalSync._data || {}; |
|
80 |
|
81 /** |
|
82 Cache to quickly access a specific object with a given ID. |
|
83 |
|
84 @property _store |
|
85 @type Array |
|
86 @private |
|
87 **/ |
|
88 |
|
89 LocalSync._store = LocalSync._store || {}; |
|
90 |
|
91 LocalSync.prototype = { |
|
92 |
|
93 // -- Public Methods ------------------------------------------------------- |
|
94 |
|
95 /** |
|
96 Root used as the key inside of localStorage and/or the in-memory store. |
|
97 |
|
98 @property root |
|
99 @type String |
|
100 @default "" |
|
101 @since 3.13.0 |
|
102 **/ |
|
103 root: '', |
|
104 |
|
105 /** |
|
106 Shortcut for access to localStorage. |
|
107 |
|
108 @property storage |
|
109 @type Storage |
|
110 @default null |
|
111 @since 3.13.0 |
|
112 **/ |
|
113 storage: null, |
|
114 |
|
115 // -- Lifecycle Methods ----------------------------------------------------- |
|
116 initializer: function (config) { |
|
117 var store, data; |
|
118 |
|
119 config || (config = {}); |
|
120 |
|
121 if ('root' in config) { |
|
122 this.root = config.root || ''; |
|
123 } |
|
124 |
|
125 // This is checking to see if the sync layer is being applied to |
|
126 // a ModelList, and if so, is looking for a `root` property on its |
|
127 // Model's prototype instead. |
|
128 if (!this.root && this.model && this.model.prototype.root) { |
|
129 this.root = this.model.prototype.root; |
|
130 } |
|
131 |
|
132 if (LocalSync._hasLocalStorage) { |
|
133 this.storage = Y.config.win.localStorage; |
|
134 store = this.storage.getItem(this.root); |
|
135 } else { |
|
136 Y.log("Could not access localStorage.", "warn"); |
|
137 } |
|
138 |
|
139 // Pull in existing data from localStorage, if possible. |
|
140 // Otherwise, see if there's existing data on the local cache. |
|
141 if (store) { |
|
142 LocalSync._store[this.root] = store.split('|') || []; |
|
143 |
|
144 Y.Array.each(LocalSync._store[this.root], function (id) { |
|
145 LocalSync._data[id] = Y.JSON.parse(this.storage.getItem(id)); |
|
146 }, this); |
|
147 } else { |
|
148 LocalSync._store[this.root] || (LocalSync._store[this.root] = []); |
|
149 } |
|
150 }, |
|
151 |
|
152 // -- Public Methods ----------------------------------------------------------- |
|
153 |
|
154 /** |
|
155 Creates a synchronization layer with the localStorage API, if available. |
|
156 Otherwise, falls back to a in-memory data store. |
|
157 |
|
158 This method is called internally by load(), save(), and destroy(). |
|
159 |
|
160 @method sync |
|
161 @param {String} action Sync action to perform. May be one of the following: |
|
162 |
|
163 * **create**: Store a newly-created model for the first time. |
|
164 * **read** : Load an existing model. |
|
165 * **update**: Update an existing model. |
|
166 * **delete**: Delete an existing model. |
|
167 |
|
168 @param {Object} [options] Sync options |
|
169 @param {Function} [callback] Called when the sync operation finishes. |
|
170 @param {Error|null} callback.err If an error occurred, this parameter will |
|
171 contain the error. If the sync operation succeeded, _err_ will be |
|
172 falsey. |
|
173 @param {Any} [callback.response] The response from our sync. This value will |
|
174 be passed to the parse() method, which is expected to parse it and |
|
175 return an attribute hash. |
|
176 **/ |
|
177 sync: function (action, options, callback) { |
|
178 options || (options = {}); |
|
179 var response, errorInfo; |
|
180 |
|
181 try { |
|
182 switch (action) { |
|
183 case 'read': |
|
184 if (this._isYUIModelList) { |
|
185 response = this._index(options); |
|
186 } else { |
|
187 response = this._show(options); |
|
188 } |
|
189 break; |
|
190 case 'create': |
|
191 response = this._create(options); |
|
192 break; |
|
193 case 'update': |
|
194 response = this._update(options); |
|
195 break; |
|
196 case 'delete': |
|
197 response = this._destroy(options); |
|
198 break; |
|
199 } |
|
200 } catch (error) { |
|
201 errorInfo = error.message; |
|
202 } |
|
203 |
|
204 if (response) { |
|
205 callback(null, response); |
|
206 } else if (errorInfo) { |
|
207 callback(errorInfo); |
|
208 } else { |
|
209 callback("Data not found in LocalStorage"); |
|
210 } |
|
211 }, |
|
212 |
|
213 /** |
|
214 Generate a random GUID for our Models. This can be overriden if you have |
|
215 another method of generating different IDs. |
|
216 |
|
217 @method generateID |
|
218 @protected |
|
219 @param {String} pre Optional GUID prefix |
|
220 **/ |
|
221 generateID: function (pre) { |
|
222 return Y.guid(pre + '_'); |
|
223 }, |
|
224 |
|
225 // -- Protected Methods ---------------------------------------------------- |
|
226 |
|
227 /** |
|
228 Sync method correlating to the "read" operation, for a Model List |
|
229 |
|
230 @method _index |
|
231 @return {Object[]} Array of objects found for that root key |
|
232 @protected |
|
233 @since 3.13.0 |
|
234 **/ |
|
235 _index: function () { |
|
236 var store = LocalSync._store[this.root], |
|
237 data = Y.Array.map(store, function (id) { |
|
238 return LocalSync._data[id]; |
|
239 }); |
|
240 |
|
241 return data; |
|
242 }, |
|
243 |
|
244 /** |
|
245 Sync method correlating to the "read" operation, for a Model |
|
246 |
|
247 @method _show |
|
248 @return {Object} Object found for that root key and model ID |
|
249 @protected |
|
250 @since 3.13.0 |
|
251 **/ |
|
252 _show: function () { |
|
253 return LocalSync._data[this.get('id')] || null; |
|
254 }, |
|
255 |
|
256 /** |
|
257 Sync method correlating to the "create" operation |
|
258 |
|
259 @method _show |
|
260 @return {Object} The new object created. |
|
261 @protected |
|
262 @since 3.13.0 |
|
263 **/ |
|
264 _create: function () { |
|
265 var hash = this.toJSON(); |
|
266 |
|
267 hash.id = this.generateID(this.root); |
|
268 |
|
269 LocalSync._data[hash.id] = hash; |
|
270 if (this.storage) { |
|
271 this.storage.setItem(hash.id, hash); |
|
272 } |
|
273 |
|
274 LocalSync._store[this.root].push(hash.id); |
|
275 |
|
276 this._save(); |
|
277 return hash; |
|
278 }, |
|
279 |
|
280 /** |
|
281 Sync method correlating to the "update" operation |
|
282 |
|
283 @method _update |
|
284 @return {Object} The updated object. |
|
285 @protected |
|
286 @since 3.13.0 |
|
287 **/ |
|
288 _update: function () { |
|
289 var hash = this.toJSON(), |
|
290 id = this.get('id'); |
|
291 |
|
292 LocalSync._data[id] = hash; |
|
293 |
|
294 if (this.storage) { |
|
295 this.storage.setItem(id, hash); |
|
296 } |
|
297 |
|
298 if (Y.Array.indexOf(LocalSync._store[this.root], id) === -1) { |
|
299 LocalSync._store[this.root].push(id); |
|
300 } |
|
301 |
|
302 this._save(); |
|
303 |
|
304 return hash; |
|
305 }, |
|
306 |
|
307 /** |
|
308 Sync method correlating to the "delete" operation. Deletes the data |
|
309 from the in-memory object, and saves into localStorage if available. |
|
310 |
|
311 @method _destroy |
|
312 @protected |
|
313 @since 3.13.0 |
|
314 **/ |
|
315 _destroy: function () { |
|
316 var id = this.get('id'), |
|
317 storage = this.storage; |
|
318 |
|
319 if (!LocalSync._data[id]) { |
|
320 return; |
|
321 } |
|
322 |
|
323 delete LocalSync._data[id]; |
|
324 |
|
325 if (storage) { |
|
326 storage.removeItem(id); |
|
327 } |
|
328 |
|
329 LocalSync._store[this.root] = Y.Array.filter(LocalSync._store[this.root], function (item) { |
|
330 return item.id != id; |
|
331 }); |
|
332 |
|
333 this._save(); |
|
334 return this.toJSON(); |
|
335 }, |
|
336 |
|
337 /** |
|
338 Saves the current in-memory store into a localStorage key/value pair |
|
339 if localStorage is available; otherwise, does nothing. |
|
340 |
|
341 @method _save |
|
342 @protected |
|
343 @since 3.13.0 |
|
344 **/ |
|
345 _save: function () { |
|
346 if (LocalSync._hasLocalStorage && this.storage) { |
|
347 this.storage.setItem( |
|
348 this.root, |
|
349 LocalSync._store[this.root].join('|') |
|
350 ); |
|
351 } |
|
352 } |
|
353 }; |
|
354 |
|
355 // -- Namespace --------------------------------------------------------------- |
|
356 |
|
357 Y.namespace('ModelSync').Local = LocalSync; |
|
358 |
|
359 |
|
360 }, '@VERSION@', {"requires": ["model", "json-stringify"]}); |