From 58d0e8945d772eddbfecbe6a645b2f1c4dd38bf2 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Mon, 22 Nov 2010 12:05:01 -0800 Subject: allow documentation to be in external file * Load templates once instead of per request * show timing information * load files ending in .ngdoc and process them --- docs/angular.directive.ngdoc | 53 +++++++++++++++ docs/angular.element.ngdoc | 43 ++++++++++++ docs/angular.filter.ngdoc | 76 +++++++++++++++++++++ docs/angular.formatter.ngdoc | 78 +++++++++++++++++++++ docs/angular.ngdoc | 4 ++ docs/angular.service.ngdoc | 159 +++++++++++++++++++++++++++++++++++++++++++ docs/angular.validator.ngdoc | 73 ++++++++++++++++++++ docs/angular.widget.ngdoc | 73 ++++++++++++++++++++ docs/collect.js | 112 +++++++++++++++++++++--------- docs/index.html | 2 +- 10 files changed, 639 insertions(+), 34 deletions(-) create mode 100644 docs/angular.directive.ngdoc create mode 100644 docs/angular.element.ngdoc create mode 100644 docs/angular.filter.ngdoc create mode 100644 docs/angular.formatter.ngdoc create mode 100644 docs/angular.ngdoc create mode 100644 docs/angular.service.ngdoc create mode 100644 docs/angular.validator.ngdoc create mode 100644 docs/angular.widget.ngdoc (limited to 'docs') diff --git a/docs/angular.directive.ngdoc b/docs/angular.directive.ngdoc new file mode 100644 index 00000000..9a08e4c7 --- /dev/null +++ b/docs/angular.directive.ngdoc @@ -0,0 +1,53 @@ +@workInProgress +@ngdoc overview +@name angular.directive +@namespace Namespace for all directives. + +@description +A directive is an HTML attribute that you can use in an existing HTML element type or in a +DOM element type that you create as {@link angular.widget}, to modify that element's +properties. You can use any number of directives per element. + +For example, you can add the ng:bind directive as an attribute of an HTML span element, as in +``. How does this work? The compiler passes the attribute value +`1+2` to the ng:bind extension, which in turn tells the {@link angular.scope} to watch that +expression and report changes. On any change it sets the span text to the expression value. + +Here's how to define {@link angular.directive.ng:bind ng:bind}: +
+  angular.directive('ng:bind', function(expression, compiledElement) {
+    var compiler = this;
+    return function(linkElement) {
+      var currentScope = this;
+      currentScope.$watch(expression, function(value) {
+        linkElement.text(value);
+      });
+    };
+  });
+
+ +# Directive vs. Attribute Widget +Both [attribute widgets](#!angular.widget) and directives can compile a DOM element +attribute. So why have two different ways to do the same thing? The answer is that order +matters, but we have no control over the order in which attributes are read. To solve this +we apply attribute widget before the directive. + +For example, consider this piece of HTML, which uses the directives `ng:repeat`, `ng:init`, +and `ng:bind`: +
+  
+
+ +Notice that the order of execution matters here. We need to execute +{@link angular.directive.ng:repeat ng:repeat} before we run the +{@link angular.directive.ng:init ng:init} and `ng:bind` on the `
  • ;`. This is because we +want to run the `ng:init="a=a+1` and `ng:bind="person"` once for each person in people. We +could not have used directive to create this template because attributes are read in an +unspecified order and there is no way of guaranteeing that the repeater attribute would +execute first. Using the `ng:repeat` attribute directive ensures that we can transform the +DOM element into a template. + +Widgets run before directives. Widgets may manipulate the DOM whereas directives are not +expected to do so, and so they run last. diff --git a/docs/angular.element.ngdoc b/docs/angular.element.ngdoc new file mode 100644 index 00000000..a636cc25 --- /dev/null +++ b/docs/angular.element.ngdoc @@ -0,0 +1,43 @@ +@workInProgress +@ngdoc function +@name angular.element +@function + +@description +Wraps a raw DOM element or HTML string as [jQuery](http://jquery.com) element. +`angular.element` is either an alias for [jQuery](http://api.jquery.com/jQuery/) function if +jQuery is loaded or a function that wraps the element or string in angular's jQuery lite +implementation. + +Real jQuery always takes precedence if it was loaded before angular. + +Angular's jQuery lite implementation is a tiny API-compatible subset of jQuery which allows +angular to manipulate DOM. The functions implemented are usually just the basic versions of +them and might not support arguments and invocation styles. + +NOTE: All element references in angular are always wrapped with jQuery (lite) and are never +raw DOM references. + +Angular's jQuery lite implements these functions: + +- [addClass()](http://api.jquery.com/addClass/) +- [after()](http://api.jquery.com/after/) +- [append()](http://api.jquery.com/append/) +- [attr()](http://api.jquery.com/attr/) +- [bind()](http://api.jquery.com/bind/) +- [children()](http://api.jquery.com/children/) +- [clone()](http://api.jquery.com/clone/) +- [css()](http://api.jquery.com/css/) +- [data()](http://api.jquery.com/data/) +- [hasClass()](http://api.jquery.com/hasClass/) +- [parent()](http://api.jquery.com/parent/) +- [remove()](http://api.jquery.com/remove/) +- [removeAttr()](http://api.jquery.com/removeAttr/) +- [removeClass()](http://api.jquery.com/removeClass/) +- [removeData()](http://api.jquery.com/removeData/) +- [replaceWith()](http://api.jquery.com/replaceWith/) +- [text()](http://api.jquery.com/text/) +- [trigger()](http://api.jquery.com/trigger/) + +@param {string|DOMElement} element HTML string or DOMElement to be wrapped into jQuery. +@returns {Object} jQuery object. diff --git a/docs/angular.filter.ngdoc b/docs/angular.filter.ngdoc new file mode 100644 index 00000000..9d1191c5 --- /dev/null +++ b/docs/angular.filter.ngdoc @@ -0,0 +1,76 @@ +@workInProgress +@ngdoc overview +@name angular.filter +@namespace Namespace for all filters. +@description +# Overview +Filters are a standard way to format your data for display to the user. For example, you +might have the number 1234.5678 and would like to display it as US currency: $1,234.57. +Filters allow you to do just that. In addition to transforming the data, filters also modify +the DOM. This allows the filters to for example apply css styles to the filtered output if +certain conditions were met. + + +# Standard Filters + +The Angular framework provides a standard set of filters for common operations, including: +{@link angular.filter.currency currency}, {@link angular.filter.json json}, +{@link angular.filter.number number}, and {@link angular.filter.html html}. You can also add +your own filters. + + +# Syntax + +Filters can be part of any {@link angular.scope} evaluation but are typically used with +{{bindings}}. Filters typically transform the data to a new data type, formating the data in +the process. Filters can be chained and take optional arguments. Here are few examples: + +* No filter: {{1234.5678}} => 1234.5678 +* Number filter: {{1234.5678|number}} => 1,234.57. Notice the “,” and rounding to two + significant digits. +* Filter with arguments: {{1234.5678|number:5}} => 1,234.56780. Filters can take optional + arguments, separated by colons in a binding. To number, the argument “5” requests 5 digits + to the right of the decimal point. + + +# Writing your own Filters + +Writing your own filter is very easy: just define a JavaScript function on `angular.filter`. +The framework passes in the input value as the first argument to your function. Any filter +arguments are passed in as additional function arguments. + +You can use these variables in the function: + +* `this` — The current scope. +* `this.$element` — The DOM element containing the binding. This allows the filter to manipulate + the DOM in addition to transforming the input. + + +@exampleDescription + The following example filter reverses a text string. In addition, it conditionally makes the + text upper-case (to demonstrate optional arguments) and assigns color (to demonstrate DOM + modification). + +@example + + +
    + No filter: {{text}}
    + Reverse: {{text|reverse}}
    + Reverse + uppercase: {{text|reverse:true}}
    + Reverse + uppercase + blue: {{text|reverse:true:"blue"}} + diff --git a/docs/angular.formatter.ngdoc b/docs/angular.formatter.ngdoc new file mode 100644 index 00000000..ba28471f --- /dev/null +++ b/docs/angular.formatter.ngdoc @@ -0,0 +1,78 @@ +@workInProgress +@ngdoc overview +@name angular.formatter +@namespace Namespace for all formats. +@description +# Overview +The formatters are responsible for translating user readable text in an input widget to a +data model stored in an application. + +# Writting your own Formatter +Writing your own formatter is easy. Just register a pair of JavaScript functions with +`angular.formatter`. One function for parsing user input text to the stored form, +and one for formatting the stored data to user-visible text. + +Here is an example of a "reverse" formatter: The data is stored in uppercase and in +reverse, while it is displayed in lower case and non-reversed. User edits are +automatically parsed into the internal form and data changes are automatically +formatted to the viewed form. + +
    +function reverse(text) {
    +  var reversed = [];
    +  for (var i = 0; i < text.length; i++) {
    +    reversed.unshift(text.charAt(i));
    +  }
    +  return reversed.join('');
    +}
    +
    +angular.formatter('reverse', {
    +  parse: function(value){
    +    return reverse(value||'').toUpperCase();
    +  },
    +  format: function(value){
    +    return reverse(value||'').toLowerCase();
    +  }
    +});
    +
    + +@example + + +Formatted: + +
    + +Stored: +
    +
    {{data}}
    + + +@scenario +it('should store reverse', function(){ + expect(element('.doc-example input:first').val()).toEqual('angular'); + expect(element('.doc-example input:last').val()).toEqual('RALUGNA'); + + this.addFutureAction('change to XYZ', function($window, $document, done){ + $document.elements('.doc-example input:last').val('XYZ').trigger('change'); + done(); + }); + expect(element('.doc-example input:first').val()).toEqual('zyx'); +}); diff --git a/docs/angular.ngdoc b/docs/angular.ngdoc new file mode 100644 index 00000000..3f342d1b --- /dev/null +++ b/docs/angular.ngdoc @@ -0,0 +1,4 @@ +@workInProgress +@ngdoc overview +@name angular +@namespace The exported angular namespace. diff --git a/docs/angular.service.ngdoc b/docs/angular.service.ngdoc new file mode 100644 index 00000000..30fb8512 --- /dev/null +++ b/docs/angular.service.ngdoc @@ -0,0 +1,159 @@ +@workInProgress +@ngdoc overview +@name angular.service + +@description +# Overview +Services are substituable objects, which are wired together using dependency injection. +Each service could have dependencies (other services), which are passed in constructor. +Because JS is dynamicaly typed language, dependency injection can not use static types +to satisfy these dependencies, so each service must explicitely define its dependencies. +This is done by `$inject` property. + +For now, life time of all services is the same as the life time of page. + + +# Built-in services +The Angular framework provides a standard set of services for common operations. +You can write your own services and rewrite these standard services as well. +Like other core angular variables, the built-in services always start with $. + + * `angular.service.$browser` + * `angular.service.$window` + * `angular.service.$document` + * `angular.service.$location` + * `angular.service.$log` + * `angular.service.$exceptionHandler` + * `angular.service.$hover` + * `angular.service.$invalidWidgets` + * `angular.service.$route` + * `angular.service.$xhr` + * `angular.service.$xhr.error` + * `angular.service.$xhr.bulk` + * `angular.service.$xhr.cache` + * `angular.service.$resource` + * `angular.service.$cookies` + * `angular.service.$cookieStore` + +# Writing your own custom services +Angular provides only set of basic services, so you will probably need to write your custom +service very soon. To do so, you need to write a factory function and register this function +to angular's dependency injector. This factory function must return an object - your service +(it is not called with new operator). + +**angular.service** has three parameters: + + - `{string} name` - Name of the service + - `{function()} factory` - Factory function (called just once by DI) + - `{Object} config` - Hash of configuration (`$inject`, `$creation`) + +If your service requires - depends on other services, you need to specify them +in config hash - property $inject. This property is an array of strings (service names). +These dependencies will be passed as parameters to the factory function by DI. +This approach is very useful when testing, as you can inject mocks/stubs/dummies. + +Here is an example of very simple service. This service requires $window service (it's +passed as a parameter to factory function) and it's just a function. + +This service simple stores all notifications and after third one, it displays all of them by +window alert. +
    +       angular.service('notify', function(win) {
    +         var msgs = [];
    +         return function(msg) {
    +           msgs.push(msg);
    +           if (msgs.length == 3) {
    +             win.alert(msgs.join("\n"));
    +             msgs = [];
    +           }
    +         };
    +       }, {$inject: ['$window']});
    +
    + +And here is a unit test for this service. We use Jasmine spy (mock) instead of real browser's alert. +
    +var mock, notify;
    +
    +beforeEach(function() {
    +  mock = {alert: jasmine.createSpy()};
    +  notify = angular.service('notify')(mock);
    +});
    + 
    +it('should not alert first two notifications', function() {
    +  notify('one');
    +  notify('two');
    +  expect(mock.alert).not.toHaveBeenCalled();
    +});
    +
    +it('should alert all after third notification', function() {
    +  notify('one');
    +  notify('two');
    +  notify('three');
    +  expect(mock.alert).toHaveBeenCalledWith("one\ntwo\nthree");
    +});
    +
    +it('should clear messages after alert', function() {
    +  notify('one');
    +  notify('two');
    +  notify('third');
    +  notify('more');
    +  notify('two');
    +  notify('third');
    +  expect(mock.alert.callCount).toEqual(2);
    +  expect(mock.alert.mostRecentCall.args).toEqual(["more\ntwo\nthird"]);
    +});
    +
    + +# Injecting services into controllers +Using services in a controllers is very similar to using service in other service. +Again, we will use dependency injection. + +JavaScript is dynamic language, so DI is not able to figure out which services to inject by +static types (like in static typed languages). Therefore you must specify the service name +by the `$inject` property - it's an array that contains strings with names of services to be +injected. The name must match the id that service has been registered as with angular. +The order of the services in the array matters, because this order will be used when calling +the factory function with injected parameters. The names of parameters in factory function +don't matter, but by convention they match the service ids. +
    +function myController($loc, $log) {
    +  this.firstMethod = function() {
    +    // use $location service
    +    $loc.setHash();
    +  };
    +  this.secondMethod = function() {
    +    // use $log service
    +    $log.info('...');
    +  };
    +}
    +// which services to inject ?
    +myController.$inject = ['$location', '$log']; 
    +
    + +@example + + +
    +

    Let's try this simple notify service, injected into the controller...

    + + +
    diff --git a/docs/angular.validator.ngdoc b/docs/angular.validator.ngdoc new file mode 100644 index 00000000..acd3caf2 --- /dev/null +++ b/docs/angular.validator.ngdoc @@ -0,0 +1,73 @@ +@workInProgress +@ngdoc overview +@name angular.validator +@namespace Namespace for all filters. +@description +# Overview +Validators are a standard way to check the user input against a specific criteria. For +example, you might need to check that an input field contains a well-formed phone number. + +# Syntax +Attach a validator on user input widgets using the `ng:validate` attribute. + + + + Change me: <input type="text" name="number" ng:validate="integer" value="123"> + + + it('should validate the default number string', function() { + expect(element('input[name=number]').attr('class')). + not().toMatch(/ng-validation-error/); + }); + it('should not validate "foo"', function() { + input('number').enter('foo'); + expect(element('input[name=number]').attr('class')). + toMatch(/ng-validation-error/); + }); + + + + +# Writing your own Validators +Writing your own validator is easy. To make a function available as a +validator, just define the JavaScript function on the `angular.validator` +object. passes in the input to validate as the first argument +to your function. Any additional validator arguments are passed in as +additional arguments to your function. + +You can use these variables in the function: + +* `this` — The current scope. +* `this.$element` — The DOM element containing the binding. This allows the filter to manipulate + the DOM in addition to transforming the input. + +In this example we have written a upsTrackingNo validator. +It marks the input text "valid" only when the user enters a well-formed +UPS tracking number. + +@css ng-validation-error + When validation fails, this css class is applied to the binding, making its borders red by + default. + +@example + + + +@scenario +it('should validate correct UPS tracking number', function() { + expect(element('input[name=trackNo]').attr('class')). + not().toMatch(/ng-validation-error/); +}); + +it('should not validate in correct UPS tracking number', function() { + input('trackNo').enter('foo'); + expect(element('input[name=trackNo]').attr('class')). + toMatch(/ng-validation-error/); +}); diff --git a/docs/angular.widget.ngdoc b/docs/angular.widget.ngdoc new file mode 100644 index 00000000..5f15398f --- /dev/null +++ b/docs/angular.widget.ngdoc @@ -0,0 +1,73 @@ +@workInProgress +@ngdoc overview +@name angular.widget +@namespace Namespace for all widgets. +@description +# Overview +Widgets allow you to create DOM elements that the browser doesn't +already understand. You create the widget in your namespace and +assign it behavior. You can only bind one widget per DOM element +(unlike directives, in which you can use any number per DOM +element). Widgets are expected to manipulate the DOM tree by +adding new elements whereas directives are expected to only modify +element properties. + +Widgets come in two flavors: element and attribute. + +# Element Widget +Let's say we would like to create a new element type in the +namespace `my` that can watch an expression and alert() the user +with each new value. + +
    +<my:watch exp="name"/>
    +
    + +You can implement `my:watch` like this: +
    +angular.widget('my:watch', function(compileElement) {
    +  var compiler = this;
    +  var exp = compileElement.attr('exp');
    +  return function(linkElement) {
    +    var currentScope = this;
    +    currentScope.$watch(exp, function(value){
    +      alert(value);
    +    }};
    +  };
    +});
    +
    + +# Attribute Widget +Let's implement the same widget, but this time as an attribute +that can be added to any existing DOM element. +
    +<div my-watch="name">text</div>
    +
    +You can implement `my:watch` attribute like this: +
    +angular.widget('@my:watch', function(expression, compileElement) {
    +  var compiler = this;
    +  return function(linkElement) {
    +    var currentScope = this;
    +    currentScope.$watch(expression, function(value){
    +      alert(value);
    +    });
    +  };
    +});
    +
    + +@example + + + \ No newline at end of file diff --git a/docs/collect.js b/docs/collect.js index 666c92bf..e2f3b940 100644 --- a/docs/collect.js +++ b/docs/collect.js @@ -16,39 +16,29 @@ var keywordPages = []; var SRC_DIR = "docs/"; var OUTPUT_DIR = "build/docs/"; var NEW_LINE = /\n\r?/; +var TEMPLATES = {}; +var start = now(); +function now(){ return new Date().getTime(); } var work = callback.chain(function () { console.log('Parsing Angular Reference Documentation'); - mkdirPath(OUTPUT_DIR, work.waitFor(function(){ - findJsFiles('src', work.waitMany(function(file) { - //console.log('reading', file, '...'); - findNgDoc(file, work.waitMany(function(doc) { - parseNgDoc(doc); - processNgDoc(documentation, doc); - })); + findJsFiles('src', work.waitMany(function(file) { + //console.log('reading', file, '...'); + findNgDocInJsFile(file, work.waitMany(function(doc) { + parseNgDoc(doc); + processNgDoc(documentation, doc); })); })); + findNgDocInDir(SRC_DIR, work.waitMany(function(doc){ + parseNgDoc(doc); + processNgDoc(documentation, doc); + })); + loadTemplates(TEMPLATES, work.waitFor()); + mkdirPath(OUTPUT_DIR, work.waitFor()); }).onError(function(err){ console.log('ERROR:', err.stack || err); }).onDone(function(){ - keywordPages.sort(function(a,b){ - // supper ugly comparator that orders all utility methods and objects before all the other stuff - // like widgets, directives, services, etc. - // Mother of all beatiful code please forgive me for the sin that this code certainly is. - - if (a.name === b.name) return 0; - if (a.name === 'angular') return -1; - if (b.name === 'angular') return 1; - - function namespacedName(page) { - return (page.name.match(/\./g).length === 1 && page.type !== 'overview' ? '0' : '1') + page.name; - } - - var namespacedA = namespacedName(a), - namespacedB = namespacedName(b); - - return namespacedA < namespacedB ? -1 : 1; - }); + keywordPages.sort(keywordSort); writeDoc(documentation.pages); mergeTemplate('docs-data.js', 'docs-data.js', {JSON:JSON.stringify(keywordPages)}, callback.chain()); mergeTemplate('docs-scenario.js', 'docs-scenario.js', documentation, callback.chain()); @@ -58,7 +48,7 @@ var work = callback.chain(function () { mergeTemplate('docs.js', 'docs.js', documentation, callback.chain()); mergeTemplate('doc_widgets.css', 'doc_widgets.css', documentation, callback.chain()); mergeTemplate('doc_widgets.js', 'doc_widgets.js', documentation, callback.chain()); - console.log('DONE'); + console.log('DONE', now() - start, 'ms.'); }); if (!this.testmode) work(); //////////////////// @@ -163,7 +153,7 @@ function markdownTag(doc, name, value) { replace(/\<\/pre\>/gmi, ''); } -R_LINK = /{@link ([^\s}]+)((\s|\n)+(.+?))?\s*}/m +var R_LINK = /{@link ([^\s}]+)((\s|\n)+(.+?))?\s*}/m; // 1 123 3 4 42 function markdown(text) { @@ -313,7 +303,7 @@ function parseNgDoc(doc){ } } -function findNgDoc(file, callback) { +function findNgDocInJsFile(file, callback) { fs.readFile(file, callback.waitFor(function(err, content){ var lines = content.toString().split(NEW_LINE); var doc; @@ -346,6 +336,22 @@ function findNgDoc(file, callback) { })); } +function loadTemplates(cache, callback){ + fs.readdir('docs', callback.waitFor(function(err, files){ + if (err) return this.error(err); + files.forEach(function(file){ + var match = file.match(/^(.*)\.template$/); + if (match) { + fs.readFile(SRC_DIR + file, callback.waitFor(function(err, content){ + if (err) return this.error(err); + cache[match[1]] = content.toString(); + })); + } + }); + callback(); + })); +}; + function findJsFiles(dir, callback){ fs.readdir(dir, callback.waitFor(function(err, files){ if (err) return this.error(err); @@ -365,7 +371,7 @@ function findJsFiles(dir, callback){ function processNgDoc(documentation, doc) { if (!doc.ngdoc) return; - console.log('Found:', doc.ngdoc + ':' + doc.name); + //console.log('Found:', doc.ngdoc + ':' + doc.name); documentation.byName[doc.name] = doc; @@ -385,10 +391,50 @@ function processNgDoc(documentation, doc) { } } -function writeDoc(pages) { +function writeDoc(pages, callback) { pages.forEach(function(doc) { - mergeTemplate( - doc.ngdoc + '.template', - doc.name + '.html', doc, callback.chain()); + var template = TEMPLATES[doc.ngdoc]; + if (!template) throw new Error("No template for:" + doc.ngdoc); + var content = mustache.to_html(template, doc); + fs.writeFile(OUTPUT_DIR + doc.name + '.html', content, callback); }); } + +function findNgDocInDir(directory, docNotify) { + fs.readdir(directory, docNotify.waitFor(function(err, files){ + if (err) return this.error(err); + files.forEach(function(file){ + console.log(file); + if (!file.match(/\.ngdoc$/)) return; + fs.readFile(directory + file, docNotify.waitFor(function(err, content){ + if (err) return this.error(err); + docNotify({ + raw:{ + text:content.toString(), + file: directory + file, + line: 1} + }); + })); + }); + docNotify.done(); + })); +} + +function keywordSort(a,b){ + // supper ugly comparator that orders all utility methods and objects before all the other stuff + // like widgets, directives, services, etc. + // Mother of all beautiful code please forgive me for the sin that this code certainly is. + + if (a.name === b.name) return 0; + if (a.name === 'angular') return -1; + if (b.name === 'angular') return 1; + + function namespacedName(page) { + return (page.name.match(/\./g).length === 1 && page.type !== 'overview' ? '0' : '1') + page.name; + } + + var namespacedA = namespacedName(a), + namespacedB = namespacedName(b); + + return namespacedA < namespacedB ? -1 : 1; +} diff --git a/docs/index.html b/docs/index.html index f5502664..fcc36a2d 100644 --- a/docs/index.html +++ b/docs/index.html @@ -22,7 +22,7 @@ - +