From 0c5fee07a7eb7cf0fe4efa4ca87ba3f5aaf10b00 Mon Sep 17 00:00:00 2001
From: anchann <anchannnnnnnn@gmail.com>
Date: Mon, 27 Jul 2015 15:56:10 +0900
Subject: [PATCH] In rt-scope subsequent aliases should be able to reference
 preceding ones

Rationale: It is typical to want to alias multiple subtrees of a nested
structure in a single rt-scope statement, like so:

<div rt-scope="this.props.user as user; user.profile as profile;
user.friends as friends">

This was not possible because of the function parameter based
implementation of rt-scope. I tweaked it a bit to use var declaration
instead, while preserving the parameter-based passing for all child
scopes.
---
 .gitignore                                  |  2 ++
 src/reactTemplates.js                       | 27 ++++++++++++++++-----
 test/data/scope-variable-references.rt      | 11 +++++++++
 test/data/scope-variable-references.rt.html |  1 +
 test/src/test.js                            | 11 +++++++--
 5 files changed, 44 insertions(+), 8 deletions(-)
 create mode 100644 test/data/scope-variable-references.rt
 create mode 100644 test/data/scope-variable-references.rt.html

diff --git a/.gitignore b/.gitignore
index caf1810..5c04a5b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,3 +20,5 @@ npm-debug.log
 /target
 /coverage
 
+### generated code output ###
+test/data/*.code.js
diff --git a/src/reactTemplates.js b/src/reactTemplates.js
index 47557d7..fc9c429 100644
--- a/src/reactTemplates.js
+++ b/src/reactTemplates.js
@@ -287,11 +287,16 @@ function convertHtmlToReact(node, context) {
 
         var data = {name: convertTagNameToConstructor(node.name, context)};
         if (node.attribs[scopeProp]) {
-            data.scopeMapping = {};
             data.scopeName = '';
+
+            // these are variables that were already in scope, unrelated to the ones declared in rt-scope
+            data.outerScopeMapping = {};
             _.each(context.boundParams, function (boundParam) {
-                data.scopeMapping[boundParam] = boundParam;
+                data.outerScopeMapping[boundParam] = boundParam;
             });
+
+            // these are variables declared in the rt-scope attribute
+            data.innerScopeMapping = {};
             _.each(node.attribs[scopeProp].split(';'), function (scopePart) {
                 var scopeSubParts = scopePart.split(' as ');
                 if (scopeSubParts.length < 2) {
@@ -299,10 +304,15 @@ function convertHtmlToReact(node, context) {
                 }
                 var scopeName = scopeSubParts[1].trim();
                 validateJS(scopeName, node, context);
+
+                // this adds both parameters to the list of parameters passed further down
+                // the scope chain, as well as variables that are locally bound before any
+                // function call, as with the ones we generate for rt-scope.
                 stringUtils.addIfMissing(context.boundParams, scopeName);
+
                 data.scopeName += stringUtils.capitalize(scopeName);
-                data.scopeMapping[scopeName] = scopeSubParts[0].trim();
-                validateJS(data.scopeMapping[scopeName], node, context);
+                data.innerScopeMapping[scopeName] = scopeSubParts[0].trim();
+                validateJS(data.innerScopeMapping[scopeName], node, context);
             });
         }
 
@@ -357,8 +367,13 @@ function convertHtmlToReact(node, context) {
             data.body = ifTemplate(data);
         }
         if (node.attribs[scopeProp]) {
-            var generatedFuncName = generateInjectedFunc(context, 'scope' + data.scopeName, 'return ' + data.body, _.keys(data.scopeMapping));
-            data.body = generatedFuncName + '.apply(this, [' + _.values(data.scopeMapping).join(',') + '])';
+            var scopeVarDeclarations = _.reduce(data.innerScopeMapping, function(acc, rightHandSide, leftHandSide) {
+                var declaration = "var " + leftHandSide + " = " + rightHandSide + ";"
+                return acc + declaration;
+            }, "");
+            var functionBody = scopeVarDeclarations + 'return ' + data.body;
+            var generatedFuncName = generateInjectedFunc(context, 'scope' + data.scopeName, functionBody, _.keys(data.outerScopeMapping));
+            data.body = generatedFuncName + '.apply(this, [' + _.values(data.outerScopeMapping).join(',') + '])';
         }
         return data.body;
     } else if (node.type === 'comment') {
diff --git a/test/data/scope-variable-references.rt b/test/data/scope-variable-references.rt
new file mode 100644
index 0000000..929caaf
--- /dev/null
+++ b/test/data/scope-variable-references.rt
@@ -0,0 +1,11 @@
+<div>
+	<div rt-repeat="foo in [1]">
+		<!-- can't seem to have an object literal here; will deal with this separately -->
+		<div rt-scope="[{first: 'Jack', last: 'Sparrow', skills: ['talking', 'fighting', 'commandeering']}][0] as pirate; pirate.first as first">
+			<h1>{first} {pirate.last}</h1>
+			<div rt-scope="pirate.skills as skills">
+				<span rt-repeat="skill in skills">{skill}</span>
+			</div>
+		</div>
+	</div>
+</div>
diff --git a/test/data/scope-variable-references.rt.html b/test/data/scope-variable-references.rt.html
new file mode 100644
index 0000000..8e57fd2
--- /dev/null
+++ b/test/data/scope-variable-references.rt.html
@@ -0,0 +1 @@
+<div><div><div><h1>Jack Sparrow</h1><div><span>talking</span><span>fighting</span><span>commandeering</span></div></div></div></div>
diff --git a/test/src/test.js b/test/src/test.js
index cfd3865..54734a1 100644
--- a/test/src/test.js
+++ b/test/src/test.js
@@ -119,12 +119,15 @@ test('conversion test', function (t) {
  * @param {string} actual
  * @param {string} expected
  * @param {string} filename
+ * @return {boolean} whether actual is equal to expected
  */
 function compareAndWrite(t, actual, expected, filename) {
     t.equal(actual, expected);
     if (actual !== expected) {
         fs.writeFileSync(filename + '.actual.js', actual);
+        return false;
     }
+    return true;
 }
 
 test('convert div with all module types', function (t) {
@@ -175,7 +178,7 @@ function normalizeHtml(html) {
 }
 
 test('html tests', function (t) {
-    var files = ['scope.rt', 'lambda.rt', 'eval.rt', 'props.rt', 'custom-element.rt', 'style.rt', 'concat.rt', 'js-in-attr.rt', 'props-class.rt', 'rt-class.rt'];
+    var files = ['scope.rt', 'scope-variable-references.rt', 'lambda.rt', 'eval.rt', 'props.rt', 'custom-element.rt', 'style.rt', 'concat.rt', 'js-in-attr.rt', 'props-class.rt', 'rt-class.rt'];
     t.plan(files.length);
 
     files.forEach(check);
@@ -202,9 +205,13 @@ test('html tests', function (t) {
             var actual = React.renderToStaticMarkup(comp());
             actual = normalizeHtml(actual);
             expected = normalizeHtml(expected);
-            compareAndWrite(t, actual, expected, filename);
+            var equal = compareAndWrite(t, actual, expected, filename);
+            if (!equal) {
+                fs.writeFileSync(filename + '.code.js', code);
+            }
         } catch (e) {
             console.log(testFile, e);
+            fs.writeFileSync(filename + '.code.js', code);
         }
     }
 });