Compare commits

...

46 Commits

Author SHA1 Message Date
ea21cd7ccf [Search] Initialize ngModel if not present
...and fix a typo while we're at it.
2016-03-10 11:45:34 -08:00
d1f74677a2 Merge branch 'open314' into open315 2016-03-10 11:42:16 -08:00
8a783d4a87 Merge branch 'open313' into open315
Conflicts:
	platform/representation/test/MCTRepresentationSpec.js
2016-03-10 11:42:10 -08:00
342a6b4579 Merge branch 'open311' into open315
Conflicts:
	platform/commonUI/general/res/templates/label.html
	platform/commonUI/general/res/templates/tree-node.html
	platform/representation/test/suite.json
2016-03-10 11:41:12 -08:00
7fb45888d4 Merge branch 'master' into open313
Conflicts:
	platform/representation/test/MCTRepresentationSpec.js
	platform/representation/test/suite.json
2016-03-10 11:37:24 -08:00
173be63844 Merge branch 'master' into open314
Conflicts:
	platform/representation/test/suite.json
2016-03-10 11:35:35 -08:00
9df54e875f [Common UI] Restore mct-refresh usage
...unintentionally overwritten during resolution of merge
conflicts.
2016-03-10 11:33:50 -08:00
c500d0b84e Merge branch 'master' into open311
Conflicts:
	platform/commonUI/general/bundle.json
	platform/commonUI/general/res/templates/label.html
	test-main.js
2016-03-10 11:21:36 -08:00
1c0be97a6d [Performance] Refresh tree on expansion changes 2015-12-04 09:28:51 -08:00
0d06fe3c07 [Performance] Trigger refresh on expand/collapse 2015-12-03 17:03:45 -08:00
0946e82dd1 [Performance] Begin refactoring tree-node
...to remove watches.
2015-12-03 16:37:04 -08:00
c7b094f26e [Performance] One-time bind from subtree template 2015-12-03 15:38:02 -08:00
e898fee504 [Performance] One-time bind from tree template 2015-12-03 15:36:06 -08:00
c02d1ae902 [Tree] One-time bind remaining expression in label
https://github.com/nasa/openmctweb/issues/315
2015-12-03 15:27:45 -08:00
3f80951ba9 Merge remote-tracking branch 'github/open313' into open315
Conflicts:
	platform/representation/test/MCTRepresentationSpec.js
2015-12-03 15:21:00 -08:00
deb4314dce Merge remote-tracking branch 'github/open314' into open315 2015-12-03 15:17:54 -08:00
b0812bf8e3 Merge remote-tracking branch 'github/open311' into open315 2015-12-03 15:17:35 -08:00
4fea6b932c [Tests] Increase timeout for Require
...for cases when continuous integration server is slow to
load test scripts.
2015-12-02 16:08:04 -08:00
6a7727e1c6 [Tests] Increase timeout for Require
...for cases when continuous integration server is slow to
load test scripts.
2015-12-02 16:04:12 -08:00
de136bc19a [Common UI] Allow angular in karma-run tests 2015-12-02 15:59:54 -08:00
404327e583 [Representation] Add note about deprecation of ng-model 2015-12-02 15:46:26 -08:00
e12e0d72da [Representation] Utilize mct-model
Include one utilization of mct-model to verify equivalence
with ng-model (in the context of representations)
2015-12-02 15:41:48 -08:00
4eb17342f6 [Representation] Ignore unspecified attributes
...when watching or otherwise one-way-binding to attributes.
2015-12-02 15:39:27 -08:00
e453868994 [Representation] Bind mct-model without aliasing 2015-12-02 15:38:17 -08:00
111ebe83da [Representation] Update developer guide
...to use mct-model instead of ng-model.
2015-12-02 15:36:54 -08:00
a16cb16c31 [Representation] Update JSDoc
...to reflect transition from ng-model to mct-model.
2015-12-02 15:24:21 -08:00
b5fb2176e9 [Representation] Update specs
...to verify that both mct-model and ng-model are used.
2015-12-02 15:16:45 -08:00
70e11d66e1 [Representation] Use mct-model instead of ng-model
...to avoid extraneous watches introduced by ng-model.

https://github.com/nasa/openmctweb/issues/313
2015-12-02 14:52:37 -08:00
a039b9b5fe [Representation] Test OneWayBinder 2015-12-02 14:36:26 -08:00
5fdffee9a5 [Representation] Add spec for OneWayBinder
...with an initial test case.
2015-12-02 14:22:18 -08:00
1781e9be32 [Representation] Update spec for mct-representation
...to reflect changes to data binding.
2015-12-02 14:11:39 -08:00
756e445a80 [Representation] Update spec for mct-include
...to reflect changes to data binding.
2015-12-02 13:58:39 -08:00
b450d36472 [Representation] Add JSDoc to OneWayBinder 2015-12-02 13:23:46 -08:00
b5d2949b8f [Representation] Handle anonymous objects
Set objectEquality to true for anonymous objects, such that
they are compared using angular.equals instead of tested for
reference equality. This avoids redundant firing of watches
when values within the anonymous object are unchanged.
2015-12-02 13:14:19 -08:00
c6eb07a810 [Representation] Remove redundant watches 2015-12-02 13:07:37 -08:00
63ce7349e3 [Representation] Use OneWayBinder from mct-representation 2015-12-02 13:04:51 -08:00
add4e22cd3 [Representation] Use OneWayBinder from mct-include 2015-12-02 12:56:01 -08:00
40bd04f455 [Representation] Add one-way binder
...to handle common one-way binding behavior shared by
both mct-include and mct-representation.
2015-12-02 12:52:13 -08:00
4fee1ee153 [Representation] Do isolate mct-include's scope
...even though we are not using this to pass any state
from attributes inward.
2015-12-02 12:24:26 -08:00
cfb6d4ccbf [Representation] Pass attributes one way
Instead of two-way binding attributes, only pass them in
from the parent scope. This permits users of mct-include
to use one-time binding to reduce watch counts (two-way
binding across an isolate scope does not permit this.)

https://github.com/nasa/openmctweb/issues/314
2015-12-02 12:22:37 -08:00
50db3287db [Common UI] Add spec for mct-refresh 2015-12-02 11:37:22 -08:00
4a3ecf1435 [Common UI] Restrict mct-refresh to attributes 2015-12-02 11:37:06 -08:00
c24c3d4534 [Common UI] Check value of mct-refresh more closely 2015-12-02 11:23:30 -08:00
d6d57a396a [Common UI] Initially utilize mct-refresh
Add one usage of mct-refresh (in the tree, where it will
ultimately be most relevant.)
2015-12-02 11:17:56 -08:00
16781b6156 [Common UI] Expose mct-refresh directive 2015-12-02 11:17:14 -08:00
24e1c1ff8c [Common UI] Add mct-refresh directive
...to allow explicit refresh of sections of templates,
allowing watches to be removed.

https://github.com/nasa/openmctweb/issues/311
2015-12-02 11:09:22 -08:00
18 changed files with 753 additions and 130 deletions

View File

@ -1113,9 +1113,9 @@ contents of this object are managed entirely by the view/representation which
receives it.
* `representation`: An empty object, useful as a 'scratch pad' for
representation state.
* `ngModel`: An object passed through the ng-model attribute of the
* `mctModel`: An object passed through the `mct-model` attribute of the
`mct-representation` , if any.
* `parameters`: An object passed through the parameters attribute of the
* `parameters`: An object passed through the `parameters` attribute of the
`mct-representation`, if any.
* Any capabilities requested by the uses property of the representation
definition.
@ -1507,10 +1507,12 @@ attributes, all of which are specified as Angular expressions:
* `key`: Machine-readable identifier for the template (of extension category
templates ) to be displayed.
* `ng-model`: _Optional_; will be passed into the template's scope as `ngModel`.
Intended usage is for two-way bound user input.
* `mct-model`: _Optional_; will be passed into the template's scope as `mctModel`.
Intended usage is for modification by the template.
Note that this value will _not_ be two-way bound, so bi-directional
communication should be achieved by modifying _properties_ on the object.
* `parameters`: _Optional_; will be passed into the template's scope as
parameters. Intended usage is for template-specific display parameters.
`parameters`. Intended usage is for template-specific display parameters.
## Representation
@ -1522,11 +1524,14 @@ attributes, all of which are specified as Angular expressions:
* `key`: Machine-readable identifier for the representation (of extension
category _representations_ or _views_ ) to be displayed.
* `mct-object`: The domain object being represented.
* `ng-model`: Optional; will be passed into the template's scope as `ngModel`.
Intended usage is for two-way bound user input.
* `parameters`: Optional; will be passed into the template's scope as
parameters . Intended usage is for template-specific display parameters.
* `mct-object`: The domain object being represented. Will be available in the
representation's scope as `domainObject`.
* `mct-model`: Optional; will be passed into the template's scope as `mctModel`.
Intended usage is for modification by the template.
Note that this value will _not_ be two-way bound, so bi-directional
communication should be achieved by modifying _properties_ on the object.
* `parameters`: Optional; will be passed into the representation's scope as
`parameters`. Intended usage is for representation-specific display parameters.
## Resize

View File

@ -44,6 +44,7 @@ define([
"./src/directives/MCTContainer",
"./src/directives/MCTDrag",
"./src/directives/MCTClickElsewhere",
"./src/directives/MCTRefresh",
"./src/directives/MCTResize",
"./src/directives/MCTPopup",
"./src/directives/MCTScroll",
@ -92,6 +93,7 @@ define([
MCTContainer,
MCTDrag,
MCTClickElsewhere,
MCTRefresh,
MCTResize,
MCTPopup,
MCTScroll,
@ -344,6 +346,10 @@ define([
"$document"
]
},
{
"key": "mctRefresh",
"implementation": MCTRefresh
},
{
"key": "mctResize",
"implementation": MCTResize,

View File

@ -19,9 +19,10 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<div class="t-object-label l-flex-row flex-elem grows">
<div class="t-item-icon flex-elem" ng-class="{ 'l-icon-link':location.isLink() }">
<div class="t-item-icon-glyph">{{type.getGlyph()}}</div>
<div class="t-object-label l-flex-row flex-elem grows"
mct-refresh="domainObject.getCapability('mutation').listen(callback)">
<div class="t-item-icon flex-elem" ng-class="::{ 'l-icon-link':location.isLink() }">
<div class="t-item-icon-glyph">{{::type.getGlyph()}}</div>
</div>
<div class='t-title-label flex-elem grows'>{{model.name}}</div>
<div class='t-title-label flex-elem grows'>{{::model.name}}</div>
</div>

View File

@ -19,18 +19,19 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<ul class="tree">
<li ng-if="!composition">
<ul class="tree"
mct-refresh="domainObject.getCapability('mutation').listen(callback)">
<li ng-hide="::composition">
<span class="tree-item">
<span class="icon wait-spinner"></span>
<span class="title-label">Loading...</span>
</span>
</li>
<li ng-repeat="child in composition">
<li ng-repeat="child in ::composition">
<mct-representation key="'tree-node'"
mct-object="child"
parameters="parameters"
ng-model="ngModel">
mct-object="::child"
parameters="::parameters"
mct-model="::mctModel">
</mct-representation>
</li>
</ul>

View File

@ -19,15 +19,15 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<span ng-controller="ToggleController as toggle">
<span ng-controller="TreeNodeController as treeNode">
<span ng-controller="TreeNodeController as treeNode">
<span mct-refresh="treeNode.listen(callback)">
<span
class="tree-item menus-to-left"
ng-class="{selected: treeNode.isSelected()}"
>
ng-class="::{selected: treeNode.isSelected()}">
<span
class='ui-symbol view-control flex-elem'
ng-class="{ 'has-children': model.composition !== undefined, expanded: toggle.isActive() }"
ng-class="::{ 'has-children': model.composition !== undefined, expanded: toggle.isActive() }"
ng-click="toggle.toggle(); treeNode.trackExpansion()"
>
</span>
@ -35,21 +35,21 @@
class="rep-object-label"
key="'label'"
mct-object="domainObject"
parameters="{suppressMenuOnEdit: true}"
parameters="::{suppressMenuOnEdit: true}"
ng-click="treeNode.select()"
>
</mct-representation>
</span>
<span
class="tree-item-subtree"
ng-show="toggle.isActive()"
ng-if="model.composition !== undefined"
ng-if="::treeNode.isExpanded()"
>
<mct-representation key="'subtree'"
ng-model="ngModel"
parameters="parameters"
mct-object="treeNode.hasBeenExpanded() && domainObject">
mct-model="::mctModel"
parameters="::parameters"
mct-object="::(treeNode.hasBeenExpanded() && domainObject)"
>
</mct-representation>
</span>

View File

@ -22,9 +22,9 @@
<ul class="tree">
<li>
<mct-representation key="'tree-node'"
mct-object="domainObject"
ng-model="ngModel"
parameters="parameters">
mct-object="::domainObject"
mct-model="::mctModel"
parameters="::parameters">
</mct-representation>
</li>
</ul>

View File

@ -97,6 +97,7 @@ define(
navContext = navObject &&
navObject.getCapability('context'),
nodePath,
wasSelected = self.isSelected(),
navPath;
// Deselect; we will reselect below, iff we are
@ -121,15 +122,17 @@ define(
// otherwise, expand.
if (nodePath.length === navPath.length) {
self.isSelectedFlag = true;
} else { // node path is shorter: Expand!
if ($scope.toggle) {
$scope.toggle.setState(true);
}
self.trackExpansion();
} else if (!self.isExpanded()) {
// node path is shorter: Expand!
self.toggle();
}
}
}
if (self.isSelected() !== wasSelected) {
self.notifyListener();
}
}
// Callback for the selection updates; track the currently
@ -139,16 +142,53 @@ define(
checkSelection();
}
this.isExpandedFlag = false;
this.isSelectedFlag = false;
this.hasBeenExpandedFlag = false;
this.$timeout = $timeout;
this.$scope = $scope;
checkSelection();
// Listen for changes which will effect display parameters
$scope.$watch("ngModel.selectedObject", setSelection);
$scope.$watch("domainObject", checkSelection);
}
TreeNodeController.prototype.notifyListener = function () {
if (this.listener) {
this.listener();
}
};
TreeNodeController.prototype.isExpanded = function () {
return this.isExpandedFlag;
};
TreeNodeController.prototype.toggle = function () {
this.isExpandedFlag = !this.isExpandedFlag;
if (this.isExpanded() && !this.hasBeenExpanded()) {
this.trackExpansion();
}
this.notifyListener();
};
TreeNodeController.prototype.listen = function (callback) {
var self = this,
domainObject = this.$scope.domainObject,
unlistenToMutation;
this.listener = callback;
unlistenToMutation = domainObject.getCapability('mutation')
.listen(callback);
return function () {
unlistenToMutation();
if (self.listener === callback) {
delete self.listener;
}
};
};
/**
* Select the domain object represented by this node in the tree.
* This will both update the `selectedObject` property in
@ -178,6 +218,7 @@ define(
// want this to be spread across multiple digest cycles.
self.$timeout(function () {
self.hasBeenExpandedFlag = true;
self.notifyListener();
}, 0);
}
};

View File

@ -0,0 +1,91 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define*/
define(
['angular'],
function (angular) {
'use strict';
/**
* The `mct-refresh` directive may be used to explicitly
* trigger the refresh of the contents of the HTML element
* which has this attribute. When used in combination with
* one-time binding, this allows templates (or sections thereof)
* to eschew watches and instead use other strategies for
* change detection.
*
* The `mct-refresh` directive is applied as an attribute
* whose value should be an Angular expression which:
*
* * Will be evaluated with a variable `callback`, which is
* a function that, when invoked, will trigger a refresh.
* * May return a function which will be invoked by `mct-refresh`
* when the directive is no longer applicable; this should
* be used to release any resources associated with the
* above callback.
*
* Example usage:
*
* ```
* <span mct-refresh="someObservable.observe(callback)">
* <div>{{::someObservable.getValue()}}</div>
* </span>
* ```
*
* @constructor
* @memberof platform/commonUI/general
*/
function MCTRefresh() {
function link(scope, elem, attrs, ctrl, transclude) {
var unlisten;
function recreateContents() {
transclude(function (clone) {
elem.empty();
elem.append(clone);
});
}
recreateContents();
unlisten = scope.$eval(
attrs.mctRefresh,
{ callback: recreateContents }
);
if (angular.isFunction(unlisten)) {
scope.$on("$destroy", unlisten);
}
}
return {
restrict: "A",
transclude: true,
link: link
};
}
return MCTRefresh;
}
);

View File

@ -0,0 +1,122 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../../src/directives/MCTRefresh"],
function (MCTRefresh) {
"use strict";
describe("The mct-refresh directive", function () {
var mctRefresh;
beforeEach(function () {
mctRefresh = new MCTRefresh();
});
it("is applicable as an attribute only", function () {
expect(mctRefresh.restrict).toEqual("A");
});
describe("when linked", function () {
var mockScope,
mockElement,
testAttrs,
mockTransclude,
mockClone,
mockUnlisten;
function fireEvent(event) {
mockScope.$on.calls.forEach(function (call) {
if (call.args[0] === event) {
call.args[1]();
}
});
}
beforeEach(function () {
mockScope = jasmine.createSpyObj(
"$scope",
[ "$eval", "$on", "$apply" ]
);
mockElement = jasmine.createSpyObj(
"elem",
[ "empty", "append" ]
);
testAttrs = { mctRefresh: "some-expr" };
mockTransclude = jasmine.createSpy();
mockTransclude.andCallFake(function (fn) {
fn(mockClone);
});
mockClone = jasmine.createSpyObj(
"elem",
[ "empty", "append" ]
);
mockUnlisten = jasmine.createSpy();
mockScope.$eval.andReturn(mockUnlisten);
mctRefresh.link(
mockScope,
mockElement,
testAttrs,
{},
mockTransclude
);
});
it("adds its transcluded content", function () {
expect(mockElement.append)
.toHaveBeenCalledWith(mockClone);
});
it("passes a callback into its associated expression", function () {
expect(mockScope.$eval).toHaveBeenCalledWith(
testAttrs.mctRefresh,
{ callback: jasmine.any(Function) }
);
});
describe("and triggered via callback", function () {
beforeEach(function () {
mockScope.$eval.mostRecentCall.args[1].callback();
});
it("transcludes its content again", function () {
expect(mockTransclude.calls.length).toEqual(2);
});
});
describe("and then destroyed", function () {
beforeEach(function () {
fireEvent("$destroy");
});
it("stops listening", function () {
expect(mockUnlisten).toHaveBeenCalled();
});
});
});
});
}
);

View File

@ -16,6 +16,7 @@
"directives/MCTContainer",
"directives/MCTDrag",
"directives/MCTPopup",
"directives/MCTRefresh",
"directives/MCTResize",
"directives/MCTScroll",
"directives/MCTSplitPane",

View File

@ -25,8 +25,8 @@
* Module defining MCTInclude. Created by vwoeltje on 11/7/14.
*/
define(
[],
function () {
["./OneWayBinder"],
function (OneWayBinder) {
"use strict";
/**
@ -35,19 +35,26 @@ define(
* key which can be exposed by bundles, instead of requiring
* an explicit path.
*
* This directive uses two-way binding for three attributes:
* This directive uses the following attributes:
*
* * `key`, matched against the key of a defined template extension
* in order to determine which actual template to include.
* * `ng-model`, populated as `ngModel` in the loaded template's
* scope; used for normal ng-model purposes (e.g. if the
* included template is meant to two-way bind to a data model.)
* * `parameters`, used to communicate display parameters to
* the included template (e.g. title.) The difference between
* `parameters` and `ngModel` is intent: Both are two-way
* bound, but `ngModel` is useful for data models (more like
* an output) and `parameters` is meant to be useful for
* display parameterization (more like an input.)
* * `key`: An Angular expression, matched against the key of a
* defined template extension in order to determine which actual
* template to include.
* * `mct-model`: An Angular expression; its value is watched
* and passed into the template's scope as property `mctModel`.
* * `parameters`: An Angular expression; its value is watched
* and passed into the template's scope as property `parameters`.
*
* The difference between `parameters` and `mct-model` is intent;
* `parameters` should be used for display-time parameters which
* are not meant to be changed, whereas `mct-model` should be
* used to pass in objects whose properties will (or may) be
* modified by the included template.
*
* (For backwards compatibility, `ng-model` is treated identically
* to `mct-model`, and the property `ngModel` will be provided
* in scope with the same value as `mctModel`. This usage is
* deprecated and should be avoided.)
*
* @memberof platform/representation
* @constructor
@ -57,14 +64,24 @@ define(
function MCTInclude(templates, templateLinker) {
var templateMap = {};
function link(scope, element) {
var changeTemplate = templateLinker.link(
scope,
element,
scope.key && templateMap[scope.key]
);
function link(scope, element, attrs) {
var parent = scope.$parent,
key = parent.$eval(attrs.key),
changeTemplate = templateLinker.link(
scope,
element,
key && templateMap[key]
),
binder = new OneWayBinder(scope, attrs);
scope.$watch('key', function (key) {
binder.bind('ngModel');
binder.bind('mctModel');
binder.bind('parameters');
binder.alias('ngModel', 'mctModel');
binder.alias('mctModel', 'ngModel');
binder.watch('key', function (key) {
changeTemplate(key && templateMap[key]);
});
}
@ -87,8 +104,8 @@ define(
// May hide the element, so let other directives act first
priority: -1000,
// Two-way bind key, ngModel, and parameters
scope: { key: "=", ngModel: "=", parameters: "=" }
// Isolate this scope; do not inherit properties from parent
scope: {}
};
}

View File

@ -27,26 +27,40 @@
* @namespace platform/representation
*/
define(
[],
function () {
["./OneWayBinder"],
function (OneWayBinder) {
"use strict";
/**
* Defines the mct-representation directive. This may be used to
* present domain objects as HTML (with event wiring), with the
* specific representation being mapped to a defined extension
* (as defined in either the `representation` category-of-extension,
* (as defined in either the `representations` category-of-extension,
* or the `views` category-of-extension.)
*
* This directive uses two-way binding for three attributes:
*
* * `key`, matched against the key of a defined template extension
* in order to determine which actual template to include.
* * `mct-object`, populated as `domainObject` in the loaded
* template's scope. This is the domain object being
* represented as HTML by this directive.
* * `parameters`, used to communicate display parameters to
* the included template (e.g. title.)
* * `key`: An Angular expression, matched against the key of a
* defined representation or view extension in order to determine
* which actual template to include.
* * `mct-model`: An Angular expression; its value is watched
* and passed into the template's scope as property `mctModel`.
* * `parameters`: An Angular expression; its value is watched
* and passed into the template's scope as property `parameters`.
*
* The difference between `parameters` and `mct-model` is intent;
* `parameters` should be used for display-time parameters which
* are not meant to be changed, whereas `mct-model` should be
* used to pass in objects whose properties will (or may) be
* modified by the included representation.
*
* (For backwards compatibility, `ng-model` is treated identically
* to `mct-model`, and the property `ngModel` will be provided
* in scope with the same value as `mctModel`. This usage is
* deprecated and should be avoided.)
*
* @memberof platform/representation
* @constructor
@ -94,7 +108,8 @@ define(
couldEdit = false,
lastIdPath = [],
lastKey,
changeTemplate = templateLinker.link($scope, element);
changeTemplate = templateLinker.link($scope, element),
binder = new OneWayBinder($scope, attrs);
// Populate scope with any capabilities indicated by the
// representation's extension definition
@ -236,13 +251,20 @@ define(
}
}
binder.bind('parameters');
binder.bind('mctModel');
binder.bind('ngModel');
binder.alias('ngModel', 'mctModel');
binder.alias('mctModel', 'ngModel');
// Update the representation when the key changes (e.g. if a
// different representation has been selected)
$scope.$watch("key", refresh);
binder.bind('key', refresh);
// Also update when the represented domain object changes
// (to a different object)
$scope.$watch("domainObject", refresh);
binder.alias('mctObject', 'domainObject', refresh);
// Finally, also update when there is a new version of that
// same domain object; these changes should be tracked in the
@ -270,14 +292,8 @@ define(
// May hide the element, so let other directives act first
priority: -1000,
// Two-way bind key and parameters, get the represented domain
// object as "mct-object"
scope: {
key: "=",
domainObject: "=mctObject",
ngModel: "=",
parameters: "="
}
// Isolate this scope
scope: {}
};
}

View File

@ -0,0 +1,113 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define*/
define(
[],
function () {
'use strict';
/**
* Supports the "one-way" binding behavior of `mct-representation`
* and `mct-include`; watches expression associated with attributes
* in a parent scope, then passes these into the child scope when
* they change (but does not assign anything back to the parent
* scope if the child changes.)
* @constructor
* @memberof platform/representation
* @param scope the Angular scope to watch
* @param attrs the relevant attributes, as passed into the `link`
* function of the relevant directive
*/
function OneWayBinder(scope, attrs) {
var self = this;
this.unwatches = [];
this.scope = scope;
this.parent = scope.$parent;
this.attrs = attrs;
// Detach any listeners from the parent
scope.$on('$destroy', function () {
self.unwatches.forEach(function (unwatch) {
unwatch();
});
});
}
/**
* One-way bind an attribute. The value of the named attribute will
* be watched as an Angular expression in the parent scope; its
* value will be exposed in the child scope as a property of
* the same name.
* @param {string} attr the name of the attribute to watch
* @param {Function} [callback] a callback to invoke with new values
*/
OneWayBinder.prototype.bind = function (attr, callback) {
this.alias(attr, attr, callback);
};
/**
* One-way bind an attribute. As `bind`, but allows the property
* name in the child scope used to expose these values to be
* specified as something different from the attribute name.
* @param {string} attr the name of the attribute to watch
* @param {string} property the name of the property to use in scope
* @param {Function} [callback] a callback to invoke with new values
*/
OneWayBinder.prototype.alias = function (attr, property, callback) {
var scope = this.scope;
this.watch(attr, function expose(value) {
scope[property] = value;
if (callback) {
callback(value);
}
});
// Expose in scope immediately, similar to scope: { attr: "=" }
// in a directive definition object.
scope[property] = this.parent.$eval(this.attrs[attr]);
};
/**
* Watch for changes in this attribute. The named attribute's value
* will be watched as an Angular expression in the parent scope,
* and the provided callback will be invoked with the value of that
* expression when changes occur.
* @param {string} attr the name of the attribute to watch
* @param {Function} callback the callback to invoke with new values
*/
OneWayBinder.prototype.watch = function (attr, callback) {
var expr = this.attrs[attr];
if (expr) {
this.unwatches.push(this.parent.$watch(
expr,
callback,
expr && expr[0] === '{'
));
}
};
return OneWayBinder;
}
);

View File

@ -35,11 +35,12 @@ define(
mockLinker,
mockScope,
mockElement,
testAttrs,
mockChangeTemplate,
mctInclude;
function fireWatch(expr, value) {
mockScope.$watch.calls.forEach(function (call) {
mockScope.$parent.$watch.calls.forEach(function (call) {
if (call.args[0] === expr) {
call.args[1](value);
}
@ -68,6 +69,8 @@ define(
['link', 'getPath']
);
mockScope = jasmine.createSpyObj('$scope', ['$watch', '$on']);
mockScope.$parent =
jasmine.createSpyObj('parent', ['$watch', '$eval']);
mockElement = jasmine.createSpyObj('element', ['empty']);
mockChangeTemplate = jasmine.createSpy('changeTemplate');
mockLinker.link.andReturn(mockChangeTemplate);
@ -75,7 +78,12 @@ define(
return testUrls[template.key];
});
mctInclude = new MCTInclude(testTemplates, mockLinker);
mctInclude.link(mockScope, mockElement, {});
testAttrs = {
key: "parentKey",
mctModel: "someExpr",
ngModel: "someOtherExpr"
};
mctInclude.link(mockScope, mockElement, testAttrs);
});
it("is restricted to elements", function () {
@ -88,17 +96,28 @@ define(
});
it("reads a template location from a scope's key variable", function () {
mockScope.key = 'abc';
fireWatch('key', mockScope.key);
fireWatch(testAttrs.key, 'abc');
expect(mockChangeTemplate)
.toHaveBeenCalledWith(testTemplates[0]);
mockScope.key = 'xyz';
fireWatch('key', mockScope.key);
fireWatch(testAttrs.key, 'xyz');
expect(mockChangeTemplate)
.toHaveBeenCalledWith(testTemplates[1]);
});
it("watches for changes on both ng-model and mct-model", function () {
expect(mockScope.$parent.$watch).toHaveBeenCalledWith(
testAttrs.ngModel,
jasmine.any(Function),
false
);
expect(mockScope.$parent.$watch).toHaveBeenCalledWith(
testAttrs.mctModel,
jasmine.any(Function),
false
);
});
});
}
);

View File

@ -44,6 +44,7 @@ define(
mockChangeTemplate,
mockScope,
mockElement,
testAttrs,
mockDomainObject,
testModel,
mctRepresentation;
@ -57,7 +58,7 @@ define(
}
function fireWatch(expr, value) {
mockScope.$watch.calls.forEach(function (call) {
mockScope.$parent.$watch.calls.forEach(function (call) {
if (call.args[0] === expr) {
call.args[1](value);
}
@ -102,6 +103,13 @@ define(
testUrls[t.key] = "some URL " + String(i);
});
testAttrs = {
"mctObject": "someExpr",
"key": "someOtherExpr",
"ngModel": "yetAnotherExpr",
"mctModel": "theExprsKeepOnComing"
};
mockRepresenters = ["A", "B"].map(function (name) {
var constructor = jasmine.createSpy("Representer" + name),
representer = jasmine.createSpyObj(
@ -121,6 +129,8 @@ define(
mockLog = jasmine.createSpyObj("$log", LOG_FUNCTIONS);
mockScope = jasmine.createSpyObj("scope", [ "$watch", "$on" ]);
mockScope.$parent =
jasmine.createSpyObj('parent', ['$watch', '$eval']);
mockElement = jasmine.createSpyObj("element", JQLITE_FUNCTIONS);
mockDomainObject = jasmine.createSpyObj("domainObject", DOMAIN_OBJECT_METHODS);
@ -138,7 +148,7 @@ define(
mockLinker,
mockLog
);
mctRepresentation.link(mockScope, mockElement);
mctRepresentation.link(mockScope, mockElement, testAttrs);
});
it("is restricted to elements", function () {
@ -150,15 +160,7 @@ define(
.toHaveBeenCalledWith(mockScope, mockElement);
});
it("watches scope when linked", function () {
expect(mockScope.$watch).toHaveBeenCalledWith(
"key",
jasmine.any(Function)
);
expect(mockScope.$watch).toHaveBeenCalledWith(
"domainObject",
jasmine.any(Function)
);
it("watches for model changes when linked", function () {
expect(mockScope.$watch).toHaveBeenCalledWith(
"domainObject.getModel().modified",
jasmine.any(Function)
@ -166,24 +168,16 @@ define(
});
it("recognizes keys for representations", function () {
mockScope.key = "abc";
mockScope.domainObject = mockDomainObject;
// Trigger the watch
fireWatch('key', mockScope.key);
fireWatch('domainObject', mockDomainObject);
fireWatch(testAttrs.key, "abc");
fireWatch(testAttrs.mctObject, mockDomainObject);
expect(mockChangeTemplate)
.toHaveBeenCalledWith(testRepresentations[0]);
});
it("recognizes keys for views", function () {
mockScope.key = "xyz";
mockScope.domainObject = mockDomainObject;
// Trigger the watches
fireWatch('key', mockScope.key);
fireWatch('domainObject', mockDomainObject);
fireWatch(testAttrs.key, "xyz");
fireWatch(testAttrs.mctObject, mockDomainObject);
expect(mockChangeTemplate)
.toHaveBeenCalledWith(testViews[1]);
@ -192,25 +186,20 @@ define(
it("does not load templates until there is an object", function () {
mockScope.key = "xyz";
// Trigger the watch
fireWatch('key', mockScope.key);
fireWatch(testAttrs.key, "xyz");
expect(mockChangeTemplate)
.not.toHaveBeenCalledWith(jasmine.any(Object));
mockScope.domainObject = mockDomainObject;
fireWatch('domainObject', mockDomainObject);
fireWatch(testAttrs.mctObject, mockDomainObject);
expect(mockChangeTemplate)
.toHaveBeenCalledWith(jasmine.any(Object));
});
it("loads declared capabilities", function () {
mockScope.key = "def";
mockScope.domainObject = mockDomainObject;
// Trigger the watch
mockScope.$watch.calls[0].args[1]();
fireWatch(testAttrs.key, "def");
fireWatch(testAttrs.mctObject, mockDomainObject);
expect(mockDomainObject.useCapability)
.toHaveBeenCalledWith("testCapability");
@ -219,35 +208,43 @@ define(
});
it("logs when no representation is available for a key", function () {
mockScope.key = "someUnknownThing";
// Verify precondition
expect(mockLog.warn).not.toHaveBeenCalled();
// Trigger the watch
mockScope.$watch.calls[0].args[1]();
fireWatch(testAttrs.key, "someUnkownThing");
// Should have gotten a warning - that's an unknown key
expect(mockLog.warn).toHaveBeenCalled();
});
it("clears out obsolete peroperties from scope", function () {
mockScope.key = "def";
mockScope.domainObject = mockDomainObject;
mockDomainObject.useCapability.andReturn("some value");
// Trigger the watch
mockScope.$watch.calls[0].args[1]();
fireWatch(testAttrs.key, "def");
fireWatch(testAttrs.mctObject, mockDomainObject);
expect(mockScope.testCapability).toBeDefined();
// Change the view
mockScope.key = "xyz";
// Change the view; should clear capabilities from scope
fireWatch(testAttrs.key, "xyz");
// Trigger the watch again; should clear capability from scope
mockScope.$watch.calls[0].args[1]();
expect(mockScope.testCapability).toBeUndefined();
});
it("watches for changes on both ng-model and mct-model", function () {
expect(mockScope.$parent.$watch).toHaveBeenCalledWith(
testAttrs.ngModel,
jasmine.any(Function),
false
);
expect(mockScope.$parent.$watch).toHaveBeenCalledWith(
testAttrs.mctModel,
jasmine.any(Function),
false
);
});
it("detects changes among linked instances", function () {
var mockContext = jasmine.createSpyObj('context', ['getPath']),
mockContext2 = jasmine.createSpyObj('context', ['getPath']),
@ -295,6 +292,19 @@ define(
expect(mockChangeTemplate.calls.length)
.toEqual(callCount + 1);
});
it("watches for changes on both ng-model and mct-model", function () {
expect(mockScope.$parent.$watch).toHaveBeenCalledWith(
testAttrs.ngModel,
jasmine.any(Function),
false
);
expect(mockScope.$parent.$watch).toHaveBeenCalledWith(
testAttrs.mctModel,
jasmine.any(Function),
false
);
});
});
}
);

View File

@ -0,0 +1,177 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../src/OneWayBinder"],
function (OneWayBinder) {
'use strict';
describe("OneWayBinder", function () {
var mockScope,
testAttrs,
testValues,
mockUnwatches,
binder;
function fireEvent(event) {
mockScope.$on.calls.forEach(function (call) {
if (call.args[0] === event) {
call.args[1]();
}
});
}
function fireParentWatch(expr) {
mockScope.$parent.$watch.calls.forEach(function (call) {
if (call.args[0] === expr) {
call.args[1](mockScope.$parent.$eval(expr));
}
});
}
beforeEach(function () {
mockUnwatches = [];
mockScope = jasmine.createSpyObj('$scope', ['$on']);
mockScope.$parent = jasmine.createSpyObj(
'$parent',
[ '$watch', '$eval' ]
);
testAttrs = { a: 'attrA', b: 'attrB', c: 'attrC' };
testValues = { attrA: 42, attrB: ['foo'], attrC: { a: 0 } };
mockScope.$parent.$eval.andCallFake(function (expr) {
return testValues[expr];
});
mockScope.$parent.$watch.andCallFake(function () {
var mockUnwatch = jasmine.createSpy();
mockUnwatches.push(mockUnwatch);
return mockUnwatch;
});
binder = new OneWayBinder(mockScope, testAttrs);
});
describe("bind", function () {
var attrNames;
beforeEach(function () {
attrNames = Object.keys(testAttrs);
attrNames.forEach(function (attr) {
binder.bind(attr);
});
});
it("exposes values from the parent in scope", function () {
attrNames.forEach(function (attr) {
expect(mockScope[attr])
.toEqual(testValues[testAttrs[attr]]);
});
});
it("updates values from the parent in scope", function () {
var oldValues = testValues,
newValues = {};
Object.keys(oldValues).forEach(function (key) {
newValues[key] = oldValues[key] + " a change";
});
testValues = newValues;
attrNames.forEach(function (attr) {
expect(mockScope[attr])
.toEqual(oldValues[testAttrs[attr]]);
fireParentWatch(testAttrs[attr]);
expect(mockScope[attr])
.toEqual(newValues[testAttrs[attr]]);
});
});
it("attaches one watch per attribute", function () {
expect(mockUnwatches.length).toEqual(3);
});
});
describe("alias", function () {
var attrNames;
beforeEach(function () {
binder.alias('a', 'someAlias');
});
it("exposes values under a different name", function () {
expect(mockScope.someAlias).toEqual(testValues.attrA);
});
it("updates values under a different name", function () {
var newValue = "some new value";
testValues.attrA = newValue;
expect(mockScope.someAlias).not.toEqual(newValue);
fireParentWatch(testAttrs.a);
expect(mockScope.someAlias).toEqual(newValue);
});
});
describe("watch", function () {
var mockCallback = jasmine.createSpy();
beforeEach(function () {
binder.watch('b', mockCallback);
});
it("invokes callbacks when values change", function () {
var newValue = "some new value";
testValues.attrB = newValue;
expect(mockCallback).not.toHaveBeenCalled();
fireParentWatch(testAttrs.b);
expect(mockCallback).toHaveBeenCalledWith(newValue);
});
it("generally watches for reference equality", function () {
expect(mockScope.$parent.$watch.mostRecentCall.args[2])
.toBeFalsy();
});
it("watches for equivalence when expressions are anonymous objects", function () {
testAttrs.d = "{ a: 'foo' }";
binder.watch('d', mockCallback);
expect(mockScope.$parent.$watch.mostRecentCall.args[2])
.toBeTruthy();
});
});
it("releases watches from parent when scope is destroyed", function () {
binder.bind('a');
binder.alias('b', 'xyz');
binder.watch('c', jasmine.createSpy());
fireEvent('$destroy');
mockUnwatches.forEach(function (mockUnwatch) {
expect(mockUnwatch).toHaveBeenCalled();
});
});
});
}
);

View File

@ -48,10 +48,11 @@ define(function () {
this.$scope = $scope;
this.searchService = searchService;
this.numberToDisplay = this.RESULTS_PER_PAGE;
this.availabileResults = 0;
this.availableResults = 0;
this.$scope.results = [];
this.$scope.loading = false;
this.pendingQuery = undefined;
this.$scope.ngModel = this.$scope.ngModel || {};
this.$scope.ngModel.filter = function () {
return controller.onFilterChange.apply(controller, arguments);
};

View File

@ -68,6 +68,8 @@ requirejs.config({
}
},
waitSeconds: 30,
// dynamically load all test files
deps: allTestFiles,