├── .gitignore ├── AUTHORS.md ├── web ├── images │ └── cat.jpg ├── api │ └── users.json ├── talk_to_me_main.dart ├── app.html └── css │ └── talk-to-me.css ├── lib ├── components │ ├── show_call.html │ ├── global_alert.html │ ├── global_alert_component.dart │ ├── create_call_component.dart │ ├── list_calls_component.dart │ ├── call_component.dart │ ├── create_call.html │ ├── agenda.html │ ├── list_calls.html │ ├── agenda_component.dart │ ├── call.html │ ├── agenda_item_component.dart │ ├── show_call_component.dart │ └── agenda_item.html ├── models │ ├── call.dart │ ├── user.dart │ └── agenda_item.dart ├── services │ ├── messages.dart │ ├── users_repository.dart │ ├── call_storage.dart │ ├── call_serializer.dart │ └── parse_agenda_item.dart ├── talk_to_me_route_initializer.dart ├── decorators │ ├── toggle.dart │ └── agenda_item_text_input.dart ├── global_http_interceptors.dart └── talk_to_me.dart ├── package.json ├── pubspec.yaml ├── LICENSE ├── karma.conf.js ├── test ├── unit │ ├── agenda_item_test.dart │ ├── create_call_component_test.dart │ ├── parse_agenda_item_test.dart │ ├── users_repository_test.dart │ └── agenda_item_component_test.dart └── talk_to_me_test.dart ├── README.md └── pubspec.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | packages 3 | *.dart.* 4 | build 5 | /.project 6 | node_modules 7 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | * Victor Savkin 2 | * Seth Ladd 3 | * Pavel Jbanov 4 | * Patrice Chalin 5 | * Adam Singer -------------------------------------------------------------------------------- /web/images/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsavkin/angulardart-sample-app/HEAD/web/images/cat.jpg -------------------------------------------------------------------------------- /lib/components/show_call.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/models/call.dart: -------------------------------------------------------------------------------- 1 | part of talk_to_me; 2 | 3 | class Call { 4 | String id; 5 | String name = ""; 6 | List agenda = []; 7 | } 8 | -------------------------------------------------------------------------------- /web/api/users.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"name": "Jim", "isOnline" : false}, 3 | {"name": "Bob", "isOnline" : false}, 4 | {"name": "Ruby", "isOnline" : true} 5 | ] -------------------------------------------------------------------------------- /lib/components/global_alert.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |
-------------------------------------------------------------------------------- /lib/models/user.dart: -------------------------------------------------------------------------------- 1 | part of talk_to_me; 2 | 3 | class User { 4 | String name; 5 | bool isOnline; 6 | 7 | User(this.name, this.isOnline); 8 | 9 | String toString() => "${name} - ${isOnline}"; 10 | } 11 | -------------------------------------------------------------------------------- /lib/services/messages.dart: -------------------------------------------------------------------------------- 1 | part of talk_to_me; 2 | 3 | @Injectable() 4 | class Messages { 5 | RootScope rootScope; 6 | 7 | Messages(this.rootScope); 8 | 9 | void alert(String message){ 10 | rootScope.broadcast("globalAlert", message); 11 | } 12 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "talk_to_me", 3 | "repository": { 4 | "type": "git", 5 | "url": "https://github.com/vsavkin/angulardart-sample-app" 6 | }, 7 | "devDependencies": { 8 | "karma" : "~0.12.0", 9 | "karma-dart" : "~0.2.6", 10 | "karma-chrome-launcher": "~0.1.3", 11 | "karma-junit-reporter": "~0.2.1" 12 | } 13 | } -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: talk_to_me 2 | description: A sample web application built using AngularDart 3 | dependencies: 4 | browser: ">=0.10.0+2 <0.11.0" 5 | angular: ">=1.0.0 <2.0.0" 6 | uuid: ">=0.1.6 <=1.0.0" 7 | dev_dependencies: 8 | dartmocks: ">= 0.5.0" 9 | guinness: ">= 0.1.3" 10 | environment: 11 | sdk: ">=0.8.10 <2.0.0" 12 | transformers: 13 | - angular 14 | 15 | -------------------------------------------------------------------------------- /lib/services/users_repository.dart: -------------------------------------------------------------------------------- 1 | part of talk_to_me; 2 | 3 | @Injectable() 4 | class UsersRepository { 5 | Http http; 6 | 7 | UsersRepository(this.http); 8 | 9 | Future> all() => 10 | http. 11 | get("api/users.json"). 12 | then((_) => _.data). 13 | then((_) => _.map(_parseUser).toList()); 14 | 15 | User _parseUser(map) => new User(map["name"], map["isOnline"]); 16 | } -------------------------------------------------------------------------------- /web/talk_to_me_main.dart: -------------------------------------------------------------------------------- 1 | library talk_to_me_main; 2 | 3 | import 'package:angular/application_factory.dart'; 4 | import 'package:di/di.dart'; 5 | import 'package:logging/logging.dart'; 6 | import 'package:talk_to_me/talk_to_me.dart'; 7 | 8 | main(){ 9 | Logger.root.level = Level.FINEST; 10 | Logger.root.onRecord.listen((LogRecord r) { print(r.message); }); 11 | 12 | final inj = applicationFactory().addModule(new TalkToMeApp()).run(); 13 | GlobalHttpInterceptors.setUp(inj); 14 | } -------------------------------------------------------------------------------- /lib/components/global_alert_component.dart: -------------------------------------------------------------------------------- 1 | part of talk_to_me; 2 | 3 | @Component( 4 | selector: 'global-alert', 5 | templateUrl: 'lib/components/global_alert.html', 6 | useShadowDom: false 7 | ) 8 | class GlobalAlertComponent implements ScopeAware { 9 | String message; 10 | 11 | void set scope(Scope scope) { 12 | scope.on("globalAlert").listen(this._showMessage); 13 | } 14 | 15 | void _showMessage(ScopeEvent event) { 16 | this.message = event.data; 17 | } 18 | } -------------------------------------------------------------------------------- /lib/models/agenda_item.dart: -------------------------------------------------------------------------------- 1 | part of talk_to_me; 2 | 3 | class AgendaItem { 4 | String description; 5 | bool done; 6 | num priority; 7 | 8 | AgendaItem(this.description, this.done, this.priority); 9 | 10 | num get extPriority => done ? 4 : priority; 11 | 12 | bool get valid => description.isNotEmpty && priority != null; 13 | bool get isNew => description.isEmpty && priority == 3; 14 | 15 | operator == (AgendaItem a) => description == a.description && done == a.done && priority == a.priority; 16 | } 17 | -------------------------------------------------------------------------------- /lib/components/create_call_component.dart: -------------------------------------------------------------------------------- 1 | part of talk_to_me; 2 | 3 | @Component( 4 | selector: 'create-call', 5 | templateUrl: 'lib/components/create_call.html', 6 | useShadowDom: false 7 | ) 8 | class CreateCallComponent { 9 | Call call = new Call(); 10 | CallStorage storage; 11 | Router router; 12 | NgForm createForm; 13 | 14 | CreateCallComponent(this.storage, this.router); 15 | 16 | void create() { 17 | var callId = storage.store(call); 18 | router.go("list.show", {"callId": callId}); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/components/list_calls_component.dart: -------------------------------------------------------------------------------- 1 | part of talk_to_me; 2 | 3 | @Component( 4 | selector: 'list', 5 | templateUrl: 'lib/components/list_calls.html', 6 | useShadowDom: false 7 | ) 8 | class ListCallsComponent { 9 | List calls; 10 | Router router; 11 | 12 | ListCallsComponent(CallStorage storage, this.router) { 13 | calls = storage.all; 14 | } 15 | 16 | bool isSelected(Call call) => _callId == call.id; 17 | bool get isAnySelected => _callId != null; 18 | 19 | String get _callId => router.activePath.last.parameters["callId"]; 20 | } -------------------------------------------------------------------------------- /lib/talk_to_me_route_initializer.dart: -------------------------------------------------------------------------------- 1 | part of talk_to_me; 2 | 3 | void talkToMeRouteInitializer(Router router, RouteViewFactory view) { 4 | view.configure({ 5 | 'create' : ngRoute( 6 | path: '/create', 7 | viewHtml: '' 8 | ), 9 | 'list' : ngRoute( 10 | path: '/list', 11 | viewHtml: '', 12 | defaultRoute: true, 13 | mount: { 14 | "show" : ngRoute( 15 | path: '/:callId/show', 16 | viewHtml: '' 17 | ) 18 | } 19 | ) 20 | }); 21 | } -------------------------------------------------------------------------------- /lib/components/call_component.dart: -------------------------------------------------------------------------------- 1 | part of talk_to_me; 2 | 3 | @Component( 4 | selector: 'call', 5 | templateUrl: 'lib/components/call.html', 6 | useShadowDom: false 7 | ) 8 | class CallComponent implements AttachAware { 9 | Object videoSrc; 10 | 11 | @NgOneWay("is-online") 12 | bool isOnline; 13 | 14 | @NgOneWay("model") 15 | Call model; 16 | 17 | bool open = false; 18 | 19 | Future attach() { 20 | return html.window.navigator.getUserMedia(video:true, audio: true) 21 | .then((localStream) { 22 | videoSrc = html.Url.createObjectUrl(localStream); 23 | }); 24 | } 25 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013 - 2014 "Talk to Me" authors. Please see AUTHORS.md. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /lib/components/create_call.html: -------------------------------------------------------------------------------- 1 |
2 |

Create a Call

3 |
4 | 5 |
6 |
7 |
8 |

Name

9 | 10 |
11 | 12 |
13 | 14 |
15 | 16 |
17 | 18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | basePath: '.', 4 | frameworks: ['dart-unittest'], 5 | 6 | files: [ 7 | {pattern: 'test/talk_to_me_test.dart', included: true}, 8 | {pattern: '**/*.dart', included: false}, 9 | {pattern: '**/*.html', included: false} 10 | ], 11 | 12 | exclude: [ 13 | ], 14 | 15 | autoWatch: true, 16 | captureTimeout: 20000, 17 | browserNoActivityTimeout: 300000, 18 | 19 | plugins: [ 20 | 'karma-dart', 21 | 'karma-chrome-launcher' 22 | ], 23 | 24 | browsers: ['Dartium'] 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /lib/components/agenda.html: -------------------------------------------------------------------------------- 1 |

Call Agenda

2 | 3 |
4 |
5 | 6 |
7 | {{newAgendaItem.description}}, Priority: {{newAgendaItem.priority}} 8 |
9 |
10 | 11 | 12 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /lib/components/list_calls.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Calls

5 |
6 |
7 | Create 8 |
9 |
10 | 11 | 16 |
17 | 18 |
19 |
20 | 21 |
22 |
23 | No calls are selected. 24 |
25 |
-------------------------------------------------------------------------------- /lib/services/call_storage.dart: -------------------------------------------------------------------------------- 1 | part of talk_to_me; 2 | 3 | @Injectable() 4 | class CallStorage { 5 | CallSerializer serializer; 6 | 7 | CallStorage(this.serializer); 8 | 9 | List get all => _s.keys 10 | .where((k) => k.startsWith('talk-to-me:')) 11 | .map(_fetch).toList(); 12 | 13 | String store(Call call){ 14 | var id = _getId(call); 15 | _s[id] = serializer.serialize(call, id); 16 | return id; 17 | } 18 | 19 | find(String id) => _s.containsKey(id) ? _fetch(id) : null; 20 | 21 | String _getId(call) => call.id != null ? call.id : 'talk-to-me:${new Uuid().v4()}'; 22 | 23 | _fetch(id) => serializer.deserialize(_s[id]); 24 | 25 | html.Storage get _s => html.window.localStorage; 26 | } 27 | -------------------------------------------------------------------------------- /test/unit/agenda_item_test.dart: -------------------------------------------------------------------------------- 1 | part of talk_to_me_test; 2 | 3 | testAgendaItem(){ 4 | describe("[AgendaItem]", (){ 5 | describe("[valid]", (){ 6 | it("is true when the description and priority fields are set", (){ 7 | final item = new AgendaItem("description", true, 1); 8 | expect(item.valid).toBeTruthy(); 9 | }); 10 | 11 | it("is false when description is blank", (){ 12 | final item = new AgendaItem("", true, 1); 13 | expect(item.valid).toBeFalsy(); 14 | }); 15 | 16 | it("is false when priority is blank", (){ 17 | final item = new AgendaItem("description", true, null); 18 | expect(item.valid).toBeFalsy(); 19 | }); 20 | }); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /web/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Talk to Me! 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

Talk to Me!

18 |
19 |
20 | 21 |
22 |
23 | 24 |
25 |
26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /lib/components/agenda_component.dart: -------------------------------------------------------------------------------- 1 | part of talk_to_me; 2 | 3 | @Component( 4 | selector: 'agenda', 5 | templateUrl: 'lib/components/agenda.html', 6 | useShadowDom: false, 7 | exportExpressions: const ['extPriority'] 8 | ) 9 | class AgendaComponent { 10 | @NgOneWayOneTime("checkable") 11 | bool checkable; 12 | AgendaItem newAgendaItem; 13 | 14 | @NgOneWay("model") 15 | List model; 16 | 17 | AgendaComponent() { 18 | newAgendaItem = new AgendaItem("", false, 3); 19 | } 20 | 21 | void addItem() { 22 | model.add(newAgendaItem); 23 | newAgendaItem = new AgendaItem("", false, 3); 24 | } 25 | 26 | void deleteItem(AgendaItem item) { 27 | model.remove(item); 28 | } 29 | 30 | bool get valid => newAgendaItem.valid; 31 | } -------------------------------------------------------------------------------- /lib/decorators/toggle.dart: -------------------------------------------------------------------------------- 1 | part of talk_to_me; 2 | 3 | @Decorator(selector: 'toggle') 4 | class Toggle implements AttachAware { 5 | @NgTwoWay("toggle-property") 6 | bool open = false; 7 | 8 | html.Element el; 9 | Scope scope; 10 | 11 | Toggle(this.el, this.scope); 12 | 13 | void attach() { 14 | final whenOpen = el.querySelector("when-open"); 15 | final whenClosed = el.querySelector("when-closed"); 16 | 17 | whenOpen.hidden = true; 18 | whenClosed.hidden = true; 19 | 20 | scope.watch("open", (newValue, _) { 21 | if(newValue) { 22 | whenOpen.hidden = false; 23 | whenClosed.hidden = true; 24 | } else { 25 | whenOpen.hidden = true; 26 | whenClosed.hidden = false; 27 | } 28 | }); 29 | } 30 | } -------------------------------------------------------------------------------- /lib/services/call_serializer.dart: -------------------------------------------------------------------------------- 1 | part of talk_to_me; 2 | 3 | @Injectable() 4 | class CallSerializer { 5 | String serialize(Call call, String id){ 6 | var map = { 7 | "id" : id, 8 | "name": call.name, 9 | "agenda" : call.agenda.map(_serializeItem).toList() 10 | }; 11 | return JSON.encode(map); 12 | } 13 | 14 | Call deserialize(String str){ 15 | var json = JSON.decode(str); 16 | return new Call() 17 | ..id = json["id"] 18 | ..name = json["name"] 19 | ..agenda = json["agenda"].map(_deserializeItem).toList(); 20 | } 21 | 22 | _serializeItem(_) => {"description": _.description, "done": _.done, "priority" : _.priority}; 23 | 24 | _deserializeItem(_) => new AgendaItem(_["description"], _["done"], _["priority"]); 25 | } 26 | -------------------------------------------------------------------------------- /lib/services/parse_agenda_item.dart: -------------------------------------------------------------------------------- 1 | part of talk_to_me; 2 | 3 | @Injectable() 4 | class ParseAgendaItem { 5 | AgendaItem call(String description){ 6 | final r = new RegExp(r'![123](\s|$)'); 7 | var matches = r.allMatches(description); 8 | 9 | return (matches.length != 1) ? 10 | new AgendaItem(description, false, 3) : 11 | _parseDescription(description, matches.first); 12 | } 13 | 14 | AgendaItem _parseDescription(description, match){ 15 | var before = description.substring(0, match.start).trim(); 16 | var after = description.substring(match.end).trim(); 17 | 18 | var newDescription = [before, after].where((_) => _.isNotEmpty).join(" "); 19 | var priority = int.parse(match.group(0)[1]); 20 | 21 | return new AgendaItem(newDescription, false, priority); 22 | } 23 | } -------------------------------------------------------------------------------- /lib/global_http_interceptors.dart: -------------------------------------------------------------------------------- 1 | part of talk_to_me; 2 | 3 | class GlobalHttpInterceptors { 4 | static setUp(Injector inj) => 5 | new GlobalHttpInterceptors(inj)..addGlobalAlertInterceptor(); 6 | 7 | Injector inj; 8 | GlobalHttpInterceptors(this.inj); 9 | 10 | addGlobalAlertInterceptor() => 11 | inj.get(HttpInterceptors)..add(_buildGlobalAlertInterceptor()); 12 | 13 | _buildGlobalAlertInterceptor() => 14 | new HttpInterceptor() 15 | ..responseError = (error) { 16 | final messages = inj.get(Messages); 17 | messages.alert(_globalAlertMessage()); 18 | 19 | return new Future.error(error); 20 | }; 21 | 22 | _globalAlertMessage(){ 23 | final loc = inj.get(LocationWrapper); 24 | return "Something went wrong! Go to the main page."; 25 | } 26 | } -------------------------------------------------------------------------------- /test/talk_to_me_test.dart: -------------------------------------------------------------------------------- 1 | library talk_to_me_test; 2 | 3 | import 'package:guinness/guinness_html.dart'; 4 | import 'package:dartmocks/dartmocks.dart'; 5 | import 'dart:async'; 6 | import 'dart:html' as html; 7 | 8 | import 'package:angular/angular.dart'; 9 | import 'package:angular/mock/module.dart'; 10 | import 'package:angular/mock/test_injection.dart'; 11 | 12 | import 'package:talk_to_me/talk_to_me.dart'; 13 | 14 | part 'unit/parse_agenda_item_test.dart'; 15 | part 'unit/agenda_item_test.dart'; 16 | part 'unit/create_call_component_test.dart'; 17 | part 'unit/users_repository_test.dart'; 18 | part 'unit/agenda_item_component_test.dart'; 19 | 20 | main(){ 21 | guinnessEnableHtmlMatchers(); 22 | 23 | testParseAgendaItem(); 24 | testAgendaItem(); 25 | testCreateCallComponent(); 26 | testUsersRepository(); 27 | testAgendaItemComponent(); 28 | 29 | guinness.initSpecs(); 30 | } -------------------------------------------------------------------------------- /lib/decorators/agenda_item_text_input.dart: -------------------------------------------------------------------------------- 1 | part of talk_to_me; 2 | 3 | @Decorator(selector: 'input[type=agenda-item][ng-model]') 4 | class AgendaItemTextInput { 5 | ParseAgendaItem parseAgendaItem; 6 | Scope scope; 7 | 8 | html.InputElement element; 9 | NgModel model; 10 | 11 | AgendaItemTextInput(this.scope, this.model, html.Element element){ 12 | this.parseAgendaItem = new ParseAgendaItem(); 13 | this.element = element; 14 | 15 | model.render = _modelToView; 16 | element.onKeyUp.listen(_viewToModel); 17 | } 18 | 19 | _modelToView(item){ 20 | if(item.isNew){ 21 | element.value = ""; 22 | } else if (element.value.isEmpty){ 23 | element.value = "${item.description} !${item.priority}"; 24 | } 25 | } 26 | 27 | _viewToModel(_){ 28 | var newItem = parseAgendaItem(element.value); 29 | if (model.viewValue != newItem) 30 | model.viewValue = newItem; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/components/call.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | 8 |
9 | 10 |
11 | {{model.name}} is Offline 12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 |
25 |
-------------------------------------------------------------------------------- /lib/components/agenda_item_component.dart: -------------------------------------------------------------------------------- 1 | part of talk_to_me; 2 | 3 | @Component( 4 | selector: 'agenda-item', 5 | templateUrl: 'lib/components/agenda_item.html', 6 | useShadowDom: false 7 | ) 8 | class AgendaItemComponent { 9 | AgendaComponent agenda; 10 | 11 | @NgTwoWay("item") 12 | AgendaItem item; 13 | 14 | String mode = "show"; 15 | AgendaItem editItem; 16 | 17 | bool get isShow => mode == "show"; 18 | bool get isEdit => mode == "edit"; 19 | 20 | AgendaItemComponent(this.agenda); 21 | 22 | switchToEdit() { 23 | editItem = item; 24 | mode = "edit"; 25 | } 26 | 27 | void save() { 28 | item.description = editItem.description; 29 | item.priority = editItem.priority; 30 | 31 | mode = "show"; 32 | } 33 | 34 | void delete() { 35 | agenda.deleteItem(item); 36 | } 37 | 38 | void cancel() { 39 | mode = "show"; 40 | } 41 | 42 | bool get valid => editItem.valid; 43 | 44 | bool get checkable { 45 | return agenda.checkable; 46 | } 47 | } -------------------------------------------------------------------------------- /test/unit/create_call_component_test.dart: -------------------------------------------------------------------------------- 1 | part of talk_to_me_test; 2 | 3 | class CallStorageTestDouble extends TestDouble implements CallStorage {} 4 | class RouterTestDouble extends TestDouble implements Router {} 5 | 6 | testCreateCallComponent(){ 7 | describe("[CreateCallComponent]", (){ 8 | describe("[create]", (){ 9 | var storage, router; 10 | 11 | beforeEach((){ 12 | storage = new CallStorageTestDouble(); 13 | router = new RouterTestDouble(); 14 | 15 | storage.stub("store").andReturn("ID"); 16 | router.stub("go"); 17 | }); 18 | 19 | it("stores the call", (){ 20 | final c = new CreateCallComponent(storage, router); 21 | 22 | storage.shouldReceive("store").args(c.call); 23 | 24 | c.create(); 25 | 26 | storage.verify(); 27 | }); 28 | 29 | it("redirects to the show url", (){ 30 | final c = new CreateCallComponent(storage, router); 31 | 32 | router.shouldReceive("go").args("list.show", {"callId": "ID"}); 33 | 34 | c.create(); 35 | 36 | router.verify(); 37 | }); 38 | }); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /test/unit/parse_agenda_item_test.dart: -------------------------------------------------------------------------------- 1 | part of talk_to_me_test; 2 | 3 | testParseAgendaItem(){ 4 | var parse = new ParseAgendaItem(); 5 | 6 | describe("[AgendaItemParser]", (){ 7 | it("parses a simple description", (){ 8 | var item = parse("simple description"); 9 | 10 | expect(item.description).toEqual("simple description"); 11 | expect(item.done).toBeFalsy(); 12 | expect(item.priority).toEqual(3); 13 | }); 14 | 15 | it("parses a description with a priority", (){ 16 | var item = parse("description !2"); 17 | 18 | expect(item.description).toEqual("description"); 19 | expect(item.priority).toEqual(2); 20 | }); 21 | 22 | it("parses a description with a priority in the middle of the input", (){ 23 | var item = parse("before !2 after"); 24 | 25 | expect(item.description).toEqual("before after"); 26 | expect(item.priority).toEqual(2); 27 | }); 28 | 29 | it("ignores the priority when it is invalid", (){ 30 | var item = parse("description !5"); 31 | 32 | expect(item.description).toEqual("description !5"); 33 | expect(item.priority).toEqual(3); 34 | }); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /lib/components/show_call_component.dart: -------------------------------------------------------------------------------- 1 | part of talk_to_me; 2 | 3 | @Component( 4 | selector: "show-call", 5 | exportExpressions: const ['watchExp'], 6 | templateUrl: 'lib/components/show_call.html', 7 | useShadowDom: false 8 | ) 9 | class ShowCallComponent implements ScopeAware { 10 | Call call; 11 | CallStorage storage; 12 | CallSerializer serializer; 13 | bool userIsOnline = false; 14 | 15 | ShowCallComponent(this.storage, this.serializer, RouteProvider routeProvider, 16 | UsersRepository repo){ 17 | call = storage.find(_callId(routeProvider)); 18 | _checkIfOnline(call.name, repo.all()); 19 | } 20 | 21 | void set scope(Scope scope) { 22 | scope.watch("watchExp()", _store, context: this); 23 | } 24 | 25 | Future _checkIfOnline(String userName, Future> users) { 26 | return users.then((_) { 27 | userIsOnline = _.any((u) => u.name == call.name && u.isOnline); 28 | }); 29 | } 30 | 31 | String watchExp() => serializer.serialize(call, call.id); 32 | 33 | void _store(value, previousValue) { 34 | storage.store(call); 35 | } 36 | 37 | String _callId(routeProvider) => routeProvider.parameters["callId"]; 38 | } 39 | 40 | -------------------------------------------------------------------------------- /lib/components/agenda_item.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | 7 | {{item.description}} 8 | 9 | 10 | 13 | 14 | 17 | 18 |
19 |
20 | 21 |
22 |
23 |
24 | 25 |
26 | {{editItem.description}}, Priority: {{editItem.priority}} 27 |
28 |
29 | 30 | 31 | 34 | 35 | 38 | 39 |
40 |
-------------------------------------------------------------------------------- /test/unit/users_repository_test.dart: -------------------------------------------------------------------------------- 1 | part of talk_to_me_test; 2 | 3 | class TestResponse { 4 | final data; 5 | TestResponse(this.data); 6 | 7 | static async(data) => new Future.value(new TestResponse(data)); 8 | } 9 | 10 | waitForHttp(future, callback) => 11 | scheduleMicrotask(() { 12 | inject((MockHttpBackend http) => http.flush()); 13 | future.then(callback); 14 | }); 15 | 16 | class HttpTestDouble extends TestDouble implements Http {} 17 | 18 | testUsersRepository(){ 19 | describe("[UsersRepository - without using Angular helpers]", (){ 20 | describe("[all]", (){ 21 | it("gets a list of users", (){ 22 | final http = new HttpTestDouble() 23 | ..stub("get"). 24 | args("api/users.json"). 25 | andReturn(TestResponse.async([{"name" : "Jerry", "isOnline" : true}])); 26 | 27 | final repo = new UsersRepository(http); 28 | 29 | repo.all().then((users){ 30 | expect(users[0].name).toEqual("Jerry"); 31 | expect(users[0].isOnline).toBeTruthy(); 32 | }); 33 | }); 34 | }); 35 | }); 36 | 37 | describe("[UsersRepository - with using Angular helpers]", () { 38 | beforeEach(setUpInjector); 39 | afterEach(tearDownInjector); 40 | 41 | describe("[all]", () { 42 | beforeEach(module((Module _) => _ 43 | ..bind(MockHttpBackend) 44 | ..bind(UsersRepository))); 45 | 46 | it("gets a list of users", inject((MockHttpBackend http, UsersRepository repo) { 47 | http.whenGET("api/users.json").respond('[{"name":"Jerry", "isOnline":true}]'); 48 | 49 | waitForHttp(repo.all(), (users) { 50 | expect(users[0].name).toEqual("Jerry"); 51 | expect(users[0].isOnline).toBeTruthy(); 52 | }); 53 | })); 54 | }); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /lib/talk_to_me.dart: -------------------------------------------------------------------------------- 1 | library talk_to_me; 2 | 3 | import 'dart:html' as html; 4 | import 'dart:async'; 5 | import 'dart:convert'; 6 | 7 | import 'package:angular/angular.dart'; 8 | import 'package:angular/routing/module.dart'; 9 | import 'package:angular/animate/module.dart'; 10 | import 'package:uuid/uuid_client.dart'; 11 | 12 | part 'services/call_serializer.dart'; 13 | part 'services/call_storage.dart'; 14 | part 'services/users_repository.dart'; 15 | part 'services/messages.dart'; 16 | part 'services/parse_agenda_item.dart'; 17 | 18 | part 'talk_to_me_route_initializer.dart'; 19 | part 'global_http_interceptors.dart'; 20 | 21 | part 'decorators/agenda_item_text_input.dart'; 22 | part 'decorators/toggle.dart'; 23 | 24 | part 'components/call_component.dart'; 25 | part 'components/agenda_component.dart'; 26 | part 'components/agenda_item_component.dart'; 27 | part 'components/global_alert_component.dart'; 28 | 29 | part 'components/list_calls_component.dart'; 30 | part 'components/create_call_component.dart'; 31 | part 'components/show_call_component.dart'; 32 | 33 | part 'models/call.dart'; 34 | part 'models/agenda_item.dart'; 35 | part 'models/user.dart'; 36 | 37 | class TalkToMeApp extends Module { 38 | TalkToMeApp(){ 39 | bind(ListCallsComponent); 40 | bind(ShowCallComponent); 41 | bind(CreateCallComponent); 42 | 43 | bind(AgendaItemTextInput); 44 | bind(AgendaItemComponent); 45 | bind(AgendaComponent); 46 | bind(CallComponent); 47 | bind(Toggle); 48 | 49 | bind(CallSerializer); 50 | bind(CallStorage); 51 | bind(UsersRepository); 52 | 53 | bind(Messages); 54 | bind(GlobalAlertComponent); 55 | 56 | bind(RouteInitializerFn, toValue: talkToMeRouteInitializer); 57 | bind(NgRoutingUsePushState, toValue: new NgRoutingUsePushState.value(false)); 58 | 59 | bind(UrlRewriter, toImplementation: TalkToMeUrlRewriter); 60 | bind(ResourceResolverConfig, toValue: new ResourceResolverConfig.resolveRelativeUrls(false)); 61 | 62 | install(new AnimationModule()); 63 | } 64 | } 65 | 66 | @Injectable() 67 | class TalkToMeUrlRewriter implements UrlRewriter { 68 | String call(url) => 69 | url.startsWith('lib/') ? 'packages/talk_to_me/${url.substring(4)}' : url; 70 | } -------------------------------------------------------------------------------- /test/unit/agenda_item_component_test.dart: -------------------------------------------------------------------------------- 1 | part of talk_to_me_test; 2 | 3 | loadTemplates(List templates){ 4 | updateCache(template, response) => inject((TemplateCache cache) => cache.put(template, response)); 5 | 6 | final futures = templates.map((template) => 7 | html.HttpRequest.request('packages/talk_to_me/' + template.substring(4), method: "GET"). 8 | then((_) => updateCache(template, new HttpResponse(200, _.response)))); 9 | 10 | return Future.wait(futures); 11 | } 12 | 13 | compileComponent(String html, Map scope, callback){ 14 | return async(() { 15 | inject((TestBed tb) { 16 | final s = tb.rootScope.createChild(scope); 17 | final el = tb.compile(html, scope: s); 18 | 19 | microLeap(); 20 | digest(); 21 | 22 | callback(el); 23 | }); 24 | }); 25 | } 26 | 27 | digest(){ 28 | inject((TestBed tb) { tb.rootScope.apply(); }); 29 | } 30 | 31 | testAgendaItemComponent(){; 32 | describe("[AgendaItemComponent]", () { 33 | beforeEach(setUpInjector); 34 | afterEach(tearDownInjector); 35 | 36 | ddescribe("[swiching between modes]", () { 37 | html() => ''; 38 | scope() => {"item" : new AgendaItem("description", true, 1)}; 39 | 40 | beforeEach((){ 41 | module((Module _) => _ 42 | ..bind(TestBed) 43 | ..bind(AgendaItemComponent) 44 | ..bind(AgendaComponent) 45 | ..bind(ResourceResolverConfig, toValue: new ResourceResolverConfig.resolveRelativeUrls(false)) 46 | ); 47 | return loadTemplates(['lib/components/agenda_item.html']); 48 | }); 49 | 50 | it("defaults to the show mode", compileComponent(html(), scope(), (shadowRoot){ 51 | expect(shadowRoot.query("input[type=agenda-item]")).toBeNull(); 52 | })); 53 | 54 | it("switches to edit", compileComponent(html(), scope(), (shadowRoot) { 55 | final switchBtn = shadowRoot.query("button.switch-to-edit"); 56 | 57 | switchBtn.click(); 58 | 59 | digest(); 60 | 61 | expect(shadowRoot.query("input[type=agenda-item]")).toBeDefined(); 62 | })); 63 | 64 | it("switches to show", compileComponent(html(), scope(), (shadowRoot) { 65 | shadowRoot.query("button.switch-to-edit").click(); 66 | 67 | digest(); 68 | 69 | final cancelBtn = shadowRoot.query("button[type=reset]"); 70 | cancelBtn.click(); 71 | 72 | digest(); 73 | 74 | expect(shadowRoot.query("input[type=agenda-item]")).toBeNull(); 75 | })); 76 | }); 77 | }); 78 | } 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Talk to Me 2 | 3 | ## Dart and AngularDart 4 | 5 | `Dart` is a new platform for Web development that includes a language, tools, and libraries. `AngularDart` is a framework that enables building rich client-side applications. 6 | 7 | ## Goal 8 | 9 | The goal of this project is to be a sample application that can help you get started with `AngularDart`. I began working on it a few days ago to learn the framework. And since there is not much documentation about `AngularDart`, I decided to make it public, so others can benefit from it. 10 | 11 | ## Learn About AngularDart 12 | 13 | * [AngularDart for AngularJS Developers. Introduction to the best Angular yet.](http://victorsavkin.com/post/72452331552/angulardart-for-angularjs-developers-introduction-to) 14 | * I found [AngularDart tutorial](https://angulardart.org/tutorial) extremely useful. Check it out. 15 | 16 | ## Work in Progress 17 | 18 | The project is still very much a work in progress. 19 | 20 | ## Done 21 | 22 | * Controllers, Components 23 | * Routing 24 | * HTTP 25 | * Configuring injectables 26 | * Using scopes for message passing 27 | 28 | ## Try It 29 | 30 | [http://vsavkin.github.io/angulardart-sample-app/app.html](http://vsavkin.github.io/angulardart-sample-app/app.html) 31 | 32 | ## Problems 33 | 34 | If you have any data in your local storage the application may not work as expected. To fix it run the following command in the browser console: 35 | 36 | `window.localStorage.clear()` 37 | 38 | ## To do 39 | 40 | * Validations 41 | * Application state management 42 | * Shadow DOM 43 | * Use factory, value, CreationStrategy, and Visibility 44 | 45 | ## Index 46 | 47 | * Building components => `agenda.html`, `agenda_item.html`, `agenda_component.dart`, `agenda_component_input.dart` 48 | * Building decorators => `toggle.dart`, `agenda_item_text_input.dart` 49 | * Setting up route (including default) => `app_route_initializer.dart` 50 | * Nested routes and nested views => `list.html`, and `app_route_initializer.dart` 51 | * Using RouteProvider => `show_call_ctrl.dart` 52 | * Using formatters => `agenda.html` and `list.html` 53 | * Registering components, controllers, and other injectables => talk_to_me.dart 54 | * Creating services => `parse_agenda_item.dart`, `storage.dart` 55 | * Using the Http service => `users_repository.dart` 56 | * Configuring injectables => `global_http_interceptors.dart` 57 | * Using scopes for message passing => `messages.dart` and `global_alert_component.dart` 58 | * Testing => All files in `test/unit` 59 | 60 | # Credits 61 | 62 | * Victor Savkin 63 | * Seth Ladd 64 | * Pavel Jbanov 65 | * Patrice Chalin 66 | * Adam Singer 67 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See http://pub.dartlang.org/doc/glossary.html#lockfile 3 | packages: 4 | analyzer: 5 | description: analyzer 6 | source: hosted 7 | version: "0.15.7" 8 | angular: 9 | description: angular 10 | source: hosted 11 | version: "1.0.0" 12 | args: 13 | description: args 14 | source: hosted 15 | version: "0.10.0+2" 16 | barback: 17 | description: barback 18 | source: hosted 19 | version: "0.14.0+3" 20 | bignum: 21 | description: bignum 22 | source: hosted 23 | version: "0.0.5" 24 | browser: 25 | description: browser 26 | source: hosted 27 | version: "0.10.0+2" 28 | cipher: 29 | description: cipher 30 | source: hosted 31 | version: "0.7.1" 32 | code_transformers: 33 | description: code_transformers 34 | source: hosted 35 | version: "0.1.6" 36 | collection: 37 | description: collection 38 | source: hosted 39 | version: "0.9.4" 40 | crypto: 41 | description: crypto 42 | source: hosted 43 | version: "0.9.0" 44 | dartmocks: 45 | description: dartmocks 46 | source: hosted 47 | version: "0.5.2" 48 | di: 49 | description: di 50 | source: hosted 51 | version: "3.3.2" 52 | fixnum: 53 | description: fixnum 54 | source: hosted 55 | version: "0.9.0" 56 | guinness: 57 | description: guinness 58 | source: hosted 59 | version: "0.1.14" 60 | html5lib: 61 | description: html5lib 62 | source: hosted 63 | version: "0.10.0" 64 | intl: 65 | description: intl 66 | source: hosted 67 | version: "0.8.10+4" 68 | logging: 69 | description: logging 70 | source: hosted 71 | version: "0.9.2" 72 | matcher: 73 | description: matcher 74 | source: hosted 75 | version: "0.10.1+1" 76 | meta: 77 | description: meta 78 | source: hosted 79 | version: "0.8.8" 80 | mock: 81 | description: mock 82 | source: hosted 83 | version: "0.11.0+2" 84 | observe: 85 | description: observe 86 | source: hosted 87 | version: "0.10.1+2" 88 | path: 89 | description: path 90 | source: hosted 91 | version: "1.3.0" 92 | perf_api: 93 | description: perf_api 94 | source: hosted 95 | version: "0.0.9" 96 | route_hierarchical: 97 | description: route_hierarchical 98 | source: hosted 99 | version: "0.5.0" 100 | smoke: 101 | description: smoke 102 | source: hosted 103 | version: "0.1.0+1" 104 | source_maps: 105 | description: source_maps 106 | source: hosted 107 | version: "0.9.4" 108 | source_span: 109 | description: source_span 110 | source: hosted 111 | version: "1.0.0" 112 | stack_trace: 113 | description: stack_trace 114 | source: hosted 115 | version: "0.9.3+2" 116 | unittest: 117 | description: unittest 118 | source: hosted 119 | version: "0.11.0+4" 120 | utf: 121 | description: utf 122 | source: hosted 123 | version: "0.9.0+1" 124 | uuid: 125 | description: uuid 126 | source: hosted 127 | version: "0.3.2" 128 | -------------------------------------------------------------------------------- /web/css/talk-to-me.css: -------------------------------------------------------------------------------- 1 | header { 2 | height: 150px; 3 | background-color: grey; 4 | border-top: 5px solid #000000; 5 | border-bottom: 1px solid RGB(212,212,212); 6 | } 7 | 8 | h1 a { color: #f0f0f0; font-size: 40px;} 9 | h1 a:hover { color: #f0f0f0; font-size: 40px; text-decoration: none;} 10 | .row { 11 | background-color: #f0f0f0; 12 | } 13 | ul { padding: 0; } 14 | 15 | ul li { 16 | padding: 8px 12px 8px 25px; 17 | margin: 0 0 5px; 18 | display: block; 19 | font-size: 1.8rem; 20 | background: darkgrey; 21 | } 22 | 23 | ul li > a { color: #333; display: inline-block; width: 100%;} 24 | ul li > a:hover { color: #333; text-decoration: none; } 25 | 26 | .btn-create { 27 | margin-top: 20px; 28 | } 29 | 30 | .call-row { 31 | background-color: darkgrey; 32 | padding: 20px; 33 | margin-top: 20px; 34 | } 35 | 36 | .call-row button { 37 | float: right; 38 | } 39 | 40 | .create-row { 41 | margin-top: 30px; 42 | } 43 | 44 | agenda-item { 45 | display: block; 46 | margin: 10px 0; 47 | } 48 | 49 | .done { 50 | color: lightgrey; 51 | } 52 | 53 | .agenda-item-input { 54 | position: relative; 55 | padding-bottom: 20px; 56 | overflow: hidden; 57 | display: block; 58 | } 59 | .container { padding: 40px 0;} 60 | .hint { position: absolute; font-size: 12px; color: #333333; } 61 | .input-group { font-size: 1.8rem; border-bottom: 1px solid lightgrey; padding-bottom: 10px;} 62 | .input-group-btn { vertical-align: top;} 63 | 64 | 65 | .priority-indicator { 66 | float: left; 67 | height: 30px; 68 | width: 10px; 69 | margin-right: 10px; 70 | display: inline-block; 71 | } 72 | 73 | .selected { 74 | background-color: grey; 75 | } 76 | 77 | .empty-call { 78 | color: darkgrey; 79 | font-size: 2rem; 80 | text-align: center; 81 | } 82 | 83 | .priority-1 { 84 | background-color: #FFB5B5; 85 | } 86 | 87 | .priority-2 { 88 | background-color: #B5C6FF; 89 | } 90 | 91 | .priority-3 { 92 | background-color: darkgrey; 93 | } 94 | 95 | .video-box { 96 | border: 1px solid #000000; 97 | float: left; 98 | } 99 | 100 | .video-box-transfer { 101 | float: left; 102 | margin: 70px 20px; 103 | } 104 | 105 | .overlay { 106 | position: absolute; 107 | z-index: 100; 108 | left:0; 109 | top:0; 110 | width: 100%; 111 | height: 100%; 112 | background-color: black; 113 | opacity: 0.7; 114 | } 115 | 116 | .message { 117 | position: absolute; 118 | z-index: 1000; 119 | left: 50%; 120 | width: 500px; 121 | margin-left: -250px; 122 | top: 250px; 123 | 124 | } 125 | 126 | 127 | agenda-item.ng-enter { 128 | transition: all 200ms; 129 | -webkit-transform: scale(1.1, 1.1); 130 | opacity: 0; 131 | } 132 | 133 | agenda-item.ng-enter.ng-enter-active { 134 | -webkit-transform: scale(1.0, 1.0); 135 | opacity: 1; 136 | } 137 | 138 | 139 | agenda-item.ng-leave { 140 | transition: all 200ms; 141 | -webkit-transform: scale(1.0, 1.0); 142 | opacity: 1; 143 | } 144 | 145 | agenda-item.ng-leave-active { 146 | -webkit-transform: scale(1.1, 1.1); 147 | opacity: 0; 148 | } --------------------------------------------------------------------------------