Skip to content
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

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
117 changes: 117 additions & 0 deletions observe/clone/clone.js
@@ -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});
Copy link
Contributor

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.

Copy link
Contributor

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.

}

return Observe;

});
45 changes: 45 additions & 0 deletions observe/clone/clone.md
@@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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"
})

90 changes: 90 additions & 0 deletions observe/clone/clone_test.js
@@ -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");
});

});

})();
29 changes: 29 additions & 0 deletions observe/clone/test.html
@@ -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>
1 change: 1 addition & 0 deletions test/build/dojo.html
Expand Up @@ -48,6 +48,7 @@ <h2 id="qunit-userAgent"></h2>
<script type="text/javascript" src="observe/attributes/attributes_test.js"></script>
<script type="text/javascript" src="observe/validations/validations_test.js"></script>
<script type="text/javascript" src="observe/backup/backup_test.js"></script>
<script type="text/javascript" src="observe/clone/clone_test.js"></script>
</body>

</html>
1 change: 1 addition & 0 deletions test/build/jquery.html
Expand Up @@ -50,6 +50,7 @@ <h2 id="qunit-userAgent"></h2>
<script type="text/javascript" src="observe/attributes/attributes_test.js"></script>
<script type="text/javascript" src="observe/validations/validations_test.js"></script>
<script type="text/javascript" src="observe/backup/backup_test.js"></script>
<script type="text/javascript" src="observe/clone/clone_test.js"></script>
<script type="text/javascript" src="control/plugin/plugin_test.js"></script>
<script type="text/javascript" src="view/modifiers/modifiers_test.js"></script>
</body>
Expand Down
1 change: 1 addition & 0 deletions test/build/mootools.html
Expand Up @@ -48,6 +48,7 @@ <h2 id="qunit-userAgent"></h2>
<script type="text/javascript" src="observe/attributes/attributes_test.js"></script>
<script type="text/javascript" src="observe/validations/validations_test.js"></script>
<script type="text/javascript" src="observe/backup/backup_test.js"></script>
<script type="text/javascript" src="observe/clone/clone_test.js"></script>
</body>

</html>
1 change: 1 addition & 0 deletions test/build/yui.html
Expand Up @@ -48,6 +48,7 @@ <h2 id="qunit-userAgent"></h2>
<script type="text/javascript" src="observe/attributes/attributes_test.js"></script>
<script type="text/javascript" src="observe/validations/validations_test.js"></script>
<script type="text/javascript" src="observe/backup/backup_test.js"></script>
<script type="text/javascript" src="observe/clone/clone_test.js"></script>
</body>

</html>
1 change: 1 addition & 0 deletions test/build/zepto.html
Expand Up @@ -48,6 +48,7 @@ <h2 id="qunit-userAgent"></h2>
<script type="text/javascript" src="observe/attributes/attributes_test.js"></script>
<script type="text/javascript" src="observe/validations/validations_test.js"></script>
<script type="text/javascript" src="observe/backup/backup_test.js"></script>
<script type="text/javascript" src="observe/clone/clone_test.js"></script>
</body>

</html>
3 changes: 2 additions & 1 deletion test/dojo.html
Expand Up @@ -43,7 +43,7 @@ <h2 id="qunit-userAgent"></h2>
"can/construct/super", "can/construct/proxy",
"can/observe/delegate", "can/observe/setter",
"can/observe/attributes", "can/observe/validations",
"can/observe/backup", "can/util/object",
"can/observe/backup", "can/observe/clone", "can/util/object",
"can/util/string", "can/util/fixture")
.then("can/test").then(
"can/construct/construct_test.js")
Expand All @@ -64,6 +64,7 @@ <h2 id="qunit-userAgent"></h2>
.then("can/observe/attributes/attributes_test.js")
.then("can/observe/validations/validations_test.js")
.then("can/observe/backup/backup_test.js")
.then("can/observe/clone/clone_test.js")
.then("can/util/object/object_test.js")
.then("can/util/string/string_test.js")
.then("can/util/fixture/fixture_test.js", function() {
Expand Down
3 changes: 2 additions & 1 deletion test/jquery.html
Expand Up @@ -48,7 +48,7 @@ <h2 id="qunit-userAgent"></h2>
"can/construct/super", "can/construct/proxy",
"can/observe/delegate", "can/observe/setter",
"can/observe/attributes", "can/observe/validations",
"can/observe/backup", "can/control/plugin",
"can/observe/backup", "can/observe/clone", "can/control/plugin",
"can/view/modifiers", "can/util/object",
"can/util/string", "can/util/fixture")
.then("can/test").then(
Expand All @@ -70,6 +70,7 @@ <h2 id="qunit-userAgent"></h2>
.then("can/observe/attributes/attributes_test.js")
.then("can/observe/validations/validations_test.js")
.then("can/observe/backup/backup_test.js")
.then("can/observe/clone/clone_test.js")
.then("can/control/plugin/plugin_test.js")
.then("can/view/modifiers/modifiers_test.js")
.then("can/util/object/object_test.js")
Expand Down
3 changes: 2 additions & 1 deletion test/mootools.html
Expand Up @@ -43,7 +43,7 @@ <h2 id="qunit-userAgent"></h2>
"can/construct/super", "can/construct/proxy",
"can/observe/delegate", "can/observe/setter",
"can/observe/attributes", "can/observe/validations",
"can/observe/backup", "can/util/object",
"can/observe/backup", "can/observe/clone", "can/util/object",
"can/util/string", "can/util/fixture")
.then("can/test").then(
"can/construct/construct_test.js")
Expand All @@ -64,6 +64,7 @@ <h2 id="qunit-userAgent"></h2>
.then("can/observe/attributes/attributes_test.js")
.then("can/observe/validations/validations_test.js")
.then("can/observe/backup/backup_test.js")
.then("can/observe/clone/clone_test.js")
.then("can/util/object/object_test.js")
.then("can/util/string/string_test.js")
.then("can/util/fixture/fixture_test.js", function() {
Expand Down
3 changes: 2 additions & 1 deletion test/yui.html
Expand Up @@ -43,7 +43,7 @@ <h2 id="qunit-userAgent"></h2>
"can/construct/super", "can/construct/proxy",
"can/observe/delegate", "can/observe/setter",
"can/observe/attributes", "can/observe/validations",
"can/observe/backup", "can/util/object",
"can/observe/backup", "can/observe/clone", "can/util/object",
"can/util/string", "can/util/fixture")
.then("can/test").then(
"can/construct/construct_test.js")
Expand All @@ -64,6 +64,7 @@ <h2 id="qunit-userAgent"></h2>
.then("can/observe/attributes/attributes_test.js")
.then("can/observe/validations/validations_test.js")
.then("can/observe/backup/backup_test.js")
.then("can/observe/clone/clone_test.js")
.then("can/util/object/object_test.js")
.then("can/util/string/string_test.js")
.then("can/util/fixture/fixture_test.js", function() {
Expand Down