diff options
| -rw-r--r-- | scenario/widgets.html | 18 | ||||
| -rw-r--r-- | src/Angular.js | 78 | ||||
| -rw-r--r-- | src/Scope.js | 2 | ||||
| -rw-r--r-- | src/Widgets.js | 78 | ||||
| -rw-r--r-- | src/angular-bootstrap.js | 39 | ||||
| -rw-r--r-- | src/delete/Model.js (renamed from src/moveToAngularCom/Model.js) | 0 | ||||
| -rw-r--r-- | src/markups.js | 1 | ||||
| -rw-r--r-- | src/services.js | 52 | ||||
| -rw-r--r-- | test/BinderTest.js | 4 | ||||
| -rw-r--r-- | test/servicesSpec.js | 36 | ||||
| -rw-r--r-- | test/widgetsSpec.js | 10 |
11 files changed, 235 insertions, 83 deletions
diff --git a/scenario/widgets.html b/scenario/widgets.html index 21060ebf..c2042b68 100644 --- a/scenario/widgets.html +++ b/scenario/widgets.html @@ -2,9 +2,9 @@ <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&rootScope=$view"></script> + <script type="text/javascript" src="../src/angular-bootstrap.js#autobind"></script> </head> - <body> + <body ng-init="$window.$scope = $root"> <table> <tr> <th>Description</th> @@ -32,7 +32,7 @@ <td>radio</td> <td> <input type="radio" name="gender" value="female"/> Female <br/> - <input type="radio" name="gender" value="male"/> Male + <input type="radio" name="gender" value="male" checked="checked"/> Male </td> <td>gender={{gender}}</td> </tr> @@ -42,7 +42,9 @@ <input type="checkbox" name="checkbox.tea" checked value="on"/> Tea<br/> <input type="checkbox" name="checkbox.coffee" value="on"/> Coffe </td> - <td>checkbox={{checkbox}}</td> + <td> + <pre>checkbox={{checkbox}}</pre> + </td> </tr> <tr> <td>select</td> @@ -71,10 +73,10 @@ <td>ng-action</td> <td> <form ng-init="button.count = 0"> - <input type="button" value="button" ng-action="button.count = button.count + 1"/> <br/> - <input type="submit" value="submit" ng-action="button.count = button.count + 1"/><br/> - <input type="image" src="" ng-action="button.count = button.count + 1"/><br/> - <a href="" ng-action="button.count = button.count + 1">action</a> + <input type="button" value="button" ng-change="button.count = button.count + 1"/> <br/> + <input type="submit" value="submit" ng-change="button.count = button.count + 1"/><br/> + <input type="image" src="" ng-change="button.count = button.count + 1"/><br/> + <a href="" ng-click="button.count = button.count + 1">action</a> </form> </td> <td>button={{button}}</td> diff --git a/src/Angular.js b/src/Angular.js index 8eef6ac0..d97c2282 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -1,3 +1,52 @@ + +////////////////////////////// +//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; + } +}; + +//////////////////////////////////// + if (typeof document.getAttribute == 'undefined') document.getAttribute = function() {}; @@ -16,7 +65,7 @@ var consoleNode, msie = !!/(msie) ([\w.]+)/.exec(lowercase(navigator.userAgent)), jqLite = jQuery || jqLiteWrap, slice = Array.prototype.slice, - angular = window['angular'] || (window['angular'] = {}), + angular = window['angular'] || (window['angular'] = {}), angularTextMarkup = extensionMap(angular, 'textMarkup'), angularAttrMarkup = extensionMap(angular, 'attrMarkup'), angularDirective = extensionMap(angular, 'directive'), @@ -336,3 +385,30 @@ function compile(element, config) { return compiler.compile($element)($element, rootScope); } ///////////////////////////////////////////////// + +function parseKeyValue(keyValue) { + var obj = {}, key_value, key; + foreach((keyValue || "").split('&'), function(keyValue){ + if (keyValue) { + key_value = keyValue.split('='); + key = decodeURIComponent(key_value[0]); + obj[key] = key_value[1] ? decodeURIComponent(key_value[1]) : true; + } + }); + return obj; +} + +function toKeyValue(obj) { + var parts = []; + foreach(obj, function(value, key){ + parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(value)); + }); + return parts.length ? parts.join('&') : ''; +}; + +function angularInit(config){ + if (config.autobind) { + compile(window.document, config).$init(); + } +} + diff --git a/src/Scope.js b/src/Scope.js index ba86e24f..2b2db189 100644 --- a/src/Scope.js +++ b/src/Scope.js @@ -102,6 +102,7 @@ function createScope(parent, Class) { instance = new Behavior(); extend(api, { + 'this': instance, $parent: parent, $bind: bind(instance, bind, instance), $get: bind(instance, getter, instance), @@ -163,6 +164,7 @@ function createScope(parent, Class) { behavior.$parent = instance; } + (parent.$onEval || noop)(instance.$eval); Class.apply(instance, slice.call(arguments, 2, arguments.length)); return instance; diff --git a/src/Widgets.js b/src/Widgets.js index f172eae2..e42d981c 100644 --- a/src/Widgets.js +++ b/src/Widgets.js @@ -85,8 +85,8 @@ function optionsAccessor(scope, element) { function noopAccessor() { return { get: noop, set: noop }; } -var textWidget = inputWidget('keyup change', modelAccessor, valueAccessor, ''), - buttonWidget = inputWidget('click', noopAccessor, noopAccessor, undefined), +var textWidget = inputWidget('keyup change', modelAccessor, valueAccessor, initWidgetValue('')), + buttonWidget = inputWidget('click', noopAccessor, noopAccessor, noop), INPUT_TYPE = { 'text': textWidget, 'textarea': textWidget, @@ -96,29 +96,42 @@ var textWidget = inputWidget('keyup change', modelAccessor, valueAccessor, ''), 'submit': buttonWidget, 'reset': buttonWidget, 'image': buttonWidget, - 'checkbox': inputWidget('click', modelAccessor, checkedAccessor, false), - 'radio': inputWidget('click', modelAccessor, radioAccessor, undefined), - 'select-one': inputWidget('click', modelAccessor, valueAccessor, null), - 'select-multiple': inputWidget('click', modelAccessor, optionsAccessor, []) + 'checkbox': inputWidget('click', modelAccessor, checkedAccessor, initWidgetValue(false)), + 'radio': inputWidget('click', modelAccessor, radioAccessor, radioInit), + 'select-one': inputWidget('change', modelAccessor, valueAccessor, initWidgetValue(null)), + 'select-multiple': inputWidget('change', modelAccessor, optionsAccessor, initWidgetValue([])) // 'file': fileWidget??? }; -function inputWidget(events, modelAccessor, viewAccessor, initValue) { +function initWidgetValue(initValue) { + return function (model, view) { + var value = view.get() || copy(initValue); + if (isUndefined(model.get()) && isDefined(value)) + model.set(value); + }; +} + +function radioInit(model, view) { + var modelValue = model.get(), viewValue = view.get(); + if (isUndefined(modelValue)) model.set(null); + if (viewValue != null) model.set(viewValue); +} + +function inputWidget(events, modelAccessor, viewAccessor, initFn) { return function(element) { var scope = this, model = modelAccessor(scope, element), view = viewAccessor(scope, element), - action = element.attr('ng-change') || '', - value = view.get() || copy(initValue); - if (isUndefined(model.get()) && isDefined(value)) model.set(value); + action = element.attr('ng-change') || ''; + initFn(model, view); this.$eval(element.attr('ng-init')||''); element.bind(events, function(){ model.set(view.get()); scope.$tryEval(action, element); scope.$root.$eval(); - // if we have no initValue than we are just a button, + // if we have noop initFn than we are just a button, // therefore we want to prevent default action - return isDefined(initValue); + return initFn != noop; }); view.set(model.get()); scope.$watch(model.get, view.set); @@ -137,3 +150,44 @@ angularWidget('SELECT', function(element){ this.descend(true); return inputWidgetSelector.call(this, element); }); + + +angularWidget('INLINE', function(element){ + element.replaceWith(this.element("div")); + 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(); + }); + }; +}); + +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)}); + }); + var compiler = this, + watchExpr = element.attr("watch"); + return function(boundElement){ + 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(); + }); + } + }); + }); + }; +}); diff --git a/src/angular-bootstrap.js b/src/angular-bootstrap.js index 7798afa5..b0a3aa4f 100644 --- a/src/angular-bootstrap.js +++ b/src/angular-bootstrap.js @@ -22,23 +22,16 @@ * THE SOFTWARE. */ (function(previousOnLoad){ - var filename = /(.*)\/angular-(.*).js(#(.*))?/; - var scripts = document.getElementsByTagName("SCRIPT"); - var serverPath; - var config = {}; + var filename = /(.*)\/angular-(.*).js(#(.*))?/, + scripts = document.getElementsByTagName("SCRIPT"), + serverPath, + config, + match; for(var j = 0; j < scripts.length; j++) { - var match = (scripts[j].src || "").match(filename); + match = (scripts[j].src || "").match(filename); if (match) { serverPath = match[1]; - parseConfig(match[4]); - } - } - - function parseConfig(args) { - var keyValues = args.split('&'), keyValue, i = 0; - for (; i < keyValues.length; i++) { - keyValue = keyValues[i].split('='); - config[keyValue[0]] = keyValue[1] || true; + config = match[4]; } } @@ -53,7 +46,6 @@ addScript("/jqlite.js"); addScript("/Parser.js"); addScript("/Resource.js"); - addScript("/URLWatcher.js"); // Extension points addScript("/apis.js"); @@ -63,17 +55,14 @@ addScript("/directives.js"); addScript("/markups.js"); addScript("/widgets.js"); + addScript("/services.js"); - if (config.autobind) { - window.onload = function(){ - try { - if (previousOnLoad) previousOnLoad(); - } catch(e) {} - var scope = angular.compile(window.document, config); - if (config.rootScope) window[config.rootScope] = scope; - scope.$init(); - }; - } + window.onload = function(){ + try { + if (previousOnLoad) previousOnLoad(); + } catch(e) {} + angularInit(parseKeyValue(config)); + }; })(window.onload); diff --git a/src/moveToAngularCom/Model.js b/src/delete/Model.js index b09efd0e..b09efd0e 100644 --- a/src/moveToAngularCom/Model.js +++ b/src/delete/Model.js diff --git a/src/markups.js b/src/markups.js index 6bc27c85..3ae713fb 100644 --- a/src/markups.js +++ b/src/markups.js @@ -51,6 +51,7 @@ angularTextMarkup('{{}}', function(text, textNode, parentElement) { } }); +// TODO: this should be widget not a markup angularTextMarkup('OPTION', function(text, textNode, parentElement){ if (parentElement[0].nodeName == "OPTION") { var select = document.createElement('select'); diff --git a/src/services.js b/src/services.js index 14c71363..fc12b22b 100644 --- a/src/services.js +++ b/src/services.js @@ -1,34 +1,40 @@ angularService("$window", bind(window, identity, window)); -angularService("$anchor", function(){ +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(){ var scope = this; - function anchor(url){ + function location(url){ if (isDefined(url)) { - if (url.charAt(0) == '#') url = url.substr(1); - var pathQuery = url.split('?'); - anchor.path = decodeURIComponent(pathQuery[0]); - anchor.param = {}; - foreach((pathQuery[1] || "").split('&'), function(keyValue){ - if (keyValue) { - var parts = keyValue.split('='); - var key = decodeURIComponent(parts[0]); - var value = parts[1]; - if (!value) value = true; - anchor.param[key] = decodeURIComponent(value); - } - }); + var match = URL_MATCH.exec(url); + if (match) { + location.href = url; + location.protocol = match[1]; + location.host = match[3] || ''; + location.port = match[5] || DEFAULT_PORTS[location.href] || null; + location.path = match[6]; + location.search = parseKeyValue(match[8]); + location.hash = match[9]; + if (location.hash) location.hash = location.hash.substr(1); + location.hashPath = match[11] || ''; + location.hashSearch = parseKeyValue(match[13]); + } } - var params = []; - foreach(anchor.param, function(value, key){ - params.push(encodeURIComponent(key) + '=' + encodeURIComponent(value)); - }); - return (anchor.path ? anchor.path : '') + (params.length ? '?' + params.join('&') : ''); + var hashKeyValue = toKeyValue(location.hashSearch); + return location.href + + (location.hashPath ? location.hashPath : '') + + (hashKeyValue ? '?' + hashKeyValue : ''); }; this.$config.location.watch(function(url){ - anchor(url); + location(url); }); + location(this.$config.location.get()); this.$onEval(PRIORITY_LAST, function(){ - scope.$config.location.set(anchor()); + var href = location(); + if (href != location.href) { + scope.$config.location.set(location()); + location.href = href; + } }); - return anchor; + return location; }); diff --git a/test/BinderTest.js b/test/BinderTest.js index 67800e62..9c5c5dc6 100644 --- a/test/BinderTest.js +++ b/test/BinderTest.js @@ -721,13 +721,13 @@ BinderTest.prototype.testItShouldSelectTheCorrectRadioBox = function() { var male = jqLite(c.node[0].childNodes[1]); female.click(); - assertEquals("female", c.scope.$get("sex")); + assertEquals("female", c.scope.sex); assertEquals(true, female[0].checked); assertEquals(false, male[0].checked); assertEquals("female", female.val()); male.click(); - assertEquals("male", c.scope.$get("sex")); + assertEquals("male", c.scope.sex); assertEquals(false, female[0].checked); assertEquals(true, male[0].checked); assertEquals("male", male.val()); diff --git a/test/servicesSpec.js b/test/servicesSpec.js index 5a6bcedc..b92975d0 100644 --- a/test/servicesSpec.js +++ b/test/servicesSpec.js @@ -13,15 +13,37 @@ describe("services", function(){ expect(scope.$window).toEqual(window); }); - it("should inject $anchor", function(){ - scope.$anchor('#path?key=value'); - expect(scope.$anchor.path).toEqual("path"); - expect(scope.$anchor.param).toEqual({key:'value'}); + it("should inject $location", function(){ + scope.$location('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"); + expect(scope.$location.port).toEqual("123"); + expect(scope.$location.path).toEqual("/p/a/t/h.html"); + expect(scope.$location.search).toEqual({query:'value'}); + expect(scope.$location.hash).toEqual('path?key=value'); + expect(scope.$location.hashPath).toEqual('path'); + expect(scope.$location.hashSearch).toEqual({key:'value'}); - scope.$anchor.path = 'page=http://path'; - scope.$anchor.param = {k:'a=b'}; + scope.$location.hashPath = 'page=http://path'; + scope.$location.hashSearch = {k:'a=b'}; - expect(scope.$anchor()).toEqual('page=http://path?k=a%3Db'); + expect(scope.$location()).toEqual('http://host:123/p/a/t/h.html?query=value#path?key=valuepage=http://path?k=a%3Db'); + }); + + it('should parse file://', function(){ + scope.$location('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(""); + expect(scope.$location.port).toEqual(null); + expect(scope.$location.path).toEqual("/Users/Shared/misko/work/angular.js/scenario/widgets.html"); + expect(scope.$location.search).toEqual({}); + expect(scope.$location.hash).toEqual(''); + expect(scope.$location.hashPath).toEqual(''); + expect(scope.$location.hashSearch).toEqual({}); + expect(scope.$location()).toEqual('file:///Users/Shared/misko/work/angular.js/scenario/widgets.html'); }); + }); diff --git a/test/widgetsSpec.js b/test/widgetsSpec.js index 6123e229..6c47e5dd 100644 --- a/test/widgetsSpec.js +++ b/test/widgetsSpec.js @@ -114,23 +114,23 @@ describe("input widget", function(){ it('should type="checkbox"', function(){ compile('<input type="checkbox" name="checkbox" checked ng-change="action = true"/>'); - expect(scope.$get('checkbox')).toEqual(true); + expect(scope.checkbox).toEqual(true); element.click(); - expect(scope.$get('checkbox')).toEqual(false); - expect(scope.$get('action')).toEqual(true); + expect(scope.checkbox).toEqual(false); + expect(scope.action).toEqual(true); element.click(); - expect(scope.$get('checkbox')).toEqual(true); + expect(scope.checkbox).toEqual(true); }); it('should type="radio"', function(){ compile('<div>' + '<input type="radio" name="chose" value="A" ng-change="clicked = 1"/>' + '<input type="radio" name="chose" value="B" checked ng-change="clicked = 2"/>' + + '<input type="radio" name="chose" value="C" ng-change="clicked = 3"/>' + '</div>'); var a = element[0].childNodes[0]; var b = element[0].childNodes[1]; expect(scope.chose).toEqual('B'); - expect(scope.clicked).not.toBeDefined(); scope.chose = 'A'; scope.$eval(); expect(a.checked).toEqual(true); |
