diff --git a/Chapter_03/lib/recipe_book.dart b/Chapter_03/lib/recipe_book.dart new file mode 100644 index 0000000..c673269 --- /dev/null +++ b/Chapter_03/lib/recipe_book.dart @@ -0,0 +1,58 @@ +library recipe_book_controller; + +import 'package:angular/angular.dart'; + +@NgController( + selector: '[recipe-book]', + publishAs: 'ctrl') +class RecipeBookController { + + List recipes; + + RecipeBookController() { + recipes = _loadData(); + } + + Recipe selectedRecipe; + + void selectRecipe(Recipe recipe) { + selectedRecipe = recipe; + } + + List _loadData() { + return [ + new Recipe('My Appetizer','Appetizers', + ["Ingredient 1", "Ingredient 2"], + "Some Directions", 1), + new Recipe('My Salad','Salads', + ["Ingredient 1", "Ingredient 2"], + "Some Directions", 3), + new Recipe('My Soup','Soups', + ["Ingredient 1", "Ingredient 2"], + "Some Directions", 4), + new Recipe('My Main Dish','Main Dishes', + ["Ingredient 1", "Ingredient 2"], + "Some Directions", 2), + new Recipe('My Side Dish','Side Dishes', + ["Ingredient 1", "Ingredient 2"], + "Some Directions", 3), + new Recipe('My Awesome Dessert','Desserts', + ["Ingredient 1", "Ingredient 2"], + "Some Directions", 5), + new Recipe('My So-So Dessert','Desserts', + ["Ingredient 1", "Ingredient 2"], + "Some Directions", 3), + ]; + } +} + +class Recipe { + String name; + String category; + List ingredients; + String directions; + int rating; + + Recipe(this.name, this.category, this.ingredients, this.directions, + this.rating); +} diff --git a/Chapter_03/web/main.dart b/Chapter_03/web/main.dart index 64f9333..42d6726 100644 --- a/Chapter_03/web/main.dart +++ b/Chapter_03/web/main.dart @@ -5,61 +5,7 @@ import 'package:di/di.dart'; import 'package:perf_api/perf_api.dart'; import 'package:angular_dart_demo/rating/rating_component.dart'; - -@NgController( - selector: '[recipe-book]', - publishAs: 'ctrl') -class RecipeBookController { - - List recipes; - - RecipeBookController() { - recipes = _loadData(); - } - - Recipe selectedRecipe; - - void selectRecipe(Recipe recipe) { - selectedRecipe = recipe; - } - - List _loadData() { - return [ - new Recipe('My Appetizer','Appetizers', - ["Ingredient 1", "Ingredient 2"], - "Some Directions", 1), - new Recipe('My Salad','Salads', - ["Ingredient 1", "Ingredient 2"], - "Some Directions", 3), - new Recipe('My Soup','Soups', - ["Ingredient 1", "Ingredient 2"], - "Some Directions", 4), - new Recipe('My Main Dish','Main Dishes', - ["Ingredient 1", "Ingredient 2"], - "Some Directions", 2), - new Recipe('My Side Dish','Side Dishes', - ["Ingredient 1", "Ingredient 2"], - "Some Directions", 3), - new Recipe('My Awesome Dessert','Desserts', - ["Ingredient 1", "Ingredient 2"], - "Some Directions", 5), - new Recipe('My So-So Dessert','Desserts', - ["Ingredient 1", "Ingredient 2"], - "Some Directions", 3), - ]; - } -} - -class Recipe { - String name; - String category; - List ingredients; - String directions; - int rating; - - Recipe(this.name, this.category, this.ingredients, this.directions, - this.rating); -} +import 'package:angular_dart_demo/recipe_book.dart'; class MyAppModule extends Module { MyAppModule() { diff --git a/Chapter_04/lib/recipe_book.dart b/Chapter_04/lib/recipe_book.dart new file mode 100644 index 0000000..08feaa9 --- /dev/null +++ b/Chapter_04/lib/recipe_book.dart @@ -0,0 +1,73 @@ +library recipe_book_controller; + +import 'package:angular/angular.dart'; + +import 'tooltip/tooltip_directive.dart'; + +@NgController( + selector: '[recipe-book]', + publishAs: 'ctrl') +class RecipeBookController { + + List recipes; + + RecipeBookController() { + recipes = _loadData(); + } + + Recipe selectedRecipe; + + void selectRecipe(Recipe recipe) { + selectedRecipe = recipe; + } + + // Tooltip + static final tooltip = new Expando(); + TooltipModel tooltipForRecipe(Recipe recipe) { + if (tooltip[recipe] == null) { + tooltip[recipe] = new TooltipModel(recipe.imgUrl, + "I don't have a picture of these recipes, " + "so here's one of my cat instead!", + 80); + } + return tooltip[recipe]; // recipe.tooltip + } + + List _loadData() { + return [ + new Recipe('My Appetizer','Appetizers', + ["Ingredient 1", "Ingredient 2"], + "Some Directions", 1, 'fonzie1.jpg'), + new Recipe('My Salad','Salads', + ["Ingredient 1", "Ingredient 2"], + "Some Directions", 3, 'fonzie2.jpg'), + new Recipe('My Soup','Soups', + ["Ingredient 1", "Ingredient 2"], + "Some Directions", 4, 'fonzie1.jpg'), + new Recipe('My Main Dish','Main Dishes', + ["Ingredient 1", "Ingredient 2"], + "Some Directions", 2, 'fonzie2.jpg'), + new Recipe('My Side Dish','Side Dishes', + ["Ingredient 1", "Ingredient 2"], + "Some Directions", 3, 'fonzie1.jpg'), + new Recipe('My Awesome Dessert','Desserts', + ["Ingredient 1", "Ingredient 2"], + "Some Directions", 5, 'fonzie2.jpg'), + new Recipe('My So-So Dessert','Desserts', + ["Ingredient 1", "Ingredient 2"], + "Some Directions", 3, 'fonzie1.jpg'), + ]; + } +} + +class Recipe { + String name; + String category; + List ingredients; + String directions; + int rating; + String imgUrl; + + Recipe(this.name, this.category, this.ingredients, this.directions, + this.rating, this.imgUrl); +} diff --git a/Chapter_04/web/main.dart b/Chapter_04/web/main.dart index 8a7d9ac..88dca98 100644 --- a/Chapter_04/web/main.dart +++ b/Chapter_04/web/main.dart @@ -4,77 +4,10 @@ import 'package:angular/angular.dart'; import 'package:di/di.dart'; import 'package:perf_api/perf_api.dart'; +import 'package:angular_dart_demo/recipe_book.dart'; import 'package:angular_dart_demo/rating/rating_component.dart'; import 'package:angular_dart_demo/tooltip/tooltip_directive.dart'; -@NgController( - selector: '[recipe-book]', - publishAs: 'ctrl') -class RecipeBookController { - - List recipes; - - RecipeBookController() { - recipes = _loadData(); - } - - Recipe selectedRecipe; - - void selectRecipe(Recipe recipe) { - selectedRecipe = recipe; - } - - // Tooltip - static final tooltip = new Expando(); - TooltipModel tooltipForRecipe(Recipe recipe) { - if (tooltip[recipe] == null) { - tooltip[recipe] = new TooltipModel(recipe.imgUrl, - "I don't have a picture of these recipes, " - "so here's one of my cat instead!", - 80); - } - return tooltip[recipe]; // recipe.tooltip - } - - List _loadData() { - return [ - new Recipe('My Appetizer','Appetizers', - ["Ingredient 1", "Ingredient 2"], - "Some Directions", 1, 'fonzie1.jpg'), - new Recipe('My Salad','Salads', - ["Ingredient 1", "Ingredient 2"], - "Some Directions", 3, 'fonzie2.jpg'), - new Recipe('My Soup','Soups', - ["Ingredient 1", "Ingredient 2"], - "Some Directions", 4, 'fonzie1.jpg'), - new Recipe('My Main Dish','Main Dishes', - ["Ingredient 1", "Ingredient 2"], - "Some Directions", 2, 'fonzie2.jpg'), - new Recipe('My Side Dish','Side Dishes', - ["Ingredient 1", "Ingredient 2"], - "Some Directions", 3, 'fonzie1.jpg'), - new Recipe('My Awesome Dessert','Desserts', - ["Ingredient 1", "Ingredient 2"], - "Some Directions", 5, 'fonzie2.jpg'), - new Recipe('My So-So Dessert','Desserts', - ["Ingredient 1", "Ingredient 2"], - "Some Directions", 3, 'fonzie1.jpg'), - ]; - } -} - -class Recipe { - String name; - String category; - List ingredients; - String directions; - int rating; - String imgUrl; - - Recipe(this.name, this.category, this.ingredients, this.directions, - this.rating, this.imgUrl); -} - class MyAppModule extends Module { MyAppModule() { type(RecipeBookController); diff --git a/Chapter_06/web/filter/category_filter.dart b/Chapter_05/lib/filter/category_filter.dart similarity index 88% rename from Chapter_06/web/filter/category_filter.dart rename to Chapter_05/lib/filter/category_filter.dart index d5377dd..6c94208 100644 --- a/Chapter_06/web/filter/category_filter.dart +++ b/Chapter_05/lib/filter/category_filter.dart @@ -1,4 +1,6 @@ -part of recipe_book; +library category_filter; + +import 'package:angular/angular.dart'; @NgFilter(name: 'categoryfilter') class CategoryFilter { @@ -13,4 +15,3 @@ class CategoryFilter { } } } - diff --git a/Chapter_05/web/recipe.dart b/Chapter_05/lib/recipe.dart similarity index 95% rename from Chapter_05/web/recipe.dart rename to Chapter_05/lib/recipe.dart index 04e0d91..21a3946 100644 --- a/Chapter_05/web/recipe.dart +++ b/Chapter_05/lib/recipe.dart @@ -1,4 +1,6 @@ -part of recipe_book; +library recipe; + +import 'dart:convert'; class Recipe { String id; diff --git a/Chapter_05/lib/recipe_book.dart b/Chapter_05/lib/recipe_book.dart new file mode 100644 index 0000000..5d3e697 --- /dev/null +++ b/Chapter_05/lib/recipe_book.dart @@ -0,0 +1,91 @@ +library recipe_book_controller; + +import 'package:angular/angular.dart'; + +import 'recipe.dart'; +import 'tooltip/tooltip_directive.dart'; + +@NgController( + selector: '[recipe-book]', + publishAs: 'ctrl') +class RecipeBookController { + + static const String LOADING_MESSAGE = "Loading recipe book..."; + static const String ERROR_MESSAGE = """Sorry! The cook stepped out of the +kitchen and took the recipe book with him!"""; + + Http _http; + + // Determine the initial load state of the app + String message = LOADING_MESSAGE; + bool recipesLoaded = false; + bool categoriesLoaded = false; + + // Data objects that are loaded from the server side via json + List categories = []; + List recipes = []; + + // Filter box + Map categoryFilterMap = {}; + String nameFilterString = ""; + + RecipeBookController(Http this._http) { + _loadData(); + } + + Recipe selectedRecipe; + + void selectRecipe(Recipe recipe) { + selectedRecipe = recipe; + } + + // Tooltip + static final tooltip = new Expando(); + TooltipModel tooltipForRecipe(Recipe recipe) { + if (tooltip[recipe] == null) { + tooltip[recipe] = new TooltipModel(recipe.imgUrl, + "I don't have a picture of these recipes, " + "so here's one of my cat instead!", + 80); + } + return tooltip[recipe]; // recipe.tooltip + } + + void clearFilters() { + categoryFilterMap.keys.forEach((f) => categoryFilterMap[f] = false); + nameFilterString = ""; + } + + void _loadData() { + recipesLoaded = false; + categoriesLoaded = false; + _http.get('recipes.json') + .then((HttpResponse response) { + print(response); + for (Map recipe in response.data) { + recipes.add(new Recipe.fromJsonMap(recipe)); + } + recipesLoaded = true; + }, + onError: (Object obj) { + print(obj); + recipesLoaded = false; + message = ERROR_MESSAGE; + }); + + _http.get('categories.json') + .then((HttpResponse response) { + print(response); + for (String category in response.data) { + categories.add(category); + categoryFilterMap[category] = false; + } + categoriesLoaded = true; + }, + onError: (Object obj) { + print(obj); + categoriesLoaded = false; + message = ERROR_MESSAGE; + }); + } +} diff --git a/Chapter_05/web/main.dart b/Chapter_05/web/main.dart index 840281e..fc592db 100644 --- a/Chapter_05/web/main.dart +++ b/Chapter_05/web/main.dart @@ -1,115 +1,14 @@ library recipe_book; -import 'dart:convert'; import 'package:angular/angular.dart'; import 'package:di/di.dart'; import 'package:perf_api/perf_api.dart'; +import 'package:angular_dart_demo/recipe_book.dart'; +import 'package:angular_dart_demo/filter/category_filter.dart'; import 'package:angular_dart_demo/rating/rating_component.dart'; import 'package:angular_dart_demo/tooltip/tooltip_directive.dart'; -part 'recipe.dart'; - - -@NgFilter(name: 'categoryfilter') -class CategoryFilter { - call(recipeList, filterMap) { - if (recipeList is List && filterMap != null && filterMap is Map) { - // If there is nothing checked, treat it as "everything is checked" - bool nothingChecked = filterMap.values.every((isChecked) => !isChecked); - if (nothingChecked) { - return recipeList.toList(); - } - return recipeList.where((i) => filterMap[i.category] == true).toList(); - } - } -} - -@NgController( - selector: '[recipe-book]', - publishAs: 'ctrl') -class RecipeBookController { - - static const String LOADING_MESSAGE = "Loading recipe book..."; - static const String ERROR_MESSAGE = """Sorry! The cook stepped out of the -kitchen and took the recipe book with him!"""; - - Http _http; - - // Determine the initial load state of the app - String message = LOADING_MESSAGE; - bool recipesLoaded = false; - bool categoriesLoaded = false; - - // Data objects that are loaded from the server side via json - List categories = []; - List recipes = []; - - // Filter box - Map categoryFilterMap = {}; - String nameFilterString = ""; - - RecipeBookController(Http this._http) { - _loadData(); - } - - Recipe selectedRecipe; - - void selectRecipe(Recipe recipe) { - selectedRecipe = recipe; - } - - // Tooltip - static final tooltip = new Expando(); - TooltipModel tooltipForRecipe(Recipe recipe) { - if (tooltip[recipe] == null) { - tooltip[recipe] = new TooltipModel(recipe.imgUrl, - "I don't have a picture of these recipes, " - "so here's one of my cat instead!", - 80); - } - return tooltip[recipe]; // recipe.tooltip - } - - void clearFilters() { - categoryFilterMap.keys.forEach((f) => categoryFilterMap[f] = false); - nameFilterString = ""; - } - - void _loadData() { - recipesLoaded = false; - categoriesLoaded = false; - _http.get('recipes.json') - .then((HttpResponse response) { - print(response); - for (Map recipe in response.data) { - recipes.add(new Recipe.fromJsonMap(recipe)); - } - recipesLoaded = true; - }, - onError: (Object obj) { - print(obj); - recipesLoaded = false; - message = ERROR_MESSAGE; - }); - - _http.get('categories.json') - .then((HttpResponse response) { - print(response); - for (String category in response.data) { - categories.add(category); - categoryFilterMap[category] = false; - } - categoriesLoaded = true; - }, - onError: (Object obj) { - print(obj); - categoriesLoaded = false; - message = ERROR_MESSAGE; - }); - } -} - class MyAppModule extends Module { MyAppModule() { type(RecipeBookController); diff --git a/Chapter_06/web/view/search_recipe_component.css b/Chapter_06/lib/component/search_recipe_component.css similarity index 100% rename from Chapter_06/web/view/search_recipe_component.css rename to Chapter_06/lib/component/search_recipe_component.css diff --git a/Chapter_06/web/view/search_recipe_component.dart b/Chapter_06/lib/component/search_recipe_component.dart similarity index 63% rename from Chapter_06/web/view/search_recipe_component.dart rename to Chapter_06/lib/component/search_recipe_component.dart index 74d1f29..6dfa5ca 100644 --- a/Chapter_06/web/view/search_recipe_component.dart +++ b/Chapter_06/lib/component/search_recipe_component.dart @@ -1,9 +1,12 @@ -part of recipe_book; +library search_recipe_component; + +import '../service/recipe.dart'; +import 'package:angular/angular.dart'; @NgComponent( selector: 'search-recipe', - templateUrl: 'view/search_recipe_component.html', - cssUrl: 'view/search_recipe_component.css', + templateUrl: 'packages/angular_dart_demo/component/search_recipe_component.html', + cssUrl: 'packages/angular_dart_demo/component/search_recipe_component.css', publishAs: 'ctrl', map: const { 'name-filter-string': '<=>nameFilterString', diff --git a/Chapter_06/web/view/search_recipe_component.html b/Chapter_06/lib/component/search_recipe_component.html similarity index 100% rename from Chapter_06/web/view/search_recipe_component.html rename to Chapter_06/lib/component/search_recipe_component.html diff --git a/Chapter_06/web/view/view_recipe_component.css b/Chapter_06/lib/component/view_recipe_component.css similarity index 100% rename from Chapter_06/web/view/view_recipe_component.css rename to Chapter_06/lib/component/view_recipe_component.css diff --git a/Chapter_06/web/view/view_recipe_component.dart b/Chapter_06/lib/component/view_recipe_component.dart similarity index 58% rename from Chapter_06/web/view/view_recipe_component.dart rename to Chapter_06/lib/component/view_recipe_component.dart index 73c4472..a2beb9e 100644 --- a/Chapter_06/web/view/view_recipe_component.dart +++ b/Chapter_06/lib/component/view_recipe_component.dart @@ -1,9 +1,12 @@ -part of recipe_book; +library view_recipe_component; + +import '../service/recipe.dart'; +import 'package:angular/angular.dart'; @NgComponent( selector: 'view-recipe', - templateUrl: 'view/view_recipe_component.html', - cssUrl: 'view/view_recipe_component.css', + templateUrl: 'packages/angular_dart_demo/component/view_recipe_component.html', + cssUrl: 'packages/angular_dart_demo/component/view_recipe_component.css', publishAs: 'ctrl', map: const { 'recipe-map':'<=>recipeMap' diff --git a/Chapter_06/web/view/view_recipe_component.html b/Chapter_06/lib/component/view_recipe_component.html similarity index 100% rename from Chapter_06/web/view/view_recipe_component.html rename to Chapter_06/lib/component/view_recipe_component.html diff --git a/Chapter_06/lib/filter/category_filter.dart b/Chapter_06/lib/filter/category_filter.dart new file mode 100644 index 0000000..e7d8347 --- /dev/null +++ b/Chapter_06/lib/filter/category_filter.dart @@ -0,0 +1,18 @@ +library category_filter; + +import 'package:angular/angular.dart'; + +@NgFilter(name: 'categoryfilter') +class CategoryFilter { + call(recipeList, filterMap) { + if (recipeList is List && filterMap != null && filterMap is Map) { + // If there is nothing checked, treat it as "everything is checked" + bool nothingChecked = filterMap.values.every((isChecked) => !isChecked); + if (nothingChecked) { + return recipeList.toList(); + } + return recipeList.where((i) => filterMap[i.category] == true).toList(); + } + } +} + diff --git a/Chapter_06/lib/recipe_book.dart b/Chapter_06/lib/recipe_book.dart new file mode 100644 index 0000000..9c6e84e --- /dev/null +++ b/Chapter_06/lib/recipe_book.dart @@ -0,0 +1,84 @@ +library recipe_book_controller; + +import 'package:angular/angular.dart'; + +import 'tooltip/tooltip_directive.dart'; +import 'service/recipe.dart'; +import 'service/query_service.dart'; + +@NgController( + selector: '[recipe-book]', + publishAs: 'ctrl') +class RecipeBookController { + + static const String LOADING_MESSAGE = "Loading recipe book..."; + static const String ERROR_MESSAGE = """Sorry! The cook stepped out of the +kitchen and took the recipe book with him!"""; + + Http _http; + QueryService _queryService; + QueryService get queryService => _queryService; + + // Determine the initial load state of the app + String message = LOADING_MESSAGE; + bool recipesLoaded = false; + bool categoriesLoaded = false; + + // Data objects that are loaded from the server side via json + List _categories = []; + get categories => _categories; + Map _recipeMap = {}; + get recipeMap => _recipeMap; + get allRecipes => _recipeMap.values.toList(); + + // Filter box + Map categoryFilterMap = {}; + String nameFilter = ""; + + RecipeBookController(Http this._http, QueryService this._queryService) { + _loadData(); + } + + Recipe selectedRecipe; + + void selectRecipe(Recipe recipe) { + selectedRecipe = recipe; + } + + // Tooltip + static final tooltip = new Expando(); + TooltipModel tooltipForRecipe(Recipe recipe) { + if (tooltip[recipe] == null) { + tooltip[recipe] = new TooltipModel(recipe.imgUrl, + "I don't have a picture of these recipes, " + "so here's one of my cat instead!", + 80); + } + return tooltip[recipe]; // recipe.tooltip + } + + void _loadData() { + _queryService.getAllRecipes() + .then((Map allRecipes) { + _recipeMap = allRecipes; + recipesLoaded = true; + }, + onError: (Object obj) { + recipesLoaded = false; + message = ERROR_MESSAGE; + }); + + _queryService.getAllCategories() + .then((List allCategories) { + _categories = allCategories; + for (String category in _categories) { + categoryFilterMap[category] = false; + } + categoriesLoaded = true; + }, + onError: (Object obj) { + categoriesLoaded = false; + message = ERROR_MESSAGE; + }); + } +} \ No newline at end of file diff --git a/Chapter_06/web/routing/recipe_book_router.dart b/Chapter_06/lib/routing/recipe_book_router.dart similarity index 93% rename from Chapter_06/web/routing/recipe_book_router.dart rename to Chapter_06/lib/routing/recipe_book_router.dart index cc55bfa..e5d1318 100644 --- a/Chapter_06/web/routing/recipe_book_router.dart +++ b/Chapter_06/lib/routing/recipe_book_router.dart @@ -1,4 +1,6 @@ -part of recipe_book; +library recipe_book_routing; + +import 'package:angular/angular.dart'; class RecipeBookRouteInitializer implements RouteInitializer { diff --git a/Chapter_06/web/service/query_service.dart b/Chapter_06/lib/service/query_service.dart similarity index 88% rename from Chapter_06/web/service/query_service.dart rename to Chapter_06/lib/service/query_service.dart index 2df7d8d..1202d3c 100644 --- a/Chapter_06/web/service/query_service.dart +++ b/Chapter_06/lib/service/query_service.dart @@ -1,8 +1,13 @@ -part of recipe_book; +library query_service; + +import 'dart:async'; + +import 'recipe.dart'; +import 'package:angular/angular.dart'; class QueryService { - String _recipesUrl = 'service/recipes.json'; - String _categoriesUrl = 'service/categories.json'; + String _recipesUrl = 'recipes.json'; + String _categoriesUrl = 'categories.json'; Future _loaded; diff --git a/Chapter_06/web/service/recipe.dart b/Chapter_06/lib/service/recipe.dart similarity index 95% rename from Chapter_06/web/service/recipe.dart rename to Chapter_06/lib/service/recipe.dart index 04e0d91..21a3946 100644 --- a/Chapter_06/web/service/recipe.dart +++ b/Chapter_06/lib/service/recipe.dart @@ -1,4 +1,6 @@ -part of recipe_book; +library recipe; + +import 'dart:convert'; class Recipe { String id; diff --git a/Chapter_06/web/service/categories.json b/Chapter_06/web/categories.json similarity index 100% rename from Chapter_06/web/service/categories.json rename to Chapter_06/web/categories.json diff --git a/Chapter_06/web/main.dart b/Chapter_06/web/main.dart index d0b828a..e4bf8bd 100644 --- a/Chapter_06/web/main.dart +++ b/Chapter_06/web/main.dart @@ -1,99 +1,21 @@ library recipe_book; import 'dart:async'; -import 'dart:convert'; import 'package:angular/angular.dart'; import 'package:angular/routing/module.dart'; import 'package:di/di.dart'; import 'package:logging/logging.dart'; import 'package:perf_api/perf_api.dart'; +import 'package:angular_dart_demo/recipe_book.dart'; +import 'package:angular_dart_demo/filter/category_filter.dart'; import 'package:angular_dart_demo/rating/rating_component.dart'; import 'package:angular_dart_demo/tooltip/tooltip_directive.dart'; - -part 'filter/category_filter.dart'; -part 'service/query_service.dart'; -part 'service/recipe.dart'; -part 'routing/recipe_book_router.dart'; -part 'view/view_recipe_component.dart'; -part 'view/search_recipe_component.dart'; - -@NgController( - selector: '[recipe-book]', - publishAs: 'ctrl') -class RecipeBookController { - - static const String LOADING_MESSAGE = "Loading recipe book..."; - static const String ERROR_MESSAGE = """Sorry! The cook stepped out of the -kitchen and took the recipe book with him!"""; - - Http _http; - QueryService _queryService; - QueryService get queryService => _queryService; - - // Determine the initial load state of the app - String message = LOADING_MESSAGE; - bool recipesLoaded = false; - bool categoriesLoaded = false; - - // Data objects that are loaded from the server side via json - List _categories = []; - get categories => _categories; - Map _recipeMap = {}; - get recipeMap => _recipeMap; - get allRecipes => _recipeMap.values.toList(); - - // Filter box - Map categoryFilterMap = {}; - String nameFilter = ""; - - RecipeBookController(Http this._http, QueryService this._queryService) { - _loadData(); - } - - Recipe selectedRecipe; - - void selectRecipe(Recipe recipe) { - selectedRecipe = recipe; - } - - // Tooltip - static final tooltip = new Expando(); - TooltipModel tooltipForRecipe(Recipe recipe) { - if (tooltip[recipe] == null) { - tooltip[recipe] = new TooltipModel(recipe.imgUrl, - "I don't have a picture of these recipes, " - "so here's one of my cat instead!", - 80); - } - return tooltip[recipe]; // recipe.tooltip - } - - void _loadData() { - _queryService.getAllRecipes() - .then((Map allRecipes) { - _recipeMap = allRecipes; - recipesLoaded = true; - }, - onError: (Object obj) { - recipesLoaded = false; - message = ERROR_MESSAGE; - }); - - _queryService.getAllCategories() - .then((List allCategories) { - _categories = allCategories; - for (String category in _categories) { - categoryFilterMap[category] = false; - } - categoriesLoaded = true; - }, - onError: (Object obj) { - categoriesLoaded = false; - message = ERROR_MESSAGE; - }); - } -} +import 'package:angular_dart_demo/service/query_service.dart'; +import 'package:angular_dart_demo/service/recipe.dart'; +import 'package:angular_dart_demo/routing/recipe_book_router.dart'; +import 'package:angular_dart_demo/component/view_recipe_component.dart'; +import 'package:angular_dart_demo/component/search_recipe_component.dart'; class MyAppModule extends Module { MyAppModule() { diff --git a/Chapter_06/web/service/recipes.json b/Chapter_06/web/recipes.json similarity index 100% rename from Chapter_06/web/service/recipes.json rename to Chapter_06/web/recipes.json diff --git a/Chapter_07/.gitignore b/Chapter_07/.gitignore new file mode 100644 index 0000000..de87f59 --- /dev/null +++ b/Chapter_07/.gitignore @@ -0,0 +1,2 @@ +web/di_factories_gen.dart +web/ng_parser_gen.dart diff --git a/Chapter_07/README.md b/Chapter_07/README.md new file mode 100644 index 0000000..fa0bb55 --- /dev/null +++ b/Chapter_07/README.md @@ -0,0 +1,241 @@ +Chapter 7: Production Deployment +========================== + +# Running the Sample App + +Before running the app, make sure you run the code generators (see below for +more info) like so: + +``` +dart -c bin/generator.dart +``` + +# Overview + +When deploying your app in production you need to make sure that: + +1. compiled ([dart2js][dart2js]) output is small +1. application performs as well in JavaScript as it does in Dart VM +1. application runs not only in Chrome, but other supported modern browsers + +AngularDart and di heavily rely on [dart:mirrors][dart-mirrors-api] APIs. +Mirrors allow AngularDart to provide super fast developer friendly edit-refresh +cycle, without needing to run slow compilers and code generators. +However, mirrors come at a cost: + +1. use of mirrors disables very important optimizations performed by + [dart2js][dart2js] compiler, such as tree-shaking, which allows removal + of unused code from the output, resulting in very large JavaScript file. +1. mirrors are much slower compared to static Dart code, which might not be + an issue for smaller/medium applications, but in larger apps might become + noticeable. Dart team is constantly working on improving performance of + mirrors, so long-term it's not a problem, but in short-term it's something + you might need to think about. + +Here we will provide some tip on these subjects. + +# Managing Compiled Code Size + +## Minification + +dart2js allows you to minify the resulting JavaScript, which: + +* removes unnecessary whitespace +* shortens the class and field names + +Minification can reduce your resulting JavaScript by 2-3x. + +All you need to do, is to include ```--minify``` flag in dart2js command line. + +``` +dart2js web/main.dart --minify -o web/main.dart.js +``` + +## ```@MirrorsUsed``` + +To help manage the code size of applications that use mirrors, Dart provides +[```@MirrorsUsed```][mirrors-used] annotation using which you can tell dart2js +compiler which targets (classes, libraries, annotations, etc.) are being +reflected on. This way dart2js can skip all the unused stuff thus radically +reducing the output size. + +```@MirrorsUsed``` is often hard to get right as it really depends on how/if +you use code generation (discussed later in "Optimizing Runtime Performance" +chapter). Assuming you do use code generation (as we do in this chapter) your +annotation could look something like this: + +``` +@MirrorsUsed( + targets: const [ + 'angular.core', + 'angular.core.dom', + 'angular.core.parser', + 'angular.routing', + NodeTreeSanitizer + ], + metaTargets: const [ + NgInjectableService, + NgComponent, + NgDirective, + NgController, + NgFilter, + NgAttr, + NgOneWay, + NgOneWayOneTime, + NgTwoWay, + NgCallback + ], + override: '*' +) +import 'dart:mirrors'; +``` + +Here you are essentually telling dart2js that your application reflects on +```angular.core```, ```angular.core.dom```, ```angular.core.parser```, etc. +libraries, as well as on ```NodeTreeSanitizer``` class, and annotations +(metaTargets) like ```NgInjectableService```, ```NgComponent```, +```NgDirective```, etc. + +### Debugging + +If it happens that you have misconfigured ```@MirrorsUsed```, you will likely +be seeing errors like "Cannot find class for: Foo" or your +directives/components/controllers will be ignored when running in JavaScript. +Usually, the easiest fix is to just add that class (or the whole library) +to ```@MirrorsUsed.targets```. + + +# Optimizing Runtime Performance + +Currently there are two code generators: di and AngularDart Parser generators. + +## di Code Generator + +di.dart Injector uses dart:mirrors APIs for retrieving types of constructor +parameters and invoking the constructor to create new instances. The generator +generates static code for creating new instances and resolving dependencies. + +You can find an example of how to use the di generator in +```bin/generator.dart``` file. + +### Discovering Instantiable Types + +Ideally, types that are instantiated by the injector should be extracted from +the module definitions, however currently di modules are dynamically defined +and are mutable, making them very hard (impossible in some cases) to analyze +statically. + +The generator has to rely on some guidance from the user to mark classes that +injector has to instantiate. There are two ways of doing this: @Injectables +or custom class annotation. + +`@Injectables` is an annotation provided by the di package which can be +applied on a library definition with a list of types that the generator +should process. + +``` +@Injectables(const [ + MyService +]) +library my_service_librarry; + +import 'package:di/annotations.dart'; + +class MyService { + // ... +} +``` + +@Injectables annotation should be mainly used with classes that are out of +your control (ex. you can't modify the source code -- third party library). +In all other cases it's preferable to use custom class annotation(s). + +You can also define your own custom class annotations and apply them on +classes that you need to be instantiated by the injector. + +``` +library injectable; + +/** + * An annotation specifying that the annotated class will be instantiated by + * di Injector and type factory code generator should include it in its output. + */ +class InjectableService { + const InjectableService(); +} +``` + +``` +@InjectableService() +class QueryService { + // ... +} +``` + +You can then then configure generator with those annotations. + +When configuring the generator with the custom annotation you need to pass +a fully qualified class name (including the library prefix). In case of the +above example the fully qualified name of Service annotation would be +`injectable.InjectableService`. + +## AngularDart Parser Generator + +AngularDart Parser Generator extracts all expressions from your application +and then compiles them into Dart, so at runtime it doesn't have to parse those +expressions and while invoking the expressions it uses pre-generated code to +access fields and methods, so it doesn't have to use mirrors. + +You can find an example of how to use the parser generator in +`bin/generator.dart` file. + +## Code Generators and Development Mode + +You should not be using code generators during development, as they are slow +and can significanly degrade productivity. Instead, during development it's +better to use dynamic versions of di Injector and Parser and use generators +only for testing and production. + +In ```lib/main.dart``` you can see ```initializer-prod.dart``` file being +imported, which has ```initializer-dev.dart``` counterpart. Switching between +those two file will allow you to switch between prod and dev modes. You will +need to run the generator script before using the prod mode. + +It is highly recommended that you automate (via a script or a flag on the +server) the prod/dev mode switching to minimize the chance of dev mode being +released into production. + +# Cross-browser Support + +Angular components use [Shadow DOM][shadowdom101], but unfortunately it's not +natively supported in all modern browsers, so you would need to use a +[polyfill][shadow-dom-polyfill]. + +Make sure you include ```shadow_dom``` package in dependencies in pubspec.yaml. + +``` +dependencies: + shadow_dom: any +``` + +And include the script tag: + +``` + +``` + +or the debug version: + +``` + +``` + +**NOTE:** using the polyfill has [some limitations][shadowdom-limitations], +so make sure you are aware of those limitations before you start using it. + +[dart-mirrors-api]: https://api.dartlang.org/docs/channels/stable/latest/dart_mirrors.html +[shadowdom101]: http://www.html5rocks.com/en/tutorials/webcomponents/shadowdom/ +[shadowdom-limitations]: https://github.com/polymer/ShadowDOM#known-limitations +[shadow-dom-polyfill]: http://pub.dartlang.org/packages/shadow_dom +[dart2js]: https://www.dartlang.org/docs/dart-up-and-running/contents/ch04-tools-dart2js.html +[mirrors-used]: https://api.dartlang.org/docs/channels/stable/latest/dart_mirrors/MirrorsUsed.html diff --git a/Chapter_07/bin/generator.dart b/Chapter_07/bin/generator.dart new file mode 100644 index 0000000..b9ab4da --- /dev/null +++ b/Chapter_07/bin/generator.dart @@ -0,0 +1,58 @@ +library chapter07.generator; + +import 'dart:io'; +import 'package:di/generator.dart' as di_generator; +import 'package:angular/tools/expression_extractor.dart' as ng_generator; + +main() { + var dartSdkPath = Platform.environment['DART_SDK']; + if (dartSdkPath == null || dartSdkPath.isEmpty) { + throw 'export DART_SDK=/path/to/dart/sdk'; + } + var entryPointDartFile = 'web/main.dart'; + var injectablesAnnotations = 'angular.core.NgComponent,' + 'angular.core.NgController,' + 'angular.core.NgDirective,' + 'angular.core.NgFilter,' + 'injectable.InjectableService,' + 'angular.core.service.NgInjectableService'; + var diOutputFile = 'web/di_factories_gen.dart'; + var packageRoots = 'packages'; + + _runDiGenerator(dartSdkPath, entryPointDartFile, injectablesAnnotations, + diOutputFile, packageRoots); + + var htmlRoot = '.'; + var parserOutputFile = 'web/ng_parser_gen.dart'; + var parserHeaderFile = 'lib/parser_gen_header.dart'; + var parserFooterFile = ''; // we don't need anything in the footer, for now. + + _runNgGenerator(entryPointDartFile, htmlRoot, parserHeaderFile, + parserFooterFile, parserOutputFile, packageRoots); +} + +_runDiGenerator(dartSdkPath, entryPointDartFile, injectablesAnnotations, + outputFile, packageRoots) { + di_generator.main([ + dartSdkPath, + entryPointDartFile, + injectablesAnnotations, + outputFile, + packageRoots + ]); +} + +_runNgGenerator(entryPointDartFile, htmlRoot, parserHeaderFile, + parserFooterFile, parserOutputFile, packageRoots) { + // create empty output file to start with + new File(parserOutputFile).createSync(); + + ng_generator.main([ + entryPointDartFile, + htmlRoot, + parserHeaderFile, + parserFooterFile, + parserOutputFile, + packageRoots + ]); +} \ No newline at end of file diff --git a/Chapter_07/lib/component/search_recipe_component.css b/Chapter_07/lib/component/search_recipe_component.css new file mode 100644 index 0000000..e69de29 diff --git a/Chapter_07/lib/component/search_recipe_component.dart b/Chapter_07/lib/component/search_recipe_component.dart new file mode 100644 index 0000000..6dfa5ca --- /dev/null +++ b/Chapter_07/lib/component/search_recipe_component.dart @@ -0,0 +1,28 @@ +library search_recipe_component; + +import '../service/recipe.dart'; +import 'package:angular/angular.dart'; + +@NgComponent( + selector: 'search-recipe', + templateUrl: 'packages/angular_dart_demo/component/search_recipe_component.html', + cssUrl: 'packages/angular_dart_demo/component/search_recipe_component.css', + publishAs: 'ctrl', + map: const { + 'name-filter-string': '<=>nameFilterString', + 'category-filter-map' : '<=>categoryFilterMap' + } +) +class SearchRecipeComponent { + String nameFilterString = ""; + Map categoryFilterMap; + + get categories { + return categoryFilterMap.keys.toList(); + } + + void clearFilters() { + categoryFilterMap.keys.forEach((f) => categoryFilterMap[f] = false); + nameFilterString = ""; + } +} \ No newline at end of file diff --git a/Chapter_07/lib/component/search_recipe_component.html b/Chapter_07/lib/component/search_recipe_component.html new file mode 100644 index 0000000..8885afd --- /dev/null +++ b/Chapter_07/lib/component/search_recipe_component.html @@ -0,0 +1,16 @@ +
+
+ + +
+
+ + + {{category}} + +
+ +
diff --git a/Chapter_07/lib/component/view_recipe_component.css b/Chapter_07/lib/component/view_recipe_component.css new file mode 100644 index 0000000..391b722 --- /dev/null +++ b/Chapter_07/lib/component/view_recipe_component.css @@ -0,0 +1,3 @@ +ul { + list-style-type: none; +} diff --git a/Chapter_07/lib/component/view_recipe_component.dart b/Chapter_07/lib/component/view_recipe_component.dart new file mode 100644 index 0000000..c5127ee --- /dev/null +++ b/Chapter_07/lib/component/view_recipe_component.dart @@ -0,0 +1,27 @@ +library view_recipe_component; + +import '../service/recipe.dart'; +import 'package:angular/angular.dart'; + +@NgComponent( + selector: 'view-recipe', + templateUrl: 'packages/angular_dart_demo/component/view_recipe_component.html', + cssUrl: 'packages/angular_dart_demo/component/view_recipe_component.css', + publishAs: 'ctrl', + map: const { + 'recipe-map':'<=>recipeMap' + }, + exportExpressions: const['name'] +) +class ViewRecipeComponent { + Map recipeMap; + String _recipeId; + + get recipe { + return recipeMap[_recipeId]; + } + + ViewRecipeComponent(RouteProvider routeProvider) { + _recipeId = routeProvider.parameters['recipeId']; + } +} diff --git a/Chapter_07/lib/component/view_recipe_component.html b/Chapter_07/lib/component/view_recipe_component.html new file mode 100644 index 0000000..f75f677 --- /dev/null +++ b/Chapter_07/lib/component/view_recipe_component.html @@ -0,0 +1,25 @@ +
+

Recipe Details

+ + + +
+
Name: {{ctrl.recipe.name}}
+
Category: {{ctrl.recipe.category}}
+
Rating: + +
+
+ Ingredients: +
    +
  • + {{ingredient}} +
  • +
+
+
+ Directions: + {{ctrl.recipe.directions}} +
+
+
diff --git a/Chapter_07/lib/filter/category_filter.dart b/Chapter_07/lib/filter/category_filter.dart new file mode 100644 index 0000000..e7d8347 --- /dev/null +++ b/Chapter_07/lib/filter/category_filter.dart @@ -0,0 +1,18 @@ +library category_filter; + +import 'package:angular/angular.dart'; + +@NgFilter(name: 'categoryfilter') +class CategoryFilter { + call(recipeList, filterMap) { + if (recipeList is List && filterMap != null && filterMap is Map) { + // If there is nothing checked, treat it as "everything is checked" + bool nothingChecked = filterMap.values.every((isChecked) => !isChecked); + if (nothingChecked) { + return recipeList.toList(); + } + return recipeList.where((i) => filterMap[i.category] == true).toList(); + } + } +} + diff --git a/Chapter_07/lib/injectable.dart b/Chapter_07/lib/injectable.dart new file mode 100644 index 0000000..800cf61 --- /dev/null +++ b/Chapter_07/lib/injectable.dart @@ -0,0 +1,9 @@ +library injectable; + +/** + * An annotation specifying that the annotated class will be instantiated by + * di Injector and type factory code generator should include it in its output. + */ +class InjectableService { + const InjectableService(); +} \ No newline at end of file diff --git a/Chapter_07/lib/parser_gen_header.dart b/Chapter_07/lib/parser_gen_header.dart new file mode 100644 index 0000000..b2322d1 --- /dev/null +++ b/Chapter_07/lib/parser_gen_header.dart @@ -0,0 +1,6 @@ +library parser_gen; + +import 'package:angular/angular.dart'; +import 'package:angular/utils.dart'; + +typedef Function FilterLookup(String filterName); \ No newline at end of file diff --git a/Chapter_07/lib/rating/rating_component.css b/Chapter_07/lib/rating/rating_component.css new file mode 100644 index 0000000..114c9ae --- /dev/null +++ b/Chapter_07/lib/rating/rating_component.css @@ -0,0 +1,10 @@ +.star-off { + color: #6E6E6E; +} +.star-on { + color: #FACC2E; +} +.stars { + letter-spacing: -2px; + cursor: pointer; +} diff --git a/Chapter_07/lib/rating/rating_component.dart b/Chapter_07/lib/rating/rating_component.dart new file mode 100644 index 0000000..588c79e --- /dev/null +++ b/Chapter_07/lib/rating/rating_component.dart @@ -0,0 +1,72 @@ +library rating; + +import 'package:angular/angular.dart'; + +/* Use the NgComponent annotation to indicate that this class is an + * Angular Component. + * + * The selector field defines the CSS selector that will trigger the + * component. Typically, the CSS selector is an element name. + * + * The templateUrl field tells the component which HTML template to use + * for its view. + * + * The cssUrl field tells the component which CSS file to use. + * + * The publishAs field specifies that the component instance should be + * assigned to the current scope under the name specified. + * + * The map field publishes the list of attributes that can be set on + * the component. Users of this component will specify these attributes + * in the html tag that is used to create the component. For example: + * + * + * + * The compnoent's public fields are available for data binding from the + * component's view. Similarly, the component's public methods can be + * invoked from the component's view. + */ +@NgComponent( + selector: 'rating', + templateUrl: 'packages/angular_dart_demo/rating/rating_component.html', + cssUrl: 'packages/angular_dart_demo/rating/rating_component.css', + publishAs: 'ctrl', + map: const { + 'max-rating' : '@maxRating', + 'rating' : '<=>rating' + } +) +class RatingComponent { + String _starOnChar = "\u2605"; + String _starOffChar = "\u2606"; + String _starOnClass = "star-on"; + String _starOffClass = "star-off"; + + List stars = []; + + int rating; + + set maxRating(String value) { + stars = []; + var count = value == null ? 5 : int.parse(value); + for(var i=1; i <= count; i++) { + stars.add(i); + } + } + + String starClass(int star) { + return star > rating ? _starOffClass : _starOnClass; + } + + String starChar(int star) { + return star > rating ? _starOffChar : _starOnChar; + } + + void handleClick(int star) { + if (star == 1 && rating == 1) { + rating = 0; + } else { + rating = star; + } + } +} diff --git a/Chapter_07/lib/rating/rating_component.html b/Chapter_07/lib/rating/rating_component.html new file mode 100644 index 0000000..4d4c94e --- /dev/null +++ b/Chapter_07/lib/rating/rating_component.html @@ -0,0 +1,7 @@ + + {{ctrl.starChar(star)}} + diff --git a/Chapter_07/lib/recipe_book.dart b/Chapter_07/lib/recipe_book.dart new file mode 100644 index 0000000..9c6e84e --- /dev/null +++ b/Chapter_07/lib/recipe_book.dart @@ -0,0 +1,84 @@ +library recipe_book_controller; + +import 'package:angular/angular.dart'; + +import 'tooltip/tooltip_directive.dart'; +import 'service/recipe.dart'; +import 'service/query_service.dart'; + +@NgController( + selector: '[recipe-book]', + publishAs: 'ctrl') +class RecipeBookController { + + static const String LOADING_MESSAGE = "Loading recipe book..."; + static const String ERROR_MESSAGE = """Sorry! The cook stepped out of the +kitchen and took the recipe book with him!"""; + + Http _http; + QueryService _queryService; + QueryService get queryService => _queryService; + + // Determine the initial load state of the app + String message = LOADING_MESSAGE; + bool recipesLoaded = false; + bool categoriesLoaded = false; + + // Data objects that are loaded from the server side via json + List _categories = []; + get categories => _categories; + Map _recipeMap = {}; + get recipeMap => _recipeMap; + get allRecipes => _recipeMap.values.toList(); + + // Filter box + Map categoryFilterMap = {}; + String nameFilter = ""; + + RecipeBookController(Http this._http, QueryService this._queryService) { + _loadData(); + } + + Recipe selectedRecipe; + + void selectRecipe(Recipe recipe) { + selectedRecipe = recipe; + } + + // Tooltip + static final tooltip = new Expando(); + TooltipModel tooltipForRecipe(Recipe recipe) { + if (tooltip[recipe] == null) { + tooltip[recipe] = new TooltipModel(recipe.imgUrl, + "I don't have a picture of these recipes, " + "so here's one of my cat instead!", + 80); + } + return tooltip[recipe]; // recipe.tooltip + } + + void _loadData() { + _queryService.getAllRecipes() + .then((Map allRecipes) { + _recipeMap = allRecipes; + recipesLoaded = true; + }, + onError: (Object obj) { + recipesLoaded = false; + message = ERROR_MESSAGE; + }); + + _queryService.getAllCategories() + .then((List allCategories) { + _categories = allCategories; + for (String category in _categories) { + categoryFilterMap[category] = false; + } + categoriesLoaded = true; + }, + onError: (Object obj) { + categoriesLoaded = false; + message = ERROR_MESSAGE; + }); + } +} \ No newline at end of file diff --git a/Chapter_07/lib/routing/recipe_book_router.dart b/Chapter_07/lib/routing/recipe_book_router.dart new file mode 100644 index 0000000..249b097 --- /dev/null +++ b/Chapter_07/lib/routing/recipe_book_router.dart @@ -0,0 +1,34 @@ +library recipe_book_routing; + +import 'package:angular/angular.dart'; +import '../injectable.dart'; + +@InjectableService() +class RecipeBookRouteInitializer implements RouteInitializer { + + init(Router router, ViewFactory view) { + router.root + ..addRoute( + name: 'add', + path: '/add', + enter: view('view/addRecipe.html')) + ..addRoute( + name: 'recipe', + path: '/recipe/:recipeId', + mount: (Route route) => route + ..addRoute( + name: 'view', + path: '/view', + enter: view('view/viewRecipe.html')) + ..addRoute( + name: 'edit', + path: '/edit', + enter: view('view/editRecipe.html')) + ..addRoute( + name: 'view_default', + defaultRoute: true, + enter: (_) => + router.go('view', {'recipeId': ':recipeId'}, + startingFrom: route, replace:true))); + } +} diff --git a/Chapter_07/lib/service/query_service.dart b/Chapter_07/lib/service/query_service.dart new file mode 100644 index 0000000..3d592a8 --- /dev/null +++ b/Chapter_07/lib/service/query_service.dart @@ -0,0 +1,72 @@ +library query_service; + +import 'dart:async'; + +import 'recipe.dart'; +import '../injectable.dart'; +import 'package:angular/angular.dart'; + +@InjectableService() +class QueryService { + String _recipesUrl = 'recipes.json'; + String _categoriesUrl = 'categories.json'; + + Future _loaded; + + Map _recipesCache; + List _categoriesCache; + + Http _http; + + QueryService(Http this._http) { + _loaded = Future.wait([_loadRecipes(), _loadCategories()]); + } + + Future _loadRecipes() { + return _http.get(_recipesUrl) + .then((HttpResponse response) { + _recipesCache = new Map(); + for (Map recipe in response.data) { + Recipe r = new Recipe.fromJsonMap(recipe); + _recipesCache[r.id] = r; + } + }); + } + + Future _loadCategories() { + return _http.get(_categoriesUrl) + .then((HttpResponse response) { + _categoriesCache = new List(); + for (String category in response.data) { + _categoriesCache.add(category); + } + }); + } + + Future getRecipeById(String id) { + if (_recipesCache == null) { + return _loaded.then((_) { + return _recipesCache[id]; + }); + } + return new Future.value(_recipesCache[id]); + } + + Future> getAllRecipes() { + if (_recipesCache == null) { + return _loaded.then((_) { + return _recipesCache; + }); + } + return new Future.value(_recipesCache); + } + + Future> getAllCategories() { + if (_categoriesCache == null) { + return _loaded.then((_) { + return _categoriesCache; + }); + } + return new Future.value(_categoriesCache); + } +} diff --git a/Chapter_07/lib/service/recipe.dart b/Chapter_07/lib/service/recipe.dart new file mode 100644 index 0000000..21a3946 --- /dev/null +++ b/Chapter_07/lib/service/recipe.dart @@ -0,0 +1,35 @@ +library recipe; + +import 'dart:convert'; + +class Recipe { + String id; + String name; + String category; + List ingredients; + String directions; + int rating; + String imgUrl; + + Recipe(this.id, this.name, this.category, this.ingredients, this.directions, + this.rating, this.imgUrl); + + String toJsonString() { + Map data = { + "id" : id, + "name" : name, + "category" : category, + "ingredients" : ingredients, + "directions" : directions, + "rating" : rating, + "imgUrl" : imgUrl + }; + return JSON.encode(data); + } + + factory Recipe.fromJsonMap(Map json) { + return new Recipe(json['id'], json['name'], json['category'], + json['ingredients'], json['directions'], json['rating'], + json['imgUrl']); + } +} diff --git a/Chapter_07/lib/tooltip/tooltip_directive.dart b/Chapter_07/lib/tooltip/tooltip_directive.dart new file mode 100644 index 0000000..f091685 --- /dev/null +++ b/Chapter_07/lib/tooltip/tooltip_directive.dart @@ -0,0 +1,95 @@ +library tooltip; + +import 'dart:html' as dom; +import 'dart:math'; +import 'package:angular/angular.dart'; + +@NgDirective( + selector: '[tooltip]', + map: const { + 'tooltip': '=>displayModel' + } +) +class Tooltip { + // not sure which one I will need. + // ng-click uses node. + // ng-show-hide uses element. + dom.Element element; + dom.Node node; + Scope scope; + TooltipModel displayModel; + + dom.Element tooltipElem; + + Tooltip(dom.Element this.element, dom.Node this.node, + Scope this.scope) { + + element.onMouseEnter.listen((event) { + _createTemplate(); + }); + + element.onMouseLeave.listen((event) { + _destroyTemplate(); + }); + } + + _createTemplate() { + assert(displayModel != null); + + tooltipElem = new dom.DivElement(); + + dom.ImageElement imgElem = new dom.ImageElement(); + imgElem.width = displayModel.imgWidth; + imgElem.src = displayModel.imgUrl; + tooltipElem.append(imgElem); + + if (displayModel.text != null) { + dom.DivElement textSpan = new dom.DivElement(); + textSpan.appendText(displayModel.text); + textSpan.style.color = "white"; + textSpan.style.fontSize = "smaller"; + textSpan.style.paddingBottom = "5px"; + + tooltipElem.append(textSpan); + } + + tooltipElem.style.padding = "5px"; + tooltipElem.style.paddingBottom = "0px"; + tooltipElem.style.backgroundColor = "black"; + tooltipElem.style.borderRadius = "5px"; + tooltipElem.style.width = "${displayModel.imgWidth.toString()}px"; + + // find the coordinates of the parent DOM element + Rectangle bounds = element.getBoundingClientRect(); + int left = (bounds.left + dom.window.pageXOffset).toInt(); + int top = (bounds.top + dom.window.pageYOffset).toInt(); + int width = (bounds.width).toInt(); + int height = (bounds.height).toInt(); + + // position the tooltip. + // Figure out where the containing element sits in the window. + int tooltipLeft = left + width + 10; + int tooltipTop = top - height; + tooltipElem.style.position = "absolute"; + tooltipElem.style.top = "${tooltipTop}px"; + tooltipElem.style.left = "${tooltipLeft}px"; + + // Add the tooltip to the document body. We add it here because + // we need to position it absolutely, without reference to its + // parent element. + dom.document.body.append(tooltipElem); + } + + _destroyTemplate() { + tooltipElem.remove(); + } +} + +class TooltipModel { + String imgUrl; + String text; + int imgWidth; + + TooltipModel(String this.imgUrl, String this.text, + int this.imgWidth); +} diff --git a/Chapter_07/pubspec.yaml b/Chapter_07/pubspec.yaml new file mode 100644 index 0000000..fe3381d --- /dev/null +++ b/Chapter_07/pubspec.yaml @@ -0,0 +1,9 @@ +name: angular_dart_demo +version: 0.0.1 +dependencies: + angular: any + browser: any + js: any + shadow_dom: any +dev_dependencies: + unittest: any diff --git a/Chapter_07/web/categories.json b/Chapter_07/web/categories.json new file mode 100644 index 0000000..9d84deb --- /dev/null +++ b/Chapter_07/web/categories.json @@ -0,0 +1,8 @@ +[ + "Appetizers", + "Salads", + "Soups", + "Main Dishes", + "Side Dishes", + "Desserts" +] diff --git a/Chapter_07/web/fonzie1.jpg b/Chapter_07/web/fonzie1.jpg new file mode 100644 index 0000000..69906e7 Binary files /dev/null and b/Chapter_07/web/fonzie1.jpg differ diff --git a/Chapter_07/web/fonzie2.jpg b/Chapter_07/web/fonzie2.jpg new file mode 100644 index 0000000..55999dc Binary files /dev/null and b/Chapter_07/web/fonzie2.jpg differ diff --git a/Chapter_07/web/index.html b/Chapter_07/web/index.html new file mode 100644 index 0000000..6e61d6a --- /dev/null +++ b/Chapter_07/web/index.html @@ -0,0 +1,54 @@ + + + + Chapter Six - A Simple Recipe Book + + + + +
+ + + +
+ {{ctrl.message}} +
+ +
+ + + + +
+ +
+ +
+ +
+ + + + + + diff --git a/Chapter_07/web/initializer-dev.dart b/Chapter_07/web/initializer-dev.dart new file mode 100644 index 0000000..9380d40 --- /dev/null +++ b/Chapter_07/web/initializer-dev.dart @@ -0,0 +1,13 @@ +library app_initializer_dev; + +import 'package:angular/angular.dart'; +import 'package:di/di.dart'; +import 'package:di/dynamic_injector.dart'; + +createInjector(List modules) { + return new DynamicInjector(modules: modules, allowImplicitInjection: false); +} + +createParser(Module module) { + // Do nothing, user default DynamicParser. +} \ No newline at end of file diff --git a/Chapter_07/web/initializer-prod.dart b/Chapter_07/web/initializer-prod.dart new file mode 100644 index 0000000..ca3341f --- /dev/null +++ b/Chapter_07/web/initializer-prod.dart @@ -0,0 +1,21 @@ +library app_initializer; + +import 'package:angular/angular.dart'; +import 'package:di/di.dart'; +import 'package:di/static_injector.dart'; + +// Generated Files! +import 'di_factories_gen.dart' as di_factories_gen; +import 'ng_parser_gen.dart' as ng_parser_gen; + +createInjector(List modules) { + di_factories_gen.main(); + return new StaticInjector(modules: modules, + typeFactories: di_factories_gen.typeFactories); +} + +createParser(Module module) { + module.type(Parser, implementedBy: StaticParser); + module.factory(StaticParserFunctions, + (i) => ng_parser_gen.functions(i.get(FilterMap))); +} \ No newline at end of file diff --git a/Chapter_07/web/main.dart b/Chapter_07/web/main.dart new file mode 100644 index 0000000..2a28dad --- /dev/null +++ b/Chapter_07/web/main.dart @@ -0,0 +1,82 @@ +// Used by di codegen to identify instantiable types. +@Injectables(const [ + Profiler +]) +library recipe_book; + +// Used by dart2js to indicate which targets are being reflected on, to allow +// tree-shaking. +@MirrorsUsed( + targets: const [ + 'angular.core', + 'angular.core.dom', + 'angular.core.parser', + 'angular.routing', + NodeTreeSanitizer + ], + metaTargets: const [ + NgInjectableService, + NgComponent, + NgDirective, + NgController, + NgFilter, + NgAttr, + NgOneWay, + NgOneWayOneTime, + NgTwoWay, + NgCallback + ], + override: '*' +) +import 'dart:mirrors'; + +import 'dart:async'; +import 'dart:html'; +import 'package:angular/angular.dart'; +import 'package:angular/routing/module.dart'; +import 'package:di/di.dart'; +import 'package:di/annotations.dart'; +import 'package:logging/logging.dart'; +import 'package:perf_api/perf_api.dart'; + +import 'package:angular_dart_demo/injectable.dart'; +import 'package:angular_dart_demo/recipe_book.dart'; +import 'package:angular_dart_demo/filter/category_filter.dart'; +import 'package:angular_dart_demo/rating/rating_component.dart'; +import 'package:angular_dart_demo/tooltip/tooltip_directive.dart'; +import 'package:angular_dart_demo/service/query_service.dart'; +import 'package:angular_dart_demo/service/recipe.dart'; +import 'package:angular_dart_demo/routing/recipe_book_router.dart'; +import 'package:angular_dart_demo/component/view_recipe_component.dart'; +import 'package:angular_dart_demo/component/search_recipe_component.dart'; + + +// During development it's easier to use dynamic parser and injector, so use +// initializer-dev.dart instead. Before using initializer-prod.dart make sure +// you run: dart -c bin/generator.dart +import 'initializer-prod.dart' as init; // Use in prod/test. +// import 'initializer-dev.dart' as init; // Use in dev. + +class MyAppModule extends Module { + MyAppModule() { + type(RecipeBookController); + type(RatingComponent); + type(Tooltip); + type(CategoryFilter); + type(Profiler, implementedBy: Profiler); // comment out to enable profiling + type(SearchRecipeComponent); + type(ViewRecipeComponent); + type(QueryService); + type(RouteInitializer, implementedBy: RecipeBookRouteInitializer); + factory(NgRoutingUsePushState, + (_) => new NgRoutingUsePushState.value(false)); + + init.createParser(this); + } +} + +main() { + Logger.root.level = Level.FINEST; + Logger.root.onRecord.listen((LogRecord r) { print(r.message); }); + ngBootstrap(module: new MyAppModule(), injectorFactory: init.createInjector); +} diff --git a/Chapter_07/web/recipes.json b/Chapter_07/web/recipes.json new file mode 100644 index 0000000..ea615fa --- /dev/null +++ b/Chapter_07/web/recipes.json @@ -0,0 +1,135 @@ +[ + { + "id": "1", + "name":"Bleu Cheese Stuffed Mushrooms", + "category":"Appetizers", + "ingredients":[ + "12 mushrooms", + "8 oz bleu cheese", + "1/2 red onion, diced", + "1/4 cup bread crumbs", + "1/4 cup parmesan cheese" + ], + "directions":"Preheat oven to 250 degrees. Clean the mushrooms. Break the stems off, chop and set aside. Combine bleu cheese, red onions, bread crumbs and chopped mushroom stems. Fill each mushroom with the bleu cheese mixture and place on a baking sheet. Bake for 20 minutes, or until the tops are golden. Sprinkle with parmesan cheese if desired.", + "rating": 1, + "imgUrl": "fonzie1.jpg" + }, + { + "id": "2", + "name":"Cannelini Bean and Mushroom Salad", + "category":"Salads", + "ingredients":[ + "2 cups cannelini", + "a large handful of mushrooms, sliced", + "3 tbsp italian parsley", + "2 tbsp fresh thyme", + "3 tbsp fresh chives", + "a handful of cherry tomatoes, halved", + "slices of parmesan cheese", + "lemon juice", + "olive oil", + "1 garlic clove", + "salt" + ], + "directions":"Cook and drain the beans. Coat mushrooms with olive oil and grill or pan fry them. Combine the beans, mushrooms, herbs and tomatoes. Combine lemon juice, olive oil, garlic and salt and make an emulsion. Pour the dressing over the bean mixture and stir to combine. Use a carrot peeler to peel some parmesan cheese over the top.", + "rating": 3, + "imgUrl": "fonzie2.jpg" + }, + { + "id": "3", + "name":"Pumpkin Black Bean Soup", + "category":"Soups", + "ingredients":[ + "2 tablespoon extra-virgin olive oil", + "1 medium onion, finely chopped", + "3 cups canned or packaged vegetable stock, found on soup aisle", + "1 can (14 1/2 ounces) diced tomatoes in juice", + "1 can (15 ounces) black beans, drained", + "2 cans (15 ounces) pumpkin puree", + "1 cup heavy cream", + "1 tablespoon curry powder", + "1 1/2 teaspoons ground cumin", + "1/2 teaspoon cayenne pepper", + "Coarse salt", + "20 blades fresh chives, chopped or snipped, for garnish" + ], + "directions":"Add oil to a soup pot on medium heat. When oil is hot, add onion. Saute onions 5 minutes. Add broth, tomatoes, black beans and pumpkin puree. Stir to combine ingredients and bring soup to a boil. Reduce heat to medium low and stir in cream, curry, cumin, cayenne and salt, to taste. Simmer 5 minutes, adjust seasonings and serve garnished with chopped chives.", + "rating": 3, + "imgUrl": "fonzie1.jpg" + }, + { + "id": "4", + "name":"Smoked Salmon Pasta", + "category":"Main Dishes", + "ingredients":[ + "8 ounces spaghetti (or other) pasta", + "1/4 cup pine nuts", + "2 Tbsp olive oil", + "1/3 cup chopped shallots (can substitute onions)", + "2 cloves garlic, minced", + "1/3 cup dry white wine", + "1/4 cup cream", + "2 Tbsp lemon zest", + "1 Tbsp lemon juice", + "2 Tbsp chopped fresh parsley or dill", + "4 ounces smoked salmon, cut into bite sized pieces", + "pinch salt", + "Fresh ground black pepper" + ], + "directions":"Bring a pot of water to a boil over high heat to cook the pasta. Meanwhile, toast the pine nuts in a dry pan or toaster oven and set aside. Add olive oil to a sauce pan on medium heat. Add shallots and cook for 5 minutes until soft and just beginning to caramelize. Add garlic and cook until garlic is soft, but not brown. Add wine and reduce. Turn heat to low and add cream (heat needs to be low, or the cream will curdle). When the pasta is finished cooking, drain it and add to the sauce pan. Turn the heat off and stir to combine. Add lemon juice, parsley or dill, pine nuts, and salmon. Add salt and pepper to taste.", + "rating": 2, + "imgUrl": "fonzie2.jpg" + }, + { + "id": "5", + "name":"Pancetta Brussels Sprouts", + "category":"Side Dishes", + "ingredients":[ + "1 lb brussels sprouts", + "1 tbsp olive oil", + "1 tbsp butter", + "1/4 cup pancetta, chopped", + "splash of balsamic vinegar" + ], + "directions":"Wash brussels sprouts, and chop in half. In a pan over medium heat, melt the oil and butter. Add sprouts and pancetta and turn heat to high. Cook until the sprouts are caramelized. Deglaze the pan with a splash of balsamic vinegar.", + "rating": 3, + "imgUrl": "fonzie1.jpg" + }, + { + "id": "6", + "name":"Almond Cookies With Chai Spices", + "category":"Desserts", + "ingredients":[ + "1/2 cup unsalted butter, room temperature", + "1 1/3 cups powdered sugar, divided", + "2 tsp vanilla extract", + "1 tsp almond extract", + "3/4 tsp ground allspice", + "3/4 tsp ground cardamom", + "1/2 tsp ground cinnamon", + "1/4 tsp salt", + "1 cup all purpose flour", + "3/4 cup finely chopped toasted almonds" + ], + "directions":"Preheat oven to 350 degrees. Using electric mixer, beat butter, 1/3 cup sugar, both extracts, spices, and salt in medium bowl. Beat in flour, then stir in almonds. Using hands, roll dough into tablespoon-size balls. Place on large baking sheet, spacing apart. Bake until pale golden, about 25 minutes. Cool on sheet 5 minutes. Place remaining sugar in large bowl. Working in batches, gently coat hot cookies in sugar. Cool cookies on rack. Roll again in sugar and serve. ", + "rating": 5, + "imgUrl": "fonzie2.jpg" + }, + { + "id": "7", + "name":"Cardamom Poached Pears", + "category":"Desserts", + "ingredients":[ + "2 pears, peeled", + "1 1/4 cups water", + "1/2 cup sugar", + "3 2 inch strips of lemon zest", + "1 cracked whole cardamom", + "A couple grinds of fresh shaved nutmeg", + "vanilla" + ], + "directions":"Combine all ingredients except the pears in a sauce pan and simmer for 10 minutes. Add the pears to the pan and cook, turning the pears occasionally so that all sides cook evenly. Cook for about 10 minutes, or until pears are soft enough to be poked with a fork.", + "rating": 3, + "imgUrl": "fonzie1.jpg" + } +] diff --git a/Chapter_07/web/style.css b/Chapter_07/web/style.css new file mode 100644 index 0000000..f3cf10a --- /dev/null +++ b/Chapter_07/web/style.css @@ -0,0 +1,15 @@ +[ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { + display: none !important; +} + +ul { + list-style-type: none; +} + +a { + text-decoration: none; +} + +.extra-space { + padding-left: 10px; +} diff --git a/Chapter_07/web/view/addRecipe.html b/Chapter_07/web/view/addRecipe.html new file mode 100644 index 0000000..ee10028 --- /dev/null +++ b/Chapter_07/web/view/addRecipe.html @@ -0,0 +1,4 @@ +
+

Add recipe

+ Now it's your turn. Write some code to add a new recipe +
diff --git a/Chapter_07/web/view/editRecipe.html b/Chapter_07/web/view/editRecipe.html new file mode 100644 index 0000000..ee96204 --- /dev/null +++ b/Chapter_07/web/view/editRecipe.html @@ -0,0 +1,4 @@ +
+

Edit recipe

+ Now it's your turn. Write some code to edit the recipe +
diff --git a/Chapter_07/web/view/viewRecipe.html b/Chapter_07/web/view/viewRecipe.html new file mode 100644 index 0000000..92d7ce5 --- /dev/null +++ b/Chapter_07/web/view/viewRecipe.html @@ -0,0 +1,4 @@ +
+ + +