added ng:switch widget

This commit is contained in:
Misko Hevery 2010-04-05 11:46:53 -07:00
parent 4bfa4e230d
commit 7a4b480206
22 changed files with 199 additions and 191 deletions

View file

@ -11,6 +11,7 @@ load:
- src/scenario/_namespace.js
- src/scenario/*.js
- test/testabilityPatch.js
- test/angular-mocks.js
- test/scenario/*.js
- test/*.js
@ -18,3 +19,4 @@ exclude:
- src/angular.prefix
- src/angular.suffix
- src/angular-bootstrap.js
- src/AngularPublic.js

View file

@ -0,0 +1,4 @@
<div>
account page goes here!
</div>

19
scenario/application.html Normal file
View file

@ -0,0 +1,19 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<link rel="stylesheet" type="text/css" href="style.css"></link>
<script type="text/javascript" src="../src/angular-bootstrap.js#autobind"></script>
</head>
<body ng-init="$window.$scope = this">
[ <a href="#login">login</a>
| <a href="#account">account</a>
]
<ng:switch on="$location.hashPath">
<div ng-switch-when="login">login screen</div>
<ng:include ng-switch-when="account" src="application-account.html"></ng:include>
</ng:switch>
<pre>$location={{$location}}</pre>
</body>
</html>

View file

@ -247,7 +247,16 @@ function escapeHtml(html) {
replace(/>/g, '&gt;');
}
function isRenderableElement(element) {
var name = element && element[0] && element[0].nodeName;
return name && name.charAt(0) != '#' &&
!includes(['TR', 'COL', 'COLGROUP', 'TBODY', 'THEAD', 'TFOOT'], name);
}
function elementError(element, type, error) {
while (!isRenderableElement(element)) {
element = element.parent() || jqLite(document.body);
}
if (error) {
element.addClass(type);
element.attr(NG_ERROR, error);

35
src/AngularPublic.js Normal file
View file

@ -0,0 +1,35 @@
var browserSingleton;
angularService('$browser', function browserFactory(){
if (!browserSingleton) {
var XHR = XMLHttpRequest;
if (isUndefined(XHR)) {
XHR = function () {
try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch (e1) {}
try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch (e2) {}
try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e3) {}
throw new Error("This browser does not support XMLHttpRequest.");
};
}
browserSingleton = new Browser(window.location, XHR);
browserSingleton.startUrlWatcher();
}
return browserSingleton;
});
extend(angular, {
'element': jqLite,
'compile': compile,
'scope': createScope,
'copy': copy,
'extend': extend,
'foreach': foreach,
'noop':noop,
'identity':identity,
'isUndefined': isUndefined,
'isDefined': isDefined,
'isString': isString,
'isFunction': isFunction,
'isNumber': isNumber,
'isArray': isArray
});

View file

@ -3,9 +3,10 @@
// Browser
//////////////////////////////
function Browser(location) {
function Browser(location, XHR) {
this.location = location;
this.delay = 25;
this.XHR = XHR;
this.setTimeout = function(fn, delay) {
window.setTimeout(fn, delay);
};
@ -14,6 +15,17 @@ function Browser(location) {
}
Browser.prototype = {
xhr: function(method, url, callback){
var xhr = new this.XHR();
xhr.open(method, url, true);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
callback(xhr.status, xhr.responseText);
}
};
xhr.send('');
},
watchUrl: function(fn){
this.listeners.push(fn);
},
@ -23,7 +35,11 @@ Browser.prototype = {
(function pull () {
if (self.expectedUrl !== self.location.href) {
foreach(self.listeners, function(listener){
listener(self.location.href);
try {
listener(self.location.href);
} catch (e) {
error(e);
}
});
self.expectedUrl = self.location.href;
}

View file

@ -71,6 +71,7 @@ Compiler.prototype = {
$init: function() {
template.init(element, scope);
scope.$eval();
delete scope.$init;
return scope;
}
});

View file

@ -70,17 +70,8 @@ function parserNewScopeAdapter(fn) {
};
}
function isRenderableElement(element) {
var name = element && element[0] && element[0].nodeName;
return name && name.charAt(0) != '#' &&
!includes(['TR', 'COL', 'COLGROUP', 'TBODY', 'THEAD', 'TFOOT'], name);
}
function rethrow(e) { throw e; }
function errorHandlerFor(element, error) {
while (!isRenderableElement(element)) {
element = element.parent() || jqLite(document.body);
}
elementError(element, NG_EXCEPTION, isDefined(error) ? toJson(error) : error);
}
@ -132,14 +123,16 @@ function createScope(parent, services, existing) {
$watch: function(watchExp, listener, exceptionHandler) {
var watch = expressionCompile(watchExp),
last = watch.call(instance);
instance.$onEval(PRIORITY_WATCH, function(){
last;
function watcher(){
var value = watch.call(instance);
if (last !== value) {
instance.$tryEval(listener, exceptionHandler, value, last);
last = value;
}
});
}
instance.$onEval(PRIORITY_WATCH, watcher);
watcher();
},
$onEval: function(priority, expr, exceptionHandler){

View file

@ -1,46 +0,0 @@
// ////////////////////////////
// UrlWatcher
// ////////////////////////////
function UrlWatcher(location) {
this.location = location;
this.delay = 25;
this.setTimeout = function(fn, delay) {
window.setTimeout(fn, delay);
};
this.expectedUrl = location.href;
this.listeners = [];
}
UrlWatcher.prototype = {
watch: function(fn){
this.listeners.push(fn);
},
start: function() {
var self = this;
(function pull () {
if (self.expectedUrl !== self.location.href) {
foreach(self.listeners, function(listener){
listener(self.location.href);
});
self.expectedUrl = self.location.href;
}
self.setTimeout(pull, self.delay);
})();
},
set: function(url) {
var existingURL = this.location.href;
if (!existingURL.match(/#/))
existingURL += '#';
if (existingURL != url)
this.location.href = url;
this.existingURL = url;
},
get: function() {
return this.location.href;
}
};

View file

@ -156,40 +156,47 @@ angularWidget('SELECT', function(element){
});
angularWidget('INLINE', function(element){
element.replaceWith(this.element("div"));
angularWidget('NG:INCLUDE', function(element){
var compiler = this,
behavior = element.attr("behavior"),
template = element.attr("template"),
initExpr = element.attr("init");
return function(boundElement){
var scope = this;
boundElement.load(template, function(){
var templateScope = compiler.compile(boundElement)(boundElement, scope);
templateScope.$tryEval(initExpr, boundElement);
templateScope.$init();
src = element.attr("src");
return element.attr('switch-instance') ? null : function(element){
var scope = this, childScope;
element.attr('switch-instance', 'compiled');
scope.$browser.xhr('GET', src, function(code, response){
element.html(response);
childScope = createScope(scope);
compiler.compile(element)(element, childScope);
childScope.$init();
});
scope.$onEval(function(){ if (childScope) childScope.$eval(); });
};
});
angularWidget('INCLUDE', function(element){
element.replaceWith(this.element("div"));
var matches = [];
element.find("INLINE").each(function(){
matches.push({match: jQuery(this).attr("match"), element: jQuery(this)});
});
angularWidget('NG:SWITCH', function(element){
var compiler = this,
watchExpr = element.attr("watch");
return function(boundElement){
watchExpr = element.attr("on"),
cases = [];
eachNode(element, function(caseElement){
var when = caseElement.attr('ng-switch-when');
if (when) {
cases.push({
when: function(value){ return value == when; },
element: caseElement,
template: compiler.compile(caseElement)
});
}
});
element.html('');
return function(element){
var scope = this;
this.$watch(watchExpr, function(value){
foreach(matches, function(inline){
if(inline.match == value) {
var template = inline.element.attr("template");
boundElement.load(template, function(){
var templateScope = compiler.compile(boundElement)(boundElement, scope);
templateScope.$init();
});
element.html('');
foreach(cases, function(switchCase){
if (switchCase.when(value)) {
element.append(switchCase.element);
var childScope = createScope(scope);
switchCase.template(switchCase.element, childScope);
childScope.$init();
}
});
});

View file

@ -47,7 +47,7 @@
addScript("/Parser.js");
addScript("/Resource.js");
addScript("/Browser.js");
addScript("/AngularPublic.js");
addScript("/~AngularPublic.js");
// Extension points
addScript("/apis.js");

View file

@ -109,6 +109,10 @@ JQLite.prototype = {
this[0].parentNode.replaceChild(jqLite(replaceNode)[0], this[0]);
},
append: function(node) {
this[0].appendChild(jqLite(node)[0]);
},
remove: function() {
this.dealoc();
this[0].parentNode.removeChild(this[0]);
@ -182,6 +186,9 @@ JQLite.prototype = {
html: function(value) {
if (isDefined(value)) {
for ( var i = 0, children = this[0].childNodes; i < children.length; i++) {
jqLite(children[i]).dealoc();
}
this[0].innerHTML = value;
}
return this[0].innerHTML;

View file

@ -6,8 +6,8 @@ angularService("$document", function(window){
var URL_MATCH = /^(file|ftp|http|https):\/\/(\w+:{0,1}\w*@)?([\w\.]*)(:([0-9]+))?([^\?#]+)(\?([^#]*))?((#([^\?]*))?(\?([^\?]*))?)$/;
var DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp':21};
angularService("$location", function(browser){
var scope = this;
function location(url){
var scope = this, location = {parse:parse, toString:toString};
function parse(url){
if (isDefined(url)) {
var match = URL_MATCH.exec(url);
if (match) {
@ -23,17 +23,19 @@ angularService("$location", function(browser){
location.hashSearch = parseKeyValue(match[13]);
}
}
var hashKeyValue = toKeyValue(location.hashSearch);
var hash = (location.hashPath ? location.hashPath : '') +
(hashKeyValue ? '?' + hashKeyValue : '');
}
function toString() {
var hashKeyValue = toKeyValue(location.hashSearch),
hash = (location.hashPath ? location.hashPath : '') + (hashKeyValue ? '?' + hashKeyValue : '');
return location.href.split('#')[0] + '#' + (hash ? hash : '');
}
browser.watchUrl(function(url){
location(url);
parse(url);
scope.$root.$eval();
});
location(browser.getUrl());
parse(browser.getUrl());
this.$onEval(PRIORITY_LAST, function(){
var href = location();
var href = toString();
if (href != location.href) {
browser.setUrl(href);
location.href = href;
@ -42,14 +44,3 @@ angularService("$location", function(browser){
return location;
}, {inject: ['$browser']});
if (!angularService['$browser']) {
var browserSingleton;
angularService('$browser', function browserFactory(){
if (!browserSingleton) {
browserSingleton = new Browser(window.location);
browserSingleton.startUrlWatcher();
}
return browserSingleton;
});
}

View file

@ -1,17 +0,0 @@
extend(angular, {
'element': jqLite,
'compile': compile,
'scope': createScope,
'copy': copy,
'extend': extend,
'foreach': foreach,
'noop':noop,
'identity':identity,
'isUndefined': isUndefined,
'isDefined': isDefined,
'isString': isString,
'isFunction': isFunction,
'isNumber': isNumber,
'isArray': isArray
});

View file

@ -629,21 +629,6 @@ BinderTest.prototype.testDeleteAttributeIfEvaluatesFalse = function() {
assertChild(5, false);
};
BinderTest.prototype.testRepeaterErrorShouldBePlacedOnInstanceNotOnTemplateComment = function () {
var c = this.compile(
'<input name="person.{{name}}" ng-repeat="name in [\'a\', \'b\']" />');
c.scope.$eval();
assertTrue(c.node.hasClass("ng-exception"));
};
BinderTest.prototype.testItShouldApplyAttributesBeforeTheWidgetsAreMaterialized = function() {
var c = this.compile(
'<input name="person.{{name}}" ng-repeat="name in [\'a\', \'b\']" />');
c.scope.$set('person', {a:'misko', b:'adam'});
c.scope.$eval();
assertEquals("", c.node.html());
};
BinderTest.prototype.XtestItShouldCallListenersWhenAnchorChanges = function() {
var log = "";
var c = this.compile('<div ng-watch="$anchor.counter:count = count+1">');

View file

@ -1,26 +1,26 @@
describe("ScenarioSpec: Compilation", function(){
it("should compile dom node and return scope", function(){
var node = jqLite('<div ng-init="a=1">{{b=a+1}}</div>')[0];
var scope = angular.compile(node);
var scope = compile(node);
scope.$init();
expect(scope.a).toEqual(1);
expect(scope.b).toEqual(2);
});
it("should compile jQuery node and return scope", function(){
var scope = angular.compile(jqLite('<div>{{a=123}}</div>')).$init();
var scope = compile(jqLite('<div>{{a=123}}</div>')).$init();
expect(jqLite(scope.$element).text()).toEqual('123');
});
it("should compile text node and return scope", function(){
var scope = angular.compile('<div>{{a=123}}</div>').$init();
var scope = compile('<div>{{a=123}}</div>').$init();
expect(jqLite(scope.$element).text()).toEqual('123');
});
});
describe("ScenarioSpec: Scope", function(){
xit("should have set, get, eval, $init, updateView methods", function(){
var scope = angular.compile('<div>{{a}}</div>').$init();
var scope = compile('<div>{{a}}</div>').$init();
scope.$eval("$invalidWidgets.push({})");
expect(scope.$set("a", 2)).toEqual(2);
expect(scope.$get("a")).toEqual(2);
@ -31,7 +31,7 @@ describe("ScenarioSpec: Scope", function(){
});
xit("should have $ objects", function(){
var scope = angular.compile('<div></div>', {a:"b"});
var scope = compile('<div></div>', {a:"b"});
expect(scope.$get('$anchor')).toBeDefined();
expect(scope.$get('$eval')).toBeDefined();
expect(scope.$get('$config')).toBeDefined();
@ -49,7 +49,7 @@ xdescribe("ScenarioSpec: configuration", function(){
set:function(u){url = u;},
get:function(){return url;}
};
var scope = angular.compile("<div>{{$anchor}}</div>", {location:location});
var scope = compile("<div>{{$anchor}}</div>", {location:location});
var $anchor = scope.$get('$anchor');
expect($anchor.book).toBeUndefined();
expect(onUrlChange).toBeUndefined();

View file

@ -44,7 +44,7 @@ describe('scope/model', function(){
model.$onEval(function(){evalCount ++;});
model.name = 'misko';
model.$eval();
expect(nameCount).toEqual(1);
expect(nameCount).toEqual(2);
expect(evalCount).toEqual(1);
expect(model.newValue).toEqual('misko');
expect(model.oldValue).toEqual('adam');

View file

@ -106,7 +106,7 @@ describe('Validator:asynchronous', function(){
it('should make a request and show spinner', function(){
var value, fn;
var scope = angular.compile('<input type="text" name="name" ng-validate="asynchronous:asyncFn"/>');
var scope = compile('<input type="text" name="name" ng-validate="asynchronous:asyncFn"/>');
scope.$init();
var input = scope.$element;
scope.asyncFn = function(v,f){

26
test/angular-mocks.js vendored Normal file
View file

@ -0,0 +1,26 @@
function MockBrowser() {
this.url = "http://server";
this.watches = [];
}
MockBrowser.prototype = {
xhr: function(method, url, callback) {
},
getUrl: function(){
return this.url;
},
setUrl: function(url){
this.url = url;
},
watchUrl: function(fn) {
this.watches.push(fn);
}
};
angular.service('$browser', function(){
return new MockBrowser();
});

View file

@ -10,7 +10,7 @@ describe("services", function(){
});
it("should inject $location", function(){
scope.$location('http://host:123/p/a/t/h.html?query=value#path?key=value');
scope.$location.parse('http://host:123/p/a/t/h.html?query=value#path?key=value');
expect(scope.$location.href).toEqual("http://host:123/p/a/t/h.html?query=value#path?key=value");
expect(scope.$location.protocol).toEqual("http");
expect(scope.$location.host).toEqual("host");
@ -24,11 +24,11 @@ describe("services", function(){
scope.$location.hashPath = 'page=http://path';
scope.$location.hashSearch = {k:'a=b'};
expect(scope.$location()).toEqual('http://host:123/p/a/t/h.html?query=value#page=http://path?k=a%3Db');
expect(scope.$location.toString()).toEqual('http://host:123/p/a/t/h.html?query=value#page=http://path?k=a%3Db');
});
it('should parse file://', function(){
scope.$location('file:///Users/Shared/misko/work/angular.js/scenario/widgets.html');
scope.$location.parse('file:///Users/Shared/misko/work/angular.js/scenario/widgets.html');
expect(scope.$location.href).toEqual("file:///Users/Shared/misko/work/angular.js/scenario/widgets.html");
expect(scope.$location.protocol).toEqual("file");
expect(scope.$location.host).toEqual("");
@ -39,7 +39,7 @@ describe("services", function(){
expect(scope.$location.hashPath).toEqual('');
expect(scope.$location.hashSearch).toEqual({});
expect(scope.$location()).toEqual('file:///Users/Shared/misko/work/angular.js/scenario/widgets.html#');
expect(scope.$location.toString()).toEqual('file:///Users/Shared/misko/work/angular.js/scenario/widgets.html#');
});
xit('should add stylesheets', function(){

View file

@ -5,46 +5,6 @@ function nakedExpect(obj) {
return expect(angular.fromJson(angular.toJson(obj)));
}
swfobject = {
createSwf:function() {
fail("must mock out swfobject.createSwf in test.");
}
};
function html(content) {
return jQuery("<div></div>").html(content);
}
function report(reportTest){
$("#tests").children().each(function(i){
var success = this.className == "pass";
var strong = this.firstChild;
var msg = strong.firstChild.nodeValue;
var parts = msg.split(" module: ");
var module = parts[0];
var name = parts[1].replace(/ *$/, "");
reportTest(success, module, name, this.nodeValue);
});
}
function MockBrowser() {
this.url = "http://server";
this.watches = [];
}
MockBrowser.prototype = {
getUrl: function(){
return this.url;
},
setUrl: function(url){
this.url = url;
},
watchUrl: function(fn) {
this.watches.push(fn);
}
};
angularService('$browser', function(){
return new MockBrowser();
});

View file

@ -188,5 +188,21 @@ describe("input widget", function(){
expect(element.hasClass('ng-exception')).toBeTruthy();
});
it('should switch on value change', function(){
compile('<ng:switch on="select"><div ng-switch-when="1">first</div><div ng-switch-when="2">second</div></ng:switch>');
expect(element.html()).toEqual('');
scope.select = 1;
scope.$eval();
expect(element.text()).toEqual('first');
scope.select = 2;
scope.$eval();
expect(element.text()).toEqual('second');
});
});
describe('ng:include', function(){
it('should include on external file', function() {
var element = jqLite('<ng:include src="myUrl"></ng:include>');
var scope = compile(element).$init();
});
});