Makes it easy to work with models through a buffered proxy. Designed to work with the model relationship mixins and supports an identical API for buffered relationship management. This builds on the functionality of the Ember Buffered Proxy library.
Normally a model has two states:
- Clean: no unsaved changes
- Dirty: some unsaved changes
A buffered model has four states:
- Clean: no unsaved changes
- Dirty: some unsaved changes
- Dirty & Changed: some unsaved changes and some unapplied changes
- Changed: some unapplied changes
These states correspond to three computed property values:
- Clean:
item.get('isDirty') === false - Dirty:
item.get('isDirty') === true - Dirty & Changed:
item.get('isDirty') === true && buffer.get('hasChanges') === true - Changed:
buffer.get('hasChanges') === true
Note: isDirty has been deprecated. Use hasDirtyAttributes instead.
Note: hasChanges is awkward compared to the Model API. I wish it was hasChangedAttributes instead. Even isChanged would be more in line with the deprecated API
bufferedBelongsTo('this-model-name', 'to-model-name')bufferedHasMany('this-model-name', 'has-model-name')bufferedHasManyThrough('this-model-name', 'has-model-name', 'through-model-name')
These mixin factories will enable a BufferedProxy to manage external relationships for a proxied model.
import BufferedProxy from 'ember-buffered-proxy/proxy';
import { bufferedHasMany, bufferedHasManyThrough } from 'buffered-proxy-relationship/mixins';
// Post Buffered Proxy
export default BufferedProxy.extend(
bufferedHasMany('post', 'comment'),
bufferedHasMany('post', 'post-tag'),
bufferedHasManyThrough('post', 'tag', 'post-tag'),
{
// ... other needed buffered proxy overrides
}
);You can put your model in charge of handing out it's own BufferedProxy using the bufferable() mixin factory. Without any arguments your model will wrap itself in a plain BufferedProxy. But if you pass in an extended object as bufferable(CustomBufferedProxy) then it will use that instead.
If you are not passing anything to the factory, import the BufferableModelMixin instead.
bufferable()BufferableModelMixinbufferable(CustomBufferedProxy)
Presume that PostBufferedProxy is defined as above.
import DS from 'embder-data';
import { hasMany, hasManyThrough } from 'relationship/mixins';
import PostBufferedProxy from 'local/post-buffered-proxy';
import { bufferable } from 'buffered-proxy-relationship/mixins';
// Post
export default DS.Model.extend(
hasMany('post', 'comment'),
hasMany('post', 'post-tag'),
hasManyThrough('post', 'tag', 'post-tag'),
bufferable(PostBufferedProxy),
{
title: DS.attr('string'),
body: DS.attr('string'),
}
);Presume the post model is defined as shown above. Further presume the PostBufferedProxy is defined as shown above.
// get a post from the store
const post = this.store.find('post', 1);
// get the buffer from the post
const buffer = post.buffer();
// title is the same
buffer.get('title'); // "Hi"
post.get('title'); // "Hi"
// now we can do standard BufferedProxy operations
// change the title
buffer.set('title', 'Hello');
// title is different
buffer.get('title'); // "Hello"
post.get('title'); // "Hi"
// buffer has changes
buffer.get('hasChanges'); // true
// but it's not dirty
buffer.get('isDirty'); // false
post.get('isDirty'); // false
// apply the changes
buffer.applyChanges();
// now it's dirty
buffer.get('isDirty'); // true
post.get('isDirty'); // true
post.get('title'); // "Hello"
// you can save it also
buffer.save();
// ... after save completes
buffer.get('isDirty'); // false
post.get('isDirty'); // falseWe'll be using these example models for the rest of the document. The custom buffered proxy is defined at the top of the model and exported for convenience. It could be defined anywhere you please.
A post has many comment and tag relationships. The tag is related through a post-tag to the post.
import DS from 'ember-data';
import { hasMany, hasManyThrough } from 'relationship/mixins';
import BufferedProxy from 'ember-buffered-proxy/proxy';
import { bufferable, bufferedHasMany, bufferedHasManyThrough } from 'buffered-proxy-relationship/mixins';
// Post Buffered Proxy
const PostBufferedProxy = BufferedProxy.extend(
bufferedHasMany('post', 'comment'),
bufferedHasMany('post', 'post-tag'),
bufferedHasManyThrough('post', 'tag', 'post-tag'),
);
export PostBufferedProxy;
// Post
export default DS.Model.extend(
hasMany('post', 'comment'),
hasMany('post', 'post-tag'),
hasManyThrough('post', 'tag', 'post-tag'),
bufferable(PostBufferedProxy),
{
title: DS.attr('string'),
body: DS.attr('string'),
}
);A comment belongs to a post.
import DS from 'ember-data';
import { belongsTo } from 'relationship/mixins';
import BufferedProxy from 'ember-buffered-proxy/proxy';
import { bufferable, bufferedBelongsTo } from 'buffered-proxy-relationship/mixins';
// Post Buffered Proxy
const CommentBufferedProxy = BufferedProxy.extend(
bufferedBelongsTo('comment', 'post'),
);
export CommentBufferedProxy;
// Comment
export default DS.Model.extend(
belongsTo('comment', 'post'),
bufferable(CommentBufferedProxy),
{
body: DS.attr('string'),
}
);The post-tag belongs to a post and a tag.
import DS from 'ember-data';
import { belongsTo } from 'relationship/mixins';
import BufferedProxy from 'ember-buffered-proxy/proxy';
import { bufferable, bufferedBelongsTo } from 'buffered-proxy-relationship/mixins';
// Post Buffered Proxy
const PostTagBufferedProxy = BufferedProxy.extend(
bufferedBelongsTo('post-tag', 'post'),
bufferedBelongsTo('post-tag', 'tag'),
);
export PostTagBufferedProxy;
// PostTag
export default DS.Model.extend(
belongsTo('post-tag', 'post'),
belongsTo('post-tag', 'tag'),
bufferable(PostTagBufferedProxy),
);The tag has many post-tag relationships.
import DS from 'ember-data';
import { hasMany } from 'relationship/mixins';
import BufferedProxy from 'ember-buffered-proxy/proxy';
import { bufferable, bufferedHasMany } from 'buffered-proxy-relationship/mixins';
// Post Buffered Proxy
const TagBufferedProxy = BufferedProxy.extend(
bufferedHasMany('tag', 'post-tag'),
);
export TagBufferedProxy;
// Tag
export default DS.Model.extend(
hasMany('tag', 'post-tag'),
bufferable(TagBufferedProxy),
{
name: DS.attr('string'),
}
);Using bufferedBelongsTo.
// get a comment from the store
const comment = this.store.find('comment', 1);
// get a buffer from the comment
const buffer = comment.buffer();
buffer.get('isBuffered'); // true
// has no post
buffer.get('post'); // null
// add a post
const post = this.store.find('post', 1);
buffer.addPost(post);
// buffer status
buffer.get('post'); // post
buffer.get('_addedPost'); // post
buffer.get('hasChanges'); // true
buffer.get('hasAddedPost'); // true
buffer.get('isDirty'); // false
// comment status
comment.get('post'); // null
comment.get('isDirty'); // false
// buffer doesn't have a postId
buffer.get('postId'); // null
// apply changes
buffer.applyChanges();
// buffer status
buffer.get('post'); // post
buffer.get('_addedPost'); // null
buffer.get('hasChanges'); // false
buffer.get('hasAddedPost'); // false
buffer.get('isDirty'); // true
// comment status
comment.get('post'); // post
comment.get('isDirty'); // true
// now it has a postId
buffer.get('postId'); // "1"
// save
buffer.save();
// ... after save completes
buffer.get('isDirty'); // false
comment.get('isDirty'); // falseUsing bufferedHasMany.
// get a post from the store
const post = this.store.find('post', 1);
// get a buffer from the post
const buffer = post.buffer();
buffer.get('isBuffered'); // true
// has no comments
buffer.get('comments'); // []
// create a comment
buffer.createComment({body: 'Hello'});
// buffer status
buffer.get('comments'); // [comment]
buffer.get('hasDirtyComments'); // true
buffer.get('hasChangedComments'); // true
buffer.get('hasAddedComments'); // true
buffer.get('_addedComments'); // [comment]
// the bufffered post doesn't have changes
buffer.get('hasChanges'); // false
buffer.get('isDirty'); // false
// post status
post.get('comments'); // []
post.get('hasDirtyComments'); // false
// comment is a buffer too!
// internally, the mixin called comment.buffer();
const comment = buffer.get('comments.firstObject');
comment.get('isBuffered'); // true
// because it's a new comment, a new record was buffered
// so it initially matches the buffer
comment.get('isDirty'); // true
comment.get('content.isDirty'); // true
// comment doesn't have a postId or post
comment.get('content.postId'); // null
comment.get('content.post'); // null
// the buffered comment has a post
// but no postId
comment.get('post'); // post
comment.get('postId'); // null
// comment status
comment.get('isDirty'); // true
comment.get('hasChanges'); // true
comment.get('hasAddedPost'); // true
comment.get('_addedPost'); // post
// back to the post ...
// apply comment changes
buffer.applyCommentChanges();
// buffer status
buffer.get('hasDirtyComments'); // true
buffer.get('hasChangedComments'); // false
buffer.get('hasAddedComments'); // false
buffer.get('_addedComments'); // []
// post status
post.get('comments'); // [comment]
post.get('hasDirtyComments'); // true
// comment status
comment.get('isDirty'); // true
comment.get('hasChanges'); // false
comment.get('hasAddedPost'); // false
comment.get('_addedPost'); // null
comment.get('postId'); // 1
// save it
buffer.saveComments();
// ... after save completes
buffer.get('hasDirtyComments'); // false
post.get('hasDirtyComments'); // false
comment.get('isDirty'); // falseUsing bufferedHasManyThrough.
// get a post from the store
const post = this.store.find('post', 1);
// get a buffer from the post
const buffer = post.buffer();
buffer.get('isBuffered'); // true
// has no tags
buffer.get('tags'); // []
// create a tag
buffer.createTag({name: 'Hello'});
// buffer status
buffer.get('isDirty'); // false
buffer.get('hasChanges'); // false
buffer.get('tags'); // [tag]
buffer.get('postTags'); // [postTag]
buffer.get('_addedTags'); // [tag]
buffer.get('_addedPostTags'); // [postTag]
buffer.get('hasChangedTags'); // true
buffer.get('hasAddedTags'); // true
buffer.get('hasDirtyTags'); // true
buffer.get('hasChangedPostTags'); // true
buffer.get('hasAddedPostTags'); // true
buffer.get('hasDirtyPostTags'); // true
// post status
post.get('tags'); // []
post.get('postTags'); // []
post.get('isDirty'); // false
post.get('hasDirtyTags'); // false
post.get('hasDirtyPostTags'); // false
// postTag status
const postTag = bugger.get('postTags.firstObject');
postTag.get('isBuffered'); // true
postTag.get('isDirty'); // true
postTag.get('content.isDirty'); // true
postTag.get('hasChanges'); // true
postTag.get('hasAddedPost'); // true
postTag.get('hasAddedTag'); // true
postTag.get('post'); // post
postTag.get('tag'); // tag
postTag.get('_addedPost'); // post
postTag.get('_addedTag'); // tag
postTag.get('postId'); // null
postTag.get('tagId'); // null
// tag status
const tag = bugger.get('tags.firstObject');
tag.get('isBuffered'); // true
tag.get('isDirty'); // true
tag.get('content.isDirty'); // true
tag.get('hasChanges'); // true
tag.get('hasAddedPostTag'); // true
tag.get('postTag'); // postTag
tag.get('_addedPostTag'); // postTag
// apply tag changes
buffer.applyTagChanges();
// buffer status
buffer.get('hasChangedTags'); // false
buffer.get('hasAddedTags'); // false
buffer.get('hasDirtyTags'); // true
// post status
post.get('tags'); // [tag]
post.get('postTags'); // [postTag]
post.get('hasDirtyTags'); // true
post.get('hasDirtyPostTags'); // true
// postTag status
postTag.get('hasChanges'); // false
postTag.get('hasAddedPost'); // false
postTag.get('hasAddedTag'); // false
// tag status
tag.get('hasChanges'); // false
tag.get('hasAddedPostTag'); // false
// save it
buffer.saveTags();
// ... after save completes
post.get('hasDirtyTags'); // false
post.get('hasDirtyPostTags'); // false
postTag.get('isDirty'); // false
postTag.get('postId'); // 1
postTag.get('tagId'); // 1
tag.get('isDirty'); // falseaddItem(item)removeItem(item)removeItem()applyChanges()discardChanges()save()
itemhasRemovedItem_removedItemhasAddedItem_addedItem
addItem(item)createItem(props)removeItem(item)deleteItem(item)destroyItem(item)applyItemChanges()applyItemChanges(false)applyChanges()applyChanges(false)discardItemChanges()discardItemChanges(false)discardChanges()discardChanges(false)saveItems()saveItems(false)save()save(false)
note: Passing false to methods that support it will prevent cascading. Meaning that save(false) or applyChanges(false) will only apply directly to the record it is called on, not any child or parent relationships.
Maybe?
removeItems(items)deleteItems(items)destroyItems(items)clearItems()
itemshasDirtyItemshasRemovedItems_removedItemshasAddedItems_addedItems