├── .gitignore ├── Gemfile ├── Gemfile.lock ├── README.markdown ├── Rakefile ├── _config.yml ├── _data └── nav.yml ├── _includes └── side-nav.html ├── _layouts ├── default.html ├── overview.html ├── post.html └── recipe.html ├── _plugins ├── chapter_filter.rb ├── jekyll_lunr_js_search.rb ├── jsfiddle.rb ├── prism.rb ├── raw.rb ├── sitemap_generator.rb ├── sorted_for_tag.rb ├── table_of_contents_generator.rb └── url_encode.rb ├── _sass ├── foundation.scss ├── foundation │ ├── _functions.scss │ ├── _settings.scss │ └── components │ │ ├── _accordion.scss │ │ ├── _alert-boxes.scss │ │ ├── _block-grid.scss │ │ ├── _breadcrumbs.scss │ │ ├── _button-groups.scss │ │ ├── _buttons.scss │ │ ├── _clearing.scss │ │ ├── _dropdown-buttons.scss │ │ ├── _dropdown.scss │ │ ├── _flex-video.scss │ │ ├── _forms.scss │ │ ├── _global.scss │ │ ├── _grid.scss │ │ ├── _icon-bar.scss │ │ ├── _inline-lists.scss │ │ ├── _joyride.scss │ │ ├── _keystrokes.scss │ │ ├── _labels.scss │ │ ├── _magellan.scss │ │ ├── _offcanvas.scss │ │ ├── _orbit.scss │ │ ├── _pagination.scss │ │ ├── _panels.scss │ │ ├── _pricing-tables.scss │ │ ├── _progress-bars.scss │ │ ├── _range-slider.scss │ │ ├── _reveal.scss │ │ ├── _side-nav.scss │ │ ├── _split-buttons.scss │ │ ├── _sub-nav.scss │ │ ├── _switches.scss │ │ ├── _tables.scss │ │ ├── _tabs.scss │ │ ├── _thumbs.scss │ │ ├── _toolbar.scss │ │ ├── _tooltips.scss │ │ ├── _top-bar.scss │ │ ├── _type.scss │ │ └── _visibility.scss ├── foundation_overrides.scss ├── normalize.scss ├── prism.scss ├── syntax.scss └── variables.scss ├── backend-integration-with-node-express ├── consuming-rest-apis.markdown ├── implementing-client-side-routing.markdown └── index.markdown ├── backend-integration-with-ruby-on-rails ├── consuming-rest-apis.markdown ├── implementing-client-side-routing.markdown ├── index.markdown └── validating-forms-server-side.markdown ├── common-user-interface-patterns ├── displaying-a-flash-notice-failure-message.markdown ├── displaying-a-loading-spinner.markdown ├── displaying-a-modal-dialog.markdown ├── editing-text-in-place-using-html5-content-editable.markdown ├── filtering-and-sorting-a-list.markdown ├── index.markdown ├── paginating-through-client-side-data.markdown ├── paginating-through-server-side-data.markdown ├── paginating-using-infinite-results.markdown └── source │ ├── recipe1 │ ├── app.js │ └── index.html │ ├── recipe2 │ ├── app.js │ └── index.html │ ├── recipe3 │ ├── app.js │ └── index.html │ ├── recipe4 │ ├── app.js │ └── index.html │ ├── recipe5 │ ├── app.js │ └── index.html │ └── recipe6 │ ├── app.js │ ├── index.html │ └── style.css ├── consuming-external-services ├── consuming-jsonp-apis.markdown ├── consuming-restful-apis.markdown ├── deferred-and-promise.markdown ├── index.markdown ├── requesting-json-data-with-ajax.markdown └── testing-services.markdown ├── controllers ├── assigning-a-default-value-to-a-model.markdown ├── changing-a-model-value-with-a-controller-function.markdown ├── encapsulation-a-model-value-with-a-controller-function.markdown ├── index.markdown ├── responding-to-scope-changes.markdown ├── sharing-code-between-controllers-using-services.markdown ├── sharing-models-between-nested-controllers.markdown ├── source │ ├── recipe1 │ │ ├── app.js │ │ └── index.html │ ├── recipe2 │ │ ├── app.js │ │ └── index.html │ ├── recipe3 │ │ ├── app.js │ │ └── index.html │ ├── recipe4 │ │ ├── app.js │ │ └── index.html │ ├── recipe5 │ │ ├── app.js │ │ ├── index.html │ │ └── style.css │ └── recipe6 │ │ ├── app.js │ │ ├── index.html │ │ └── style.css └── testing-controllers.markdown ├── css └── application.scss ├── directives ├── changing-the-dom-in-response-to-user-actions.markdown ├── directive-to-directive-communication.markdown ├── enabling-disabling-dom-elements-conditionally.markdown ├── index.markdown ├── passing-configuration-params-using-html-attributes.markdown ├── rendering-a-directives-dom-node-children.markdown ├── rendering-an-html-snippet-in-a-directive.markdown ├── repeatedly-rendering-directives-dom-node-children.markdown ├── source │ ├── recipe1 │ │ └── index.html │ ├── recipe2 │ │ ├── app.js │ │ └── index.html │ ├── recipe3 │ │ ├── app.js │ │ └── index.html │ ├── recipe4 │ │ ├── app.js │ │ └── index.html │ ├── recipe5 │ │ ├── app.js │ │ └── index.html │ ├── recipe6 │ │ ├── app.js │ │ └── index.html │ └── recipe7 │ │ ├── app.js │ │ └── index.html └── testing-directives.markdown ├── filters ├── chaining-filters-together.markdown ├── filtering-a-list-of-dom-nodes.markdown ├── formatting-string-with-currency-filter.markdown ├── implementing-custom-filter-to-reverse-an-input-string.markdown ├── index.markdown ├── passing-configuration-params-to-filters.markdown ├── source │ ├── recipe2 │ │ ├── app.js │ │ └── index.html │ ├── recipe3 │ │ ├── app.js │ │ └── index.html │ ├── recipe4 │ │ ├── app.js │ │ └── index.html │ └── recipe5 │ │ └── index.html └── testing-filters.markdown ├── images ├── AngularJS-Shield-small.png ├── book-medium.png └── chitchat_screen_1.png ├── index.html ├── introduction ├── binding-text-input-to-an-expression.markdown ├── converting-expression-output-with-filters.markdown ├── including-the-angular-library-code-in-an-html-page.markdown ├── index.markdown ├── responding-to-click-events-using-controllers.markdown └── source │ ├── recipe1 │ └── index.html │ ├── recipe2 │ └── index.html │ ├── recipe3 │ ├── app.js │ └── index.html │ └── recipe4 │ └── index.html ├── js ├── URI.min.js ├── angular-resource.js ├── angular-sanitize.js ├── angular.js ├── angular │ ├── app.js │ ├── controller.js │ └── directive.js ├── application.js ├── date.format.js ├── foundation.min.js ├── jquery.js ├── jquery.lunr.search.js ├── lunr.js ├── mustache.js ├── prism.js └── search.min.js ├── urls-routing-and-partials ├── client-side-routing-with-hashbang-urls.markdown ├── index.markdown ├── listening-on-route-changes-to-implement-a-login-mechanism.markdown ├── using-regular-urls-with-the-html5-history-api.markdown └── using-route-location-to-implement-a-navigation-menu.markdown └── using-forms ├── displaying-form-validation-errors-with-the-twitter-bootstrap-framework.markdown ├── displaying-form-validation-errors.markdown ├── implement-a-basic-form.markdown ├── implementing-custom-validations.markdown ├── index.markdown ├── only-enabling-submit-button-if-the-form-is-valid.markdown ├── source ├── recipe1 │ ├── app.js │ └── index.html ├── recipe2 │ ├── app.js │ └── index.html ├── recipe3 │ ├── app.js │ └── index.html ├── recipe4 │ ├── app.js │ └── index.html └── recipe5 │ ├── app.js │ ├── index.html │ └── style.css └── validating-a-form-model-client-side.markdown /.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | .sass-cache/ -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem "rake" 4 | gem "jekyll" 5 | gem "directory_watcher"#, "1.4.1" 6 | # gem 'jekyll-assets' 7 | gem 'sass' 8 | gem 'uglifier' 9 | gem 'nokogiri' -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | blankslate (2.1.2.4) 5 | celluloid (0.16.0) 6 | timers (~> 4.0.0) 7 | classifier-reborn (2.0.3) 8 | fast-stemmer (~> 1.0) 9 | coffee-script (2.3.0) 10 | coffee-script-source 11 | execjs 12 | coffee-script-source (1.8.0) 13 | colorator (0.1) 14 | directory_watcher (1.5.1) 15 | execjs (2.2.2) 16 | fast-stemmer (1.0.2) 17 | ffi (1.9.6) 18 | hitimes (1.2.2) 19 | jekyll (2.5.3) 20 | classifier-reborn (~> 2.0) 21 | colorator (~> 0.1) 22 | jekyll-coffeescript (~> 1.0) 23 | jekyll-gist (~> 1.0) 24 | jekyll-paginate (~> 1.0) 25 | jekyll-sass-converter (~> 1.0) 26 | jekyll-watch (~> 1.1) 27 | kramdown (~> 1.3) 28 | liquid (~> 2.6.1) 29 | mercenary (~> 0.3.3) 30 | pygments.rb (~> 0.6.0) 31 | redcarpet (~> 3.1) 32 | safe_yaml (~> 1.0) 33 | toml (~> 0.1.0) 34 | jekyll-coffeescript (1.0.1) 35 | coffee-script (~> 2.2) 36 | jekyll-gist (1.1.0) 37 | jekyll-paginate (1.1.0) 38 | jekyll-sass-converter (1.3.0) 39 | sass (~> 3.2) 40 | jekyll-watch (1.2.0) 41 | listen (~> 2.7) 42 | json (1.8.2) 43 | kramdown (1.5.0) 44 | liquid (2.6.1) 45 | listen (2.8.5) 46 | celluloid (>= 0.15.2) 47 | rb-fsevent (>= 0.9.3) 48 | rb-inotify (>= 0.9) 49 | mercenary (0.3.5) 50 | mini_portile (0.6.2) 51 | nokogiri (1.6.5) 52 | mini_portile (~> 0.6.0) 53 | parslet (1.5.0) 54 | blankslate (~> 2.0) 55 | posix-spawn (0.3.9) 56 | pygments.rb (0.6.0) 57 | posix-spawn (~> 0.3.6) 58 | yajl-ruby (~> 1.1.0) 59 | rake (10.4.2) 60 | rb-fsevent (0.9.4) 61 | rb-inotify (0.9.5) 62 | ffi (>= 0.5.0) 63 | redcarpet (3.2.2) 64 | safe_yaml (1.0.4) 65 | sass (3.4.9) 66 | timers (4.0.1) 67 | hitimes 68 | toml (0.1.2) 69 | parslet (~> 1.5.0) 70 | uglifier (2.7.0) 71 | execjs (>= 0.3.0) 72 | json (>= 1.8.0) 73 | yajl-ruby (1.1.0) 74 | 75 | PLATFORMS 76 | ruby 77 | 78 | DEPENDENCIES 79 | directory_watcher 80 | jekyll 81 | nokogiri 82 | rake 83 | sass 84 | uglifier 85 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Recipes with Angular.js 2 | 3 | A community project dedicated to collecting resources for the [Angular.js](http://angularjs.org) framework. You find here easy to follow cookbook style recipes for issues you are likely to face when working with Angular.js. 4 | 5 | [Angular.js](http://angularjs.org) is the superheroic Javascript MVW framework developed by Google. 6 | 7 | The original content is based on my ebook [Recipes with Angular.js](https://leanpub.com/recipes-with-angular-js). If you like this project then buy the book to support me! 8 | 9 | ## How to contribute 10 | 11 | You can create an [issue](https://github.com/fdietz/recipes-with-angular-js/issues) on Github in case you find errors in the existing recipes or have ideas for new ones. 12 | 13 | New recipes are contributed by forking the [repository](https://github.com/fdietz/recipes-with-angular-js) and sending a pull request. 14 | 15 | ## License 16 | 17 | Content is released under the [Creative Commons License](http://creativecommons.org/). -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require "bundler/setup" 3 | require "shellwords" 4 | 5 | Bundler.require 6 | 7 | GITHUB_REPONAME = "fdietz/recipes-with-angular-js" 8 | 9 | namespace :site do 10 | desc "Generate blog files" 11 | task :generate do 12 | Jekyll::Site.new(Jekyll.configuration({ 13 | "source" => ".", 14 | "destination" => "_site" 15 | })).process 16 | end 17 | 18 | desc "Generate and publish blog to gh-pages" 19 | task :publish => [:generate] do 20 | Dir.mktmpdir do |tmp| 21 | cp_r "_site/.", tmp 22 | Dir.chdir tmp 23 | system "git init" 24 | system "git add ." 25 | message = "Site updated at #{Time.now.utc}" 26 | system "git commit -m #{message.shellescape}" 27 | system "git remote add origin git@github.com:#{GITHUB_REPONAME}.git" 28 | system "git push origin master:refs/heads/gh-pages --force" 29 | end 30 | end 31 | end -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | name: Recipes with Angular.js 2 | baseurl: /recipes-with-angular-js 3 | highlighter: true 4 | sass: 5 | style: compressed 6 | table_of_contents: 7 | dirs: [introduction, controllers] 8 | host: fdietz.github.io 9 | # assets: 10 | # compress: 11 | # js: uglifier 12 | # css: sass 13 | # baseurl: http://fdietz.github.io/recipes-with-angular-js/assets/ 14 | -------------------------------------------------------------------------------- /_data/nav.yml: -------------------------------------------------------------------------------- 1 | - name: "introduction" 2 | title: "Introduction" 3 | path: "/introduction" 4 | - name: "controllers" 5 | title: "Controllers" 6 | path: "/controllers" 7 | - name: "directives" 8 | title: "Directives" 9 | path: "/directives" 10 | - name: "filters" 11 | title: "Filters" 12 | path: "/filters" 13 | - name: "using-forms" 14 | title: "Using Forms" 15 | path: "/using-forms" 16 | - name: "consuming-external-services" 17 | title: "Consuming External Services" 18 | path: "/consuming-external-services" 19 | - name: "urls-routing-and-partials" 20 | title: "Urls Routing and Partials" 21 | path: "/urls-routing-and-partials" 22 | - name: "common-user-interface-patterns" 23 | title: "Common User Interface Patterns" 24 | path: "/common-user-interface-patterns" 25 | - name: "backend-integration-with-ruby-on-rails" 26 | title: "Backend Integration with Ruby on Rails" 27 | path: "/backend-integration-with-ruby-on-rails" 28 | - name: "backend-integration-with-node-express" 29 | title: "Backend Integration with Node Express" 30 | path: "/backend-integration-with-node-express" 31 | -------------------------------------------------------------------------------- /_layouts/overview.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 |
#{code}
40 | Example paragraph with some text.
28 |{{text}}
21 | {% endraw %} 22 | {% endprism %} 23 | 24 | The directive is especially interesting since it uses `ng-model` instead of custom attributes. 25 | 26 | {% prism javascript %} 27 | app.directive("contenteditable", function() { 28 | return { 29 | restrict: "A", 30 | require: "ngModel", 31 | link: function(scope, element, attrs, ngModel) { 32 | 33 | function read() { 34 | ngModel.$setViewValue(element.html()); 35 | } 36 | 37 | ngModel.$render = function() { 38 | element.html(ngModel.$viewValue || ""); 39 | }; 40 | 41 | element.bind("blur keyup change", function() { 42 | scope.$apply(read); 43 | }); 44 | } 45 | }; 46 | }); 47 | {% endprism %} 48 | 49 | You can find the complete example on [github](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter8/recipe6). 50 | 51 | ### Discussion 52 | The directive is restricted for usage as an HTML attribute since we want to use the HTML5 contenteditable attribute as it is instead of defining a new HTML element. 53 | 54 | It requires the `ngModel` controller for data binding in conjunction with the link function. The implementation binds an event listener, which executes the `read` function with [apply](http://docs.angularjs.org/api/ng.$rootScope.Scope). This ensures that even though we call the `read` function from within a DOM event handler we notify Angular about it. 55 | 56 | The `read` function updates the model based on the view's user input. And the `$render` function is doing the same in the other direction, updating the view for us whenever the model changes. 57 | 58 | The directive is surprisingly simple, leaving the `ng-model` aside. But without the `ng-model` support we would have to come up with our own model-attribute handling which would not be consistent with other directives. 59 | -------------------------------------------------------------------------------- /common-user-interface-patterns/filtering-and-sorting-a-list.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Filtering and Sorting a List 4 | chapter: common-user-interface-patterns 5 | order: 1 6 | source_path: common-user-interface-patterns/source/recipe1 7 | --- 8 | 9 | ### Problem 10 | You wish to filter and sort a relatively small list of items all available on the client. 11 | 12 | ### Solution 13 | For this example we will render a list of friends using the `ng-repeat` directive. Using the built-in `filter` and `orderBy` filters we will filter and sort the friends list client-side. 14 | 15 | {% prism markup %} 16 | {% raw %} 17 | 18 |Id | 22 |Name | 23 |Description | 24 |
---|---|---|
{{item.id}} | 29 |{{item.name}} | 30 |{{item.description}} | 31 |35 | 37 | | 38 | 39 |
Id | 7 |Name | 8 |Description | 9 |
---|---|---|
{{item.id}} | 14 |{{item.name}} | 15 |{{item.description}} | 16 |20 | 33 | | 34 | 35 |
Id | 7 |Name | 8 |Description | 9 |
---|---|---|
{{item.id}} | 14 |{{item.name}} | 15 |{{item.description}} | 16 |20 | 33 | | 34 | 35 |
Id | 7 |Name | 8 |Description | 9 |
---|---|---|
{{item.id}} | 14 |{{item.name}} | 15 |{{item.description}} | 16 |20 | 21 | | 22 | 23 |
{{flash.getMessage()}}
11 |{{text}}
5 | -------------------------------------------------------------------------------- /common-user-interface-patterns/source/recipe6/style.css: -------------------------------------------------------------------------------- 1 | [contenteditable] { 2 | border: 2px dotted #ccc; 3 | background-color: #eee; 4 | padding: 2px; 5 | } -------------------------------------------------------------------------------- /consuming-external-services/consuming-jsonp-apis.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Consuming JSONP APIs 4 | chapter: consuming-external-services 5 | order: 3 6 | --- 7 | 8 | ### Problem 9 | You wish to call a JSONP API. 10 | 11 | ### Solution 12 | Use the `$resource` service and configure it to use JSONP. As an example we will take the Twitter search API here. 13 | 14 | The HTML template lets you enter a search term in an input field and will render the search result in a list. 15 | 16 | {% prism markup %} 17 | {% raw %} 18 | 19 |{{value}}
20 |{{value}}
38 | 39 |{{value}}
31 |{{getIncrementedValue()}}
31 |{{greeting}}
34 |{{value}}
4 |{{value}}
5 |{{getIncrementedValue()}}
4 |{{greeting}}
5 |Hello World
20 |Hello World
55 |{{text}}
", 32 | link: linkFunction 33 | }; 34 | }); 35 | {% endprism %} 36 | 37 | This renders a paragraph with the text passed as the param. 38 | 39 | ### Discussion 40 | The link function has access to the element and its attributes. It is therefore straightforward to set the scope to the text passed as the attributes value and use this in the template evaluation. 41 | 42 | The scope context is important though. The `text` model we changed might already be defined in the parent scope and used in another part of your app. In order to isolate the context and thereby use it only locally inside your directive, we have to return an additional scope attribute. 43 | 44 | {% prism javascript %} 45 | return { 46 | restrict: "A", 47 | template: "{{text}}
", 48 | link: linkFunction, 49 | scope: {} 50 | }; 51 | {% endprism %} 52 | 53 | In Angular this is called an isolate scope. It does not prototypically inherit from the parent scope and is especially useful when creating reusable components. 54 | 55 | Let's look into another way of passing params to the directive. This time we will define an HTML element `my-widget2`. 56 | 57 | {% prism javascript %} 58 |{{text}}
", 64 | scope: { 65 | text: "@text" 66 | } 67 | }; 68 | }); 69 | {% endprism %} 70 | 71 | The scope definition using `@text` is binding the text model to the directive's attribute. Note that any changes to the parent scope `text` will change the local scope `text`, but not the other way around. 72 | 73 | If you want instead to have a bi-directional binding between the parent scope and the local scope, you should use the `=` equality character: 74 | 75 | {% prism javascript %} 76 | scope: { 77 | text: "=text" 78 | } 79 | {% endprism %} 80 | 81 | Changes to the local scope will also change the parent scope. 82 | 83 | Another option would be to pass an expression as a function to the directive using the `&` character. 84 | 85 | {% prism markup %} 86 |{{text}}
", 98 | link: linkFunction, 99 | scope: { 100 | fn: "&fn" 101 | } 102 | }; 103 | }); 104 | {% endprism %} 105 | 106 | We pass the attribute `fn` to the directive and since the local scope defines `fn` accordingly we can call the function in the `linkFunction` and pass in the expression arguments as a hash. 107 | -------------------------------------------------------------------------------- /directives/rendering-a-directives-dom-node-children.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Rendering a Directive's DOM Node Children 4 | chapter: directives 5 | order: 4 6 | source_path: directives/source/recipe4 7 | --- 8 | 9 | ### Problem 10 | Your widget uses the child nodes of the directive element to create a combined rendering. 11 | 12 | ### Solution 13 | Use the `transclude` attribute together with the `ng-transclude` directive. 14 | 15 | {% prism markup %} 16 |This is my paragraph text.
18 |Hello World
" 28 | }; 29 | }); 30 | {% endprism %} 31 | 32 | ### Discussion 33 | This will render the Hello World paragraph as a child node of your `my-widget` element. If you want to replace the element entirely with the paragraph you will also have to return the `replace` attribute: 34 | 35 | {% prism javascript %} 36 | app.directive("myWidget", function() { 37 | return { 38 | restrict: "E", 39 | replace: true, 40 | template: "Hello World
" 41 | }; 42 | }); 43 | {% endprism %} 44 | 45 | Another option would be to use a file for the HTML snippet. In this case you will need to use the `templateUrl` attribute, for example as follows: 46 | 47 | {% prism javascript %} 48 | app.directive("myWidget", function() { 49 | return { 50 | restrict: "E", 51 | replace: true, 52 | templateUrl: "widget.html" 53 | }; 54 | }); 55 | {% endprism %} 56 | 57 | The `widget.html` should reside in the same directory as the `index.html` file. This will only work if you use a web server to host the file. The example on Github uses angular-seed as bootstrap again. 58 | -------------------------------------------------------------------------------- /directives/repeatedly-rendering-directives-dom-node-children.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Repeatedly Rendering Directive's DOM Node Children 4 | chapter: directives 5 | order: 6 6 | source_path: directives/source/recipe6 7 | --- 8 | 9 | ### Problem 10 | You wish to render an HTML snippet repeatedly using the directive's child nodes as the "stamp" content. 11 | 12 | ### Solution 13 | Implement a compile function in your directive. 14 | 15 | {% prism markup %} 16 |This is the paragraph.
19 | 20 | {% endprism %} 21 | 22 | {% prism javascript %} 23 | var app = angular.module("MyApp", []); 24 | 25 | app.directive("repeatNtimes", function() { 26 | return { 27 | restrict: "E", 28 | compile: function(tElement, attrs) { 29 | var content = tElement.children(); 30 | for (var i=1; iHello World
4 |This is my paragraph text.
4 |