├── .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 |
6 |
7 | {% include side-nav.html %} 8 |
9 |
10 |

{{page.title}}

11 | {{ content }} 12 |
13 |
-------------------------------------------------------------------------------- /_layouts/post.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 |

{{ page.title }}

5 |

{{ page.date | date_to_string }}

6 | 7 |
8 | {{ content }} 9 |
-------------------------------------------------------------------------------- /_plugins/chapter_filter.rb: -------------------------------------------------------------------------------- 1 | module ChapterFilter 2 | def chapter(input, chapter) 3 | puts "input: #{input}, chapter: #{chapter}" 4 | input.chapter == chapter ? input : nil 5 | end 6 | end 7 | 8 | Liquid::Template.register_filter(ChapterFilter) -------------------------------------------------------------------------------- /_plugins/jsfiddle.rb: -------------------------------------------------------------------------------- 1 | require "cgi" 2 | 3 | module Jekyll 4 | class JsFiddle < Liquid::Tag 5 | def initialize(tag_name, markup, tokens) 6 | super 7 | end 8 | 9 | def escape(value) 10 | value = CGI.escapeHTML(value) 11 | 12 | # escape {{ (double curly braces) 13 | # since liquid template engine would interpret it as a liquid tag 14 | value.gsub("{{", '{{'). 15 | gsub("}}", '}}'); 16 | end 17 | 18 | def hidden_field(name, value) 19 | <<-EOF 20 | 21 | EOF 22 | end 23 | 24 | def resource_paths 25 | [angular_js_resource_path, jquery_resource_path, css_resource_path].join(',') 26 | end 27 | 28 | def angular_js_resource_path 29 | "https://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js" 30 | end 31 | 32 | def jquery_resource_path 33 | "http://code.jquery.com/jquery-1.10.1.min.js" 34 | end 35 | 36 | def css_resource_path 37 | "http://netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css" 38 | end 39 | 40 | def template(html_content, js_content, css_content) 41 | <<-EOF 42 |
43 | #{hidden_field('html', html_content) if html_content} 44 | #{hidden_field('js', js_content) if js_content} 45 | #{hidden_field('css', css_content) if css_content} 46 | #{hidden_field('resources', resource_paths)} 47 | 48 |
49 | EOF 50 | end 51 | 52 | def html_path 53 | "#{File.dirname(__FILE__)}/../#{@path}/index.html" 54 | end 55 | 56 | def js_path 57 | "#{File.dirname(__FILE__)}/../#{@path}/app.js" 58 | end 59 | 60 | def css_path 61 | "#{File.dirname(__FILE__)}/../#{@path}/style.css" 62 | end 63 | 64 | def render(context) 65 | @path = context.environments.first["page"]["source_path"] 66 | 67 | if @path 68 | html_content = IO.read(html_path) if File.exist?(html_path) 69 | js_content = IO.read(js_path) if File.exist?(js_path) 70 | css_content = IO.read(css_path) if File.exist?(css_path) 71 | template(html_content, js_content, css_content).strip 72 | else 73 | "Error processing input. Expected syntax: {% jsfiddle [html] %}" 74 | end 75 | end 76 | end 77 | end 78 | 79 | Liquid::Template.register_tag('jsfiddle', Jekyll::JsFiddle) -------------------------------------------------------------------------------- /_plugins/prism.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | 3 | class PrismBlock < Liquid::Block 4 | include Liquid::StandardFilters 5 | 6 | OPTIONS_SYNTAX = %r{^([a-zA-Z0-9.+#-]+)((\s+\w+(=[0-9,-]+)?)*)$} 7 | 8 | def initialize(tag_name, markup, tokens) 9 | super 10 | if markup.strip =~ OPTIONS_SYNTAX 11 | @lang = $1 12 | if defined?($2) && $2 != '' 13 | tmp_options = {} 14 | $2.split.each do |opt| 15 | key, value = opt.split('=') 16 | if value.nil? 17 | value = true 18 | end 19 | tmp_options[key] = value 20 | end 21 | @options = tmp_options 22 | else 23 | @options = { "linenos" => "" } 24 | end 25 | else 26 | raise SyntaxError.new("Syntax Error in 'prism' - Valid syntax: prism [linenos(='1-5')]") 27 | end 28 | end 29 | 30 | def render(context) 31 | code = h(super).strip 32 | 33 | if @options["linenos"] == true 34 | @options["linenos"] = "1-#{code.lines.count}" 35 | end 36 | 37 | <<-HTML 38 |
39 |
#{code}
40 |
41 | HTML 42 | end 43 | end 44 | 45 | end 46 | 47 | Liquid::Template.register_tag('prism', Jekyll::PrismBlock) -------------------------------------------------------------------------------- /_plugins/raw.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | class RawTag < Liquid::Block 3 | def parse(tokens) 4 | @nodelist ||= [] 5 | @nodelist.clear 6 | 7 | while token = tokens.shift 8 | if token =~ FullToken 9 | if block_delimiter == $1 10 | end_tag 11 | return 12 | end 13 | end 14 | @nodelist << token if not token.empty? 15 | end 16 | end 17 | end 18 | end 19 | 20 | Liquid::Template.register_tag('raw', Jekyll::RawTag) -------------------------------------------------------------------------------- /_plugins/sorted_for_tag.rb: -------------------------------------------------------------------------------- 1 | # https://gist.github.com/JanDupal/3765912 2 | module Jekyll 3 | class SortedForTag < Liquid::For 4 | def render(context) 5 | sorted_collection = context[@collection_name].dup 6 | sorted_collection.sort_by! { |i| i.to_liquid[@attributes['sort_by']] || 0 } 7 | 8 | sorted_collection_name = "#{@collection_name}_sorted".sub('.', '_') 9 | context[sorted_collection_name] = sorted_collection 10 | @collection_name = sorted_collection_name 11 | 12 | super 13 | end 14 | 15 | def end_tag 16 | 'endsorted_for' 17 | end 18 | end 19 | end 20 | 21 | Liquid::Template.register_tag('sorted_for', Jekyll::SortedForTag) -------------------------------------------------------------------------------- /_plugins/table_of_contents_generator.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | class TableOfContentPage < Page 3 | def initialize(site, base, dir, toc_data) 4 | @site = site 5 | @base = base 6 | @dir = dir 7 | @name = 'index.html' 8 | 9 | self.process(@name) 10 | self.read_yaml(File.join(base, '_layouts'), 'default.html') 11 | 12 | self.data['table_of_contents'] = toc_data 13 | end 14 | end 15 | 16 | class TableOfContentGenerator < Generator 17 | safe true 18 | 19 | def generate(site) 20 | unless site.layouts.has_key? 'table_of_contents' 21 | return 22 | end 23 | 24 | toc_config = site.config['table_of_contents'] 25 | 26 | if toc_config == nil or !toc_config.has_key?('dirs') 27 | return 28 | end 29 | 30 | toc_config['dirs'].each do |dir| 31 | toc_data = [] 32 | 33 | site.pages.each do |page| 34 | if is_file_in_dir(page.dir, "/#{dir}") 35 | page.data['table_of_contents'] = {} 36 | page.data['table_of_contents']['url'] = page.url 37 | toc_data << page.data 38 | end 39 | end 40 | 41 | toc_data.sort!{ |a, b| a['table_of_contents']['url'] <=> b['table_of_contents']['url'] } 42 | 43 | site.pages << TableOfContentPage.new(site, site.source, dir, toc_data) 44 | end 45 | end 46 | 47 | private 48 | 49 | # is file in dir (file could also be a directory) 50 | def is_file_in_dir(file, dir) 51 | dir_ = (dir == '/' ? dir : dir.chomp('/')) 52 | file_ = (file == '/' ? file : file.chomp('/')) 53 | 54 | while file_ != '.' and file_ != '/' and file_ != dir_ 55 | file_ = File.dirname(file_) 56 | end 57 | 58 | return (file_ == dir) ? true : false 59 | end 60 | end 61 | end -------------------------------------------------------------------------------- /_plugins/url_encode.rb: -------------------------------------------------------------------------------- 1 | # _plugins/url_encode.rb 2 | require 'liquid' 3 | require 'uri' 4 | 5 | # Percent encoding for URI conforming to RFC 3986. 6 | # Ref: http://tools.ietf.org/html/rfc3986#page-12 7 | module URLEncode 8 | def url_encode(url) 9 | return URI.escape(url, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")) 10 | end 11 | end 12 | 13 | Liquid::Template.register_filter(URLEncode) -------------------------------------------------------------------------------- /_sass/foundation.scss: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | // Foundation by ZURB 3 | // foundation.zurb.com 4 | // Licensed under MIT Open Source 5 | 6 | // Make sure the charset is set appropriately 7 | 8 | // Behold, here are all the Foundation components. 9 | @import "foundation/components/grid"; 10 | @import "foundation/components/accordion"; 11 | @import "foundation/components/alert-boxes"; 12 | @import "foundation/components/block-grid"; 13 | @import "foundation/components/breadcrumbs"; 14 | @import "foundation/components/button-groups"; 15 | @import "foundation/components/buttons"; 16 | @import "foundation/components/clearing"; 17 | @import "foundation/components/dropdown"; 18 | @import "foundation/components/dropdown-buttons"; 19 | @import "foundation/components/flex-video"; 20 | @import "foundation/components/forms"; 21 | @import "foundation/components/icon-bar"; 22 | @import "foundation/components/inline-lists"; 23 | @import "foundation/components/joyride"; 24 | @import "foundation/components/keystrokes"; 25 | @import "foundation/components/labels"; 26 | @import "foundation/components/magellan"; 27 | @import "foundation/components/orbit"; 28 | @import "foundation/components/pagination"; 29 | @import "foundation/components/panels"; 30 | @import "foundation/components/pricing-tables"; 31 | @import "foundation/components/progress-bars"; 32 | @import "foundation/components/range-slider"; 33 | @import "foundation/components/reveal"; 34 | @import "foundation/components/side-nav"; 35 | @import "foundation/components/split-buttons"; 36 | @import "foundation/components/sub-nav"; 37 | @import "foundation/components/switches"; 38 | @import "foundation/components/tables"; 39 | @import "foundation/components/tabs"; 40 | @import "foundation/components/thumbs"; 41 | @import "foundation/components/tooltips"; 42 | @import "foundation/components/top-bar"; 43 | @import "foundation/components/type"; 44 | @import "foundation/components/offcanvas"; 45 | @import "foundation/components/visibility"; 46 | -------------------------------------------------------------------------------- /_sass/foundation/_functions.scss: -------------------------------------------------------------------------------- 1 | // Foundation by ZURB 2 | // foundation.zurb.com 3 | // Licensed under MIT Open Source 4 | 5 | // This is the default html and body font-size for the base rem value. 6 | $rem-base: 16px !default; 7 | 8 | // IMPORT ONCE 9 | // We use this to prevent styles from being loaded multiple times for components that rely on other components. 10 | $modules: () !default; 11 | @mixin exports($name) { 12 | // Import from global scope 13 | $modules: $modules !global; 14 | // Check if a module is already on the list 15 | $module_index: index($modules, $name); 16 | @if (($module_index == null) or ($module_index == false)) { 17 | $modules: append($modules, $name) !global; 18 | @content; 19 | } 20 | } 21 | 22 | // 23 | // @functions 24 | // 25 | 26 | 27 | // RANGES 28 | // We use these functions to define ranges for various things, like media queries. 29 | @function lower-bound($range){ 30 | @if length($range) <= 0 { 31 | @return 0; 32 | } 33 | @return nth($range,1); 34 | } 35 | 36 | @function upper-bound($range) { 37 | @if length($range) < 2 { 38 | @return 999999999999; 39 | } 40 | @return nth($range, 2); 41 | } 42 | 43 | // STRIP UNIT 44 | // It strips the unit of measure and returns it 45 | @function strip-unit($num) { 46 | @return $num / ($num * 0 + 1); 47 | } 48 | 49 | // TEXT INPUT TYPES 50 | 51 | @function text-inputs( $types: all, $selector: input ) { 52 | 53 | $return: (); 54 | 55 | $all-text-input-types: 56 | text 57 | password 58 | date 59 | datetime 60 | datetime-local 61 | month 62 | week 63 | email 64 | number 65 | search 66 | tel 67 | time 68 | url 69 | color 70 | textarea; 71 | 72 | @if $types == all { $types: $all-text-input-types; } 73 | 74 | @each $type in $types { 75 | @if $type == textarea { 76 | @if $selector == input { 77 | $return: append($return, unquote('#{$type}'), comma) 78 | } @else { 79 | $return: append($return, unquote('#{$type}#{$selector}'), comma) 80 | } 81 | } @else { 82 | $return: append($return, unquote('#{$selector}[type="#{$type}"]'), comma) 83 | } 84 | } 85 | 86 | @return $return; 87 | 88 | } 89 | 90 | // CONVERT TO REM 91 | @function convert-to-rem($value, $base-value: $rem-base) { 92 | $value: strip-unit($value) / strip-unit($base-value) * 1rem; 93 | @if ($value == 0rem) { $value: 0; } // Turn 0rem into 0 94 | @return $value; 95 | } 96 | 97 | @function data($attr) { 98 | @if $namespace { 99 | @return '[data-' + $namespace + '-' + $attr + ']'; 100 | } 101 | 102 | @return '[data-' + $attr + ']'; 103 | } 104 | 105 | // REM CALC 106 | 107 | // New Syntax, allows to optionally calculate on a different base value to counter compounding effect of rem's. 108 | // Call with 1, 2, 3 or 4 parameters, 'px' is not required but supported: 109 | // 110 | // rem-calc(10 20 30px 40); 111 | // 112 | // Space delimited, if you want to delimit using comma's, wrap it in another pair of brackets 113 | // 114 | // rem-calc((10, 20, 30, 40px)); 115 | // 116 | // Optionally call with a different base (eg: 8px) to calculate rem. 117 | // 118 | // rem-calc(16px 32px 48px, 8px); 119 | // 120 | // If you require to comma separate your list 121 | // 122 | // rem-calc((16px, 32px, 48), 8px); 123 | 124 | @function rem-calc($values, $base-value: $rem-base) { 125 | $max: length($values); 126 | 127 | @if $max == 1 { @return convert-to-rem(nth($values, 1), $base-value); } 128 | 129 | $remValues: (); 130 | @for $i from 1 through $max { 131 | $remValues: append($remValues, convert-to-rem(nth($values, $i), $base-value)); 132 | } 133 | @return $remValues; 134 | } 135 | 136 | // OLD EM CALC 137 | // Deprecated: We'll drop support for this in 5.1.0, use rem-calc() 138 | @function emCalc($values){ 139 | @return rem-calc($values); 140 | } 141 | 142 | // OLD EM CALC 143 | // Deprecated: We'll drop support for this in 5.1.0, use rem-calc() 144 | @function em-calc($values){ 145 | @return rem-calc($values); 146 | } 147 | -------------------------------------------------------------------------------- /_sass/foundation/components/_breadcrumbs.scss: -------------------------------------------------------------------------------- 1 | // Foundation by ZURB 2 | // foundation.zurb.com 3 | // Licensed under MIT Open Source 4 | 5 | @import "global"; 6 | 7 | // 8 | // Breadcrumb Variables 9 | // 10 | $include-html-nav-classes: $include-html-classes !default; 11 | 12 | // We use this to set the background color for the breadcrumb container. 13 | $crumb-bg: scale-color($secondary-color, $lightness: 55%) !default; 14 | 15 | // We use these to set the padding around the breadcrumbs. 16 | $crumb-padding: rem-calc(9 14 9) !default; 17 | $crumb-side-padding: rem-calc(12) !default; 18 | 19 | // We use these to control border styles. 20 | $crumb-function-factor: -10% !default; 21 | $crumb-border-size: 1px !default; 22 | $crumb-border-style: solid !default; 23 | $crumb-border-color: scale-color($crumb-bg, $lightness: $crumb-function-factor) !default; 24 | $crumb-radius: $global-radius !default; 25 | 26 | // We use these to set various text styles for breadcrumbs. 27 | $crumb-font-size: rem-calc(11) !default; 28 | $crumb-font-color: $primary-color !default; 29 | $crumb-font-color-current: $oil !default; 30 | $crumb-font-color-unavailable: $aluminum !default; 31 | $crumb-font-transform: uppercase !default; 32 | $crumb-link-decor: underline !default; 33 | 34 | // We use these to control the slash between breadcrumbs 35 | $crumb-slash-color: $base !default; 36 | $crumb-slash: "/" !default; 37 | 38 | // 39 | // Breadcrumb Mixins 40 | // 41 | 42 | // We use this mixin to create a container around our breadcrumbs 43 | @mixin crumb-container { 44 | display: block; 45 | padding: $crumb-padding; 46 | overflow: hidden; 47 | margin-#{$default-float}: 0; 48 | list-style: none; 49 | border-style: $crumb-border-style; 50 | border-width: $crumb-border-size; 51 | 52 | // We control which background color and border come through. 53 | background-color: $crumb-bg; 54 | border-color: $crumb-border-color; 55 | } 56 | 57 | // We use this mixin to create breadcrumb styles from list items. 58 | @mixin crumbs { 59 | 60 | // A normal state will make the links look and act like clickable breadcrumbs. 61 | margin: 0; 62 | float: $default-float; 63 | font-size: $crumb-font-size; 64 | line-height: $crumb-font-size; 65 | text-transform: $crumb-font-transform; 66 | color: $crumb-font-color; 67 | 68 | &:hover a, &:focus a { text-decoration: $crumb-link-decor; } 69 | 70 | a { 71 | color: $crumb-font-color; 72 | } 73 | 74 | // Current is for the link of the current page 75 | &.current { 76 | cursor: $cursor-default-value; 77 | color: $crumb-font-color-current; 78 | a { 79 | cursor: $cursor-default-value; 80 | color: $crumb-font-color-current; 81 | } 82 | 83 | &:hover, &:hover a, 84 | &:focus, &:focus a { text-decoration: none; } 85 | } 86 | 87 | // Unavailable removed color and link styles so it looks inactive. 88 | &.unavailable { 89 | color: $crumb-font-color-unavailable; 90 | a { color: $crumb-font-color-unavailable; } 91 | 92 | &:hover, 93 | &:hover a, 94 | &:focus, 95 | a:focus { 96 | text-decoration: none; 97 | color: $crumb-font-color-unavailable; 98 | cursor: $cursor-disabled-value; 99 | } 100 | } 101 | 102 | &:before { 103 | content: "#{$crumb-slash}"; 104 | color: $crumb-slash-color; 105 | margin: 0 $crumb-side-padding; 106 | position: relative; 107 | top: 1px; 108 | } 109 | 110 | &:first-child:before { 111 | content: " "; 112 | margin: 0; 113 | } 114 | } 115 | 116 | @include exports("breadcrumbs") { 117 | @if $include-html-nav-classes { 118 | .breadcrumbs { 119 | @include crumb-container; 120 | @include radius($crumb-radius); 121 | 122 | &>* { 123 | @include crumbs; 124 | } 125 | } 126 | } 127 | } 128 | 129 | /* Accessibility - hides the forward slash */ 130 | [aria-label="breadcrumbs"] [aria-hidden="true"]:after { 131 | content: "/"; 132 | } 133 | -------------------------------------------------------------------------------- /_sass/foundation/components/_flex-video.scss: -------------------------------------------------------------------------------- 1 | // Foundation by ZURB 2 | // foundation.zurb.com 3 | // Licensed under MIT Open Source 4 | 5 | @import "global"; 6 | 7 | // 8 | // @variables 9 | // 10 | $include-html-media-classes: $include-html-classes !default; 11 | 12 | // We use these to control video container padding and margins 13 | $flex-video-padding-top: rem-calc(25) !default; 14 | $flex-video-padding-bottom: 67.5% !default; 15 | $flex-video-margin-bottom: rem-calc(16) !default; 16 | 17 | // We use this to control widescreen bottom padding 18 | $flex-video-widescreen-padding-bottom: 56.34% !default; 19 | 20 | // 21 | // @mixins 22 | // 23 | 24 | @mixin flex-video-container { 25 | position: relative; 26 | padding-top: $flex-video-padding-top; 27 | padding-bottom: $flex-video-padding-bottom; 28 | height: 0; 29 | margin-bottom: $flex-video-margin-bottom; 30 | overflow: hidden; 31 | 32 | &.widescreen { padding-bottom: $flex-video-widescreen-padding-bottom; } 33 | &.vimeo { padding-top: 0; } 34 | 35 | iframe, 36 | object, 37 | embed, 38 | video { 39 | position: absolute; 40 | top: 0; 41 | #{$default-float}: 0; 42 | width: 100%; 43 | height: 100%; 44 | } 45 | } 46 | 47 | @include exports("flex-video") { 48 | @if $include-html-media-classes { 49 | .flex-video { @include flex-video-container; } 50 | } 51 | } -------------------------------------------------------------------------------- /_sass/foundation/components/_inline-lists.scss: -------------------------------------------------------------------------------- 1 | // Foundation by ZURB 2 | // foundation.zurb.com 3 | // Licensed under MIT Open Source 4 | 5 | @import "global"; 6 | 7 | // 8 | // @variables 9 | // 10 | $include-html-inline-list-classes: $include-html-classes !default; 11 | 12 | // We use this to control the margins and padding of the inline list. 13 | $inline-list-top-margin: 0 !default; 14 | $inline-list-opposite-margin: 0 !default; 15 | $inline-list-bottom-margin: rem-calc(17) !default; 16 | $inline-list-default-float-margin: rem-calc(-22) !default; 17 | $inline-list-default-float-list-margin: rem-calc(22) !default; 18 | 19 | $inline-list-padding: 0 !default; 20 | 21 | // We use this to control the overflow of the inline list. 22 | $inline-list-overflow: hidden !default; 23 | 24 | // We use this to control the list items 25 | $inline-list-display: block !default; 26 | 27 | // We use this to control any elements within list items 28 | $inline-list-children-display: block !default; 29 | 30 | // 31 | // @mixins 32 | // 33 | // We use this mixin to create inline lists 34 | @mixin inline-list { 35 | margin: $inline-list-top-margin auto $inline-list-bottom-margin auto; 36 | margin-#{$default-float}: $inline-list-default-float-margin; 37 | margin-#{$opposite-direction}: $inline-list-opposite-margin; 38 | padding: $inline-list-padding; 39 | list-style: none; 40 | overflow: $inline-list-overflow; 41 | 42 | & > li { 43 | list-style: none; 44 | float: $default-float; 45 | margin-#{$default-float}: $inline-list-default-float-list-margin; 46 | display: $inline-list-display; 47 | &>* { display: $inline-list-children-display; } 48 | } 49 | } 50 | 51 | @include exports("inline-list") { 52 | @if $include-html-inline-list-classes { 53 | .inline-list { 54 | @include inline-list(); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /_sass/foundation/components/_keystrokes.scss: -------------------------------------------------------------------------------- 1 | // Foundation by ZURB 2 | // foundation.zurb.com 3 | // Licensed under MIT Open Source 4 | 5 | @import "global"; 6 | 7 | // 8 | // @variables 9 | // 10 | $include-html-keystroke-classes: $include-html-classes !default; 11 | 12 | // We use these to control text styles. 13 | $keystroke-font: "Consolas", "Menlo", "Courier", monospace !default; 14 | $keystroke-font-size: inherit !default; 15 | $keystroke-font-color: $jet !default; 16 | $keystroke-font-color-alt: $white !default; 17 | $keystroke-function-factor: -7% !default; 18 | 19 | // We use this to control keystroke padding. 20 | $keystroke-padding: rem-calc(2 4 0) !default; 21 | 22 | // We use these to control background and border styles. 23 | $keystroke-bg: scale-color($white, $lightness: $keystroke-function-factor) !default; 24 | $keystroke-border-style: solid !default; 25 | $keystroke-border-width: 1px !default; 26 | $keystroke-border-color: scale-color($keystroke-bg, $lightness: $keystroke-function-factor) !default; 27 | $keystroke-radius: $global-radius !default; 28 | 29 | // 30 | // @mixins 31 | // 32 | // We use this mixin to create keystroke styles. 33 | // $bg - Default: $keystroke-bg || scale-color($white, $lightness: $keystroke-function-factor) !default; 34 | @mixin keystroke($bg:$keystroke-bg) { 35 | // This find the lightness percentage of the background color. 36 | $bg-lightness: lightness($bg); 37 | 38 | background-color: $bg; 39 | border-color: scale-color($bg, $lightness: $keystroke-function-factor); 40 | 41 | // We adjust the font color based on the brightness of the background. 42 | @if $bg-lightness > 70% { color: $keystroke-font-color; } 43 | @else { color: $keystroke-font-color-alt; } 44 | 45 | border-style: $keystroke-border-style; 46 | border-width: $keystroke-border-width; 47 | margin: 0; 48 | font-family: $keystroke-font; 49 | font-size: $keystroke-font-size; 50 | padding: $keystroke-padding; 51 | } 52 | 53 | @include exports("keystroke") { 54 | @if $include-html-keystroke-classes { 55 | .keystroke, 56 | kbd { 57 | @include keystroke; 58 | @include radius($keystroke-radius); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /_sass/foundation/components/_labels.scss: -------------------------------------------------------------------------------- 1 | // Foundation by ZURB 2 | // foundation.zurb.com 3 | // Licensed under MIT Open Source 4 | 5 | @import "global"; 6 | 7 | // 8 | // @variables 9 | // 10 | $include-html-label-classes: $include-html-classes !default; 11 | 12 | // We use these to style the labels 13 | $label-padding: rem-calc(4 8 4) !default; 14 | $label-radius: $global-radius !default; 15 | 16 | // We use these to style the label text 17 | $label-font-sizing: rem-calc(11) !default; 18 | $label-font-weight: $font-weight-normal !default; 19 | $label-font-color: $oil !default; 20 | $label-font-color-alt: $white !default; 21 | $label-font-family: $body-font-family !default; 22 | 23 | // 24 | // @mixins 25 | // 26 | // We use this mixin to create a default label base. 27 | @mixin label-base { 28 | font-weight: $label-font-weight; 29 | font-family: $label-font-family; 30 | text-align: center; 31 | text-decoration: none; 32 | line-height: 1; 33 | white-space: nowrap; 34 | display: inline-block; 35 | position: relative; 36 | margin-bottom: auto; 37 | } 38 | 39 | // @mixins 40 | // 41 | // We use this mixin to add label size styles. 42 | // $padding - Used to determine label padding. Default: $label-padding || rem-calc(4 8 4) !default 43 | // $text-size - Used to determine label text-size. Default: $text-size found in settings 44 | @mixin label-size($padding:$label-padding, $text-size:$label-font-sizing) { 45 | @if $padding { padding: $padding; } 46 | @if $text-size { font-size: $text-size; } 47 | } 48 | 49 | // @mixins 50 | // 51 | // We use this mixin to add label styles. 52 | // $bg - Default: $primary-color (found in settings file) 53 | // $radius - Default: false, Options: true, sets radius to $global-radius (found in settings file) 54 | @mixin label-style($bg:$primary-color, $radius:false) { 55 | 56 | // We control which background color comes through 57 | @if $bg { 58 | 59 | // This find the lightness percentage of the background color. 60 | $bg-lightness: lightness($bg); 61 | 62 | background-color: $bg; 63 | 64 | // We control the text color for you based on the background color. 65 | @if $bg-lightness < 70% { color: $label-font-color-alt; } 66 | @else { color: $label-font-color; } 67 | } 68 | 69 | // We use this to control the radius on labels. 70 | @if $radius == true { @include radius($label-radius); } 71 | @else if $radius { @include radius($radius); } 72 | 73 | } 74 | 75 | // @mixins 76 | // 77 | // We use this to add close buttons to alerts 78 | // $padding - Default: $label-padding, 79 | // $text-size - Default: $label-font-sizing, 80 | // $bg - Default: $primary-color(found in settings file) 81 | // $radius - Default: false, Options: true which sets radius to $global-radius (found in settings file) 82 | @mixin label($padding:$label-padding, $text-size:$label-font-sizing, $bg:$primary-color, $radius:false) { 83 | 84 | @include label-base; 85 | @include label-size($padding, $text-size); 86 | @include label-style($bg, $radius); 87 | } 88 | 89 | @include exports("label") { 90 | @if $include-html-label-classes { 91 | .label { 92 | @include label-base; 93 | @include label-size; 94 | @include label-style; 95 | 96 | &.radius { @include label-style(false, true); } 97 | &.round { @include label-style(false, $radius:1000px); } 98 | 99 | &.alert { @include label-style($alert-color); } 100 | &.warning { @include label-style($warning-color); } 101 | &.success { @include label-style($success-color); } 102 | &.secondary { @include label-style($secondary-color); } 103 | &.info { @include label-style($info-color); } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /_sass/foundation/components/_magellan.scss: -------------------------------------------------------------------------------- 1 | // Foundation by ZURB 2 | // foundation.zurb.com 3 | // Licensed under MIT Open Source 4 | 5 | @import "global"; 6 | 7 | // 8 | // @variables 9 | // 10 | $include-html-magellan-classes: $include-html-classes !default; 11 | 12 | $magellan-bg: $white !default; 13 | $magellan-padding: 10px !default; 14 | 15 | @include exports("magellan") { 16 | @if $include-html-magellan-classes { 17 | 18 | #{data('magellan-expedition')}, #{data('magellan-expedition-clone')} { 19 | background: $magellan-bg; 20 | z-index: 50; 21 | min-width: 100%; 22 | padding: $magellan-padding; 23 | 24 | .sub-nav { 25 | margin-bottom: 0; 26 | dd { margin-bottom: 0; } 27 | a { 28 | line-height: 1.8em; 29 | } 30 | } 31 | } 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /_sass/foundation/components/_panels.scss: -------------------------------------------------------------------------------- 1 | // Foundation by ZURB 2 | // foundation.zurb.com 3 | // Licensed under MIT Open Source 4 | 5 | @import "global"; 6 | 7 | // 8 | // @variables 9 | // 10 | $include-html-panel-classes: $include-html-classes !default; 11 | 12 | // We use these to control the background and border styles 13 | $panel-bg: scale-color($white, $lightness: -5%) !default; 14 | $panel-border-style: solid !default; 15 | $panel-border-size: 1px !default; 16 | 17 | // We use this % to control how much we darken things on hover 18 | $panel-function-factor: -11% !default; 19 | $panel-border-color: scale-color($panel-bg, $lightness: $panel-function-factor) !default; 20 | 21 | // We use these to set default inner padding and bottom margin 22 | $panel-margin-bottom: rem-calc(20) !default; 23 | $panel-padding: rem-calc(20) !default; 24 | 25 | // We use these to set default font colors 26 | $panel-font-color: $oil !default; 27 | $panel-font-color-alt: $white !default; 28 | 29 | $panel-header-adjust: true !default; 30 | $callout-panel-link-color: $primary-color !default; 31 | $callout-panel-link-color-hover: scale-color($callout-panel-link-color, $lightness: -14%) !default; 32 | // 33 | // @mixins 34 | // 35 | // We use this mixin to create panels. 36 | // $bg - Sets the panel background color. Default: $panel-pg || scale-color($white, $lightness: -5%) !default 37 | // $padding - Sets the panel padding amount. Default: $panel-padding || rem-calc(20) 38 | // $adjust - Sets the font color based on the darkness of the bg & resets header line-heights for panels. Default: $panel-header-adjust || true 39 | @mixin panel($bg:$panel-bg, $padding:$panel-padding, $adjust:$panel-header-adjust) { 40 | 41 | @if $bg { 42 | $bg-lightness: lightness($bg); 43 | 44 | border-style: $panel-border-style; 45 | border-width: $panel-border-size; 46 | border-color: scale-color($bg, $lightness: $panel-function-factor); 47 | margin-bottom: $panel-margin-bottom; 48 | padding: $padding; 49 | 50 | background: $bg; 51 | @if $bg-lightness >= 50% { color: $panel-font-color; } 52 | @else { color: $panel-font-color-alt; } 53 | 54 | // Respect the padding, fool. 55 | &>:first-child { margin-top: 0; } 56 | &>:last-child { margin-bottom: 0; } 57 | 58 | @if $adjust { 59 | // We set the font color based on the darkness of the bg. 60 | @if $bg-lightness >= 50% { 61 | h1,h2,h3,h4,h5,h6,p,li,dl { color: $panel-font-color; } 62 | } 63 | @else { 64 | h1,h2,h3,h4,h5,h6,p,li,dl { color: $panel-font-color-alt; } 65 | } 66 | 67 | // reset header line-heights for panels 68 | h1,h2,h3,h4,h5,h6 { 69 | line-height: 1; margin-bottom: rem-calc(20) / 2; 70 | &.subheader { line-height: 1.4; } 71 | } 72 | } 73 | } 74 | } 75 | 76 | @include exports("panel") { 77 | @if $include-html-panel-classes { 78 | 79 | /* Panels */ 80 | .panel { @include panel; 81 | 82 | &.callout { 83 | @include panel(scale-color($primary-color, $lightness: 94%)); 84 | a:not(.button) { 85 | color: $callout-panel-link-color; 86 | 87 | &:hover, 88 | &:focus { 89 | color: $callout-panel-link-color-hover; 90 | } 91 | } 92 | } 93 | 94 | &.radius { 95 | @include radius; 96 | } 97 | 98 | } 99 | 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /_sass/foundation/components/_progress-bars.scss: -------------------------------------------------------------------------------- 1 | // Foundation by ZURB 2 | // foundation.zurb.com 3 | // Licensed under MIT Open Source 4 | 5 | @import "global"; 6 | 7 | // 8 | // @variables 9 | // 10 | $include-html-media-classes: $include-html-classes !default; 11 | 12 | // We use this to set the progress bar height 13 | $progress-bar-height: rem-calc(25) !default; 14 | $progress-bar-color: $vapor !default; 15 | 16 | // We use these to control the border styles 17 | $progress-bar-border-color: scale-color($white, $lightness: 20%) !default; 18 | $progress-bar-border-size: 1px !default; 19 | $progress-bar-border-style: solid !default; 20 | $progress-bar-border-radius: $global-radius !default; 21 | 22 | // We use these to control the margin & padding 23 | $progress-bar-pad: rem-calc(2) !default; 24 | $progress-bar-margin-bottom: rem-calc(10) !default; 25 | 26 | // We use these to set the meter colors 27 | $progress-meter-color: $primary-color !default; 28 | $progress-meter-secondary-color: $secondary-color !default; 29 | $progress-meter-success-color: $success-color !default; 30 | $progress-meter-alert-color: $alert-color !default; 31 | 32 | // @mixins 33 | // 34 | // We use this to set up the progress bar container 35 | @mixin progress-container { 36 | background-color: $progress-bar-color; 37 | height: $progress-bar-height; 38 | border: $progress-bar-border-size $progress-bar-border-style $progress-bar-border-color; 39 | padding: $progress-bar-pad; 40 | margin-bottom: $progress-bar-margin-bottom; 41 | } 42 | 43 | // @mixins 44 | // 45 | // $bg - Default: $progress-meter-color || $primary-color 46 | @mixin progress-meter($bg:$progress-meter-color) { 47 | background: $bg; 48 | height: 100%; 49 | display: block; 50 | } 51 | 52 | 53 | @include exports("progress-bar") { 54 | @if $include-html-media-classes { 55 | 56 | /* Progress Bar */ 57 | .progress { 58 | @include progress-container; 59 | 60 | // Meter 61 | .meter { 62 | @include progress-meter; 63 | } 64 | &.secondary .meter { @include progress-meter($bg:$progress-meter-secondary-color); } 65 | &.success .meter { @include progress-meter($bg:$progress-meter-success-color); } 66 | &.alert .meter { @include progress-meter($bg:$progress-meter-alert-color); } 67 | 68 | &.radius { @include radius($progress-bar-border-radius); 69 | .meter { @include radius($progress-bar-border-radius - 1); } 70 | } 71 | 72 | &.round { @include radius(1000px); 73 | .meter { @include radius(999px); } 74 | } 75 | 76 | } 77 | 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /_sass/foundation/components/_side-nav.scss: -------------------------------------------------------------------------------- 1 | // Foundation by ZURB 2 | // foundation.zurb.com 3 | // Licensed under MIT Open Source 4 | 5 | @import "global"; 6 | 7 | // 8 | // @variables 9 | // 10 | 11 | $include-html-nav-classes: $include-html-classes !default; 12 | 13 | // We use this to control padding. 14 | $side-nav-padding: rem-calc(14 0) !default; 15 | 16 | // We use these to control list styles. 17 | $side-nav-list-type: none !default; 18 | $side-nav-list-position: outside !default; 19 | $side-nav-list-margin: rem-calc(0 0 7 0) !default; 20 | 21 | // We use these to control link styles. 22 | $side-nav-link-color: $primary-color !default; 23 | $side-nav-link-color-active: scale-color($side-nav-link-color, $lightness: 30%) !default; 24 | $side-nav-link-color-hover: scale-color($side-nav-link-color, $lightness: 30%) !default; 25 | $side-nav-link-bg-hover: hsla(0, 0, 0, 0.025) !default; 26 | $side-nav-link-margin: 0 !default; 27 | $side-nav-link-padding: rem-calc(7 14) !default; 28 | $side-nav-font-size: rem-calc(14) !default; 29 | $side-nav-font-weight: $font-weight-normal !default; 30 | $side-nav-font-weight-active: $side-nav-font-weight !default; 31 | $side-nav-font-family: $body-font-family !default; 32 | $side-nav-font-family-active: $side-nav-font-family !default; 33 | 34 | // We use these to control heading styles. 35 | $side-nav-heading-color: $side-nav-link-color !default; 36 | $side-nav-heading-font-size: $side-nav-font-size !default; 37 | $side-nav-heading-font-weight: bold !default; 38 | $side-nav-heading-text-transform: uppercase !default; 39 | 40 | // We use these to control border styles 41 | $side-nav-divider-size: 1px !default; 42 | $side-nav-divider-style: solid !default; 43 | $side-nav-divider-color: scale-color($white, $lightness: 10%) !default; 44 | 45 | 46 | // 47 | // @mixins 48 | // 49 | 50 | 51 | // We use this to style the side-nav 52 | // 53 | // $divider-color - Border color of divider. Default: $side-nav-divider-color. 54 | // $font-size - Font size of nav items. Default: $side-nav-font-size. 55 | // $link-color - Color of navigation links. Default: $side-nav-link-color. 56 | // $link-color-hover - Color of navigation links when hovered. Default: $side-nav-link-color-hover. 57 | @mixin side-nav( 58 | $divider-color:$side-nav-divider-color, 59 | $font-size:$side-nav-font-size, 60 | $link-color:$side-nav-link-color, 61 | $link-color-hover:$side-nav-link-color-hover, 62 | $link-bg-hover:$side-nav-link-bg-hover) { 63 | display: block; 64 | margin: 0; 65 | padding: $side-nav-padding; 66 | list-style-type: $side-nav-list-type; 67 | list-style-position: $side-nav-list-position; 68 | font-family: $side-nav-font-family; 69 | 70 | li { 71 | margin: $side-nav-list-margin; 72 | font-size: $font-size; 73 | font-weight: $side-nav-font-weight; 74 | 75 | a:not(.button) { 76 | display: block; 77 | color: $link-color; 78 | margin: $side-nav-link-margin; 79 | padding: $side-nav-link-padding; 80 | &:hover, 81 | &:focus { 82 | background: $link-bg-hover; 83 | color: $link-color-hover; 84 | } 85 | } 86 | 87 | &.active > a:first-child:not(.button) { 88 | color: $side-nav-link-color-active; 89 | font-weight: $side-nav-font-weight-active; 90 | font-family: $side-nav-font-family-active; 91 | } 92 | 93 | &.divider { 94 | border-top: $side-nav-divider-size $side-nav-divider-style; 95 | height: 0; 96 | padding: 0; 97 | list-style: none; 98 | border-top-color: $divider-color; 99 | } 100 | 101 | &.heading { 102 | color: $side-nav-heading-color; 103 | font: { 104 | size: $side-nav-heading-font-size; 105 | weight: $side-nav-heading-font-weight; 106 | } 107 | text-transform: $side-nav-heading-text-transform; 108 | } 109 | } 110 | } 111 | 112 | @include exports("side-nav") { 113 | @if $include-html-nav-classes { 114 | .side-nav { @include side-nav; } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /_sass/foundation/components/_sub-nav.scss: -------------------------------------------------------------------------------- 1 | // Foundation by ZURB 2 | // foundation.zurb.com 3 | // Licensed under MIT Open Source 4 | 5 | @import "global"; 6 | 7 | // 8 | // @name _sub-nav.scss 9 | // @dependencies _global.scss 10 | // 11 | 12 | // 13 | // @variables 14 | // 15 | 16 | $include-html-nav-classes: $include-html-classes !default; 17 | 18 | // We use these to control margin and padding 19 | $sub-nav-list-margin: rem-calc(-4 0 18) !default; 20 | $sub-nav-list-padding-top: rem-calc(4) !default; 21 | 22 | // We use this to control the definition 23 | $sub-nav-font-family: $body-font-family !default; 24 | $sub-nav-font-size: rem-calc(14) !default; 25 | $sub-nav-font-color: $aluminum !default; 26 | $sub-nav-font-weight: $font-weight-normal !default; 27 | $sub-nav-text-decoration: none !default; 28 | $sub-nav-padding: rem-calc(3 16) !default; 29 | $sub-nav-border-radius: 3px !default; 30 | $sub-nav-font-color-hover: scale-color($sub-nav-font-color, $lightness: -25%) !default; 31 | 32 | 33 | // We use these to control the active item styles 34 | 35 | $sub-nav-active-font-weight: $font-weight-normal !default; 36 | $sub-nav-active-bg: $primary-color !default; 37 | $sub-nav-active-bg-hover: scale-color($sub-nav-active-bg, $lightness: -14%) !default; 38 | $sub-nav-active-color: $white !default; 39 | $sub-nav-active-padding: $sub-nav-padding !default; 40 | $sub-nav-active-cursor: default !default; 41 | 42 | $sub-nav-item-divider: "" !default; 43 | $sub-nav-item-divider-margin: rem-calc(12) !default; 44 | 45 | // 46 | // @mixins 47 | // 48 | 49 | 50 | // Create a sub-nav item 51 | // 52 | // $font-color - Font color. Default: $sub-nav-font-color. 53 | // $font-size - Font size. Default: $sub-nav-font-size. 54 | // $active-bg - Background of active nav item. Default: $sub-nav-active-bg. 55 | // $active-bg-hover - Background of active nav item, when hovered. Default: $sub-nav-active-bg-hover. 56 | @mixin sub-nav( 57 | $font-color: $sub-nav-font-color, 58 | $font-size: $sub-nav-font-size, 59 | $active-bg: $sub-nav-active-bg, 60 | $active-bg-hover: $sub-nav-active-bg-hover) { 61 | display: block; 62 | width: auto; 63 | overflow: hidden; 64 | margin-bottom: $sub-nav-list-margin; 65 | padding-top: $sub-nav-list-padding-top; 66 | 67 | dt { 68 | text-transform: uppercase; 69 | } 70 | 71 | dt, 72 | dd, 73 | li { 74 | float: $default-float; 75 | margin-#{$default-float}: rem-calc(16); 76 | margin-bottom: 0; 77 | font-family: $sub-nav-font-family; 78 | font-weight: $sub-nav-font-weight; 79 | font-size: $font-size; 80 | color: $font-color; 81 | 82 | a { 83 | text-decoration: $sub-nav-text-decoration; 84 | color: $sub-nav-font-color; 85 | padding: $sub-nav-padding; 86 | &:hover { 87 | color: $sub-nav-font-color-hover; 88 | } 89 | } 90 | 91 | &.active a { 92 | @include radius($sub-nav-border-radius); 93 | font-weight: $sub-nav-active-font-weight; 94 | background: $active-bg; 95 | padding: $sub-nav-active-padding; 96 | cursor: $sub-nav-active-cursor; 97 | color: $sub-nav-active-color; 98 | &:hover { 99 | background: $active-bg-hover; 100 | } 101 | } 102 | @if $sub-nav-item-divider != "" { 103 | margin-#{$default-float}: 0; 104 | 105 | &:before { 106 | content: "#{$sub-nav-item-divider}"; 107 | margin: 0 $sub-nav-item-divider-margin; 108 | } 109 | 110 | &:first-child:before { 111 | content: ""; 112 | margin: 0; 113 | } 114 | } 115 | } 116 | } 117 | 118 | @include exports("sub-nav") { 119 | @if $include-html-nav-classes { 120 | .sub-nav { @include sub-nav; } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /_sass/foundation/components/_tables.scss: -------------------------------------------------------------------------------- 1 | // Foundation by ZURB 2 | // foundation.zurb.com 3 | // Licensed under MIT Open Source 4 | 5 | @import "global"; 6 | 7 | // 8 | // @name _tables.scss 9 | // @dependencies _global.scss 10 | // 11 | 12 | // 13 | // @variables 14 | // 15 | 16 | $include-html-table-classes: $include-html-classes !default; 17 | 18 | // These control the background color for the table and even rows 19 | $table-bg: $white !default; 20 | $table-even-row-bg: $snow !default; 21 | 22 | // These control the table cell border style 23 | $table-border-style: solid !default; 24 | $table-border-size: 1px !default; 25 | $table-border-color: $gainsboro !default; 26 | 27 | // These control the table head styles 28 | $table-head-bg: $white-smoke !default; 29 | $table-head-font-size: rem-calc(14) !default; 30 | $table-head-font-color: $jet !default; 31 | $table-head-font-weight: $font-weight-bold !default; 32 | $table-head-padding: rem-calc(8 10 10) !default; 33 | 34 | // These control the table foot styles 35 | $table-foot-bg: $table-head-bg !default; 36 | $table-foot-font-size: $table-head-font-size !default; 37 | $table-foot-font-color: $table-head-font-color !default; 38 | $table-foot-font-weight: $table-head-font-weight !default; 39 | $table-foot-padding: $table-head-padding !default; 40 | 41 | // These control the caption 42 | $table-caption-bg: transparent !default; 43 | $table-caption-font-color: $table-head-font-color !default; 44 | $table-caption-font-size: rem-calc(16) !default; 45 | $table-caption-font-weight: bold !default; 46 | 47 | // These control the row padding and font styles 48 | $table-row-padding: rem-calc(9 10) !default; 49 | $table-row-font-size: rem-calc(14) !default; 50 | $table-row-font-color: $jet !default; 51 | $table-line-height: rem-calc(18) !default; 52 | 53 | // These are for controlling the layout, display and margin of tables 54 | $table-layout: auto !default; 55 | $table-display: table-cell !default; 56 | $table-margin-bottom: rem-calc(20) !default; 57 | 58 | 59 | // 60 | // @mixins 61 | // 62 | 63 | @mixin table { 64 | background: $table-bg; 65 | margin-bottom: $table-margin-bottom; 66 | border: $table-border-style $table-border-size $table-border-color; 67 | table-layout: $table-layout; 68 | 69 | caption { 70 | background: $table-caption-bg; 71 | color: $table-caption-font-color; 72 | font: { 73 | size: $table-caption-font-size; 74 | weight: $table-caption-font-weight; 75 | } 76 | } 77 | 78 | thead { 79 | background: $table-head-bg; 80 | 81 | tr { 82 | th, 83 | td { 84 | padding: $table-head-padding; 85 | font-size: $table-head-font-size; 86 | font-weight: $table-head-font-weight; 87 | color: $table-head-font-color; 88 | } 89 | } 90 | } 91 | 92 | tfoot { 93 | background: $table-foot-bg; 94 | 95 | tr { 96 | th, 97 | td { 98 | padding: $table-foot-padding; 99 | font-size: $table-foot-font-size; 100 | font-weight: $table-foot-font-weight; 101 | color: $table-foot-font-color; 102 | } 103 | } 104 | } 105 | 106 | tr { 107 | th, 108 | td { 109 | padding: $table-row-padding; 110 | font-size: $table-row-font-size; 111 | color: $table-row-font-color; 112 | text-align: $default-float; 113 | } 114 | 115 | &.even, 116 | &.alt, 117 | &:nth-of-type(even) { background: $table-even-row-bg; } 118 | } 119 | 120 | thead tr th, 121 | tfoot tr th, 122 | tfoot tr td, 123 | tbody tr th, 124 | tbody tr td, 125 | tr td { display: $table-display; line-height: $table-line-height; } 126 | } 127 | 128 | 129 | @include exports("table") { 130 | @if $include-html-table-classes { 131 | table { 132 | @include table; 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /_sass/foundation/components/_tabs.scss: -------------------------------------------------------------------------------- 1 | // Foundation by ZURB 2 | // foundation.zurb.com 3 | // Licensed under MIT Open Source 4 | 5 | @import "global"; 6 | @import "grid"; 7 | 8 | // 9 | // @variables 10 | // 11 | 12 | $include-html-tabs-classes: $include-html-classes !default; 13 | 14 | $tabs-navigation-padding: rem-calc(16) !default; 15 | $tabs-navigation-bg-color: $silver !default; 16 | $tabs-navigation-active-bg-color: $white !default; 17 | $tabs-navigation-hover-bg-color: scale-color($tabs-navigation-bg-color, $lightness: -6%) !default; 18 | $tabs-navigation-font-color: $jet !default; 19 | $tabs-navigation-active-font-color: $tabs-navigation-font-color !default; 20 | $tabs-navigation-font-size: rem-calc(16) !default; 21 | $tabs-navigation-font-family: $body-font-family !default; 22 | 23 | $tabs-content-margin-bottom: rem-calc(24) !default; 24 | $tabs-content-padding: ($column-gutter/2) !default; 25 | 26 | $tabs-vertical-navigation-margin-bottom: 1.25rem !default; 27 | 28 | @include exports("tab") { 29 | @if $include-html-tabs-classes { 30 | .tabs { 31 | @include clearfix; 32 | margin-bottom: 0 !important; 33 | margin-left: 0; 34 | dd, .tab-title { 35 | position: relative; 36 | margin-bottom: 0 !important; 37 | list-style: none; 38 | float: $default-float; 39 | > a { 40 | display: block; 41 | background: { 42 | color: $tabs-navigation-bg-color; 43 | } 44 | color: $tabs-navigation-font-color; 45 | padding: $tabs-navigation-padding $tabs-navigation-padding * 2; 46 | font-family: $tabs-navigation-font-family; 47 | font-size: $tabs-navigation-font-size; 48 | &:hover { 49 | background: { 50 | color: $tabs-navigation-hover-bg-color; 51 | } 52 | } 53 | &:focus{ 54 | outline: none; 55 | } 56 | } 57 | &.active a { 58 | background: { 59 | color: $tabs-navigation-active-bg-color; 60 | } 61 | color:$tabs-navigation-active-font-color; 62 | } 63 | } 64 | &.radius { 65 | dd:first-child, .tab:first-child { 66 | a { @include side-radius($default-float, $global-radius); } 67 | } 68 | dd:last-child, .tab:last-child { 69 | a { @include side-radius($opposite-direction, $global-radius); } 70 | } 71 | } 72 | &.vertical { 73 | dd, .tab-title { 74 | position: inherit; 75 | float: none; 76 | display: block; 77 | top: auto; 78 | } 79 | } 80 | } 81 | 82 | .tabs-content { 83 | @include clearfix; 84 | margin-bottom: $tabs-content-margin-bottom; 85 | width: 100%; 86 | > .content { 87 | display: none; 88 | float: $default-float; 89 | padding: $tabs-content-padding 0; 90 | width: 100%; 91 | &.active { display: block; float: none; } 92 | &.contained { padding: $tabs-content-padding; } 93 | } 94 | &.vertical { 95 | display: block; 96 | > .content { padding: 0 $tabs-content-padding; } 97 | } 98 | } 99 | @media #{$medium-up} { 100 | .tabs { 101 | &.vertical { 102 | width: 20%; 103 | max-width: 20%; 104 | float: $default-float; 105 | margin: 0 0 $tabs-vertical-navigation-margin-bottom; 106 | } 107 | } 108 | .tabs-content { 109 | &.vertical { 110 | width: 80%; 111 | max-width: 80%; 112 | float: $default-float; 113 | margin-#{$default-float}: -1px; 114 | padding-#{$default-float}: 1rem; 115 | } 116 | } 117 | } 118 | .no-js { 119 | .tabs-content > .content { 120 | display: block; 121 | float: none; 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /_sass/foundation/components/_thumbs.scss: -------------------------------------------------------------------------------- 1 | // Foundation by ZURB 2 | // foundation.zurb.com 3 | // Licensed under MIT Open Source 4 | 5 | @import "global"; 6 | 7 | // 8 | // @name _thumbs.scss 9 | // @dependencies _globals.scss 10 | // 11 | 12 | // 13 | // @variables 14 | // 15 | 16 | $include-html-media-classes: $include-html-classes !default; 17 | 18 | // We use these to control border styles 19 | $thumb-border-style: solid !default; 20 | $thumb-border-width: 4px !default; 21 | $thumb-border-color: $white !default; 22 | $thumb-box-shadow: 0 0 0 1px rgba($black,.2) !default; 23 | $thumb-box-shadow-hover: 0 0 6px 1px rgba($primary-color,0.5) !default; 24 | 25 | // Radius and transition speed for thumbs 26 | $thumb-radius: $global-radius !default; 27 | $thumb-transition-speed: 200ms !default; 28 | 29 | // 30 | // @mixins 31 | // 32 | 33 | // We use this to create image thumbnail styles. 34 | // 35 | // $border-width - Width of border around thumbnail. Default: $thumb-border-width. 36 | // $box-shadow - Box shadow to apply to thumbnail. Default: $thumb-box-shadow. 37 | // $box-shadow-hover - Box shadow to apply on hover. Default: $thumb-box-shadow-hover. 38 | @mixin thumb( 39 | $border-width:$thumb-border-width, 40 | $box-shadow:$thumb-box-shadow, 41 | $box-shadow-hover:$thumb-box-shadow-hover) { 42 | line-height: 0; 43 | display: inline-block; 44 | border: $thumb-border-style $border-width $thumb-border-color; 45 | max-width: 100%; 46 | box-shadow: $box-shadow; 47 | 48 | &:hover, 49 | &:focus { 50 | box-shadow: $box-shadow-hover; 51 | } 52 | } 53 | 54 | 55 | @include exports("thumb") { 56 | @if $include-html-media-classes { 57 | 58 | /* Image Thumbnails */ 59 | .th { 60 | @include thumb; 61 | @include single-transition(all,$thumb-transition-speed,ease-out); 62 | 63 | &.radius { @include radius($thumb-radius); } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /_sass/foundation/components/_toolbar.scss: -------------------------------------------------------------------------------- 1 | // Foundation by ZURB 2 | // foundation.zurb.com 3 | // Licensed under MIT Open Source 4 | // toolbar styles 5 | 6 | @import "global"; 7 | 8 | .toolbar { 9 | background: $oil; 10 | width: 100%; 11 | font-size: 0; 12 | display: inline-block; 13 | 14 | &.label-bottom .tab .tab-content { 15 | i, img { margin-bottom: 10px; } 16 | } 17 | 18 | &.label-right .tab .tab-content { 19 | i, img { margin-right: 10px; display: inline-block;} 20 | label { display: inline-block; } 21 | } 22 | 23 | &.vertical.label-right .tab .tab-content { 24 | text-align: left; 25 | } 26 | 27 | &.vertical { 28 | height: 100%; 29 | width: auto; 30 | 31 | .tab { 32 | width: auto; 33 | margin: auto; 34 | float: none; 35 | } 36 | } 37 | 38 | .tab { 39 | text-align: center; 40 | width: 25%; 41 | margin: 0 auto; 42 | display: block; 43 | padding: 20px; 44 | float: left; 45 | 46 | &:hover { 47 | background: rgba($white, 0.1); 48 | } 49 | } 50 | } 51 | 52 | .toolbar .tab-content { 53 | font-size: 16px; 54 | text-align: center; 55 | 56 | label { color: $iron; } 57 | 58 | i { 59 | font-size: 30px; 60 | display: block; 61 | margin: 0 auto; 62 | color: $iron; 63 | vertical-align: middle; 64 | } 65 | 66 | img { 67 | width: 30px; 68 | height: 30px; 69 | display: block; 70 | margin: 0 auto; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /_sass/foundation_overrides.scss: -------------------------------------------------------------------------------- 1 | // $base-font-size: 100% !default; 2 | 3 | @import "foundation/functions"; 4 | 5 | // $em-base: 12px !default; 6 | 7 | // mel: 0.9 opacity black 8 | $body-font-color: $black; 9 | $header-font-color: $black; 10 | 11 | 12 | $font-family-sans-serif: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 13 | $font-family-serif: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 14 | // $body-font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 15 | // $header-font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 16 | 17 | $font-weight-normal: 300; 18 | $font-weight-bold: 400; 19 | 20 | $header-font-weight: 400; 21 | // $body-font-weight: 300; 22 | 23 | // $paragraph-font-weight: 300; 24 | 25 | // $body-font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; 26 | // $header-font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; 27 | 28 | 29 | // $font-smoothing: antialiased; 30 | 31 | $h1-font-size: em-calc(33px); 32 | $h2-font-size: em-calc(23px); 33 | $h3-font-size: em-calc(19px); 34 | $h4-font-size: em-calc(14px); 35 | $h5-font-size: em-calc(10px); 36 | $h6-font-size: 1em; 37 | 38 | 39 | // $row-width: emCalc(1000px); 40 | $row-width: 90%; 41 | 42 | // $topbar-bg: #21242C; 43 | 44 | // $topbar-bg: #fff; 45 | // $topbar-bg: #2a3744; 46 | // $topbar-bg: $blue-light; 47 | // $topbar-bg: $blue-lighter; 48 | $topbar-bg: $gray-blue; 49 | 50 | $topbar-title-font-size: em-calc(32); 51 | $topbar-title-weight: $font-weight-bold; 52 | 53 | $topbar-height: em-calc(68); 54 | // $topbar-margin-bottom: emCalc(70px); 55 | 56 | // $topbar-link-color: #444; 57 | $topbar-link-color: #fff; 58 | // $topbar-link-color-hover: #444; 59 | // $topbar-link-color-active: #444; 60 | 61 | // $topbar-title-font-size: emCalc(15px); 62 | // $topbar-link-font-size: emCalc(15px); 63 | 64 | // $topbar-menu-link-color: $blue-lighter; 65 | // $topbar-menu-icon-color: $blue-lighter; 66 | // $topbar-menu-link-color-toggled: #444; 67 | // $topbar-menu-icon-color-toggled: #444; 68 | 69 | // $topbar-dropdown-bg: $blue-lighter; 70 | // $topbar-dropdown-link-color: #444; 71 | 72 | 73 | // $crumb-bg: #fff; 74 | // $crumb-border-style: none; 75 | 76 | $side-nav-link-color: $blue-dark; 77 | $side-nav-link-color-active: $black; 78 | 79 | $side-nav-font-size: rem-calc(14); 80 | $side-nav-font-weight-active: $font-weight-bold; 81 | 82 | // $side-nav-padding: rem-calc(0 0); 83 | 84 | $anchor-font-color: $blue-dark; 85 | $anchor-font-color-hover: $blue-light; 86 | 87 | $alert-color: #C4473A; -------------------------------------------------------------------------------- /_sass/prism.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * prism.js default theme for JavaScript, CSS and HTML 3 | * Based on dabblet (http://dabblet.com) 4 | * @author Lea Verou 5 | */ 6 | 7 | code[class*="language-"], 8 | pre[class*="language-"] { 9 | background-color: #fff; 10 | border: none; 11 | color: $black; 12 | font-family: "Source Code Pro", Consolas, monospace; 13 | 14 | font-size: 0.9rem; 15 | line-height: 1.6rem; 16 | font-weight: 300; 17 | 18 | direction: ltr; 19 | text-align: left; 20 | white-space: pre; 21 | word-spacing: normal; 22 | word-break: normal; 23 | 24 | -moz-tab-size: 4; 25 | -o-tab-size: 4; 26 | tab-size: 4; 27 | 28 | -webkit-hyphens: none; 29 | -moz-hyphens: none; 30 | -ms-hyphens: none; 31 | hyphens: none; 32 | } 33 | 34 | /* Code blocks */ 35 | pre[class*="language-"] { 36 | padding: 1em; 37 | margin: .5em 0; 38 | overflow: auto; 39 | } 40 | 41 | :not(pre) > code[class*="language-"], 42 | pre[class*="language-"] { 43 | // background: #f5f2f0; 44 | } 45 | 46 | /* Inline code */ 47 | :not(pre) > code[class*="language-"] { 48 | padding: .1em; 49 | // border-radius: .3em; 50 | } 51 | 52 | .token.comment, 53 | .token.prolog, 54 | .token.doctype, 55 | .token.cdata { 56 | color: slategray; 57 | } 58 | 59 | .token.punctuation { 60 | color: #999; 61 | } 62 | 63 | .namespace { 64 | opacity: .7; 65 | } 66 | 67 | .token.property, 68 | .token.tag, 69 | .token.boolean, 70 | .token.number, 71 | .token.constant, 72 | .token.symbol { 73 | color: #905; 74 | } 75 | 76 | .token.selector, 77 | .token.attr-name, 78 | .token.string, 79 | .token.builtin { 80 | color: #690; 81 | } 82 | 83 | .token.operator, 84 | .token.entity, 85 | .token.url, 86 | .language-css .token.string, 87 | .style .token.string, 88 | .token.variable { 89 | color: #a67f59; 90 | background: hsla(0,0%,100%,.5); 91 | } 92 | 93 | .token.atrule, 94 | .token.attr-value, 95 | .token.keyword { 96 | color: #07a; 97 | } 98 | 99 | 100 | .token.regex, 101 | .token.important { 102 | color: #e90; 103 | } 104 | 105 | .token.important { 106 | font-weight: bold; 107 | } 108 | 109 | .token.entity { 110 | cursor: help; 111 | } 112 | 113 | -------------------------------------------------------------------------------- /_sass/syntax.scss: -------------------------------------------------------------------------------- 1 | // .highlight { background: #ffffff; } 2 | // .highlight .c { color: #999988; font-style: italic } /* Comment */ 3 | // .highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */ 4 | // .highlight .k { font-weight: bold } /* Keyword */ 5 | // .highlight .o { font-weight: bold } /* Operator */ 6 | // .highlight .cm { color: #999988; font-style: italic } /* Comment.Multiline */ 7 | // .highlight .cp { color: #999999; font-weight: bold } /* Comment.Preproc */ 8 | // .highlight .c1 { color: #999988; font-style: italic } /* Comment.Single */ 9 | // .highlight .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */ 10 | // .highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ 11 | // .highlight .gd .x { color: #000000; background-color: #ffaaaa } /* Generic.Deleted.Specific */ 12 | // .highlight .ge { font-style: italic } /* Generic.Emph */ 13 | // .highlight .gr { color: #aa0000 } /* Generic.Error */ 14 | // .highlight .gh { color: #999999 } /* Generic.Heading */ 15 | // .highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ 16 | // .highlight .gi .x { color: #000000; background-color: #aaffaa } /* Generic.Inserted.Specific */ 17 | // .highlight .go { color: #888888 } /* Generic.Output */ 18 | // .highlight .gp { color: #555555 } /* Generic.Prompt */ 19 | // .highlight .gs { font-weight: bold } /* Generic.Strong */ 20 | // .highlight .gu { color: #aaaaaa } /* Generic.Subheading */ 21 | // .highlight .gt { color: #aa0000 } /* Generic.Traceback */ 22 | // .highlight .kc { font-weight: bold } /* Keyword.Constant */ 23 | // .highlight .kd { font-weight: bold } /* Keyword.Declaration */ 24 | // .highlight .kp { font-weight: bold } /* Keyword.Pseudo */ 25 | // .highlight .kr { font-weight: bold } /* Keyword.Reserved */ 26 | // .highlight .kt { color: #445588; font-weight: bold } /* Keyword.Type */ 27 | // .highlight .m { color: #009999 } /* Literal.Number */ 28 | // .highlight .s { color: #d14 } /* Literal.String */ 29 | // .highlight .na { color: #008080 } /* Name.Attribute */ 30 | // .highlight .nb { color: #0086B3 } /* Name.Builtin */ 31 | // .highlight .nc { color: #445588; font-weight: bold } /* Name.Class */ 32 | // .highlight .no { color: #008080 } /* Name.Constant */ 33 | // .highlight .ni { color: #800080 } /* Name.Entity */ 34 | // .highlight .ne { color: #990000; font-weight: bold } /* Name.Exception */ 35 | // .highlight .nf { color: #990000; font-weight: bold } /* Name.Function */ 36 | // .highlight .nn { color: #555555 } /* Name.Namespace */ 37 | // .highlight .nt { color: #000080 } /* Name.Tag */ 38 | // .highlight .nv { color: #008080 } /* Name.Variable */ 39 | // .highlight .ow { font-weight: bold } /* Operator.Word */ 40 | // .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 41 | // .highlight .mf { color: #009999 } /* Literal.Number.Float */ 42 | // .highlight .mh { color: #009999 } /* Literal.Number.Hex */ 43 | // .highlight .mi { color: #009999 } /* Literal.Number.Integer */ 44 | // .highlight .mo { color: #009999 } /* Literal.Number.Oct */ 45 | // .highlight .sb { color: #d14 } /* Literal.String.Backtick */ 46 | // .highlight .sc { color: #d14 } /* Literal.String.Char */ 47 | // .highlight .sd { color: #d14 } /* Literal.String.Doc */ 48 | // .highlight .s2 { color: #d14 } /* Literal.String.Double */ 49 | // .highlight .se { color: #d14 } /* Literal.String.Escape */ 50 | // .highlight .sh { color: #d14 } /* Literal.String.Heredoc */ 51 | // .highlight .si { color: #d14 } /* Literal.String.Interpol */ 52 | // .highlight .sx { color: #d14 } /* Literal.String.Other */ 53 | // .highlight .sr { color: #009926 } /* Literal.String.Regex */ 54 | // .highlight .s1 { color: #d14 } /* Literal.String.Single */ 55 | // .highlight .ss { color: #990073 } /* Literal.String.Symbol */ 56 | // .highlight .bp { color: #999999 } /* Name.Builtin.Pseudo */ 57 | // .highlight .vc { color: #008080 } /* Name.Variable.Class */ 58 | // .highlight .vg { color: #008080 } /* Name.Variable.Global */ 59 | // .highlight .vi { color: #008080 } /* Name.Variable.Instance */ 60 | // .highlight .il { color: #009999 } /* Literal.Number.Integer.Long */ 61 | -------------------------------------------------------------------------------- /_sass/variables.scss: -------------------------------------------------------------------------------- 1 | $dark-green: #2A3744; 2 | // $blue-dark: #004CFF; 3 | $blue-dark: #07a; 4 | 5 | // $blue-light: #195EFF; 6 | $blue-light: lighten($blue-dark, 20%); 7 | 8 | $blue-lighter: #6FB6FF; 9 | 10 | $orange-dark: #B27E00; 11 | $orange-light: #FFB300; 12 | 13 | 14 | $black: lighten(#000, 10%); 15 | 16 | $top-bar-black: #333; 17 | 18 | $primary-color: $blue-light; 19 | $secondary-color: $orange-light; 20 | 21 | $footer-bg: #efefef; 22 | 23 | $gray-blue-mel-style: rgba(43, 74, 140, 0.52); 24 | 25 | $gray-blue: #2c3240; -------------------------------------------------------------------------------- /backend-integration-with-node-express/consuming-rest-apis.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Consuming REST APIs 4 | chapter: backend-integration-with-node-express 5 | order: 1 6 | --- 7 | 8 | ### Problem 9 | You wish to consume a JSON REST API implemented in your Express application. 10 | 11 | ### Solution 12 | Using the `$resource` service we will begin by defining our Contact model and all RESTful actions. 13 | 14 | {% prism javascript %} 15 | app.factory("Contact", function($resource) { 16 | return $resource("/api/contacts/:id", { id: "@_id" }, 17 | { 18 | 'create': { method: 'POST' }, 19 | 'index': { method: 'GET', isArray: true }, 20 | 'show': { method: 'GET', isArray: false }, 21 | 'update': { method: 'PUT' }, 22 | 'destroy': { method: 'DELETE' } 23 | } 24 | ); 25 | }); 26 | {% endprism %} 27 | 28 | We can now fetch a list of contacts using `Contact.index()` and a single contact with `Contact.show(id)`. These actions can be directly mapped to the API routes defined in `app.js`. 29 | 30 | {% prism javascript %} 31 | var express = require('express'), 32 | api = require('./routes/api'); 33 | 34 | var app = module.exports = express(); 35 | 36 | app.get('/api/contacts', api.contacts); 37 | app.get('/api/contacts/:id', api.contact); 38 | app.post('/api/contacts', api.createContact); 39 | app.put('/api/contacts/:id', api.updateContact); 40 | app.delete('/api/contacts/:id', api.destroyContact); 41 | {% endprism %} 42 | 43 | I like to keep routes in a separate file `routes/api.js` and just reference them in `app.js` in order to keep it small. The API implementation first initializes the [Mongoose](http://mongoosejs.com/) library and defines a schema for our Contact model. 44 | 45 | {% prism javascript %} 46 | var mongoose = require('mongoose'); 47 | mongoose.connect('mongodb://localhost/contacts_database'); 48 | 49 | var contactSchema = mongoose.Schema({ 50 | firstname: 'string', lastname: 'string', age: 'number' 51 | }); 52 | var Contact = mongoose.model('Contact', contactSchema); 53 | {% endprism %} 54 | 55 | We can now use the `Contact` model to implement the API. Lets start with the index action: 56 | 57 | {% prism javascript %} 58 | exports.contacts = function(req, res) { 59 | Contact.find({}, function(err, obj) { 60 | res.json(obj) 61 | }); 62 | }; 63 | {% endprism %} 64 | 65 | Skipping the error handling we retrieve all contacts with the `find` function provided by Mongoose and render the result in the JSON format. The show action is pretty similar except it uses `findOne` and the id from the URL parameter to retrieve a single contact. 66 | 67 | {% prism javascript %} 68 | exports.contact = function(req, res) { 69 | Contact.findOne({ _id: req.params.id }, function(err, obj) { 70 | res.json(obj); 71 | }); 72 | }; 73 | {% endprism %} 74 | 75 | As a final example we will create a new Contact instance passing in the request body and call the `save` method to persist it: 76 | 77 | {% prism javascript %} 78 | exports.createContact = function(req, res) { 79 | var contact = new Contact(req.body); 80 | contact.save(); 81 | res.json(req.body); 82 | }; 83 | {% endprism %} 84 | 85 | You can find the complete example on [github](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter10/recipe1). 86 | 87 | ### Discussion 88 | Let have a look again at the example for the contact function, which retrieves a single Contact. It uses `_id` instead of `id` as the parameter for the `findOne` function. This underscore is intentional and used by MongoDB for its auto-generated IDs. In order to automatically map from `id` to the `_id` parameter we used a nice trick of the `$resource` service. Take a look at the second parameter of the Contact `$resource` definition: `{ id: "@_id" }`. Using this parameter Angular will automatically set the URL parameter `id` based on the value of the model attribute `_id`. 89 | -------------------------------------------------------------------------------- /backend-integration-with-node-express/implementing-client-side-routing.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Implementing Client-Side Routing 4 | chapter: backend-integration-with-node-express 5 | order: 2 6 | --- 7 | 8 | ### Problem 9 | You wish to use client-side routing in conjunction with an Express backend. 10 | 11 | ### Solution 12 | Every request to the backend should initially render the complete layout in order to load our Angular app. Only then will the client-side rendering take over. Let us first have a look at the route definition for this "catch all" route in our `app.js`. 13 | 14 | {% prism javascript %} 15 | var express = require('express'), 16 | routes = require('./routes'); 17 | 18 | app.get('/', routes.index); 19 | app.get('*', routes.index); 20 | {% endprism %} 21 | 22 | It uses the wildcard character to catch all requests in order to get processed with the `routes.index` module. Additionally, it defines the route to use the same module. The module again resides in `routes/index.js`. 23 | 24 | {% prism javascript %} 25 | exports.index = function(req, res){ 26 | res.render('layout'); 27 | }; 28 | {% endprism %} 29 | 30 | The implementation only renders the layout template. It uses the [Jade](http://jade-lang.com/) template engine. 31 | 32 | {% prism markup %} 33 | {% raw %} 34 | !!! 35 | html(ng-app="myApp") 36 | head 37 | meta(charset='utf8') 38 | title Angular Express Seed App 39 | link(rel='stylesheet', href='/css/bootstrap.css') 40 | body 41 | div 42 | ng-view 43 | 44 | script(src='js/lib/angular/angular.js') 45 | script(src='js/lib/angular/angular-resource.js') 46 | script(src='js/app.js') 47 | script(src='js/services.js') 48 | script(src='js/controllers.js') 49 | {% endraw %} 50 | {% endprism %} 51 | 52 | Now that we can actually render the initial layout we can get started with the client-side routing definition in `app.js` 53 | 54 | {% prism javascript %} 55 | var app = angular.module('myApp', ["ngResource"]). 56 | config(['$routeProvider', '$locationProvider', 57 | function($routeProvider, $locationProvider) { 58 | $locationProvider.html5Mode(true); 59 | $routeProvider 60 | .when("/contacts", { 61 | templateUrl: "partials/index.jade", 62 | controller: "ContactsIndexCtrl" }) 63 | .when("/contacts/new", { 64 | templateUrl: "partials/edit.jade", 65 | controller: "ContactsEditCtrl" }) 66 | .when("/contacts/:id", { 67 | templateUrl: "partials/show.jade", 68 | controller: "ContactsShowCtrl" }) 69 | .when("/contacts/:id/edit", { 70 | templateUrl: "partials/edit.jade", 71 | controller: "ContactsEditCtrl" }) 72 | .otherwise({ redirectTo: "/contacts" }); 73 | } 74 | ] 75 | ); 76 | {% endprism %} 77 | 78 | We define route definitions to list, show and edit contacts and use a set of partials and corresponding controllers. In order for the partials to get loaded correctly we need to add another express route in the backend which serves all these partials. 79 | 80 | {% prism javascript %} 81 | app.get('/partials/:name', function (req, res) { 82 | var name = req.params.name; 83 | res.render('partials/' + name); 84 | }); 85 | {% endprism %} 86 | 87 | It uses the name of the partial as an URL param and renders the partial with the given name from the `partial` directory. Keep in mind that you must define that route before the catch all route, otherwise it will not work. 88 | 89 | You can find the complete example on [github](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter10/recipe1). 90 | 91 | ### Discussion 92 | Compared to Rails the handling of partials is quite explicit by defining a route for partials. On the other hand it is quite nice to being able to use jade templates for our partials too. 93 | -------------------------------------------------------------------------------- /backend-integration-with-node-express/index.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Backend Integration with Node Express 4 | chapter: backend-integration-with-node-express 5 | intro: true 6 | --- 7 | In this chapter we will have a look into solving common problems when combining Angular.js with the Node.js [Express](http://expressjs.com/) framework. The examples used in this chapter are based on a Contacts app to manage a list of contacts. As an extra we use MongoDB as a backend for our contacts since it requires further customization to make it work in conjunction with Angular's `$resource` service. 8 | 9 |

Table of Contents

10 |
    11 | {% sorted_for page in site.pages | sort_by:order %} 12 | {% if page.chapter == "backend-integration-with-node-express" %} 13 |
  1. 14 | {{page.title}} 15 |
  2. 16 | {% endif %} 17 | {% endsorted_for %} 18 |
19 | -------------------------------------------------------------------------------- /backend-integration-with-ruby-on-rails/implementing-client-side-routing.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Implementing Client-Side Routing 4 | chapter: backend-integration-with-ruby-on-rails 5 | order: 2 6 | --- 7 | 8 | ### Problem 9 | You wish to use client-side routing in conjunction with a Ruby on Rails backend. 10 | 11 | ### Solution 12 | Every request to the backend should initially render the complete page in order to load our Angular app. Only then will the client-side rendering take over. Let us first have a look at the route definition for this "catch all" route. 13 | 14 | {% prism ruby %} 15 | Contacts::Application.routes.draw do 16 | root :to => "layouts#index" 17 | match "*path" => "layouts#index" 18 | end 19 | {% endprism %} 20 | 21 | It uses [Route Globbing](http://guides.rubyonrails.org/routing.html#route-globbing) to match all URLs and defines a root URL. Both will be handled by a layout controller with the sole purpose of rendering the initial layout. 22 | 23 | {% prism ruby %} 24 | class LayoutsController < ApplicationController 25 | def index 26 | render "layouts/application" 27 | end 28 | end 29 | {% endprism %} 30 | 31 | The actual layout template defines our `ng-view` directive and resides in `app/views/layouts/application.html` - nothing new here. So let's skip ahead to the Angular route definition in `app.js.erb`. 32 | 33 | {% prism javascript %} 34 | var app = angular.module("Contacts", ["ngResource"]); 35 | 36 | app.config(function($routeProvider, $locationProvider) { 37 | $locationProvider.html5Mode(true); 38 | $routeProvider 39 | .when("/contacts", 40 | { templateUrl: "<%= asset_path('contacts/index.html') %> ", 41 | controller: "ContactsIndexCtrl" }) 42 | .when("/contacts/new", 43 | { templateUrl: "<%= asset_path('contacts/edit.html') %> ", 44 | controller: "ContactsEditCtrl" }) 45 | .when("/contacts/:id", 46 | { templateUrl: "<%= asset_path('contacts/show.html') %> ", 47 | controller: "ContactsShowCtrl" }) 48 | .when("/contacts/:id/edit", 49 | { templateUrl: "<%= asset_path('contacts/edit.html') %> ", 50 | controller: "ContactsEditCtrl" }) 51 | .otherwise({ redirectTo: "/contacts" }); 52 | }); 53 | {% endprism %} 54 | 55 | We set the `$locationProvider` to use the HTML5 mode and define our client-side routes for listing, showing, editing and creating new contacts. 56 | 57 | You can find the complete example on [github](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter9/recipe1). 58 | 59 | ### Discussion 60 | Let us have a look into the route definition again. First of all the filename ends with `erb`, since it uses ERB tags in the javascript file, courtesy of the [Rails Asset Pipeline](http://guides.rubyonrails.org/asset_pipeline.html). The `asset_path` method is used to retrieve the URL to the HTML partials since it will change depending on the environment. On production the filename contains an MD5 checksum and the actual ERB output will change from `/assets/contacts/index.html` to `/assets/contacts/index-7ce113b9081a20d93a4a86e1aacce05f.html`. If your Rails app is configured to use an asset host, the path will in fact be absolute. 61 | -------------------------------------------------------------------------------- /backend-integration-with-ruby-on-rails/index.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Backend Integration with Ruby on Rails 4 | chapter: backend-integration-with-ruby-on-rails 5 | intro: true 6 | --- 7 | In this chapter we will have a look at using Angular.js with the Ruby on Rails framework. 8 |

Table of Contents

9 |
    10 | {% sorted_for page in site.pages | sort_by:order %} 11 | {% if page.chapter == "backend-integration-with-ruby-on-rails" %} 12 |
  1. 13 | {{page.title}} 14 |
  2. 15 | {% endif %} 16 | {% endsorted_for %} 17 |
18 | -------------------------------------------------------------------------------- /common-user-interface-patterns/displaying-a-modal-dialog.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Displaying a Modal Dialog 4 | chapter: common-user-interface-patterns 5 | order: 7 6 | --- 7 | 8 | ### Question 9 | You wish to use a Modal Dialog using the Twitter Bootstrap Framework. A dialog is called modal when it is blocking the rest of your web application until it is closed. 10 | 11 | ### Solution 12 | Use the `angular-ui` module's nice `modal` plugin, which directly supports Twitter Bootstrap. 13 | 14 | The template defines a button to open the modal and the modal code itself. 15 | 16 | {% prism markup %} 17 | {% raw %} 18 | 19 | 20 | 21 | 22 |
23 | 26 | 29 | 33 |
34 | 35 | 36 | {% endraw %} 37 | {% endprism %} 38 | 39 | Note that even though we don't specify it explicitly the modal dialog is hidden initially via the `modal` attribute. The controller only handles the button click and the `showModal` value used by the `modal` attribute. 40 | 41 | {% prism javascript %} 42 | var app = angular.module("MyApp", ["ui.bootstrap.modal"]); 43 | 44 | $scope.open = function() { 45 | $scope.showModal = true; 46 | }; 47 | 48 | $scope.ok = function() { 49 | $scope.showModal = false; 50 | }; 51 | 52 | $scope.cancel = function() { 53 | $scope.showModal = false; 54 | }; 55 | {% endprism %} 56 | 57 | Do not forget to download and include the angular-ui.js file in a script tag. The module dependency is defined directly to "ui.bootstrap.modal". The [full example](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter8/recipe7) is available on Github including the angular-ui module. 58 | 59 | You can find the complete example on [github](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter8/recipe7). 60 | 61 | ### Discussion 62 | The modal as defined in the template is straight from the Twitter bootstrap [documentation](http://twitter.github.com/bootstrap/javascript.html#modals). We can control the visibility with the `modal` attribute. Additionally, the `close` attribute defines a `close` function which is called whenever the dialog is closed. Note that this could happen if the user presses the `escape` key or clicking outside the modal. 63 | 64 | Our own cancel button uses the same function to close the modal manually, whereas the okay button uses the `ok` function. This makes it easy for us to distinguish between a user who simply cancelled the modal or actually pressed the okay button. 65 | -------------------------------------------------------------------------------- /common-user-interface-patterns/editing-text-in-place-using-html5-content-editable.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Editing Text In-Place using HTML5 ContentEditable 4 | chapter: common-user-interface-patterns 5 | order: 6 6 | source_path: common-user-interface-patterns/source/recipe6 7 | --- 8 | 9 | ### Problem 10 | You wish to make a div element editable in place using the HTML5 contenteditable attribute. 11 | 12 | ### Solution 13 | Implement a directive for the `contenteditable` attribute and use `ng-model` for data binding. 14 | 15 | In this example we use a div and a paragraph to render the content. 16 | 17 | {% prism markup %} 18 | {% raw %} 19 |
20 |

{{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 |
19 |
20 | 22 |
23 | 26 |
27 | 28 | {% endraw %} 29 | {% endprism %} 30 | 31 | A plain text input field is used to enter the filter query and bound to the `filter`. Any changes are therefore directly used to filter the list. 32 | 33 | The controller defines the default friends array: 34 | 35 | {% prism javascript %} 36 | app.controller("MyCtrl", function($scope) { 37 | $scope.friends = [ 38 | { name: "Peter", age: 20 }, 39 | { name: "Pablo", age: 55 }, 40 | { name: "Linda", age: 20 }, 41 | { name: "Marta", age: 37 }, 42 | { name: "Othello", age: 20 }, 43 | { name: "Markus", age: 32 } 44 | ]; 45 | }); 46 | {% endprism %} 47 | 48 | You can find the complete example on [github](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter8/recipe1). 49 | 50 | ### Discussion 51 | Chaining filters is a fantastic way of implementing such a use case as long as you have all the data available on the client. 52 | 53 | The [filter](http://docs.angularjs.org/api/ng.filter:filter) Angular.js Filter works on an array and returns a subset of items as a new array. It supports a String, Object or Function parameter. In this example we only use the String parameter, but given that the `$scope.friends` is an array of objects we could think of more complex examples where we use the Object param, as for example: 54 | 55 | {% prism markup %} 56 | {% raw %} 57 | 62 | {% endraw %} 63 | {% endprism %} 64 | 65 | That way we can filter by name and age at the same time. And lastly you could call a function defined in the controller, which does the filtering for you: 66 | 67 | {% prism markup %} 68 | {% raw %} 69 | 74 | {% endraw %} 75 | {% endprism %} 76 | 77 | {% prism javascript %} 78 | $scope.filterFunction = function(element) { 79 | return element.name.match(/^Ma/) ? true : false; 80 | }; 81 | {% endprism %} 82 | 83 | The `filterFunction` must return either `true` or `false`. In this example we use a regular expression on the name starting with `Ma` to filter the list. 84 | -------------------------------------------------------------------------------- /common-user-interface-patterns/index.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Common User Interface Patterns 4 | chapter: common-user-interface-patterns 5 | intro: true 6 | --- 7 | 8 |

Table of Contents

9 |
    10 | {% sorted_for page in site.pages | sort_by:order %} 11 | {% if page.chapter == "common-user-interface-patterns" %} 12 |
  1. 13 | {{page.title}} 14 |
  2. 15 | {% endif %} 16 | {% endsorted_for %} 17 |
-------------------------------------------------------------------------------- /common-user-interface-patterns/paginating-through-server-side-data.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Paginating Through Server-Side Data 4 | chapter: common-user-interface-patterns 5 | order: 3 6 | source_path: common-user-interface-patterns/source/recipe3 7 | --- 8 | 9 | ### Problem 10 | You wish to paginate through a large server-side result set. 11 | 12 | ### Solution 13 | You cannot use the previous method with a filter since that would require all data to be available on the client. Instead we use an implementation with a controller only instead. 14 | 15 | The template has not changed much. Only the `ng-repeat` directive looks simpler now: 16 | 17 | {% prism markup %} 18 | {% raw %} 19 | 20 | {{item.id}} 21 | {{item.name}} 22 | {{item.description}} 23 | 24 | {% endraw %} 25 | {% endprism %} 26 | 27 | In order to simplify the example we will fake a server-side service by providing an Angular service implementation for the items. 28 | 29 | {% prism javascript %} 30 | app.factory("Item", function() { 31 | 32 | var items = []; 33 | for (var i=0; i<50; i++) { 34 | items.push({ 35 | id: i, name: "name "+ i, description: "description " + i 36 | }); 37 | } 38 | 39 | return { 40 | get: function(offset, limit) { 41 | return items.slice(offset, offset+limit); 42 | }, 43 | total: function() { 44 | return items.length; 45 | } 46 | }; 47 | }); 48 | {% endprism %} 49 | 50 | The service manages a list of items and has methods for retrieving a subset of items for a given offset and limit and the total number of items. 51 | 52 | The controller uses dependency injection to access the `Item` service and contains almost the same methods as our previous recipe. 53 | 54 | {% prism javascript %} 55 | app.controller("PaginationCtrl", function($scope, Item) { 56 | 57 | $scope.itemsPerPage = 5; 58 | $scope.currentPage = 0; 59 | 60 | $scope.prevPage = function() { 61 | if ($scope.currentPage > 0) { 62 | $scope.currentPage--; 63 | } 64 | }; 65 | 66 | $scope.prevPageDisabled = function() { 67 | return $scope.currentPage === 0 ? "disabled" : ""; 68 | }; 69 | 70 | $scope.nextPage = function() { 71 | if ($scope.currentPage < $scope.pageCount() - 1) { 72 | $scope.currentPage++; 73 | } 74 | }; 75 | 76 | $scope.nextPageDisabled = function() { 77 | return $scope.currentPage === $scope.pageCount() - 1 ? "disabled" : ""; 78 | }; 79 | 80 | $scope.pageCount = function() { 81 | return Math.ceil($scope.total/$scope.itemsPerPage); 82 | }; 83 | 84 | $scope.$watch("currentPage", function(newValue, oldValue) { 85 | $scope.pagedItems = 86 | Item.get(newValue*$scope.itemsPerPage, $scope.itemsPerPage); 87 | $scope.total = Item.total(); 88 | }); 89 | 90 | }); 91 | {% endprism %} 92 | 93 | You can find the complete example on [github](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter8/recipe3). 94 | 95 | ### Discussion 96 | When you select the next/previous page you will change the `$scope.currentPage` value and the `$watch` function is triggered. It fetches fresh items for the current page and the total number of items. So, on the client side we only have five items available as defined in `itemsPerPage` and when paginating we throw away the items of the previous page and fetch new items. 97 | 98 | If you want to try this with a real backend you only have to swap out the `Item` service implementation. 99 | -------------------------------------------------------------------------------- /common-user-interface-patterns/paginating-using-infinite-results.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Paginating Using Infinite Results 4 | chapter: common-user-interface-patterns 5 | order: 4 6 | source_path: common-user-interface-patterns/source/recipe4 7 | --- 8 | 9 | ### Problem 10 | You wish to paginate through server-side data with a "Load More" button, which just keeps appending more data until no more data is available. 11 | 12 | ### Solution 13 | Let's start by looking at how the item table is rendered with the `ng-repeat` Directive. 14 | 15 | {% prism markup %} 16 | {% raw %} 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 38 | 39 |
IdNameDescription
{{item.id}}{{item.name}}{{item.description}}
35 | 37 |
40 |
41 | {% endraw %} 42 | {% endprism %} 43 | 44 | The controller uses the same `Item` Service as used for the previous recipe and handles the logic for the "Load More" button. 45 | 46 | {% prism javascript %} 47 | app.controller("PaginationCtrl", function($scope, Item) { 48 | 49 | $scope.itemsPerPage = 5; 50 | $scope.currentPage = 0; 51 | $scope.total = Item.total(); 52 | $scope.pagedItems = Item.get($scope.currentPage*$scope.itemsPerPage, 53 | $scope.itemsPerPage); 54 | 55 | $scope.loadMore = function() { 56 | $scope.currentPage++; 57 | var newItems = Item.get($scope.currentPage*$scope.itemsPerPage, 58 | $scope.itemsPerPage); 59 | $scope.pagedItems = $scope.pagedItems.concat(newItems); 60 | }; 61 | 62 | $scope.nextPageDisabledClass = function() { 63 | return $scope.currentPage === $scope.pageCount()-1 ? "disabled" : ""; 64 | }; 65 | 66 | $scope.pageCount = function() { 67 | return Math.ceil($scope.total/$scope.itemsPerPage); 68 | }; 69 | 70 | }); 71 | {% endprism %} 72 | 73 | You can find the complete example on [github](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter8/recipe4). 74 | 75 | ### Discussion 76 | The solution is actually pretty similar to the previous recipe and uses a controller only again. The `$scope.pagedItems` is retrieved initially to render the first five items. 77 | 78 | When pressing the "Load More" button we fetch another set of items incrementing the `currentPage` to change the `offset` of the `Item.get` function. The new items will be concatenated with the existing items using the Array `concat` function. The changes to `pagedItems` will be automatically rendered by the `ng-repeat` directive. 79 | 80 | The `nextPageDisabledClass` checks whether there is more data available by calculating the total number of pages in `pageCount` and comparing that to the current page. 81 | -------------------------------------------------------------------------------- /common-user-interface-patterns/source/recipe1/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module("MyApp", []); 2 | 3 | app.controller("MyCtrl", function($scope) { 4 | $scope.friends = [ 5 | { name: "Peter", age: 20 }, 6 | { name: "Pablo", age: 55 }, 7 | { name: "Linda", age: 20 }, 8 | { name: "Marta", age: 37 }, 9 | { name: "Othello", age: 20 }, 10 | { name: "Markus", age: 32 } 11 | ]; 12 | 13 | $scope.filterFunction = function(element) { 14 | return element.name.match(/^Ma/) ? true : false; 15 | }; 16 | 17 | }); -------------------------------------------------------------------------------- /common-user-interface-patterns/source/recipe1/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 6 |
7 | 10 |
11 | -------------------------------------------------------------------------------- /common-user-interface-patterns/source/recipe2/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module("MyApp", []); 2 | 3 | app.filter('offset', function() { 4 | return function(input, start) { 5 | start = parseInt(start, 10); 6 | return input.slice(start); 7 | }; 8 | }); 9 | 10 | app.controller("PaginationCtrl", function($scope) { 11 | 12 | $scope.itemsPerPage = 5; 13 | $scope.currentPage = 0; 14 | $scope.items = []; 15 | 16 | for (var i=0; i<50; i++) { 17 | $scope.items.push({ id: i, name: "name "+ i, description: "description " + i }); 18 | } 19 | 20 | $scope.range = function() { 21 | var rangeSize = 5; 22 | var ret = []; 23 | var start; 24 | 25 | start = $scope.currentPage; 26 | if ( start > $scope.pageCount()-rangeSize ) { 27 | start = $scope.pageCount()-rangeSize+1; 28 | } 29 | 30 | for (var i=start; i 0) { 38 | $scope.currentPage--; 39 | } 40 | }; 41 | 42 | $scope.prevPageDisabled = function() { 43 | return $scope.currentPage === 0 ? "disabled" : ""; 44 | }; 45 | 46 | $scope.pageCount = function() { 47 | return Math.ceil($scope.items.length/$scope.itemsPerPage)-1; 48 | }; 49 | 50 | $scope.nextPage = function() { 51 | if ($scope.currentPage < $scope.pageCount()) { 52 | $scope.currentPage++; 53 | } 54 | }; 55 | 56 | $scope.nextPageDisabled = function() { 57 | return $scope.currentPage === $scope.pageCount() ? "disabled" : ""; 58 | }; 59 | 60 | $scope.setPage = function(n) { 61 | $scope.currentPage = n; 62 | }; 63 | 64 | }); -------------------------------------------------------------------------------- /common-user-interface-patterns/source/recipe2/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 34 | 35 |
IdNameDescription
{{item.id}}{{item.name}}{{item.description}}
20 | 33 |
36 |
37 | -------------------------------------------------------------------------------- /common-user-interface-patterns/source/recipe3/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module("MyApp", []); 2 | 3 | app.factory("Item", function() { 4 | 5 | var items = []; 6 | for (var i=0; i<50; i++) { 7 | items.push({ id: i, name: "name "+ i, description: "description " + i }); 8 | } 9 | 10 | return { 11 | get: function(offset, limit) { 12 | return items.slice(offset, offset+limit); 13 | }, 14 | total: function() { 15 | return items.length; 16 | } 17 | }; 18 | }); 19 | 20 | app.controller("PaginationCtrl", function($scope, Item) { 21 | 22 | $scope.itemsPerPage = 5; 23 | $scope.currentPage = 0; 24 | 25 | $scope.range = function() { 26 | var rangeSize = 5; 27 | var ret = []; 28 | var start; 29 | 30 | start = $scope.currentPage; 31 | if ( start > $scope.pageCount()-rangeSize ) { 32 | start = $scope.pageCount()-rangeSize; 33 | } 34 | 35 | for (var i=start; i 0) { 44 | $scope.currentPage--; 45 | } 46 | }; 47 | 48 | $scope.prevPageDisabled = function() { 49 | return $scope.currentPage === 0 ? "disabled" : ""; 50 | }; 51 | 52 | $scope.nextPage = function() { 53 | if ($scope.currentPage < $scope.pageCount() - 1) { 54 | $scope.currentPage++; 55 | } 56 | }; 57 | 58 | $scope.nextPageDisabled = function() { 59 | return $scope.currentPage === $scope.pageCount() - 1 ? "disabled" : ""; 60 | }; 61 | 62 | $scope.pageCount = function() { 63 | return Math.ceil($scope.total/$scope.itemsPerPage); 64 | }; 65 | 66 | $scope.setPage = function(n) { 67 | if (n > 0 && n < $scope.pageCount()) { 68 | $scope.currentPage = n; 69 | } 70 | }; 71 | 72 | $scope.$watch("currentPage", function(newValue, oldValue) { 73 | $scope.pagedItems = Item.get(newValue*$scope.itemsPerPage, $scope.itemsPerPage); 74 | $scope.total = Item.total(); 75 | }); 76 | 77 | }); -------------------------------------------------------------------------------- /common-user-interface-patterns/source/recipe3/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 34 | 35 |
IdNameDescription
{{item.id}}{{item.name}}{{item.description}}
20 | 33 |
36 |
37 | -------------------------------------------------------------------------------- /common-user-interface-patterns/source/recipe4/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module("MyApp", []); 2 | 3 | app.factory("Item", function() { 4 | 5 | var items = []; 6 | for (var i=0; i<50; i++) { 7 | items.push({ id: i, name: "name "+ i, description: "description " + i }); 8 | } 9 | 10 | return { 11 | get: function(offset, limit) { 12 | return items.slice(offset, offset+limit); 13 | }, 14 | total: function() { 15 | return items.length; 16 | } 17 | }; 18 | }); 19 | 20 | app.controller("PaginationCtrl", function($scope, Item) { 21 | 22 | $scope.itemsPerPage = 5; 23 | $scope.currentPage = 0; 24 | $scope.total = Item.total(); 25 | $scope.pagedItems = Item.get($scope.currentPage*$scope.itemsPerPage, $scope.itemsPerPage); 26 | 27 | $scope.loadMore = function() { 28 | $scope.currentPage++; 29 | var newItems = Item.get($scope.currentPage*$scope.itemsPerPage, $scope.itemsPerPage); 30 | $scope.pagedItems = $scope.pagedItems.concat(newItems); 31 | }; 32 | 33 | $scope.nextPageDisabledClass = function() { 34 | return $scope.currentPage === $scope.pageCount()-1 ? "disabled" : ""; 35 | }; 36 | 37 | $scope.pageCount = function() { 38 | return Math.ceil($scope.total/$scope.itemsPerPage); 39 | }; 40 | 41 | }); -------------------------------------------------------------------------------- /common-user-interface-patterns/source/recipe4/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 |
IdNameDescription
{{item.id}}{{item.name}}{{item.description}}
20 | 21 |
24 |
25 | -------------------------------------------------------------------------------- /common-user-interface-patterns/source/recipe5/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module("MyApp", []); 2 | 3 | app.config(function($routeProvider) { 4 | $routeProvider. 5 | when("/home", { templateUrl: "home.html" }). 6 | when("/page", { templateUrl: "page.html" }). 7 | otherwise({ redirectTo: "/home" }); 8 | }); 9 | 10 | app.factory("flash", function($rootScope) { 11 | var queue = []; 12 | var currentMessage = ""; 13 | 14 | $rootScope.$on("$routeChangeSuccess", function() { 15 | currentMessage = queue.shift() || ""; 16 | }); 17 | 18 | return { 19 | setMessage: function(message) { 20 | queue.push(message); 21 | }, 22 | getMessage: function() { 23 | return currentMessage; 24 | } 25 | }; 26 | }); 27 | 28 | app.controller("MyCtrl", function($scope, $location, flash) { 29 | $scope.flash = flash; 30 | $scope.message = "Hello World"; 31 | 32 | $scope.submit = function(message) { 33 | flash.setMessage(message); 34 | $location.path("/page"); 35 | } 36 | }); -------------------------------------------------------------------------------- /common-user-interface-patterns/source/recipe5/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 |
9 | Alert! 10 |

{{flash.getMessage()}}

11 |
12 | 13 | 14 | 15 | 24 | 25 | 29 | 30 | -------------------------------------------------------------------------------- /common-user-interface-patterns/source/recipe6/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module("MyApp", []); 2 | 3 | app.directive("contenteditable", function() { 4 | return { 5 | restrict: "A", 6 | require: "ngModel", 7 | link: function(scope, element, attrs, ngModel) { 8 | 9 | function read() { 10 | ngModel.$setViewValue(element.html()); 11 | } 12 | 13 | ngModel.$render = function() { 14 | element.html(ngModel.$viewValue || ""); 15 | }; 16 | 17 | element.bind("blur keyup change", function() { 18 | scope.$apply(read); 19 | }); 20 | } 21 | }; 22 | }); -------------------------------------------------------------------------------- /common-user-interface-patterns/source/recipe6/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

{{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 |
20 | 21 | 22 |
    23 |
  • {{tweet.text}}
  • 24 |
25 |
26 | 27 | {% endraw %} 28 | {% endprism %} 29 | 30 | The `$resource` configuration can be done in a controller requesting the data: 31 | 32 | {% prism javascript %} 33 | var app = angular.module("MyApp", ["ngResource"]); 34 | 35 | function MyCtrl($scope, $resource) { 36 | var TwitterAPI = $resource("http://search.twitter.com/search.json", 37 | { callback: "JSON_CALLBACK" }, 38 | { get: { method: "JSONP" }}); 39 | 40 | $scope.search = function() { 41 | $scope.searchResult = TwitterAPI.get({ q: $scope.searchTerm }); 42 | }; 43 | } 44 | {% endprism %} 45 | 46 | You can find the complete example on [github](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter5/recipe3). 47 | 48 | ### Discussion 49 | The Twitter search API supports a `callback` attribute for the JSON format as described in their [documentation](https://dev.twitter.com/docs/api/1/get/search). The `$resource` definition sets the `callback` attribute to `JSON_CALLBACK`, which is a convention from Angular when using [JSONP](http://en.wikipedia.org/wiki/JSONP). It is a placeholder that is replaced with the real callback function, generated by Angular. Additionally, we configure the get method to use `JSONP`. Now, when calling the API we use the `q` URL parameter to pass the entered `searchTerm`. 50 | -------------------------------------------------------------------------------- /consuming-external-services/deferred-and-promise.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Deferred and Promise 4 | chapter: consuming-external-services 5 | order: 4 6 | --- 7 | 8 | ### Problem 9 | You wish to synchronize multiple asynchronous functions and avoid Javascript callback hell. 10 | 11 | ### Solution 12 | As an example, we want to call three services in sequence and combine the result of them. Let us start with a nested approach: 13 | 14 | {% prism javascript %} 15 | tmp = []; 16 | 17 | $http.get("/app/data/first.json").success(function(data) { 18 | tmp.push(data); 19 | $http.get("/app/data/second.json").success(function(data) { 20 | tmp.push(data); 21 | $http.get("/app/data/third.json").success(function(data) { 22 | tmp.push(data); 23 | $scope.combinedNestedResult = tmp.join(", "); 24 | }); 25 | }); 26 | }); 27 | {% endprism %} 28 | 29 | We call the `get` function three times to retrieve three JSON documents each with an array of strings. We haven't even started adding error handling but already using nested callbacks the code becomes messy and can be simplified using the `$q` service: 30 | 31 | {% prism javascript %} 32 | var first = $http.get("/app/data/first.json"), 33 | second = $http.get("/app/data/second.json"), 34 | third = $http.get("/app/data/third.json"); 35 | 36 | $q.all([first, second, third]).then(function(result) { 37 | var tmp = []; 38 | angular.forEach(result, function(response) { 39 | tmp.push(response.data); 40 | }); 41 | return tmp; 42 | }).then(function(tmpResult) { 43 | $scope.combinedResult = tmpResult.join(", "); 44 | }); 45 | {% endprism %} 46 | 47 | You can find the complete example on [github](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter5/recipe4). 48 | 49 | ### Discussion 50 | The `all` function combines multiple promises into a single promise and solves our problem quite elegantly. 51 | 52 | Let's have a closer look at the `then` method. It is rather contrived but should give you an idea of how to use `then` to sequentially call functions and pass data along. Since the `all` function returns a promise again we can call `then` on it. By returning the `tmp` variable it will be passed along to the then function as `tmpResult` argument. 53 | 54 | Before finishing this recipe let us quickly discuss an example where we have to create our own deferred object: 55 | 56 | {% prism javascript %} 57 | function deferredTimer(success) { 58 | var deferred = $q.defer(); 59 | 60 | $timeout(function() { 61 | if (success) { 62 | deferred.resolve({ message: "This is great!" }); 63 | } else { 64 | deferred.reject({ message: "Really bad" }); 65 | } 66 | }, 1000); 67 | 68 | return deferred.promise; 69 | } 70 | {% endprism %} 71 | 72 | Using the `defer` method we create a deferred instance. As an example of an asynchronous operation we will use the `$timeout` service which will either resolve or reject our operation depending on the boolean success parameter. The function will immediately return the `promise` and therefore not render any result in our HTML template. After one second, the timer will execute and return our success or failure response. 73 | 74 | This `deferredTimer` can be triggered in our HTML template by wrapping it into a function defined on the scope: 75 | 76 | {% prism javascript %} 77 | $scope.startDeferredTimer = function(success) { 78 | deferredTimer(success).then( 79 | function(data) { 80 | $scope.deferredTimerResult = "Successfully finished: " + 81 | data.message; 82 | }, 83 | function(data) { 84 | $scope.deferredTimerResult = "Failed: " + data.message; 85 | } 86 | ); 87 | }; 88 | {% endprism %} 89 | 90 | Our `startDeferredTimer` function will get a `success` parameter which it passes along to the `deferredTimer`. The `then` function expects a success and an error callback as arguments which we use to set a scope variable `deferredTimerResult` to show our result. 91 | 92 | This is just one of many examples of how promises can simplify your code, but you can find many more examples by looking into [Kris Kowal's Q implementation](https://github.com/kriskowal/q). 93 | -------------------------------------------------------------------------------- /consuming-external-services/index.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Consuming External Services 4 | chapter: consuming-external-services 5 | intro: true 6 | --- 7 | Angular has built-in support for communication with remote HTTP servers. The [$http](http://docs.angularjs.org/api/ng.$http) service handles low-level AJAX requests via the browser's [XMLHttpRequest](http://en.wikipedia.org/wiki/XMLHttpRequest) object or via [JSONP](http://en.wikipedia.org/wiki/JSONP). The [$resource](http://docs.angularjs.org/api/ngResource.$resource) service lets you interact with RESTful data sources and provides high-level behaviors which naturally map to RESTful resources. 8 | 9 |

Table of Contents

10 |
    11 | {% sorted_for page in site.pages | sort_by:order %} 12 | {% if page.chapter == "consuming-external-services" %} 13 |
  1. 14 | {{page.title}} 15 |
  2. 16 | {% endif %} 17 | {% endsorted_for %} 18 |
-------------------------------------------------------------------------------- /consuming-external-services/requesting-json-data-with-ajax.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Requesting JSON data with AJAX 4 | chapter: consuming-external-services 5 | order: 1 6 | --- 7 | 8 | ### Problem 9 | You wish to fetch JSON data via AJAX request and render it. 10 | 11 | ### Solution 12 | Implement a controller using the `$http` service to fetch the data and store it in the scope. 13 | 14 | {% prism markup %} 15 | {% raw %} 16 | 17 |
18 |
    19 |
  • {{post.title}}
  • 20 |
21 |
22 | 23 | {% endraw %} 24 | {% endprism %} 25 | 26 | {% prism javascript %} 27 | var app = angular.module("MyApp", []); 28 | 29 | app.controller("PostsCtrl", function($scope, $http) { 30 | $http.get('data/posts.json'). 31 | success(function(data, status, headers, config) { 32 | $scope.posts = data; 33 | }). 34 | error(function(data, status, headers, config) { 35 | // log error 36 | }); 37 | }); 38 | {% endprism %} 39 | 40 | You can find the complete example using the angular-seed project on [github](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter5/recipe1). 41 | 42 | ### Discussion 43 | The controller defines a dependency to the `$scope` and the `$http` module. An HTTP GET request to the `data/posts.json` endpoint is carried out with the `get` method. It returns a [$promise](http://docs.angularjs.org/api/ng.$q) object with a `success` and an `error` method. Once successful, the JSON data is assigned to `$scope.posts` to make it available in the template. 44 | 45 | The `$http` service supports the HTTP verbs `get`, `head`, `post`, `put`, `delete` and `jsonp`. We are going to look into more examples in the following chapters. 46 | 47 | The `$http` service automatically adds certain HTTP headers like for example `X-Requested-With: XMLHttpRequest`. But you can also set custom HTTP headers by yourself using the `$http.defaults` function: 48 | 49 | {% prism javascript %} 50 | $http.defaults.headers.common["X-Custom-Header"] = "Angular.js" 51 | {% endprism %} 52 | 53 | Until now the `$http` service does not really look particularly special. But if you look into the [documentation](http://docs.angularjs.org/api/ng.$http) you find a whole lot of nice features like, for example, request/response transformations, to automatically deserialize JSON for you, response caching, response interceptors to handle global error handling, authentication or other preprocessing tasks and, of course, promise support. We will look into the `$q` service, Angular's promise/deferred service in a later chapter. 54 | -------------------------------------------------------------------------------- /consuming-external-services/testing-services.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Testing Services 4 | chapter: consuming-external-services 5 | order: 5 6 | --- 7 | 8 | ### Problem 9 | You wish to unit test your controller and service consuming a JSONP API. 10 | 11 | Let's have a look again at our example we wish to test: 12 | 13 | {% prism javascript %} 14 | var app = angular.module("MyApp", ["ngResource"]); 15 | 16 | app.factory("TwitterAPI", function($resource) { 17 | return $resource("http://search.twitter.com/search.json", 18 | { callback: "JSON_CALLBACK" }, 19 | { get: { method: "JSONP" }}); 20 | }); 21 | 22 | app.controller("MyCtrl", function($scope, TwitterAPI) { 23 | $scope.search = function() { 24 | $scope.searchResult = TwitterAPI.get({ q: $scope.searchTerm }); 25 | }; 26 | }); 27 | {% endprism %} 28 | 29 | Note that it slightly changed from the previous recipe as the `TwitterAPI` is pulled out of the controller and resides in its own service now. 30 | 31 | ### Solution 32 | Use the angular-seed project and the $http_backend mocking service. 33 | 34 | {% prism javascript %} 35 | describe('MyCtrl', function(){ 36 | var scope, ctrl, httpBackend; 37 | 38 | beforeEach(module("MyApp")); 39 | 40 | beforeEach( 41 | inject( 42 | function($controller, $rootScope, TwitterAPI, $httpBackend) { 43 | httpBackend = $httpBackend; 44 | scope = $rootScope.$new(); 45 | ctrl = $controller("MyCtrl", { 46 | $scope: scope, TwitterAPI: TwitterAPI }); 47 | 48 | var mockData = { key: "test" }; 49 | var url = "http://search.twitter.com/search.json?" + 50 | "callback=JSON_CALLBACK&q=angularjs"; 51 | httpBackend.whenJSONP(url).respond(mockData); 52 | } 53 | ) 54 | ); 55 | 56 | it('should set searchResult on successful search', function() { 57 | scope.searchTerm = "angularjs"; 58 | scope.search(); 59 | httpBackend.flush(); 60 | 61 | expect(scope.searchResult.key).toBe("test"); 62 | }); 63 | 64 | }); 65 | {% endprism %} 66 | 67 | You can find the complete example on [github](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter5/recipe5). 68 | 69 | ### Discussion 70 | Since we now have a clear separation between the service and the controller, we can simply inject the `TwitterAPI` into our `beforeEach` function. 71 | 72 | Mocking with the `$httpBackend` is done as a last step in `beforeEach`. When a JSONP request happens we respond with `mockData`. After the `search()` is triggered we `flush()` the `httpBackend` in order to return our `mockData`. 73 | 74 | Have a look at the [ngMock.$httpBackend](http://docs.angularjs.org/api/ngMock.$httpBackend) module for more details. -------------------------------------------------------------------------------- /controllers/assigning-a-default-value-to-a-model.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Assigning a Default Value to a Model 4 | chapter: controllers 5 | source_path: controllers/source/recipe1 6 | order: 1 7 | --- 8 | 9 | ### Problem 10 | You wish to assign a default value to the scope in the controller's context. 11 | 12 | ### Solution 13 | 14 | Use the `ng-controller` directive in your template: 15 | 16 | {% prism markup %} 17 | {% raw %} 18 |
19 |

{{value}}

20 |
21 | {% endraw %} 22 | {% endprism %} 23 | 24 | Next, define the scope variable in your controller function: 25 | 26 | {% prism javascript %} 27 | var MyCtrl = function($scope) { 28 | $scope.value = "some value"; 29 | }; 30 | {% endprism %} 31 | 32 | ### Discussion 33 | Depending on where you use the ng-controller directive, you define its assigned scope. The scope is hierarchical and follows the DOM node hierarchy. In our example, the value expression is correctly evaluated to `some value`, since value is set in the `MyCtrl` controller. Note that this would not work if the value expression were moved outside the controllers scope: 34 | 35 | {% prism markup %} 36 | {% raw %} 37 |

{{value}}

38 | 39 |
40 |
41 | {% endraw %} 42 | {% endprism %} 43 | 44 | In this case {% raw %}{{value}}{% endraw %} will simply be not rendered at all due to the fact that expression evaluation in Angular.js is forgiving for `undefined` and `null` values. -------------------------------------------------------------------------------- /controllers/changing-a-model-value-with-a-controller-function.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Changing a Model Value with a Controller Function 4 | chapter: controllers 5 | order: 2 6 | source_path: controllers/source/recipe2 7 | --- 8 | 9 | ### Problem 10 | You wish to increment a model value by 1 using a controller function. 11 | 12 | ### Solution 13 | Implement an increment function that changes the scope. 14 | 15 | {% prism javascript %} 16 | function MyCtrl($scope) { 17 | $scope.value = 1; 18 | 19 | $scope.incrementValue = function(increment) { 20 | $scope.value += increment; 21 | }; 22 | } 23 | {% endprism %} 24 | 25 | This function can be directly called in an expression, in our example we use `ng-init`: 26 | 27 | {% prism markup %} 28 | {% raw %} 29 |
30 |

{{value}}

31 |
32 | {% endraw %} 33 | {% endprism %} 34 | 35 | ### Discussion 36 | The `ng-init` directive is executed on page load and calls the function `incrementValue` defined in `MyCtrl`. Functions are defined on the scope very similar to values but must be called with the familiar parenthesis syntax. 37 | 38 | Of course, it would have been possible to increment the value right inside of the expression with `value = value +1` but imagine the function being much more complex! Moving this function into a controller separates our business logic from our declarative view template and we can easily write unit tests for it. -------------------------------------------------------------------------------- /controllers/encapsulation-a-model-value-with-a-controller-function.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Encapsulating a Model Value with a Controller Function 4 | chapter: controllers 5 | order: 3 6 | source_path: controllers/source/recipe3 7 | --- 8 | 9 | ### Problem 10 | You wish to retrieve a model via function (instead of directly accessing the scope from the template) that encapsulates the model value. 11 | 12 | ### Solution 13 | Define a getter function that returns the model value. 14 | 15 | {% prism javascript %} 16 | function MyCtrl($scope) { 17 | $scope.value = 1; 18 | 19 | $scope.getIncrementedValue = function() { 20 | return $scope.value + 1; 21 | }; 22 | } 23 | {% endprism %} 24 | 25 | Then in the template we use an expression to call it: 26 | 27 | {% prism markup %} 28 | {% raw %} 29 |
30 |

{{getIncrementedValue()}}

31 |
32 | {% endraw %} 33 | {% endprism %} 34 | 35 | ### Discussion 36 | `MyCtrl` defines the `getIncrementedValue` function, which uses the current value and returns it incremented by one. One could argue that depending on the use case it would make more sense to use a filter. But there are use cases specific to the controllers behavior where a generic filter is not required. 37 | -------------------------------------------------------------------------------- /controllers/index.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: overview 3 | chapter: controllers 4 | title: Controllers 5 | intro: true 6 | --- 7 | 8 | Controllers in Angular provide the business logic to handle view behavior, for example responding to a user clicking a button or entering some text in a form. Additionally, controllers prepare the model for the view template. 9 | 10 | As a general rule, a controller should not reference or manipulate the DOM directly. This has the benefit of simplifying unit testing controllers. 11 | 12 |

Table of Contents

13 |
    14 | {% sorted_for page in site.pages | sort_by:order %} 15 | {% if page.chapter == "controllers" %} 16 |
  1. 17 | {{page.title}} 18 |
  2. 19 | {% endif %} 20 | {% endsorted_for %} 21 |
-------------------------------------------------------------------------------- /controllers/responding-to-scope-changes.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Responding to Scope Changes 4 | chapter: controllers 5 | order: 4 6 | source_path: controllers/source/recipe4 7 | --- 8 | 9 | ### Problem 10 | You wish to react on a model change to trigger some further actions. In our example we simple want to set another model value depending on the value we are listening to. 11 | 12 | ### Solution 13 | Use the `$watch` function in your controller. 14 | 15 | {% prism javascript %} 16 | function MyCtrl($scope) { 17 | $scope.name = ""; 18 | 19 | $scope.$watch("name", function(newValue, oldValue) { 20 | if ($scope.name.length > 0) { 21 | $scope.greeting = "Greetings " + $scope.name; 22 | } 23 | }); 24 | } 25 | {% endprism %} 26 | 27 | In our example we use the text input value to print a friendly greeting. 28 | 29 | {% prism markup %} 30 | {% raw %} 31 |
32 | 33 |

{{greeting}}

34 |
35 | {% endraw %} 36 | {% endprism %} 37 | 38 | The value `greeting` will be changed whenever there's a change to the `name` model and the value is not blank. 39 | 40 | ### Discussion 41 | The first argument `name` of the `$watch` function is actually an Angular expression, so you can use more complex expressions (for example: `[value1, value2] | json`) or even a Javascript function. In this case you need to return a string in the watcher function: 42 | 43 | {% prism javascript %} 44 | $scope.$watch(function() { 45 | return $scope.name; 46 | }, function(newValue, oldValue) { 47 | console.log("change detected: " + newValue) 48 | }); 49 | {% endprism %} 50 | 51 | The second argument is a function which gets called whenever the expression evaluation returns a different value. The first parameter is the new value and the second parameter the old value. Internally, this uses `angular.equals` to determine equality which means both objects or values pass the `===` comparison. 52 | 53 | -------------------------------------------------------------------------------- /controllers/sharing-code-between-controllers-using-services.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Sharing Code Between Controllers using Services 4 | chapter: controllers 5 | order: 6 6 | source_path: controllers/source/recipe6 7 | --- 8 | 9 | ### Problem 10 | You wish to share business logic between controllers. 11 | 12 | ### Solution 13 | Utilise a [Service](http://docs.angularjs.org/guide/dev_guide.services) to implement your business logic and use dependency injection to use this service in your controllers. 14 | 15 | The template shows access to a list of users from two controllers: 16 | 17 | {% prism markup %} 18 | {% raw %} 19 |
20 |
    21 |
  • {{user}}
  • 22 |
23 |
24 | First user: {{firstUser}} 25 |
26 |
27 | {% endraw %} 28 | {% endprism %} 29 | 30 | The service and controller implementation in `app.js` implements a user service and the controllers set the scope initially: 31 | 32 | {% prism javascript %} 33 | var app = angular.module("MyApp", []); 34 | 35 | app.factory("UserService", function() { 36 | var users = ["Peter", "Daniel", "Nina"]; 37 | 38 | return { 39 | all: function() { 40 | return users; 41 | }, 42 | first: function() { 43 | return users[0]; 44 | } 45 | }; 46 | }); 47 | 48 | app.controller("MyCtrl", function($scope, UserService) { 49 | $scope.users = UserService.all(); 50 | }); 51 | 52 | app.controller("AnotherCtrl", function($scope, UserService) { 53 | $scope.firstUser = UserService.first(); 54 | }); 55 | {% endprism %} 56 | 57 | You can find the complete example on [github](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter2/recipe6). 58 | 59 | ### Discussion 60 | The `factory` method creates a singleton `UserService`, that returns two functions for retrieving all users and the first user only. The controllers get the `UserService` injected by adding it to the `controller` function as params. 61 | 62 | Using dependency injection here is quite nice for testing your controllers, since you can easily inject a `UserService` stub. The only downside is that you can't minify the code from above without breaking it, since the injection mechanism relies on the exact string representation of `UserService`. It is therefore recommended to define dependencies using inline annotations, which keeps working even when minified: 63 | 64 | {% prism javascript %} 65 | app.controller("AnotherCtrl", ["$scope", "UserService", 66 | function($scope, UserService) { 67 | $scope.firstUser = UserService.first(); 68 | } 69 | ]); 70 | {% endprism %} 71 | 72 | The syntax looks a bit funny, but since strings in arrays are not changed during the minification process it solves our problem. Note that you could change the parameter names of the function, since the injection mechanism relies on the order of the array definition only. 73 | 74 | Another way to achieve the same is using the `$inject` annotation: 75 | 76 | {% prism javascript %} 77 | var anotherCtrl = function($scope, UserService) { 78 | $scope.firstUser = UserService.first(); 79 | }; 80 | 81 | anotherCtrl.$inject = ["$scope", "UserService"]; 82 | {% endprism %} 83 | 84 | This requires you to use a temporary variable to call the `$inject` service. Again, you could change the function parameter names. You will most probably see both versions applied in apps using Angular. 85 | 86 | -------------------------------------------------------------------------------- /controllers/sharing-models-between-nested-controllers.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Sharing Models Between Nested Controllers 4 | chapter: controllers 5 | order: 5 6 | source_path: controllers/source/recipe5 7 | --- 8 | 9 | ### Problem 10 | You wish to share a model between a nested hierarchy of controllers. 11 | 12 | ### Solution 13 | Use Javascript objects instead of primitives or direct `$parent` scope references. 14 | 15 | Our example template uses a controller `MyCtrl` and a nested controller `MyNestedCtrl`: 16 | 17 | {% prism markup %} 18 | {% raw %} 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 |
38 | 39 | {% endraw %} 40 | {% endprism %} 41 | 42 | The `app.js` file contains the controller definition and initializes the scope with some defaults: 43 | 44 | {% prism javascript %} 45 | var app = angular.module("MyApp", []); 46 | 47 | app.controller("MyCtrl", function($scope) { 48 | $scope.name = "Peter"; 49 | $scope.user = { 50 | name: "Parker" 51 | }; 52 | }); 53 | 54 | app.controller("MyNestedCtrl", function($scope) { 55 | }); 56 | {% endprism %} 57 | 58 | Play around with the various input fields and see how changes affect each other. 59 | 60 | ### Discussion 61 | All the default values are defined in `MyCtrl` which is the parent of `MyNestedCtrl`. When making changes in the first input field, the changes will be in sync with the other input fields bound to the `name` variable. They all share the same scope variable as long as they only read from the variable. If you change the nested value, a copy in the scope of the `MyNestedCtrl` will be created. From now on, changing the first input field will only change the nested input field which explicitly references the parent scope via `$parent.name` expression. 62 | 63 | The object-based value behaves differently in this regard. Whether you change the nested or the `MyCtrl` scopes input fields, the changes will stay in sync. In Angular, a scope prototypically inherits properties from a parent scope. Objects are therefore references and kept in sync. Whereas primitive types are only in sync as long they are not changed in the child scope. 64 | 65 | Generally I tend to not use `$parent.name` and instead always use objects to share model properties. If you use `$parent.name` the `MyNestedCtrl` not only requires certain model attributes but also a correct scope hierarchy to work with. 66 | 67 | Tip: The Chrome plugin [Batarang](https://github.com/angular/angularjs-batarang) simplifies debugging the scope hierarchy by showing you a tree of the nested scopes. It is awesome! 68 | 69 | -------------------------------------------------------------------------------- /controllers/source/recipe1/app.js: -------------------------------------------------------------------------------- 1 | function MyCtrl($scope) { 2 | $scope.value = "some value"; 3 | } -------------------------------------------------------------------------------- /controllers/source/recipe1/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

{{value}}

4 |
5 | 6 | -------------------------------------------------------------------------------- /controllers/source/recipe2/app.js: -------------------------------------------------------------------------------- 1 | function MyCtrl($scope) { 2 | $scope.value = 1; 3 | 4 | $scope.incrementValue = function() { 5 | $scope.value += 1; 6 | }; 7 | } -------------------------------------------------------------------------------- /controllers/source/recipe2/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

{{value}}

5 |
6 |
7 | -------------------------------------------------------------------------------- /controllers/source/recipe3/app.js: -------------------------------------------------------------------------------- 1 | function MyCtrl($scope) { 2 | $scope.value = 1; 3 | 4 | $scope.getIncrementedValue = function() { 5 | return $scope.value + 1; 6 | }; 7 | } -------------------------------------------------------------------------------- /controllers/source/recipe3/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

{{getIncrementedValue()}}

4 |
5 | -------------------------------------------------------------------------------- /controllers/source/recipe4/app.js: -------------------------------------------------------------------------------- 1 | function MyCtrl($scope) { 2 | $scope.name = ""; 3 | 4 | $scope.$watch("name", function(newValue, oldValue) { 5 | if (newValue.length > 0) { 6 | $scope.greeting = "Greetings " + newValue; 7 | } 8 | }); 9 | } -------------------------------------------------------------------------------- /controllers/source/recipe4/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |

{{greeting}}

5 |
6 | -------------------------------------------------------------------------------- /controllers/source/recipe5/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module("MyApp", []); 2 | 3 | app.controller("MyCtrl", function($scope) { 4 | $scope.name = "Peter"; 5 | $scope.user = { 6 | name: "Parker" 7 | }; 8 | }); 9 | 10 | app.controller("MyNestedCtrl", function($scope) { 11 | }); -------------------------------------------------------------------------------- /controllers/source/recipe5/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | -------------------------------------------------------------------------------- /controllers/source/recipe5/style.css: -------------------------------------------------------------------------------- 1 | .nested { 2 | border: 1px solid red; 3 | margin-left: 2em; 4 | padding: 1em; 5 | } -------------------------------------------------------------------------------- /controllers/source/recipe6/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module("MyApp", []); 2 | 3 | app.factory("UserService", function() { 4 | var users = ["Peter", "Daniel", "Nina"]; 5 | 6 | return { 7 | all: function() { 8 | return users; 9 | }, 10 | first: function() { 11 | return users[0]; 12 | } 13 | }; 14 | }); 15 | 16 | app.controller("MyCtrl", function($scope, UserService) { 17 | $scope.users = UserService.all(); 18 | }); 19 | 20 | app.controller("AnotherCtrl", function($scope, UserService) { 21 | $scope.firstUser = UserService.first(); 22 | }); -------------------------------------------------------------------------------- /controllers/source/recipe6/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
    4 |
  • {{user}}
  • 5 |
6 |
7 | First user: {{firstUser}} 8 |
9 |
10 | -------------------------------------------------------------------------------- /controllers/source/recipe6/style.css: -------------------------------------------------------------------------------- 1 | .nested { 2 | border: 1px solid red; 3 | margin-left: 2em; 4 | padding: 1em; 5 | } -------------------------------------------------------------------------------- /controllers/testing-controllers.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Testing Controllers 4 | chapter: controllers 5 | order: 7 6 | --- 7 | 8 | ### Problem 9 | You wish to unit test your business logic. 10 | 11 | ### Solution 12 | Implement a unit test using [Jasmine](http://pivotal.github.com/jasmine/) and the [angular-seed](https://github.com/angular/angular-seed) project. Following our previous `$watch` recipe, this is how our spec would look. 13 | 14 | {% prism javascript %} 15 | describe('MyCtrl', function(){ 16 | var scope, ctrl; 17 | 18 | beforeEach(inject(function($controller, $rootScope) { 19 | scope = $rootScope.$new(); 20 | ctrl = $controller(MyCtrl, { $scope: scope }); 21 | })); 22 | 23 | it('should change greeting value if name value is changed', function() { 24 | scope.name = "Frederik"; 25 | scope.$digest(); 26 | expect(scope.greeting).toBe("Greetings Frederik"); 27 | }); 28 | }); 29 | {% endprism %} 30 | 31 | ### Discussion 32 | Jasmine specs use `describe` and `it` functions to group specs and `beforeEach` and `afterEach` to setup and teardown code. The actual expectation compares the greeting from the scope with our expectation `Greetings Frederik`. 33 | 34 | The scope and controller initialization is a bit more involved. We use `inject` to initialize the scope and controller as closely as possible to how our code would behave at runtime too. We can't just initialize the scope as a Javascript object `{}` since we would then not be able to call `$watch` on it. Instead `$rootScope.$new()` will do the trick. Note that the `$controller` service requires `MyCtrl` to be available and uses an object notation to pass in dependencies. 35 | 36 | The `$digest` call is required in order to trigger a watch execution after we have changed the scope. We need to call `$digest` manually in our spec whereas at runtime Angular will do this for us automatically. 37 | 38 | -------------------------------------------------------------------------------- /directives/changing-the-dom-in-response-to-user-actions.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Changing the DOM in response to user actions 4 | chapter: directives 5 | order: 2 6 | source_path: directives/source/recipe2 7 | --- 8 | 9 | ### Problem 10 | You wish to change the CSS of an HTML element on a mouse click and encapsulate this behavior in a reusable component. 11 | 12 | ### Solution 13 | Implement a directive `my-widget` that contains an example paragraph of text we want to style. 14 | 15 | {% prism markup %} 16 | {% raw %} 17 | 18 | 19 |

Hello World

20 |
21 | 22 | {% endraw %} 23 | {% endprism %} 24 | 25 | We use a link function in our directive implementation to change the CSS of the paragraph. 26 | 27 | {% prism javascript %} 28 | var app = angular.module("MyApp", []); 29 | 30 | app.directive("myWidget", function() { 31 | var linkFunction = function(scope, element, attributes) { 32 | var paragraph = element.children()[0]; 33 | $(paragraph).on("click", function() { 34 | $(this).css({ "background-color": "red" }); 35 | }); 36 | }; 37 | 38 | return { 39 | restrict: "E", 40 | link: linkFunction 41 | }; 42 | }); 43 | {% endprism %} 44 | 45 | When clicking on the paragraph the background color changes to red. 46 | 47 | ### Discussion 48 | In the HTML document we use the new directive as an HTML element `my-widget`, which can be found in the Javascript code as `myWidget` again. The directive function returns a restriction and a link function. 49 | 50 | The restriction means that this directive can only be used as an HTML element and not for example an HTML attribute. If you want to use it as an HTML attribute, change the `restrict` to return `A` instead. The usage would then have to be adapted to: 51 | 52 | {% prism markup %} 53 |
54 |

Hello World

55 |
56 | {% endprism %} 57 | 58 | Whether you use the attribute or element mechanism will depend on your use case. Generally speaking one would use the element mechanism to define a custom reusable component. The attribute mechanism would be used whenever you want to "configure" some element or enhance it with more behavior. Other available options are using the directive as a class attribute or a comment. 59 | 60 | The `directive` method expects a function that can be used for initialization and injection of dependencies. 61 | 62 | {% prism javascript %} 63 | app.directive("myWidget", function factory(injectables) { 64 | // ... 65 | } 66 | {% endprism %} 67 | 68 | The link function is much more interesting since it defines the actual behavior. The scope, the actual HTML element `my-widget` and the HTML attributes are passed as params. Note that this has nothing to do with Angular's dependency injection mechanism. Ordering of the parameters is important! 69 | 70 | Firstly we select the paragraph element, which is a child of the `my-widget` element using Angular's `children()` function as defined by element. In the second step we use jQuery to bind to the click event and modify the css property on click. This is of particular interest since we have a mixture of Angular element functions and jQuery here. In fact under the hood Angular will use jQuery in the `children()` function if it is defined and will fall back to jqLite (shipped with Angular) otherwise. You can find all supported methods in the [API Reference of element](http://docs.angularjs.org/api/angular.element). 71 | 72 | Following a slightly altered version of the code using jQuery only: 73 | 74 | {% prism javascript %} 75 | element.on("click", function() { 76 | $(this).css({ "background-color": "red" }); 77 | }); 78 | {% endprism %} 79 | 80 | In this case `element` is alreay a jQuery element and we can directly use the `on` function. 81 | 82 | -------------------------------------------------------------------------------- /directives/directive-to-directive-communication.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Directive-to-Directive Communication 4 | chapter: directives 5 | order: 7 6 | source_path: directives/source/recipe7 7 | --- 8 | 9 | ### Problem 10 | You wish a directive to communicate with another directive and augment each other's behavior using a well-defined interface (API). 11 | 12 | ### Solution 13 | We implement a directive `basket` with a controller function and two other directive `orange` and `apple` which "require" this controller. Our example starts with an `apple` and `orange` directive used as attributes. 14 | 15 | {% prism markup %} 16 | 17 | Roll over me and check the console! 18 | 19 | {% endprism %} 20 | 21 | The `basket` directive manages an array to which one can add apples and oranges! 22 | 23 | {% prism javascript %} 24 | var app = angular.module("MyApp", []); 25 | 26 | app.directive("basket", function() { 27 | return { 28 | restrict: "E", 29 | controller: function($scope, $element, $attrs) { 30 | $scope.content = []; 31 | 32 | this.addApple = function() { 33 | $scope.content.push("apple"); 34 | }; 35 | 36 | this.addOrange = function() { 37 | $scope.content.push("orange"); 38 | }; 39 | }, 40 | link: function(scope, element) { 41 | element.bind("mouseenter", function() { 42 | console.log(scope.content); 43 | }); 44 | } 45 | }; 46 | }); 47 | {% endprism %} 48 | 49 | And finally the apple and orange directives, which add themselves to the basket using the basket's controller. 50 | 51 | {% prism javascript %} 52 | app.directive("apple", function() { 53 | return { 54 | require: "basket", 55 | link: function(scope, element, attrs, basketCtrl) { 56 | basketCtrl.addApple(); 57 | } 58 | }; 59 | }); 60 | 61 | app.directive("orange", function() { 62 | return { 63 | require: "basket", 64 | link: function(scope, element, attrs, basketCtrl) { 65 | basketCtrl.addOrange(); 66 | } 67 | }; 68 | }); 69 | {% endprism %} 70 | 71 | If you hover with the mouse over the rendered text the console should print and the basket's content. 72 | 73 | ### Discussion 74 | `Basket` is the example directive that demonstrates an API using the controller function, whereas the `apple` and `orange` directives augment the `basket` directive. They both define a dependency to the `basket` controller with the `require` attribute. The `link` function then gets `basketCtrl` injected. 75 | 76 | Note how the `basket` directive is defined as an HTML element and the `apple` and `orange` directives are defined as HTML attributes (the default for directives). This demonstrates the typical use case of a reusable component augmented by other directives. 77 | 78 | Now there might be other ways of passing data back and forth between directives - we have seen the different semantics of using the (isolated) context in directives in previous recipes - but what's especially great about the controller is the clear API contract it lets you define. -------------------------------------------------------------------------------- /directives/enabling-disabling-dom-elements-conditionally.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Enabling/Disabling DOM Elements Conditionally 4 | chapter: directives 5 | order: 1 6 | source_path: directives/source/recipe1 7 | --- 8 | 9 | ### Problem 10 | You wish to disable a button depending on a checkbox state. 11 | 12 | ### Solution 13 | Use the `ng-disabled` directive and bind its condition to the checkbox state. 14 | 15 | {% prism markup %} 16 | 17 | 18 | 19 | 20 | {% endprism %} 21 | 22 | ### Discussion 23 | The `ng-disabled` directive is a direct translation from the disabled HTML attribute, without you needing to worry about browser incompatibilities. It is bound to the `checked` model using an attribute value as is the checkbox using the `ng-model` directive. In fact the `checked` attribute value is again an Angular expression. You could for example invert the logic and use `!checked` instead. 24 | 25 | This is just one example of a directive shipped with Angular. There are many others like for example `ng-hide`, `ng-checked` or `ng-mouseenter` and I encourage you go through the [API Reference](http://docs.angularjs.org/api) and explore all the directives Angular has to offer. 26 | 27 | In the next recipes we will focus on implementing directives. 28 | -------------------------------------------------------------------------------- /directives/index.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: overview 3 | chapter: directives 4 | title: Directives 5 | intro: true 6 | --- 7 | Directives are one of the most powerful concepts in Angular since they let you invent new HTML elements specific to your application. This allows you to create reusable components which encapsulate complex DOM structures, stylesheets and even behavior. 8 | 9 |

Table of Contents

10 |
    11 | {% sorted_for page in site.pages | sort_by:order %} 12 | {% if page.chapter == "directives" %} 13 |
  1. 14 | {{page.title}} 15 |
  2. 16 | {% endif %} 17 | {% endsorted_for %} 18 |
-------------------------------------------------------------------------------- /directives/passing-configuration-params-using-html-attributes.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Passing Configuration Params Using HTML Attributes 4 | chapter: directives 5 | order: 5 6 | source_path: directives/source/recipe5 7 | --- 8 | 9 | ### Problem 10 | You wish to pass a configuration param to change the rendered output. 11 | 12 | ### Solution 13 | Use the attribute-based directive and pass an attribute value for the configuration. The attribute is passed as a parameter to the link function. 14 | 15 | {% prism markup %} 16 | 17 |
18 | 19 | {% endprism %} 20 | 21 | {% prism javascript %} 22 | var app = angular.module("MyApp", []); 23 | 24 | app.directive("myWidget", function() { 25 | var linkFunction = function(scope, element, attributes) { 26 | scope.text = attributes["myWidget"]; 27 | }; 28 | 29 | return { 30 | restrict: "A", 31 | template: "

{{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 | 59 | 60 | app.directive("myWidget2", function() { 61 | return { 62 | restrict: "E", 63 | template: "

{{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 | 87 | {% endprism %} 88 | 89 | {% prism javascript %} 90 | app.directive("myWidgetExpr", function() { 91 | var linkFunction = function(scope, element, attributes) { 92 | scope.text = scope.fn({ count: 5 }); 93 | }; 94 | 95 | return { 96 | restrict: "E", 97 | template: "

{{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 | 17 |

This is my paragraph text.

18 |
19 | {% endprism %} 20 | 21 | {% prism javascript %} 22 | var app = angular.module("MyApp", []); 23 | 24 | app.directive("myWidget", function() { 25 | return { 26 | restrict: "E", 27 | transclude: true, 28 | template: "

Heading

" 29 | }; 30 | }); 31 | {% endprism %} 32 | 33 | This will render a `div` element containing an `h3` element and append the directive's child node with the paragraph element below. 34 | 35 | ### Discussion 36 | In this context, transclusion refers to the inclusion of a part of a document into another document by reference. The `ng-transclude` attribute should be positioned depending on where you want your child nodes to be appended. 37 | -------------------------------------------------------------------------------- /directives/rendering-an-html-snippet-in-a-directive.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Rendering an HTML Snippet in a Directive 4 | chapter: directives 5 | order: 3 6 | source_path: directives/source/recipe3 7 | --- 8 | 9 | ### Problem 10 | You wish to render an HTML snippet as a reusable component. 11 | 12 | ### Solution 13 | Implement a directive and use the `template` attribute to define the HTML. 14 | 15 | {% prism markup %} 16 | 17 | 18 | 19 | {% endprism %} 20 | 21 | {% prism javascript %} 22 | var app = angular.module("MyApp", []); 23 | 24 | app.directive("myWidget", function() { 25 | return { 26 | restrict: "E", 27 | template: "

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 | 17 |

Header 1

18 |

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; i 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /directives/source/recipe2/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module("MyApp", []); 2 | 3 | app.directive("myWidget", function() { 4 | var linkFunction = function(scope, element, attributes) { 5 | var paragraph = element.children()[0]; 6 | $(paragraph).on("click", function() { 7 | $(this).css({ "background-color": "red" }); 8 | }); 9 | }; 10 | 11 | return { 12 | restrict: "E", 13 | link: linkFunction 14 | }; 15 | }); -------------------------------------------------------------------------------- /directives/source/recipe2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Hello World

4 |
5 | 6 | -------------------------------------------------------------------------------- /directives/source/recipe3/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module("MyApp", []); 2 | 3 | app.directive("myWidget", function() { 4 | return { 5 | restrict: "E", 6 | template: "

Hello World

" 7 | }; 8 | }); 9 | 10 | app.directive("myWidgetReplace", function() { 11 | return { 12 | restrict: "E", 13 | replace: true, 14 | template: "

Hello World

" 15 | }; 16 | }); 17 | 18 | app.directive("myWidgetTemplate", function() { 19 | return { 20 | restrict: "E", 21 | replace: true, 22 | templateUrl: "widget.html" 23 | }; 24 | }); -------------------------------------------------------------------------------- /directives/source/recipe3/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /directives/source/recipe4/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module("MyApp", []); 2 | 3 | app.directive("myWidget", function() { 4 | return { 5 | restrict: "E", 6 | transclude: true, 7 | template: "

Heading

" 8 | }; 9 | }); -------------------------------------------------------------------------------- /directives/source/recipe4/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

This is my paragraph text.

4 |
5 | 6 | -------------------------------------------------------------------------------- /directives/source/recipe5/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module("MyApp", []); 2 | 3 | app.directive("myWidget", function() { 4 | var linkFunction = function(scope, element, attributes) { 5 | scope.text = attributes["myWidget"]; 6 | }; 7 | 8 | return { 9 | restrict: "A", 10 | template: "

{{text}}

", 11 | link: linkFunction, 12 | scope: {} 13 | }; 14 | }); 15 | 16 | app.directive("myWidgetBi", function() { 17 | return { 18 | restrict: "E", 19 | template: "

{{text}}

", 20 | scope: { 21 | text: "@text" 22 | } 23 | }; 24 | }); 25 | 26 | app.directive("myWidgetExpr", function() { 27 | var linkFunction = function(scope, element, attributes) { 28 | scope.text = scope.fn({ count: 5 }); 29 | }; 30 | 31 | return { 32 | restrict: "E", 33 | template: "

{{text}}

", 34 | link: linkFunction, 35 | scope: { 36 | fn: "&fn" 37 | } 38 | }; 39 | }); -------------------------------------------------------------------------------- /directives/source/recipe5/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /directives/source/recipe6/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module("MyApp", []); 2 | 3 | app.directive("repeatNtimes", function() { 4 | return { 5 | restrict: "E", 6 | compile: function(tElement, attrs) { 7 | var content = tElement.children(); 8 | for (var i=1; i 2 | 3 |

Header 1

4 |

This is the paragraph.

5 | 6 | 7 | -------------------------------------------------------------------------------- /directives/source/recipe7/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module("MyApp", []); 2 | 3 | app.directive("basket", function() { 4 | return { 5 | restrict: "E", 6 | controller: function($scope, $element, $attrs) { 7 | $scope.content = []; 8 | 9 | this.addApple = function() { 10 | $scope.content.push("apple"); 11 | }; 12 | 13 | this.addOrange = function() { 14 | $scope.content.push("orange"); 15 | }; 16 | }, 17 | link: function(scope, element) { 18 | element.bind("mouseenter", function() { 19 | console.log(scope.content); 20 | }); 21 | } 22 | }; 23 | }); 24 | 25 | app.directive("apple", function() { 26 | return { 27 | require: "basket", 28 | link: function(scope, element, attrs, basketCtrl) { 29 | basketCtrl.addApple(); 30 | } 31 | }; 32 | }); 33 | 34 | app.directive("orange", function() { 35 | return { 36 | require: "basket", 37 | link: function(scope, element, attrs, basketCtrl) { 38 | basketCtrl.addOrange(); 39 | } 40 | }; 41 | }); -------------------------------------------------------------------------------- /directives/source/recipe7/index.html: -------------------------------------------------------------------------------- 1 | 2 | Roll over me and check the console! 3 | 4 | -------------------------------------------------------------------------------- /filters/chaining-filters-together.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Chaining Filters together 4 | chapter: filters 5 | order: 5 6 | source_path: filters/source/recipe5 7 | --- 8 | 9 | ### Problem 10 | You wish to combine several filters to form a single result. 11 | 12 | ### Solution 13 | Filters can be chained using the UNIX-like pipe syntax. 14 | 15 | {% prism markup %} 16 | {% raw %} 17 | 18 |
    19 |
  • 20 | {{name}} 21 |
  • 22 |
23 | 24 | {% endraw %} 25 | {% endprism %} 26 | 27 | ### Discussion 28 | The pipe symbol (`|`) is used to chain multiple filters together. First we will start with the initial Array of names. After applying the `exclude` filter the Array contains only `['Anton', 'John']` and afterwards we will sort the names in ascending order. 29 | 30 | I leave the implementation of the `sortAscending` filter as an exercise to the reader ;-) 31 | -------------------------------------------------------------------------------- /filters/filtering-a-list-of-dom-nodes.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Filtering a List of DOM Nodes 4 | chapter: filters 5 | order: 4 6 | source_path: filters/source/recipe4 7 | --- 8 | 9 | ### Problem 10 | You wish to filter a `ul` list of names. 11 | 12 | ### Solution 13 | As well as with strings as input, Angular's filters also work with arrays. 14 | 15 | {% prism markup %} 16 | {% raw %} 17 | 18 |
    19 |
  • 20 | {{name}} 21 |
  • 22 |
23 | 24 | {% endraw %} 25 | {% endprism %} 26 | 27 | {% prism javascript %} 28 | var app = angular.module("MyApp", []); 29 | 30 | app.filter("exclude", function() { 31 | return function(input, exclude) { 32 | var result = []; 33 | for (var i=0; i 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |

Default Currency: {{ amount | currency }}

25 |

Custom Currency: {{ amount | currency: "Euro" }}

26 | 27 | 28 | {% endraw %} 29 | {% endprism %} 30 | 31 | Enter an amount and it will be displayed using Angular's default locale. 32 | 33 | You can find the complete example on [github](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter4/recipe1). 34 | 35 | ### Discussion 36 | In our example we explicitly load the German locale settings and therefore the default formatting will be in German. The English locale is shipped by default, so there's no need to include the angular-locale_en.js file. If you remove the script tag, you will see the formatting change to English instead. This means in order for a localized application to work correctly you need to load the corresponding locale file. All available locale files can be seen on [github](https://github.com/angular/angular.js/tree/master/src/ngLocale). 37 | -------------------------------------------------------------------------------- /filters/implementing-custom-filter-to-reverse-an-input-string.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Implementing a Custom Filter to Reverse an Input String 4 | chapter: filters 5 | order: 2 6 | source_path: filters/source/recipe2 7 | --- 8 | 9 | ### Problem 10 | You wish to reverse user's text input. 11 | 12 | ### Solution 13 | Implement a custom filter, which reverses the input. 14 | 15 | {% prism markup %} 16 | {% raw %} 17 | 18 | 19 |

Input: {{ text }}

20 |

Filtered input: {{ text | reverse }}

21 | 22 | {% endraw %} 23 | {% endprism %} 24 | 25 | {% prism javascript %} 26 | var app = angular.module("MyApp", []); 27 | 28 | app.filter("reverse", function() { 29 | return function(input) { 30 | var result = ""; 31 | input = input || ""; 32 | for (var i=0; iTable of Contents 10 |
    11 | {% sorted_for page in site.pages | sort_by:order %} 12 | {% if page.chapter == "filters" %} 13 |
  1. 14 | {{page.title}} 15 |
  2. 16 | {% endif %} 17 | {% endsorted_for %} 18 |
-------------------------------------------------------------------------------- /filters/passing-configuration-params-to-filters.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Passing Configuration Params to Filters 4 | chapter: filters 5 | order: 3 6 | source_path: filters/source/recipe3 7 | --- 8 | 9 | ### Problem 10 | You wish to make your filter customizable by introducing config params. 11 | 12 | ### Solution 13 | Angular filters can be passed a hash of params which can be directly accessed in the filter function. 14 | 15 | {% prism markup %} 16 | {% raw %} 17 | 18 | 19 |

Input: {{ text }}

20 |

Filtered input: {{ text | reverse: { suffix: "!"} }}

21 | 22 | {% endraw %} 23 | {% endprism %} 24 | 25 | {% prism javascript %} 26 | var app = angular.module("MyApp", []); 27 | 28 | app.filter("reverse", function() { 29 | return function(input, options) { 30 | input = input || ""; 31 | var result = ""; 32 | var suffix = options["suffix"] || ""; 33 | 34 | for (var i=0; i 0) result += suffix; 39 | 40 | return result; 41 | }; 42 | }); 43 | {% endprism %} 44 | 45 | You can find the complete example on [github](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter4/recipe3). 46 | 47 | ### Discussion 48 | The suffix `!` is passed as an option to the filter function and is appended to the output. Note that we check if an actual input exists since we don't want to render the suffix without any input. 49 | -------------------------------------------------------------------------------- /filters/source/recipe2/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module("MyApp", []); 2 | 3 | app.filter("reverse", function() { 4 | return function(input) { 5 | var result = ""; 6 | input = input || ""; 7 | for (var i=0; i 2 | 3 |

Input: {{ text }}

4 |

Filtered input: {{ text | reverse }}

5 | -------------------------------------------------------------------------------- /filters/source/recipe3/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module("MyApp", []); 2 | 3 | app.filter("reverse", function() { 4 | return function(input, options) { 5 | input = input || ""; 6 | var result = ""; 7 | var suffix = options["suffix"] || ""; 8 | 9 | for (var i=0; i 0) result += suffix; 14 | 15 | return result; 16 | }; 17 | }); -------------------------------------------------------------------------------- /filters/source/recipe3/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Input: {{ text }}

4 |

Filtered input: {{ text | reverse: { suffix: "!"} }}

5 | -------------------------------------------------------------------------------- /filters/source/recipe4/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module("MyApp", []); 2 | 3 | app.filter("exclude", function() { 4 | return function(input, exclude) { 5 | var result = []; 6 | for (var i=0; i 2 |
    3 |
  • 4 | {{name}} 5 |
  • 6 |
7 | -------------------------------------------------------------------------------- /filters/source/recipe5/index.html: -------------------------------------------------------------------------------- 1 | 2 |
    3 |
  • 4 | {{name}} 5 |
  • 6 |
7 | -------------------------------------------------------------------------------- /filters/testing-filters.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Testing Filters 4 | chapter: filters 5 | order: 6 6 | source_path: filters/source/recipe6 7 | --- 8 | 9 | ### Problem 10 | You wish to unit test your new filter. Let us start with an easy filter, which renders a checkmark depending on a boolean value. 11 | 12 | {% prism markup %} 13 | {% raw %} 14 | 15 |

{{ data | checkmark}}

16 |

{{ !data | checkmark}}

17 | 18 | {% endraw %} 19 | {% endprism %} 20 | 21 | {% prism javascript %} 22 | var app = angular.module("MyApp", []); 23 | 24 | app.filter('checkmark', function() { 25 | return function(input) { 26 | return input ? '\u2713' : '\u2718'; 27 | }; 28 | }); 29 | {% endprism %} 30 | 31 | ### Solution 32 | Use the angular-seed project as a bootstrap again. 33 | 34 | {% prism javascript %} 35 | describe('MyApp Tabs', function() { 36 | beforeEach(module('MyApp')); 37 | 38 | describe('checkmark', function() { 39 | it('should convert boolean values to unicode checkmark or cross', 40 | inject(function(checkmarkFilter) { 41 | expect(checkmarkFilter(true)).toBe('\u2713'); 42 | expect(checkmarkFilter(false)).toBe('\u2718'); 43 | })); 44 | }); 45 | }); 46 | {% endprism %} 47 | 48 | ### Discussion 49 | The `beforeEach` loads the module and the `it` method injects the filter function for us. Note, that it has to be called `checkmarkFilter`, otherwise Angular can't inject our filter function correctly. -------------------------------------------------------------------------------- /images/AngularJS-Shield-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fdietz/recipes-with-angular-js/b013695058bb133c7f911b21f0ce199519f1fef0/images/AngularJS-Shield-small.png -------------------------------------------------------------------------------- /images/book-medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fdietz/recipes-with-angular-js/b013695058bb133c7f911b21f0ce199519f1fef0/images/book-medium.png -------------------------------------------------------------------------------- /images/chitchat_screen_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fdietz/recipes-with-angular-js/b013695058bb133c7f911b21f0ce199519f1fef0/images/chitchat_screen_1.png -------------------------------------------------------------------------------- /introduction/binding-text-input-to-an-expression.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Binding a Text Input to an Expression 4 | chapter: introduction 5 | order: 2 6 | source_path: introduction/source/recipe2 7 | --- 8 | ### Problem 9 | You want user input to be used in another part of your HTML page. 10 | 11 | ### Solution 12 | Use Angulars `ng-model` directive to bind the text input to the expression. 13 | 14 | {% prism markup %} 15 | {% raw %} 16 | Enter your name: 17 |

Hello {{name}}!

18 | {% endraw %} 19 | {% endprism %} 20 | 21 | ### Discussion 22 | Assigning "name" to the `ng-model` attribute and using the name variable in an expression will keep both in sync automatically. Typing in the text input will automatically reflect these changes in the paragraph element below. 23 | 24 | Consider how you would implement this traditionally using jQuery: 25 | 26 | {% prism markup %} 27 | 28 | 29 | 30 | 31 | 32 | Enter your name: 33 |

34 | 35 | 42 | 43 | 44 | 45 | {% endprism %} 46 | 47 | On document ready we bind to the keypress event in the text input and replace the text in the paragraph in the callback function. Using jQuery you need to deal with document ready callbacks, element selection, event binding and the context of this. Quite a lot of concepts to swallow and lines of code to maintain! 48 | -------------------------------------------------------------------------------- /introduction/converting-expression-output-with-filters.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Converting Expression Output with Filters 4 | chapter: introduction 5 | order: 4 6 | source_path: introduction/source/recipe4 7 | --- 8 | 9 | ### Problem 10 | When presenting data to the user, you might need to convert the data to a more user-friendly format. In our case we want to uppercase the `name` value from the previous recipe in the expression. 11 | 12 | ### Solution 13 | Use the `uppercase` Angular filter. 14 | 15 | {% prism markup %} 16 | {% raw %} 17 | Enter your name: 18 |

Hello {{name | uppercase }}!

19 | {% endraw %} 20 | {% endprism %} 21 | 22 | ### Discussion 23 | Angular uses the `|` (pipe) character to combine filters with variables in expressions. When evaluating the expression, the name variable is passed to the uppercase filter. This is similar to working with the Unix bash pipe symbol where an input can be transformed by another program. Also try the `lowercase` filter! 24 | 25 | -------------------------------------------------------------------------------- /introduction/including-the-angular-library-code-in-an-html-page.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Including the Angular.js Library Code in an HTML Page 4 | chapter: introduction 5 | order: 1 6 | source_path: introduction/source/recipe1 7 | --- 8 | ### Problem 9 | You wish to use Angular.js on a web page. 10 | 11 | ### Solution 12 | In order to get your first Angular.js app up and running you need to include the Angular Javascript file via `script` tag and make use of the `ng-app` directive. 13 | 14 | {% prism markup %} 15 | {% raw %} 16 | 17 | 18 | 20 | 21 | 22 |

This is your first angular expression: {{ 1 + 2 }}

23 | 24 | 25 | {% endraw %} 26 | {% endprism %} 27 | 28 | ### Discussion 29 | Adding the `ng-app` directive tells Angular to kick in its magic. The expression `{{ 1 + 2 }}` is evaluated by Angular and the result `3` is rendered. Note that removing `ng-app` will result in the browser rendering the expression as is instead of evaluating it. Play around with the expression! You can, for instance, concatenate Strings and invert or combine Boolean values. 30 | -------------------------------------------------------------------------------- /introduction/index.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: overview 3 | chapter: introduction 4 | intro: true 5 | title: Introduction 6 | --- 7 |

Welcome to Recipes with Angular.js. Let's get started!

8 |

Table of Contents

9 |
    10 | {% sorted_for page in site.pages | sort_by:order %} 11 | {% if page.chapter == "introduction" %} 12 |
  1. 13 | {{page.title}} 14 |
  2. 15 | {% endif %} 16 | {% endsorted_for %} 17 |
-------------------------------------------------------------------------------- /introduction/responding-to-click-events-using-controllers.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Responding to Click Events using Controllers 4 | chapter: introduction 5 | order: 3 6 | source_path: introduction/source/recipe3 7 | --- 8 | ### Problem 9 | You wish to hide an HTML element on button click. 10 | 11 | ### Solution 12 | Use the `ng-hide` directive in conjunction with a controller to change the visibility status on button click. 13 | 14 | 15 | {% prism markup %} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 |

Hello World!

26 |
27 | 28 | 29 | {% endprism %} 30 | 31 | And the controller in `js/app.js`: 32 | 33 | {% prism javascript %} 34 | function MyCtrl($scope) { 35 | $scope.visible = true; 36 | 37 | $scope.toggle = function() { 38 | $scope.visible = !$scope.visible; 39 | }; 40 | } 41 | {% endprism %} 42 | 43 | When toggling the button the "Hello World" paragraph will change its visibility. 44 | 45 | ### Discussion 46 | Using the `ng-controller` directive, we bind the `div` element including its children to the context of the `MyCtrl` controller. The `ng-click` directive will call the `toggle()` function of our controller on button click. Note that the `ng-show` directive is bound to the `visible` scope variable and will toggle the paragraph's visibility accordingly. 47 | 48 | The controller implementation defaults the `visible` attribute to true and toggles its Boolean state in the `toggle` function. Both the `visible` variable and the `toggle` function are defined on the `$scope` service which is passed to all controller functions automatically via dependency injection. 49 | 50 | The next chapter will go into all the details of controllers in Angular. For now let us quickly discuss the MVVM (Model-View-ViewModel) pattern as used by Angular. In the MVVM pattern the model is plain Javascript, the view is the HTML template and the ViewModel is the glue between the template and the model. The ViewModel makes Angular's two-way binding possible where changes in the model or the template are in sync automatically. 51 | 52 | In our example, the `visible` attribute is the model, but it could of course be much more complex , when for example retrieving data from a backend service. The controller is used to define the scope which represents the ViewModel. It interacts with the HTML template by binding the scope variable `visible` and the function `toggle()` to it. 53 | -------------------------------------------------------------------------------- /introduction/source/recipe1/index.html: -------------------------------------------------------------------------------- 1 | 2 |

This is your first angular expression {{1 + 2}}

3 | 4 | -------------------------------------------------------------------------------- /introduction/source/recipe2/index.html: -------------------------------------------------------------------------------- 1 | 2 | Enter your name: 3 |

Hello {{name}}!

4 | 5 | -------------------------------------------------------------------------------- /introduction/source/recipe3/app.js: -------------------------------------------------------------------------------- 1 | function MyCtrl($scope) { 2 | $scope.visible = true; 3 | 4 | $scope.toggle = function() { 5 | $scope.visible = !$scope.visible; 6 | }; 7 | } -------------------------------------------------------------------------------- /introduction/source/recipe3/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |

Hello World!

5 |
6 | 7 | -------------------------------------------------------------------------------- /introduction/source/recipe4/index.html: -------------------------------------------------------------------------------- 1 | 2 | Enter your name: 3 |

Hello {{name | uppercase | lowercase }}!

4 | 5 | -------------------------------------------------------------------------------- /js/angular/app.js: -------------------------------------------------------------------------------- 1 | window.app = angular.module("RecipesApp", []); -------------------------------------------------------------------------------- /js/angular/controller.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fdietz/recipes-with-angular-js/b013695058bb133c7f911b21f0ce199519f1fef0/js/angular/controller.js -------------------------------------------------------------------------------- /js/angular/directive.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fdietz/recipes-with-angular-js/b013695058bb133c7f911b21f0ce199519f1fef0/js/angular/directive.js -------------------------------------------------------------------------------- /js/application.js: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | //= require jquery 5 | //= require foundation.min 6 | //= require prism 7 | //= require date.format 8 | //= require lunr 9 | //= require mustache 10 | //= require URI.min 11 | 12 | //= require jquery.lunr.search 13 | 14 | // require ./angular 15 | // require_directory ./angular 16 | -------------------------------------------------------------------------------- /urls-routing-and-partials/index.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: URLs, Routing and Partials 4 | chapter: urls-routing-and-partials 5 | intro: true 6 | --- 7 | The [$location service](http://docs.angularjs.org/guide/dev_guide.services.$location) in Angular.js parses the current browser URL and makes it available to your application. Changes in either the browser address bar or the `$location` service will be kept in sync. 8 | 9 | Depending on the configuration, the `$location` service behaves differently and has different requirements for your application. We will first look into client-side routing with hashbang URLs since it is the default mode, and then later, look at the new HTML5-based routing. 10 | 11 |

Table of Contents

12 |
    13 | {% sorted_for page in site.pages | sort_by:order %} 14 | {% if page.chapter == "urls-routing-and-partials" %} 15 |
  1. 16 | {{page.title}} 17 |
  2. 18 | {% endif %} 19 | {% endsorted_for %} 20 |
-------------------------------------------------------------------------------- /urls-routing-and-partials/listening-on-route-changes-to-implement-a-login-mechanism.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Listening on Route Changes to Implement a Login Mechanism 4 | chapter: urls-routing-and-partials 5 | order: 4 6 | --- 7 | 8 | ### Problem 9 | You wish to ensure that a user has to login before navigating to protected pages. 10 | 11 | ### Solution 12 | Implement a listener on the `$routeChangeStart` event to track the next route navigation. Redirect to a login page if the user is not yet logged in. 13 | 14 | The most interesting part is the implementation of the route change listener: 15 | 16 | {% prism javascript %} 17 | var app = angular.module("MyApp", []). 18 | config(function($routeProvider, $locationProvider) { 19 | $routeProvider. 20 | when("/persons", 21 | { templateUrl: "partials/index.html" }). 22 | when("/login", 23 | { templateUrl: "partials/login.html", controller: "LoginCtrl" }). 24 | // event more routes here ... 25 | otherwise( { redirectTo: "/persons" }); 26 | }). 27 | run(function($rootScope, $location) { 28 | $rootScope.$on( "$routeChangeStart", function(event, next, current) { 29 | if ($rootScope.loggedInUser == null) { 30 | // no logged user, redirect to /login 31 | if ( next.templateUrl === "partials/login.html") { 32 | } else { 33 | $location.path("/login"); 34 | } 35 | } 36 | }); 37 | }); 38 | {% endprism %} 39 | 40 | Next we will define a login form to enter the username, skipping the password for the sake of simplicity: 41 | 42 | {% prism markup %} 43 | {% raw %} 44 |
45 | 46 | 47 | 48 |
49 | {% endraw %} 50 | {% endprism %} 51 | 52 | and finally the login controller, which sets the logged in user and redirects to the persons URL: 53 | 54 | {% prism javascript %} 55 | app.controller("LoginCtrl", function($scope, $location, $rootScope) { 56 | $scope.login = function() { 57 | $rootScope.loggedInUser = $scope.username; 58 | $location.path("/persons"); 59 | }; 60 | }); 61 | {% endprism %} 62 | 63 | You can find the complete example on [github](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter6/recipe4). 64 | 65 | ### Discussion 66 | This is of course not a fully fledged login system so please don't use it in any production system. But, it exemplifies how to generally handle access to specific areas of your web app. When you open the app in your browser you will be redirected to the login app in all cases. Only after you have entered a username can you access the other areas. 67 | 68 | The `run` method is defined in [Module](http://docs.angularjs.org/api/angular.Module) and is a good place for such a route change listener since it runs only once on initialization after the injector is finished loading all the modules. We check the `loggedInUser` in the `$rootScope` and if it is not set we redirect the user to the login page. Note that in order to skip this behavior when already navigating to the login page, we have to explicitly check the next `templateUrl`. 69 | 70 | The login controller sets the `$rootScope` to the username and redirects to `/persons`. Generally, I try to avoid using the `$rootScope` since it basically is a kind of global state but in our case it fits nicely since there should be a current user globally available. -------------------------------------------------------------------------------- /urls-routing-and-partials/using-route-location-to-implement-a-navigation-menu.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Using Route Location to Implement a Navigation Menu 4 | chapter: urls-routing-and-partials 5 | order: 3 6 | --- 7 | 8 | ### Problem 9 | You wish to implement a navigation menu, which shows the selected menu item to the user. 10 | 11 | ### Solution 12 | Use the `$location` service in a controller to compare the address bar URL to the navigation menu item the user selected. 13 | 14 | The navigation menu is the classic ul/li menu using a class attribute to mark one of the `li` elements as `active`: 15 | 16 | {% prism markup %} 17 | {% raw %} 18 | 19 | 23 | ... 24 | 25 | {% endraw %} 26 | {% endprism %} 27 | 28 | The controller implements the `menuClass` function: 29 | 30 | {% prism javascript %} 31 | app.controller("MainCtrl", function($scope, $location) { 32 | $scope.menuClass = function(page) { 33 | var current = $location.path().substring(1); 34 | return page === current ? "active" : ""; 35 | }; 36 | }); 37 | {% endprism %} 38 | 39 | You can find the complete example on [github](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter6/recipe3). 40 | 41 | ### Discussion 42 | When the user selects a menu item the client-side navigation will kick in as expected. The `menuClass` function is bound using the `ngClass` directive and updates the CSS class automatically for us depending on the current route. 43 | 44 | Using `$location.path()` we get the current route. The `substring` operation removes the leading slash (`/`) and converts `/persons` to `persons`. 45 | -------------------------------------------------------------------------------- /using-forms/displaying-form-validation-errors-with-the-twitter-bootstrap-framework.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Displaying Form Validation Errors with the Twitter Bootstrap framework 4 | chapter: using-forms 5 | order: 4 6 | source_path: using-forms/source/recipe4 7 | --- 8 | 9 | ### Problem 10 | You wish to display form validation errors but the form is styled using [Twitter Bootstrap](http://twitter.github.com/bootstrap/index.html). 11 | 12 | ### Solution 13 | When using the `.horizontal-form` class Twitter Bootstrap uses `div` elements to structure label, input fields and help messages into groups. The group div has the class `control-group` and the actual controls are further nested in another `div` element with the CSS class `controls`. Twitter Bootstrap shows a nice validation status when adding the CSS class `error` on the div with the `control-group` class. 14 | 15 | Let us start with the form: 16 | 17 | {% prism markup %} 18 | {% raw %} 19 |
20 |
21 | 22 |
23 | 24 |
25 | 27 | 29 | Firstname is required 30 | 31 |
32 |
33 | 34 |
35 | 36 |
37 | 39 | 41 | Lastname is required 42 | 43 |
44 |
45 | 46 |
47 |
48 | 49 |
50 |
51 |
52 |
53 | {% endraw %} 54 | {% endprism %} 55 | 56 | Note that we use the `ng-class` directive on the `control-group` div. So let's look at the controller implementation of the `error` function: 57 | 58 | {% prism javascript %} 59 | app.controller("User", function($scope) { 60 | // ... 61 | $scope.error = function(name) { 62 | var s = $scope.form[name]; 63 | return s.$invalid && s.$dirty ? "error" : ""; 64 | }; 65 | }); 66 | {% endprism %} 67 | 68 | The error function gets the input name attribute passed as a string and checks for the `$invalid` and `$dirty` flags to return either the error class or a blank string. 69 | 70 | You can find the complete example on [github](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter7/recipe4). 71 | 72 | ### Discussion 73 | Again we check both the invalid and dirty flags because we only show the error message in case the user has actually changed the form. Note that this `ng-class` function usage is pretty typical in Angular since expressions do not support ternary checks. 74 | -------------------------------------------------------------------------------- /using-forms/displaying-form-validation-errors.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Displaying Form Validation Errors 4 | chapter: using-forms 5 | order: 3 6 | source_path: using-forms/source/recipe3 7 | --- 8 | 9 | ### Problem 10 | You wish to show validation errors to the user by marking the input field red and displaying an error message. 11 | 12 | ### Solution 13 | We can use the `ng-show` directive to show an error message if a form input is invalid and CSS classes to change the input element's background color depending on its state. 14 | 15 | Let us start with the styling changes: 16 | 17 | {% prism markup %} 18 | {% raw %} 19 | 27 | {% endraw %} 28 | {% endprism %} 29 | 30 | And here is a small part of the form with an error message for the input field: 31 | 32 | {% prism markup %} 33 | {% raw %} 34 | 35 | 36 |

37 | Firstname is required 38 |

39 | {% endraw %} 40 | {% endprism %} 41 | 42 | You can find the complete example on [github](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter7/recipe3). 43 | 44 | ### Discussion 45 | The CSS classes ensure that we initially show the fresh form without any classes. When the user starts typing in some input for the first time, we change it to either green or red. That is a good example of using the `ng-dirty` and `ng-invalid` CSS classes. 46 | 47 | We use the same logic in the `ng-show` directive to only show the error message when the user starts typing for the first time. 48 | -------------------------------------------------------------------------------- /using-forms/implement-a-basic-form.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Implementing a Basic Form 4 | chapter: using-forms 5 | order: 1 6 | source_path: using-forms/source/recipe1 7 | --- 8 | 9 | ### Problem 10 | You wish to create a form to enter user details and capture this information in an Angular.js scope. 11 | 12 | ### Solution 13 | Use the standard `form` tag and the `ng-model` directive to implement a basic form: 14 | 15 | {% prism markup %} 16 | {% raw %} 17 | 18 |
19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 | 30 | {% endraw %} 31 | {% endprism %} 32 | 33 | The `novalidate` attribute disables the HTML5 validations, which are client-side validations supports by modern browsers. In our example we only want the Angular.js validations running to have complete control over the look and feel. 34 | 35 | The controller binds the form data to your user model and implements the `submit()` function: 36 | 37 | {% prism javascript %} 38 | var app = angular.module("MyApp", []); 39 | 40 | app.controller("User", function($scope) { 41 | $scope.user = {}; 42 | $scope.wasSubmitted = false; 43 | 44 | $scope.submit = function() { 45 | $scope.wasSubmitted = true; 46 | }; 47 | }); 48 | {% endprism %} 49 | 50 | You can find the complete example on [github](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter7/recipe1). 51 | 52 | ### Discussion 53 | The initial idea when using forms would be to implement them in the traditional way by serialising the form data and submit it to the server. Instead we use `ng-model` to bind the form to our model, something we have been doing a lot already in previous recipes. 54 | 55 | The submit button state is reflected in our `wasSubmitted` scope variable, but no submit to the server was actually done. The default behavior in Angular.js forms is to prevent the default action since we do not want to reload the whole page. We want to handle the submission in an application-specific way. In fact there is even more going on in the background and we are going to look into the behavior of the `form` or `ng-form` directive in the next recipe. 56 | -------------------------------------------------------------------------------- /using-forms/implementing-custom-validations.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Implementing Custom Validations 4 | chapter: using-forms 5 | order: 6 6 | --- 7 | 8 | ### Problem 9 | You wish to validate user input by comparing it to a blacklist of words. 10 | 11 | ### Solution 12 | The [angular-ui](http://angular-ui.github.com/) project offers a nice custom validation directive which lets you pass in options via expression. 13 | 14 | Let us have a look at the template first with the usage of the `ui-validate` Directive: 15 | 16 | {% prism markup %} 17 | {% raw %} 18 | 22 | 23 |

24 | This firstname is blacklisted. 25 |

26 | {% endraw %} 27 | {% endprism %} 28 | 29 | And the controller with the `notBlackListed` implementation: 30 | 31 | {% prism javascript %} 32 | var app = angular.module("MyApp", ["ui", "ui.directives"]); 33 | 34 | app.controller("User", function($scope) { 35 | $scope.blacklist = ['idiot','loser']; 36 | 37 | $scope.notBlackListed = function(value) { 38 | return $scope.blacklist.indexOf(value) === -1; 39 | }; 40 | }); 41 | {% endprism %} 42 | 43 | You can find the complete example on [github](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter7/recipe6). 44 | 45 | ### Discussion 46 | First we need to explicitly list our module dependency to the Angular UI directives module. Make sure you actually download the javascript file and load it via script tag. 47 | 48 | Our blacklist contains the words we do not want to accept as user input and the `notBlackListed` function checks if the user input matches any of the words defined in the blacklist. 49 | 50 | The `ui-validate` directive is pretty powerful since it lets you define your custom validations easily by just implementing the business logic in a controller function. 51 | 52 | If you want to know even more, have a look at how to implement custom directives for yourself in Angular's excellent [guide](http://docs.angularjs.org/guide/forms). 53 | -------------------------------------------------------------------------------- /using-forms/index.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Using Forms 4 | chapter: using-forms 5 | intro: true 6 | --- 7 | Every website eventually uses some kind of form for users to enter data. Angular makes it particularly easy to implement client-side form validations to give immediate feedback for an improved user experience. 8 | 9 |

Table of Contents

10 |
    11 | {% sorted_for page in site.pages | sort_by:order %} 12 | {% if page.chapter == "using-forms" %} 13 |
  1. 14 | {{page.title}} 15 |
  2. 16 | {% endif %} 17 | {% endsorted_for %} 18 |
-------------------------------------------------------------------------------- /using-forms/only-enabling-submit-button-if-the-form-is-valid.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Only Enabling the Submit Button if the Form is Valid 4 | chapter: using-forms 5 | order: 5 6 | source_path: using-forms/source/recipe5 7 | --- 8 | 9 | ### Problem 10 | You wish to disable the Submit button as long as the form contains invalid data. 11 | 12 | ### Solution 13 | Use the `$form.invalid` state in combination with a `ng-disabled` directive. 14 | 15 | Here is the changed submit button: 16 | 17 | {% prism markup %} 18 | {% raw %} 19 | 20 | {% endraw %} 21 | {% endprism %} 22 | 23 | You can find the complete example on [github](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter7/recipe5). 24 | 25 | ### Discussion 26 | The Form Controller attributes `form.$invalid` and friends are very useful to cover all kinds of use cases which focus on the form as a whole instead of individual fields. 27 | 28 | Note that you have to assign a `name` attribute to the form element, otherwise `form.$invalid` won't be available. 29 | -------------------------------------------------------------------------------- /using-forms/source/recipe1/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module("MyApp", []); 2 | 3 | app.controller("User", function($scope) { 4 | $scope.user = {}; 5 | $scope.wasSubmitted = false; 6 | 7 | $scope.submit = function() { 8 | $scope.wasSubmitted = true; 9 | }; 10 | }); -------------------------------------------------------------------------------- /using-forms/source/recipe1/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 |
13 | 14 | User Model: {{user}} 15 |
16 | Form was submitted: {{wasSubmitted}} 17 |
18 | -------------------------------------------------------------------------------- /using-forms/source/recipe2/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module("MyApp", []); 2 | 3 | app.controller("User", function($scope) { 4 | $scope.user = {}; 5 | $scope.wasSubmitted = false; 6 | 7 | $scope.submit = function() { 8 | $scope.wasSubmitted = true; 9 | }; 10 | }); -------------------------------------------------------------------------------- /using-forms/source/recipe2/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 |
13 | 14 | User Model: {{user}} 15 |
16 | Form was submitted: {{wasSubmitted}} 17 |
18 | Firstname input valid: {{form.firstname.$valid}} 19 |
20 | Firstname input invalid: {{form.firstname.$invalid}} 21 |
22 | Firstname validation error: {{form.firstname.$error}} 23 |
24 | Form valid: {{form.$valid}} 25 |
26 | Form validation error: {{form.$error}} 27 |
28 | -------------------------------------------------------------------------------- /using-forms/source/recipe3/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module("MyApp", []); 2 | 3 | app.controller("User", function($scope) { 4 | $scope.user = {}; 5 | $scope.wasSubmitted = false; 6 | 7 | $scope.submit = function() { 8 | $scope.wasSubmitted = true; 9 | }; 10 | }); -------------------------------------------------------------------------------- /using-forms/source/recipe3/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 | 6 |

Firstname is required

7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 | User Model: {{user}} 16 |
17 | Form was submitted: {{wasSubmitted}} 18 |
19 | Firstname input valid: {{form.firstname.$valid}} 20 |
21 | Firstname validation error: {{form.firstname.$error}} 22 |
23 | Form valid: {{form.$valid}} 24 |
25 | Form validation error: {{form.$error}} 26 |
27 | -------------------------------------------------------------------------------- /using-forms/source/recipe4/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module("MyApp", []); 2 | 3 | app.controller("User", function($scope) { 4 | $scope.user = {}; 5 | $scope.wasSubmitted = false; 6 | 7 | $scope.submit = function() { 8 | $scope.wasSubmitted = true; 9 | }; 10 | 11 | $scope.error = function(name) { 12 | var s = $scope.form[name]; 13 | return s.$invalid && s.$dirty ? "error" : ""; 14 | }; 15 | }); -------------------------------------------------------------------------------- /using-forms/source/recipe4/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | 6 |
7 | 8 | Firstname is required 9 |
10 |
11 | 12 |
13 | 14 |
15 | 16 | Lastname is required 17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 |
25 |
26 | 27 | User Model: {{user}} 28 |
29 | Form was submitted: {{wasSubmitted}} 30 |
31 | Firstname input valid: {{form.firstname.$valid}} 32 |
33 | Firstname validation error: {{form.firstname.$error}} 34 |
35 | Form valid: {{form.$valid}} 36 |
37 | Form validation error: {{form.$error}} 38 |
39 | -------------------------------------------------------------------------------- /using-forms/source/recipe5/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module("MyApp", []); 2 | 3 | app.controller("User", function($scope) { 4 | $scope.user = {}; 5 | $scope.wasSubmitted = false; 6 | 7 | $scope.submit = function() { 8 | $scope.wasSubmitted = true; 9 | }; 10 | }); -------------------------------------------------------------------------------- /using-forms/source/recipe5/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 | 6 |

Firstname is required

7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 | User Model: {{user}} 16 |
17 | Form was submitted: {{wasSubmitted}} 18 |
19 | Firstname input valid: {{form.firstname.$valid}} 20 |
21 | Firstname validation error: {{form.firstname.$error}} 22 |
23 | Form valid: {{form.$valid}} 24 |
25 | Form validation error: {{form.$error}} 26 |
27 | -------------------------------------------------------------------------------- /using-forms/source/recipe5/style.css: -------------------------------------------------------------------------------- 1 | input.ng-invalid.ng-dirty { 2 | background-color: red; 3 | } 4 | input.ng-valid.ng-dirty { 5 | background-color: green; 6 | } -------------------------------------------------------------------------------- /using-forms/validating-a-form-model-client-side.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: recipe 3 | title: Validating a Form Model Client-Side 4 | chapter: using-forms 5 | order: 2 6 | source_path: using-forms/source/recipe2 7 | --- 8 | 9 | ### Problem 10 | You wish to validate the form client-side using HTML5 form attributes. 11 | 12 | ### Solution 13 | Angular.js works in tandem with HTML5 form attributes. Let us start with the same form but let us add some HTML5 attributes to make the input required: 14 | 15 | {% prism markup %} 16 | {% raw %} 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 |
27 | {% endraw %} 28 | {% endprism %} 29 | 30 | It is still the same form but this time we defined the `name` attribute on the form and made the input `required` for the firstname. 31 | 32 | Let us add some more debug output below the form: 33 | 34 | {% prism markup %} 35 | {% raw %} 36 | Firstname input valid: {{form.firstname.$valid}} 37 |
38 | Firstname validation error: {{form.firstname.$error}} 39 |
40 | Form valid: {{form.$valid}} 41 |
42 | Form validation error: {{form.$error}} 43 | {% endraw %} 44 | {% endprism %} 45 | 46 | You can find the complete example on [github](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter7/recipe2). 47 | 48 | ### Discussion 49 | When starting with a fresh empty form, you will notice that Angular adds the css class `ng-pristine` and `ng-valid` to the form tag and each input tag. When editing the form the `ng-pristine` class will be removed from the changed input field and also from the form tag. Instead it will be replaced by the `ng-dirty` class. Very useful because it allows you to easily add new features to your app depending on these states. 50 | 51 | In addition to these two css classes there are two more to look into. The `ng-valid` class will be added whenever an input is valid, otherwise the css class `ng-invalid` is added. Note that the form tag also gets either a valid or invalid class depending on the input fields. To demonstrate this I've added the `required` HTML5 attribute. Initially, the firstname and lastname input fields are empty and therefore have the `ng-invalid` css class, whereas the age input field has the `ng-valid` class. Additionally, there's `ng-invalid-required` class alongside the `ng-invalid` for even more specificity. 52 | 53 | Since we defined the `name` attribute on the form HTML element we can now access Angular's form controller via scope variables. In the debug output we can check the validity and specific error for each named form input and the form itself. Note that this only works on the level of the form's name attributes and not on the model scope. If you output the following expression `{% raw %}{{user.firstname.$error}}{% endraw %}` it will not work. 54 | 55 | Angular's form controller exposes `$valid`, `$invalid`, `$error`, `$pristine` and `$dirty` variables. 56 | 57 | For validation, Angular provides built-in directives including `required`, `pattern`, `minlength`, `maxlength`, `min` and `max`. 58 | 59 | Let us use Angular's form integration to actually show validation errors in the next recipe. 60 | --------------------------------------------------------------------------------