From 9ee2cdff44e7d496774b340de816344126c457b3 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Tue, 22 Nov 2011 21:28:39 -0800 Subject: refactor(directives): connect new compiler - turn everything into a directive --- src/widgets.js | 591 +++++++++++++++++++++++++++++---------------------------- 1 file changed, 298 insertions(+), 293 deletions(-) (limited to 'src/widgets.js') diff --git a/src/widgets.js b/src/widgets.js index 53be8b14..cf32bdc1 100644 --- a/src/widgets.js +++ b/src/widgets.js @@ -67,7 +67,7 @@ url of the template: {{template.url}}
- +
@@ -87,64 +87,62 @@ */ -angularWidget('ng:include', function(element){ - var compiler = this, - srcExp = element.attr("src"), - scopeExp = element.attr("scope") || '', - onloadExp = element[0].getAttribute('onload') || '', //workaround for jquery bug #7537 - autoScrollExp = element.attr('autoscroll'); - - if (element[0]['ng:compiled']) { - this.descend(true); - this.directives(true); - } else { - element[0]['ng:compiled'] = true; - return ['$http', '$templateCache', '$anchorScroll', '$element', - function($http, $templateCache, $anchorScroll, element) { - var scope = this, - changeCounter = 0, - childScope; - - function incrementChange() { changeCounter++;} - this.$watch(srcExp, incrementChange); - this.$watch(function() { - var includeScope = scope.$eval(scopeExp); - if (includeScope) return includeScope.$id; - }, incrementChange); - this.$watch(function() {return changeCounter;}, function(newChangeCounter) { - var src = scope.$eval(srcExp), - useScope = scope.$eval(scopeExp); - - function clearContent() { - // if this callback is still desired - if (newChangeCounter === changeCounter) { - if (childScope) childScope.$destroy(); - childScope = null; - element.html(''); - } - } - - if (src) { - $http.get(src, {cache: $templateCache}).success(function(response) { - // if this callback is still desired - if (newChangeCounter === changeCounter) { - element.html(response); - if (childScope) childScope.$destroy(); - childScope = useScope ? useScope : scope.$new(); - compiler.compile(element)(childScope); - if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) { - $anchorScroll(); +var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile', + function($http, $templateCache, $anchorScroll, $compile) { + return { + compile: function(element, attr) { + var srcExp = attr.src, + scopeExp = attr.scope || '', + onloadExp = attr.onload || '', //workaround for jquery bug #7537 + autoScrollExp = attr.autoscroll; + if (!element[0]['ng:compiled']) { + element[0]['ng:compiled'] = true; + return function(scope, element, attr){ + var changeCounter = 0, + childScope; + + function incrementChange() { changeCounter++;} + scope.$watch(srcExp, incrementChange); + scope.$watch(function() { + var includeScope = scope.$eval(scopeExp); + if (includeScope) return includeScope.$id; + }, incrementChange); + scope.$watch(function() {return changeCounter;}, function(newChangeCounter) { + var src = scope.$eval(srcExp), + useScope = scope.$eval(scopeExp); + + function clearContent() { + // if this callback is still desired + if (newChangeCounter === changeCounter) { + if (childScope) childScope.$destroy(); + childScope = null; + element.html(''); } - scope.$eval(onloadExp); } - }).error(clearContent); - } else { - clearContent(); - } - }); - }]; + + if (src) { + $http.get(src, {cache: $templateCache}).success(function(response) { + // if this callback is still desired + if (newChangeCounter === changeCounter) { + element.html(response); + if (childScope) childScope.$destroy(); + childScope = useScope ? useScope : scope.$new(); + $compile(element)(childScope); + if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) { + $anchorScroll(); + } + scope.$eval(onloadExp); + } + }).error(clearContent); + } else { + clearContent(); + } + }); + }; + } + } } -}); +}]; /** * @ngdoc widget @@ -203,58 +201,62 @@ angularWidget('ng:include', function(element){ */ -angularWidget('ng:switch', function(element) { - var compiler = this, - watchExpr = element.attr("on"), - changeExpr = element.attr('change'), - casesTemplate = {}, - defaultCaseTemplate, - children = element.children(), - length = children.length, - child, - when; - - if (!watchExpr) throw new Error("Missing 'on' attribute."); - while(length--) { - child = jqLite(children[length]); - // this needs to be here for IE - child.remove(); - when = child.attr('ng:switch-when'); - if (isString(when)) { - casesTemplate[when] = compiler.compile(child); - } else if (isString(child.attr('ng:switch-default'))) { - defaultCaseTemplate = compiler.compile(child); - } - } - children = null; // release memory; - element.html(''); +var ngSwitchDirective = ['$compile', function($compile){ + return { + compile: function(element, attr) { + var watchExpr = attr.on, + changeExpr = attr.change, + casesTemplate = {}, + defaultCaseTemplate, + children = element.children(), + length = children.length, + child, + when; + + if (!watchExpr) throw new Error("Missing 'on' attribute."); + while(length--) { + child = jqLite(children[length]); + // this needs to be here for IE + child.remove(); + // TODO(misko): this attr reading is not normilized + when = child.attr('ng:switch-when'); + if (isString(when)) { + casesTemplate[when] = $compile(child); + // TODO(misko): this attr reading is not normilized + } else if (isString(child.attr('ng:switch-default'))) { + defaultCaseTemplate = $compile(child); + } + } + children = null; // release memory; + element.html(''); - return function(element){ - var changeCounter = 0; - var childScope; - var selectedTemplate; - var scope = this; + return function(scope, element, attr){ + var changeCounter = 0; + var childScope; + var selectedTemplate; - this.$watch(watchExpr, function(value) { - element.html(''); - if ((selectedTemplate = casesTemplate[value] || defaultCaseTemplate)) { - changeCounter++; - if (childScope) childScope.$destroy(); - childScope = scope.$new(); - childScope.$eval(changeExpr); - } - }); + scope.$watch(watchExpr, function(value) { + element.html(''); + if ((selectedTemplate = casesTemplate[value] || defaultCaseTemplate)) { + changeCounter++; + if (childScope) childScope.$destroy(); + childScope = scope.$new(); + childScope.$eval(changeExpr); + } + }); - this.$watch(function() {return changeCounter;}, function() { - element.html(''); - if (selectedTemplate) { - selectedTemplate(childScope, function(caseElement) { - element.append(caseElement); + scope.$watch(function() {return changeCounter;}, function() { + element.html(''); + if (selectedTemplate) { + selectedTemplate(childScope, function(caseElement) { + element.append(caseElement); + }); + } }); - } - }); + }; + } }; -}); +}]; /* @@ -265,25 +267,24 @@ angularWidget('ng:switch', function(element) { * changing the location or causing page reloads, e.g.: * Save */ -angularWidget('a', function() { - this.descend(true); - this.directives(true); - - return function(element) { - var hasNgHref = ((element.attr('ng:bind-attr') || '').indexOf('"href":') !== -1); - +var htmlAnchorDirective = valueFn({ + restrict: 'E', + compile: function(element, attr) { // turn link into a link in IE // but only if it doesn't have name attribute, in which case it's an anchor - if (!hasNgHref && !element.attr('name') && !element.attr('href')) { - element.attr('href', ''); + if (!attr.href) { + attr.$set('href', ''); } - if (element.attr('href') === '' && !hasNgHref) { + return function(scope, element) { element.bind('click', function(event){ - event.preventDefault(); + // if we have no href url, then don't navigate anywhere. + if (!element.attr('href')) { + event.preventDefault(); + } }); } - }; + } }); @@ -344,125 +345,131 @@ angularWidget('a', function() { */ -angularWidget('@ng:repeat', function(expression, element){ - element.removeAttr('ng:repeat'); - element.replaceWith(jqLite('')); - var linker = this.compile(element); - return function(iterStartElement){ - var match = expression.match(/^\s*(.+)\s+in\s+(.*)\s*$/), - lhs, rhs, valueIdent, keyIdent; - if (! match) { - throw Error("Expected ng:repeat in form of '_item_ in _collection_' but got '" + - expression + "'."); - } - lhs = match[1]; - rhs = match[2]; - match = lhs.match(/^([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\)$/); - if (!match) { - throw Error("'item' in 'item in collection' should be identifier or (key, value) but got '" + - keyValue + "'."); - } - valueIdent = match[3] || match[1]; - keyIdent = match[2]; - - var parentScope = this; - // Store a list of elements from previous run. This is a hash where key is the item from the - // iterator, and the value is an array of objects with following properties. - // - scope: bound scope - // - element: previous element. - // - index: position - // We need an array of these objects since the same object can be returned from the iterator. - // We expect this to be a rare case. - var lastOrder = new HashQueueMap(); - this.$watch(function(scope){ - var index, length, - collection = scope.$eval(rhs), - collectionLength = size(collection, true), - childScope, - // Same as lastOrder but it has the current state. It will become the - // lastOrder on the next iteration. - nextOrder = new HashQueueMap(), - key, value, // key/value of iteration - array, last, // last object information {scope, element, index} - cursor = iterStartElement; // current position of the node - - if (!isArray(collection)) { - // if object, extract keys, sort them and use to determine order of iteration over obj props - array = []; - for(key in collection) { - if (collection.hasOwnProperty(key) && key.charAt(0) != '$') { - array.push(key); - } +var ngRepeatDirective = ['$compile', function($compile) { + return { + priority: 1000, + terminal: true, + compile: function(element, attr) { + var expression = attr.ngRepeat; + attr.$set(attr.$attr.ngRepeat); + element.replaceWith(jqLite('')); + var linker = $compile(element); + return function(scope, iterStartElement, attr){ + var match = expression.match(/^\s*(.+)\s+in\s+(.*)\s*$/), + lhs, rhs, valueIdent, keyIdent; + if (! match) { + throw Error("Expected ng:repeat in form of '_item_ in _collection_' but got '" + + expression + "'."); } - array.sort(); - } else { - array = collection || []; - } - - // we are not using forEach for perf reasons (trying to avoid #call) - for (index = 0, length = array.length; index < length; index++) { - key = (collection === array) ? index : array[index]; - value = collection[key]; - last = lastOrder.shift(value); - if (last) { - // if we have already seen this object, then we need to reuse the - // associated scope/element - childScope = last.scope; - nextOrder.push(value, last); - - if (index === last.index) { - // do nothing - cursor = last.element; + lhs = match[1]; + rhs = match[2]; + match = lhs.match(/^([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\)$/); + if (!match) { + throw Error("'item' in 'item in collection' should be identifier or (key, value) but got '" + + keyValue + "'."); + } + valueIdent = match[3] || match[1]; + keyIdent = match[2]; + + // Store a list of elements from previous run. This is a hash where key is the item from the + // iterator, and the value is an array of objects with following properties. + // - scope: bound scope + // - element: previous element. + // - index: position + // We need an array of these objects since the same object can be returned from the iterator. + // We expect this to be a rare case. + var lastOrder = new HashQueueMap(); + scope.$watch(function(scope){ + var index, length, + collection = scope.$eval(rhs), + collectionLength = size(collection, true), + childScope, + // Same as lastOrder but it has the current state. It will become the + // lastOrder on the next iteration. + nextOrder = new HashQueueMap(), + key, value, // key/value of iteration + array, last, // last object information {scope, element, index} + cursor = iterStartElement; // current position of the node + + if (!isArray(collection)) { + // if object, extract keys, sort them and use to determine order of iteration over obj props + array = []; + for(key in collection) { + if (collection.hasOwnProperty(key) && key.charAt(0) != '$') { + array.push(key); + } + } + array.sort(); } else { - // existing item which got moved - last.index = index; - // This may be a noop, if the element is next, but I don't know of a good way to - // figure this out, since it would require extra DOM access, so let's just hope that - // the browsers realizes that it is noop, and treats it as such. - cursor.after(last.element); - cursor = last.element; + array = collection || []; } - } else { - // new item which we don't know about - childScope = parentScope.$new(); - } - childScope[valueIdent] = value; - if (keyIdent) childScope[keyIdent] = key; - childScope.$index = index; - childScope.$position = index === 0 ? - 'first' : - (index == collectionLength - 1 ? 'last' : 'middle'); - - if (!last) { - linker(childScope, function(clone){ - cursor.after(clone); - last = { - scope: childScope, - element: (cursor = clone), - index: index - }; - nextOrder.push(value, last); - }); - } - } + // we are not using forEach for perf reasons (trying to avoid #call) + for (index = 0, length = array.length; index < length; index++) { + key = (collection === array) ? index : array[index]; + value = collection[key]; + last = lastOrder.shift(value); + if (last) { + // if we have already seen this object, then we need to reuse the + // associated scope/element + childScope = last.scope; + nextOrder.push(value, last); + + if (index === last.index) { + // do nothing + cursor = last.element; + } else { + // existing item which got moved + last.index = index; + // This may be a noop, if the element is next, but I don't know of a good way to + // figure this out, since it would require extra DOM access, so let's just hope that + // the browsers realizes that it is noop, and treats it as such. + cursor.after(last.element); + cursor = last.element; + } + } else { + // new item which we don't know about + childScope = scope.$new(); + } - //shrink children - for (key in lastOrder) { - if (lastOrder.hasOwnProperty(key)) { - array = lastOrder[key]; - while(array.length) { - value = array.pop(); - value.element.remove(); - value.scope.$destroy(); + childScope[valueIdent] = value; + if (keyIdent) childScope[keyIdent] = key; + childScope.$index = index; + childScope.$position = index === 0 ? + 'first' : + (index == collectionLength - 1 ? 'last' : 'middle'); + + if (!last) { + linker(childScope, function(clone){ + cursor.after(clone); + last = { + scope: childScope, + element: (cursor = clone), + index: index + }; + nextOrder.push(value, last); + }); + } } - } - } - lastOrder = nextOrder; - }); + //shrink children + for (key in lastOrder) { + if (lastOrder.hasOwnProperty(key)) { + array = lastOrder[key]; + while(array.length) { + value = array.pop(); + value.element.remove(); + value.scope.$destroy(); + } + } + } + + lastOrder = nextOrder; + }); + }; + } }; -}); +}]; /** @@ -496,7 +503,7 @@ angularWidget('@ng:repeat', function(expression, element){ */ -angularWidget("@ng:non-bindable", noop); +var ngNonBindableDirective = valueFn({ terminal: true }); /** @@ -564,49 +571,48 @@ angularWidget("@ng:non-bindable", noop); */ -angularWidget('ng:view', function(element) { - var compiler = this; - - if (!element[0]['ng:compiled']) { - element[0]['ng:compiled'] = true; - return ['$http', '$templateCache', '$route', '$anchorScroll', '$element', - function($http, $templateCache, $route, $anchorScroll, element) { - var template; - var changeCounter = 0; - - this.$on('$afterRouteChange', function() { - changeCounter++; - }); +var ngViewDirective = ['$http', '$templateCache', '$route', '$anchorScroll', '$compile', + function($http, $templateCache, $route, $anchorScroll, $compile) { + return { + compile: function(element, attr) { + if (!element[0]['ng:compiled']) { + element[0]['ng:compiled'] = true; + + return function(scope, element, attrs) { + var changeCounter = 0; + + scope.$on('$afterRouteChange', function() { + changeCounter++; + }); - this.$watch(function() {return changeCounter;}, function(newChangeCounter) { - var template = $route.current && $route.current.template; + scope.$watch(function() {return changeCounter;}, function(newChangeCounter) { + var template = $route.current && $route.current.template; - function clearContent() { - // ignore callback if another route change occured since - if (newChangeCounter == changeCounter) { - element.html(''); - } - } + function clearContent() { + // ignore callback if another route change occured since + if (newChangeCounter == changeCounter) { + element.html(''); + } + } - if (template) { - $http.get(template, {cache: $templateCache}).success(function(response) { - // ignore callback if another route change occured since - if (newChangeCounter == changeCounter) { - element.html(response); - compiler.compile(element)($route.current.scope); - $anchorScroll(); + if (template) { + $http.get(template, {cache: $templateCache}).success(function(response) { + // ignore callback if another route change occured since + if (newChangeCounter == changeCounter) { + element.html(response); + $compile(element)($route.current.scope); + $anchorScroll(); + } + }).error(clearContent); + } else { + clearContent(); } - }).error(clearContent); - } else { - clearContent(); - } - }); - }]; - } else { - compiler.descend(true); - compiler.directives(true); - } -}); + }); + }; + } + } + }; +}]; /** @@ -715,81 +721,80 @@ angularWidget('ng:view', function(element) { Without Offset: - -
+
With Offset(2): - - + it('should show correct pluralized string', function() { - expect(element('.doc-example-live .ng-pluralize:first').text()). + expect(element('.doc-example-live ng-pluralize:first').text()). toBe('1 person is viewing.'); - expect(element('.doc-example-live .ng-pluralize:last').text()). + expect(element('.doc-example-live ng-pluralize:last').text()). toBe('Igor is viewing.'); using('.doc-example-live').input('personCount').enter('0'); - expect(element('.doc-example-live .ng-pluralize:first').text()). + expect(element('.doc-example-live ng-pluralize:first').text()). toBe('Nobody is viewing.'); - expect(element('.doc-example-live .ng-pluralize:last').text()). + expect(element('.doc-example-live ng-pluralize:last').text()). toBe('Nobody is viewing.'); using('.doc-example-live').input('personCount').enter('2'); - expect(element('.doc-example-live .ng-pluralize:first').text()). + expect(element('.doc-example-live ng-pluralize:first').text()). toBe('2 people are viewing.'); - expect(element('.doc-example-live .ng-pluralize:last').text()). + expect(element('.doc-example-live ng-pluralize:last').text()). toBe('Igor and Misko are viewing.'); using('.doc-example-live').input('personCount').enter('3'); - expect(element('.doc-example-live .ng-pluralize:first').text()). + expect(element('.doc-example-live ng-pluralize:first').text()). toBe('3 people are viewing.'); - expect(element('.doc-example-live .ng-pluralize:last').text()). + expect(element('.doc-example-live ng-pluralize:last').text()). toBe('Igor, Misko and one other person are viewing.'); using('.doc-example-live').input('personCount').enter('4'); - expect(element('.doc-example-live .ng-pluralize:first').text()). + expect(element('.doc-example-live ng-pluralize:first').text()). toBe('4 people are viewing.'); - expect(element('.doc-example-live .ng-pluralize:last').text()). + expect(element('.doc-example-live ng-pluralize:last').text()). toBe('Igor, Misko and 2 other people are viewing.'); }); it('should show data-binded names', function() { using('.doc-example-live').input('personCount').enter('4'); - expect(element('.doc-example-live .ng-pluralize:last').text()). + expect(element('.doc-example-live ng-pluralize:last').text()). toBe('Igor, Misko and 2 other people are viewing.'); using('.doc-example-live').input('person1').enter('Di'); using('.doc-example-live').input('person2').enter('Vojta'); - expect(element('.doc-example-live .ng-pluralize:last').text()). + expect(element('.doc-example-live ng-pluralize:last').text()). toBe('Di, Vojta and 2 other people are viewing.'); }); */ -angularWidget('ng:pluralize', function(element) { - var numberExp = element.attr('count'), - whenExp = element.attr('when'), - offset = element.attr('offset') || 0; - - return ['$locale', '$element', function($locale, element) { - var scope = this, +var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interpolate) { + var BRACE = /{}/g; + return function(scope, element, attr) { + var numberExp = attr.count, + whenExp = attr.when, + offset = attr.offset || 0, whens = scope.$eval(whenExp), whensExpFns = {}; forEach(whens, function(expression, key) { - whensExpFns[key] = compileBindTemplate(expression.replace(/{}/g, - '{{' + numberExp + '-' + offset + '}}')); + whensExpFns[key] = + $interpolate(expression.replace(BRACE, '{{' + numberExp + '-' + offset + '}}')); }); scope.$watch(function() { @@ -806,5 +811,5 @@ angularWidget('ng:pluralize', function(element) { }, function(newVal) { element.text(newVal); }); - }]; -}); + }; +}]; -- cgit v1.2.3