-
Notifications
You must be signed in to change notification settings - Fork 422
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
can.Observe.Clone #435
can.Observe.Clone #435
Changes from all commits
1e6a3fb
86f8793
7c99171
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
steal("can/util", "can/observe", "can/construct/super", function(can) { | ||
|
||
/** | ||
* @class can.Observe.Clone | ||
* @extends can.Observe (and all subclasses) | ||
* | ||
* Create a copy of an observable that holds a reference | ||
* to the original, allowing the properties of the copy | ||
* to be merged back to the original later. | ||
* | ||
* Clones cook, look, and taste like their original objects | ||
* but can have their attributes set independently for later | ||
* merging back into the original object. | ||
*/ | ||
var Observe = can.Observe | ||
, oldsetup = can.Observe.setup | ||
, makeClone = function(obs) { | ||
|
||
//Don't make clones all the way down. | ||
// A clone's clone class is itself. | ||
if(obs.prototype.merge) | ||
return obs; | ||
|
||
return obs({ | ||
|
||
}, { | ||
setup : function(opts) { | ||
var that = this | ||
, orig = opts.original; | ||
Observe.prototype.setup.apply(this); | ||
can.extend(this, { | ||
_deep : opts.deep | ||
, _original : opts.original | ||
, _dirty : {} | ||
, isClone : true | ||
}); | ||
|
||
orig.each(function(val, key) { | ||
if(opts.deep && val instanceof Observe && !val._original) { | ||
that.attr(key, val.clone()); | ||
} else if(orig instanceof can.Model && key === orig.constructor.id) { | ||
that.attr(that.constructor.id, val); | ||
} else { | ||
that.attr(key, val); | ||
} | ||
}); | ||
this._dirty = {}; //reset dirty after we touched everything | ||
} | ||
|
||
, merge : function() { | ||
var that = this | ||
, orig = this._original; | ||
this.each(function(val, key) { | ||
var newVal = that[key]; | ||
if(that._deep && newVal.isClone) { | ||
newVal = newVal.merge(); | ||
} | ||
if(that._dirty[key]) { | ||
orig.attr(key, newVal); | ||
} | ||
delete that[key]; | ||
delete that._dirty[key]; | ||
}); | ||
delete this._original; | ||
return orig; | ||
} | ||
|
||
// set dirty for any attributes | ||
, attr : function(name, value) { | ||
if(arguments.length > 1 && typeof name !== "object") { | ||
this._dirty[name] = true; | ||
} | ||
return this._super.apply(this, arguments); | ||
} | ||
}); | ||
}; | ||
|
||
Observe.Clone = makeClone(Observe); | ||
|
||
Observe.setup = function(){ | ||
oldsetup.apply(this, arguments); | ||
this.Clone = makeClone(this); | ||
|
||
if(can.Model && this.prototype instanceof can.Model) { | ||
//don't want to have a collision in the Model store, so rename | ||
// the id field. | ||
this.Clone.id = "original_" + this.id; | ||
this.Clone.prototype.save = function() { | ||
var that = this; | ||
return this._original.save.apply(this, arguments).then(function(nv) { | ||
return that.merge().attr(nv._data ? nv._data : nv); | ||
}); | ||
}; | ||
//overriding serialize allows save to function with the renamed id field | ||
this.Clone.prototype.serialize = function() { | ||
var serial = this._original.serialize.apply(this, arguments); | ||
serial[this._original.constructor.id] = serial[this.constructor.id]; | ||
delete serial[this.constructor.id]; | ||
return serial; | ||
} | ||
} | ||
} | ||
/** | ||
@function clone | ||
|
||
Create a clone instance of the current object, constructed | ||
by the class's Clone member constructor | ||
|
||
@param deep true to make Clones of all sub-observables. | ||
*/ | ||
Observe.prototype.clone = function(deep) { | ||
return new this.constructor.Clone({original : this, deep : !!deep}); | ||
} | ||
|
||
return Observe; | ||
|
||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
@page can.Observe.Clone clone | ||
@parent can.Observe.plugins | ||
@plugin can/observe/clone | ||
@test can/observe/clone/test.html | ||
|
||
The __clone__ plugin allows you to make a temporary copy of a | ||
[can.Observe Observe] whose changed properties can be later merged | ||
back into the original. | ||
|
||
Clone any existing observe with | ||
<code>[can.Observe::clone clone]\(deep\)</code> : | ||
- set __deep__ to __true__ if you want to make clones out of all nested [can.Observe Observes], __false__ or __undefined__ to leave nested [can.Observe Observes] as is. | ||
|
||
The new [can.Observe.Clone Clone] observe has the same prototype and class | ||
properties as the original, as well as a <code>[can.Observe.Clone::merge merge]()</code> | ||
function that will merge all of the [can.Observe.Clone Clone's] | ||
|
||
// create an observable | ||
var observe = new can.Observe({ | ||
name : "Justin Meyer" | ||
, awesomeness : 9 | ||
}) | ||
var clone = observe.clone(); | ||
//original will not be changed yet | ||
clone.attr("name", "Mustin Jeyer"); | ||
clone.name //-> "Mustin Jeyer" | ||
|
||
observe.name //-> "Justin Meyer" | ||
observe.attr("awesomeness", 10); //will not be overwritten if not explicitly set by clone | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if clone does explicitly set "awesomeness"? Does .merge overwrite changes to the "source" observe? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. The merge strategy is "theirs", i.e. the foreign Clone object being merged in has priority when the value for a particular key has been changed on both sides. To do anything else would require dirty checking on the original Map, which I didn't think was appropriate, especially since one Map object can have multiple Clones. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ...unless the Clone was listening to changes on the original Map. Then it could be responsible for all dirty checks (on itself and on its source) over its lifecycle. |
||
|
||
clone.merge(); | ||
|
||
observe.name //-> "Mustin Jeyer" | ||
observe.awesomeness //-> 10 | ||
|
||
If the original [can.Observe Observe] is a [can.Model Model] instance, the | ||
[can.Observe.Clone Clone] overrides <code>[can.Model::save save]()</code> to write itself to | ||
the server and, on success, merge the result back into the original instance. | ||
|
||
var model = new can.Model({ foo : "bar" }); | ||
var model_clone = model.clone(); | ||
model_clone.attr("foo", "baz").save().done(function() { | ||
model.foo //-> "baz" | ||
}) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
(function() { | ||
|
||
module("can/observe/clone",{ | ||
setup : function(){ | ||
} | ||
}); | ||
|
||
test("Clone creates instance of observe subclass", function() { | ||
|
||
var foo = can.Observe({}) | ||
, f = new foo() | ||
, c = f.clone(); | ||
|
||
ok(c instanceof foo); | ||
ok(c instanceof foo.Clone); | ||
ok(c.isClone); | ||
}); | ||
|
||
test("Clone attribute changes do not (initially) affect original", function() { | ||
var foo = can.Observe({}) | ||
, f = new foo({bar : "baz"}) | ||
, c = f.clone(); | ||
|
||
c.attr("bar", "quux"); | ||
equal(c.bar, "quux"); | ||
equal(f.bar, "baz"); | ||
}); | ||
|
||
test("Clone attribute changes merge back to original; unchanged ones left alone", function() { | ||
var foo = can.Observe({}) | ||
, f = new foo({bar : "baz", quux : "thud"}) | ||
, c = f.clone(); | ||
|
||
c.attr("bar", "jeek"); | ||
f.attr("quux", "plonk"); | ||
c.merge(); | ||
equal(f.bar, "jeek"); | ||
equal(f.quux, "plonk"); | ||
}); | ||
|
||
test("Deep cloning", function() { | ||
var foo = can.Observe({}) | ||
, bar = can.Observe({}) | ||
, f = new foo({ baz : new bar({ quux : "thud"}) }) | ||
, c = f.clone(true); | ||
|
||
ok(c._deep); | ||
ok(c.baz instanceof bar.Clone); | ||
c.baz.attr("quux", "jeek"); | ||
c.merge(); | ||
ok(f.baz instanceof bar); | ||
equal(f.baz.quux, "jeek"); | ||
}); | ||
|
||
test("can.Model.Clone saving", function() { | ||
|
||
var foo = can.Model({ | ||
makeCreate : function() { | ||
return function(attrs) { | ||
return can.when({id : 2, bar : "quux"}); | ||
} | ||
} | ||
, makeUpdate : function() { | ||
return function(id, attrs) { | ||
equal(id, 1); | ||
return can.when({id : 1, bar : "thud"}); | ||
} | ||
} | ||
}, {}) | ||
, f = new foo({ bar : "baz"}) | ||
, g = new foo({ id : 1, bar : "baz"}) | ||
, c = f.clone() | ||
, d = g.clone(); | ||
|
||
equal(d.original_id, 1); | ||
ok(!d.id); | ||
|
||
c.save().done(function() { | ||
equal(f.id, 2); | ||
equal(f.bar, "quux"); | ||
}); | ||
|
||
d.save().done(function() { | ||
equal(g.id, 1); | ||
equal(g.bar, "thud"); | ||
}); | ||
|
||
}); | ||
|
||
})(); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
<!DOCTYPE HTML> | ||
<html> | ||
<head> | ||
<link rel="stylesheet" type="text/css" href="../../lib/qunit/qunit.css"/> | ||
</head> | ||
<body> | ||
<h1 id="qunit-header">can.Observe Test Suite</h1> | ||
|
||
<h2 id="qunit-banner"></h2> | ||
|
||
<div id="qunit-testrunner-toolbar"></div> | ||
<h2 id="qunit-userAgent"></h2> | ||
<ol id="qunit-tests"></ol> | ||
<div id="qunit-test-area"></div> | ||
|
||
<script type="text/javascript" src="../../lib/steal/steal.js"></script> | ||
<script type="text/javascript" src="../../lib/qunit/qunit.js"></script> | ||
<script type="text/javascript"> | ||
QUnit.config.autostart = false; | ||
steal(function() { | ||
steal.config({ | ||
root: '../../' | ||
}); | ||
}, "can/observe/clone", "can/model").then("can/test", "can/observe/clone/clone_test.js", function() { | ||
QUnit.start(); | ||
}); | ||
</script> | ||
</body> | ||
</html> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's likely possible to only define the Clone "class" on demand instead of every time a can.Observe "class" is defined.
Could this check if
this.constructor.Clone
exists and create it.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is possible, but it will generate a TypeError if someone attempts to do an
instanceof
check against the nonexistent Clone class if the check happens before the first cloning creates the class.