diff --git a/platform/representation/src/MCTRepresentation.js b/platform/representation/src/MCTRepresentation.js index 10b6d3ccec..d1937389d2 100644 --- a/platform/representation/src/MCTRepresentation.js +++ b/platform/representation/src/MCTRepresentation.js @@ -31,7 +31,6 @@ define( function () { "use strict"; - /** * Defines the mct-representation directive. This may be used to * present domain objects as HTML (with event wiring), with the @@ -96,6 +95,9 @@ define( }), toClear = [], // Properties to clear out of scope on change counter = 0, + couldRepresent = false, + lastId, + lastKey, changeTemplate = templateLinker.link($scope, element); // Populate scope with any capabilities indicated by the @@ -141,6 +143,13 @@ define( }); } + function unchanged(canRepresent, id, key) { + return canRepresent && + couldRepresent && + id === lastId && + key === lastKey; + } + // General-purpose refresh mechanism; should set up the scope // as appropriate for current representation key and // domain object. @@ -149,7 +158,13 @@ define( representation = lookup($scope.key, domainObject), path = representation && getPath(representation), uses = ((representation || {}).uses || []), - canRepresent = !!(path && domainObject); + canRepresent = !!(path && domainObject), + id = domainObject && domainObject.getId(), + key = $scope.key; + + if (unchanged(canRepresent, id, key)) { + return; + } // Create an empty object named "representation", for this // representation to store local variables into. @@ -173,6 +188,11 @@ define( delete $scope[property]; }); + // To allow simplified change detection next time around + couldRepresent = canRepresent; + lastId = id; + lastKey = key; + // Populate scope with fields associated with the current // domain object (if one has been passed in) if (canRepresent) { diff --git a/platform/representation/src/TemplateLinker.js b/platform/representation/src/TemplateLinker.js index 426bbbe318..14d0aa041d 100644 --- a/platform/representation/src/TemplateLinker.js +++ b/platform/representation/src/TemplateLinker.js @@ -92,10 +92,19 @@ define( var activeElement = element, activeTemplateUrl, comment = this.$compile('')(scope), + activeScope, self = this; + function destroyScope() { + if (activeScope) { + activeScope.$destroy(); + activeScope = undefined; + } + } + function removeElement() { if (activeElement !== comment) { + destroyScope(); activeElement.replaceWith(comment); activeElement = comment; } @@ -110,8 +119,10 @@ define( } function populateElement(template) { - element.empty(); - element.append(self.$compile(template)(scope)); + destroyScope(); + activeScope = scope.$new(false); + element.html(template); + self.$compile(element.contents())(activeScope); } function badTemplate(templateUrl) { @@ -120,22 +131,21 @@ define( } function changeTemplate(templateUrl) { - if (templateUrl !== activeTemplateUrl) { - if (templateUrl) { - addElement(); - self.load(templateUrl).then(function (template) { - // Avoid race conditions - if (templateUrl === activeTemplateUrl) { - populateElement(template); - } - }, function () { - badTemplate(templateUrl); - }); - } else { - removeElement(); - } - activeTemplateUrl = templateUrl; + if (templateUrl) { + destroyScope(); + addElement(); + self.load(templateUrl).then(function (template) { + // Avoid race conditions + if (templateUrl === activeTemplateUrl) { + populateElement(template); + } + }, function () { + badTemplate(templateUrl); + }); + } else { + removeElement(); } + activeTemplateUrl = templateUrl; } if (templateUrl) { diff --git a/platform/representation/test/TemplateLinkerSpec.js b/platform/representation/test/TemplateLinkerSpec.js index bf762fba53..726e4deab1 100644 --- a/platform/representation/test/TemplateLinkerSpec.js +++ b/platform/representation/test/TemplateLinkerSpec.js @@ -27,7 +27,8 @@ define( function (TemplateLinker) { 'use strict'; - var JQLITE_METHODS = [ 'replaceWith', 'empty', 'append' ]; + var JQLITE_METHODS = [ 'replaceWith', 'empty', 'html', 'contents' ], + SCOPE_METHODS = [ '$on', '$new', '$destroy' ]; describe("TemplateLinker", function () { var mockTemplateRequest, @@ -38,6 +39,8 @@ define( mockElement, mockTemplates, mockElements, + mockContents, + mockNewScope, mockPromise, linker; @@ -46,14 +49,18 @@ define( mockSce = jasmine.createSpyObj('$sce', ['trustAsResourceUrl']); mockCompile = jasmine.createSpy('$compile'); mockLog = jasmine.createSpyObj('$log', ['error', 'warn']); - mockScope = jasmine.createSpyObj('$scope', ['$on']); + mockScope = jasmine.createSpyObj('$scope', SCOPE_METHODS); + mockNewScope = jasmine.createSpyObj('$scope', SCOPE_METHODS); mockElement = jasmine.createSpyObj('element', JQLITE_METHODS); mockPromise = jasmine.createSpyObj('promise', ['then']); mockTemplates = {}; mockElements = {}; + mockContents = {}; mockTemplateRequest.andReturn(mockPromise); - mockCompile.andCallFake(function (html) { + mockCompile.andCallFake(function (toCompile) { + var html = typeof toCompile === 'string' ? + toCompile : toCompile.testHtml; mockTemplates[html] = jasmine.createSpy('template'); mockElements[html] = jasmine.createSpyObj('templateEl', JQLITE_METHODS); @@ -63,6 +70,17 @@ define( mockSce.trustAsResourceUrl.andCallFake(function (url) { return { trusted: url }; }); + mockScope.$new.andReturn(mockNewScope); + mockElement.html.andCallFake(function (html) { + mockContents[html] = + jasmine.createSpyObj('contentsEl', JQLITE_METHODS); + mockContents[html].testHtml = html; + }); + mockElement.contents.andCallFake(function () { + return mockContents[ + mockElement.html.mostRecentCall.args[0] + ]; + }); linker = new TemplateLinker( mockTemplateRequest, @@ -131,10 +149,11 @@ define( }, false); }); - it("compiles loaded templates with linked scope", function () { - expect(mockCompile).toHaveBeenCalledWith(testTemplate); + it("compiles element contents with a new scope", function () { + expect(mockCompile) + .toHaveBeenCalledWith(mockContents[testTemplate]); expect(mockTemplates[testTemplate]) - .toHaveBeenCalledWith(mockScope); + .toHaveBeenCalledWith(mockNewScope); }); it("replaces comments with specified element", function () { @@ -142,9 +161,9 @@ define( .toHaveBeenCalledWith(mockElement); }); - it("appends rendered content to the specified element", function () { - expect(mockElement.append) - .toHaveBeenCalledWith(mockElements[testTemplate]); + it("inserts HTML content into the specified element", function () { + expect(mockElement.html) + .toHaveBeenCalledWith(testTemplate); }); it("clears templates when called with undefined", function () {