├── .analysis_options ├── .gitattributes ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── lib ├── app.dart ├── app.html ├── components │ ├── hn_item │ │ ├── hn_item.dart │ │ ├── hn_item.html │ │ └── hn_item.less │ ├── home │ │ ├── home.dart │ │ └── home.html │ ├── item │ │ ├── item.dart │ │ ├── item.html │ │ └── item_page.less │ ├── spinner.less │ └── user_page │ │ ├── user_page.dart │ │ ├── user_page.html │ │ └── user_page.less ├── directives │ └── parse_html │ │ └── parse_html.dart ├── ng2_hackernews.dart ├── pipes │ ├── domain_pipe.dart │ └── timeago_pipe.dart └── services │ └── hn_api.dart ├── pubspec.yaml ├── test ├── domain_pipe_spec.dart ├── hn_item_spec.dart ├── parse_html_directive_spec.dart └── timeago_pipe_spec.dart └── web ├── index.html ├── main.dart └── main.less /.analysis_options: -------------------------------------------------------------------------------- 1 | analyzer: 2 | strong-mode: true -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.html linguist-language=Dart -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .buildlog 2 | .DS_Store 3 | .idea 4 | .pub/ 5 | build/ 6 | packages 7 | pubspec.lock 8 | .packages 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.0.1 4 | 5 | - Initial version, created by Stagehand 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Andres Araujo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A HackerNews application made with Angular2 Dart. 2 | 3 | Pretty much inspired from the work by https://github.com/hswolff/hn-ng2 with a few changes and additions 4 | 5 | The goal of this project is to show a sample application to get you started with Angular2 Dart. 6 | It will be updated to work with the most recent Angular 2 version. 7 | 8 | ## Usage 9 | 10 | #### Install dependencies 11 | `pub get` 12 | #### Run local web server 13 | `pub serve` 14 | #### open http://localhost:8080/ 15 | 16 | ## [Live Demo](https://andresaraujo.github.io/ng2_hackernews) 17 | -------------------------------------------------------------------------------- /lib/app.dart: -------------------------------------------------------------------------------- 1 | library ng2_hackernews.app; 2 | 3 | import 'package:angular2/angular2.dart' show Component, NgFor, NgIf, View; 4 | import 'package:angular2/router.dart' 5 | show Route, RouteConfig, Router, RouterOutlet; 6 | 7 | import 'package:ng2_hackernews/components/home/home.dart' show Home; 8 | import 'package:ng2_hackernews/components/item/item.dart' show ItemPage; 9 | import 'package:ng2_hackernews/components/user_page/user_page.dart' 10 | show UserPage; 11 | 12 | @Component(selector: 'app') 13 | @View( 14 | templateUrl: 'package:ng2_hackernews/app.html', 15 | directives: const [NgIf, NgFor, RouterOutlet]) 16 | @RouteConfig(const [ 17 | const Route(path: '/user/:id', component: UserPage, name: 'User'), 18 | const Route(path: '/item/:id', component: ItemPage, name: 'Item'), 19 | const Route(path: '/home', component: Home, name: 'Home'), 20 | const Route(path: '/', component: Home) 21 | ]) 22 | class App { 23 | Router _router; 24 | App(this._router); 25 | goHome() { 26 | _router.navigateByUrl("/home"); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/app.html: -------------------------------------------------------------------------------- 1 |
2 | 4 | 5 | 6 | 12 | 13 | 19 | 20 | 21 |
7 | 8 | 10 | 11 | 14 | 15 | Hacker News written in Angular 2 Dart 16 | 17 | 18 |
22 |
23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
36 |
-------------------------------------------------------------------------------- /lib/components/hn_item/hn_item.dart: -------------------------------------------------------------------------------- 1 | library hacker_news.components.hnitem; 2 | 3 | import 'package:angular2/angular2.dart'; 4 | import 'package:angular2/router.dart'; 5 | import 'package:ng2_hackernews/directives/parse_html/parse_html.dart' 6 | show ParseHtml; 7 | import 'package:ng2_hackernews/pipes/domain_pipe.dart' show DomainPipe; 8 | import 'package:ng2_hackernews/services/hn_api.dart' show HNApi; 9 | import 'package:timeago/timeago.dart' show TimeAgo; 10 | 11 | const itemMap = const {'comment': 1, 'job': 2, 'poll': 3, 'story': 4}; 12 | final fuzzyTime = new TimeAgo(); 13 | 14 | @Component(selector: 'hn-item') 15 | @View( 16 | templateUrl: 'package:ng2_hackernews/components/hn_item/hn_item.html', 17 | directives: const [CORE_DIRECTIVES, HNItem, ParseHtml, RouterLink], 18 | pipes: const [DomainPipe]) 19 | class HNItem implements OnInit { 20 | HNApi _hnApi; 21 | String _itemId; 22 | 23 | @Input() bool loadChildren = true; 24 | @Input() bool topLevel = false; 25 | 26 | Map data; 27 | bool collapsed = false; 28 | int type = 0; 29 | String timeAgo; 30 | 31 | HNItem(this._hnApi); 32 | 33 | @Input('itemId') 34 | set newItemId(itemId) => _itemId = itemId.toString(); 35 | 36 | ngOnInit() async { 37 | await _fetchData(); 38 | } 39 | 40 | _fetchData() async { 41 | data = await _hnApi.fetchItem(_itemId); 42 | if (data != null) { 43 | type = itemMap[data['type']]; 44 | timeAgo = fuzzyTime.timeAgo(data['time'] * 1000); 45 | data['type'] = data['type'] != null ? data['type'] : ''; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/components/hn_item/hn_item.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | 31 | 42 | 56 | 57 | 71 | 72 |
73 | 74 |
-------------------------------------------------------------------------------- /lib/components/hn_item/hn_item.less: -------------------------------------------------------------------------------- 1 | hn-item { 2 | display: block; 3 | margin: 10px 0; 4 | } 5 | 6 | .hnItem-title { 7 | font-size: 10pt; 8 | color:#828282; 9 | } 10 | 11 | .hnItem > section { 12 | font-size: 7pt; 13 | 14 | &, 15 | a:link, 16 | a:visited { 17 | color: #828282; 18 | } 19 | 20 | a:hover { 21 | text-decoration: underline; 22 | } 23 | } 24 | 25 | .hnItem--comment { 26 | margin: 15px 0; 27 | } 28 | 29 | .hnItem--coment-content { 30 | margin-top: 5px; 31 | } 32 | 33 | .hnItem--comment-children { 34 | margin-left: 50px; 35 | } -------------------------------------------------------------------------------- /lib/components/home/home.dart: -------------------------------------------------------------------------------- 1 | library hacker_news.components.home; 2 | 3 | import 'package:angular2/angular2.dart' show Component, NgFor, View; 4 | import 'package:ng2_hackernews/components/hn_item/hn_item.dart' show HNItem; 5 | import 'package:ng2_hackernews/services/hn_api.dart' show HNApi; 6 | 7 | @Component(selector: 'home-page', viewProviders: const [HNApi]) 8 | @View( 9 | templateUrl: "package:ng2_hackernews/components/home/home.html", 10 | directives: const [NgFor, HNItem]) 11 | class Home { 12 | HNApi api; 13 | Iterable> topStories = []; 14 | Home(this.api) { 15 | api.fetchTopStories().then((Iterable> value) => this.topStories = value); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/components/home/home.html: -------------------------------------------------------------------------------- 1 |
2 |
    3 |
  1. 4 | 5 |
  2. 6 |
7 |
-------------------------------------------------------------------------------- /lib/components/item/item.dart: -------------------------------------------------------------------------------- 1 | library hacker_news.components.item; 2 | 3 | import 'package:angular2/angular2.dart' show Component, NgFor, View; 4 | import 'package:angular2/router.dart' show RouteParams; 5 | import 'package:ng2_hackernews/components/hn_item/hn_item.dart' show HNItem; 6 | import 'package:ng2_hackernews/services/hn_api.dart' show HNApi; 7 | 8 | @Component(selector: 'page-item', viewProviders: const [HNApi]) 9 | @View( 10 | templateUrl: 'package:ng2_hackernews/components/item/item.html', 11 | directives: const [NgFor, HNItem]) 12 | class ItemPage { 13 | HNApi api; 14 | List childrenIds = []; 15 | String itemId; 16 | 17 | ItemPage(this.api, RouteParams routeParams) { 18 | itemId = routeParams.get("id"); 19 | _fetchItem(); 20 | } 21 | _fetchItem() async { 22 | Map data = await api.fetchItem("$itemId"); 23 | if (data != null) { 24 | childrenIds = data['kids'] as List; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/components/item/item.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 |
-------------------------------------------------------------------------------- /lib/components/item/item_page.less: -------------------------------------------------------------------------------- 1 | .itemDetail { 2 | margin: 10px 20px; 3 | } 4 | 5 | .itemDetail-item { 6 | 7 | &:first-of-type { 8 | margin-top: 30px; 9 | } 10 | } -------------------------------------------------------------------------------- /lib/components/spinner.less: -------------------------------------------------------------------------------- 1 | // source: https://github.com/tobiasahlin/SpinKit/blob/master/3-wave.html 2 | 3 | .spinner { 4 | width: 50px; 5 | height: 30px; 6 | text-align: center; 7 | font-size: 10px; 8 | } 9 | 10 | .spinner > div { 11 | background-color: #333; 12 | height: 100%; 13 | width: 6px; 14 | display: inline-block; 15 | 16 | -webkit-animation: stretchdelay 1.2s infinite ease-in-out; 17 | animation: stretchdelay 1.2s infinite ease-in-out; 18 | } 19 | 20 | .spinner .rect2 { 21 | -webkit-animation-delay: -1.1s; 22 | animation-delay: -1.1s; 23 | } 24 | 25 | .spinner .rect3 { 26 | -webkit-animation-delay: -1.0s; 27 | animation-delay: -1.0s; 28 | } 29 | 30 | .spinner .rect4 { 31 | -webkit-animation-delay: -0.9s; 32 | animation-delay: -0.9s; 33 | } 34 | 35 | .spinner .rect5 { 36 | -webkit-animation-delay: -0.8s; 37 | animation-delay: -0.8s; 38 | } 39 | 40 | @-webkit-keyframes stretchdelay { 41 | 0%, 40%, 100% { -webkit-transform: scaleY(0.4) } 42 | 20% { -webkit-transform: scaleY(1.0) } 43 | } 44 | 45 | @keyframes stretchdelay { 46 | 0%, 40%, 100% { 47 | transform: scaleY(0.4); 48 | -webkit-transform: scaleY(0.4); 49 | } 20% { 50 | transform: scaleY(1.0); 51 | -webkit-transform: scaleY(1.0); 52 | } 53 | } -------------------------------------------------------------------------------- /lib/components/user_page/user_page.dart: -------------------------------------------------------------------------------- 1 | library hacker_news.components.user_page; 2 | 3 | import 'package:angular2/angular2.dart' show Component, NgFor, NgIf, View; 4 | import 'package:angular2/router.dart' show RouteParams; 5 | import 'package:ng2_hackernews/components/hn_item/hn_item.dart' show HNItem; 6 | import 'package:ng2_hackernews/directives/parse_html/parse_html.dart' 7 | show ParseHtml; 8 | import 'package:ng2_hackernews/services/hn_api.dart' show HNApi; 9 | import 'package:timeago/timeago.dart' show TimeAgo; 10 | 11 | final fuzzyTime = new TimeAgo(); 12 | 13 | @Component(selector: 'page-user', viewProviders: const [HNApi]) 14 | @View( 15 | templateUrl: 'package:ng2_hackernews/components/user_page/user_page.html', 16 | directives: const [NgFor, NgIf, HNItem, ParseHtml]) 17 | class UserPage { 18 | HNApi api; 19 | bool showSubmissions; 20 | Map data = {}; 21 | String timeAgo; 22 | 23 | UserPage(this.api, RouteParams routeParams) { 24 | _fetchUser(routeParams.get("id")); 25 | this.showSubmissions = false; 26 | } 27 | _fetchUser(String userId) async { 28 | data = await api.fetchUser(userId); 29 | data['submitted'] = (data['submitted'] as List).take(30); 30 | timeAgo = fuzzyTime.timeAgo(data['created'] * 1000); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/components/user_page/user_page.html: -------------------------------------------------------------------------------- 1 |
2 |

user: {{data['id']}}

3 |

created: {{timeAgo}}

4 |

karma: {{data['karma']}}

5 |

about: {{data['about']}}

6 | 7 | 8 | show submitted stories and comments 9 |
10 |
11 | 12 |
13 |
14 | 15 | 16 |

17 | submissions 18 | comments 19 |

20 |
21 | -------------------------------------------------------------------------------- /lib/components/user_page/user_page.less: -------------------------------------------------------------------------------- 1 | .userDetail { 2 | margin: 10px 20px; 3 | } 4 | 5 | .userDetail-item { 6 | 7 | &:first-of-type { 8 | margin-top: 30px; 9 | } 10 | } -------------------------------------------------------------------------------- /lib/directives/parse_html/parse_html.dart: -------------------------------------------------------------------------------- 1 | library hacker_news.decorators.parse_html; 2 | 3 | import 'dart:html' as dom; 4 | 5 | import 'package:angular2/angular2.dart' 6 | show Directive, ElementRef, AfterContentInit; 7 | import 'package:html/parser.dart' show parse; 8 | 9 | @Directive( 10 | selector: '[parsehtml]') 11 | class ParseHtml implements AfterContentInit { 12 | ElementRef ref; 13 | 14 | ParseHtml(this.ref); 15 | 16 | ngAfterContentInit() { 17 | dom.Element element = ref.nativeElement; 18 | String raw = element.text; 19 | element.text = ""; 20 | element.setInnerHtml(parse(raw).outerHtml); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/ng2_hackernews.dart: -------------------------------------------------------------------------------- 1 | library hacker_news; 2 | 3 | export 'app.dart'; 4 | -------------------------------------------------------------------------------- /lib/pipes/domain_pipe.dart: -------------------------------------------------------------------------------- 1 | import 'package:angular2/angular2.dart' 2 | show Pipe, PipeTransform, ChangeDetectorRef, Injectable; 3 | 4 | /** 5 | * Transforms a string that represents the url to show only the domain. 6 | * 7 | * # Example 8 | * 9 | * ``` 10 | * @Component( 11 | * selector: "domain-example" 12 | * ) 13 | * @View( 14 | * template: "domain: {{ 'http://www.google.com/accounts/recovery' | domain }}" // will be transformed to 'google.com', 15 | * pipes: const [DomainPipe] 16 | * ) 17 | * class DomainExample { 18 | * } 19 | * 20 | * ``` 21 | */ 22 | @Pipe(name: 'domain') 23 | @Injectable() 24 | class DomainPipe implements PipeTransform { 25 | bool supports(dynamic str) { 26 | return str == null || str is String || str is List; 27 | } 28 | 29 | void onDestroy() {} 30 | String transform(String value, [List args = null]) { 31 | if (value == null || value.isEmpty) { 32 | return value; 33 | } 34 | return _parseDomain(value); 35 | } 36 | 37 | create(ChangeDetectorRef cdRef) { 38 | return this; 39 | } 40 | 41 | DomainPipe(); 42 | } 43 | 44 | _parseDomain(String url) { 45 | url = url.replaceFirst(new RegExp(r'https?:\/\/(www.)?|www.'), ''); 46 | if(url.contains('/')){ 47 | var parts = url.split('/'); 48 | url = parts[0]; 49 | } else if (url.contains('?')) { 50 | var parts = url.split('?'); 51 | url = parts[0]; 52 | } 53 | return url; 54 | } 55 | -------------------------------------------------------------------------------- /lib/pipes/timeago_pipe.dart: -------------------------------------------------------------------------------- 1 | import 'package:angular2/angular2.dart' 2 | show Pipe, PipeTransform, ChangeDetectorRef, Injectable; 3 | import 'package:timeago/timeago.dart' show TimeAgo; 4 | 5 | final fuzzyTime = new TimeAgo(); 6 | 7 | /** 8 | * Transforms a string date in milliseconds to a fuzzy time. ie. "15 mins ago" 9 | * 10 | * # Example 11 | * 12 | * ``` 13 | * @Component( 14 | * selector: "timeago-example" 15 | * ) 16 | * @View( 17 | * template: "posted: {{ time | timeago }}", 18 | * pipes: const [TimeAgoPipe] 19 | * ) 20 | * class TimeAgoExample { 21 | * String millisStr = "${new DateTime.now().millisecondsSinceEpoch - (15 * 60 * 1000)}"; 22 | * } 23 | * 24 | * ``` 25 | */ 26 | @Pipe(name: 'timeago') 27 | @Injectable() 28 | class TimeAgoPipe implements PipeTransform { 29 | String _latestValue = null; 30 | String _latestResult = null; 31 | bool supports(dynamic str) { 32 | return str is String; 33 | } 34 | 35 | void onDestroy() { 36 | _latestValue = null; 37 | _latestResult = null; 38 | } 39 | 40 | String transform(String value, [List args = null]) { 41 | if (this._latestValue != value) { 42 | _latestValue = value; 43 | _latestResult = fuzzyTime.timeAgo(num.parse(value)); 44 | return _latestResult; 45 | } else { 46 | return _latestResult; 47 | } 48 | } 49 | 50 | create(ChangeDetectorRef cdRef) { 51 | return this; 52 | } 53 | 54 | TimeAgoPipe(); 55 | } 56 | -------------------------------------------------------------------------------- /lib/services/hn_api.dart: -------------------------------------------------------------------------------- 1 | library hacker_news.services.hn_api; 2 | 3 | import 'dart:async'; 4 | import 'package:firebase/firebase.dart'; 5 | import 'package:angular2/angular2.dart'; 6 | 7 | @Injectable() 8 | class HNApi { 9 | final fb = new Firebase('https://hacker-news.firebaseio.com/v0/'); 10 | 11 | Map userStore = {}; 12 | Map itemStore = {}; 13 | 14 | Future>> fetchTopStories() { 15 | return topStoriesRef().once('value') 16 | .then((DataSnapshot value) => (value.val() as Iterable>).take(10)); 17 | } 18 | 19 | Future fetchItems(List items) { 20 | List promises = []; 21 | items.forEach((String id) { 22 | promises.add(itemRef(id).onValue.first.then((value) { 23 | itemStore[id] = value.snapshot.val(); 24 | return new Future.value(itemStore[id]); 25 | })); 26 | }); 27 | 28 | return Future.wait(promises); 29 | } 30 | 31 | Future> fetchItem(String item) { 32 | if (item == null) { 33 | return new Future.error("item should not be null"); 34 | } 35 | return fetchItems([item]).then((List data) => data[0]) as Future>; 36 | } 37 | 38 | Future> fetchUser(String userId) { 39 | if (userId == null || userId.isEmpty) { 40 | return new Future.error("user id should not be null"); 41 | } 42 | 43 | return userRef(userId).onValue.first.then((Event value) { 44 | return new Future.value(value.snapshot.val()); 45 | }) as Future>; 46 | } 47 | 48 | Firebase topStoriesRef() { 49 | return fb.child('topstories/'); 50 | } 51 | 52 | Firebase itemRef(itemId) { 53 | return fb.child('item/' + itemId); 54 | } 55 | 56 | Firebase userRef(userId) { 57 | return fb.child('user/' + userId); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: 'ng2_hackernews' 2 | version: 0.0.1 3 | description: > 4 | A HackerNews application made with Angular2 Dart. 5 | author: Andres Araujo 6 | homepage: https://www.example.com 7 | environment: 8 | sdk: '>=1.0.0 <2.0.0' 9 | dependencies: 10 | firebase: ">=0.6.0 <0.7.0" 11 | angular2: "2.0.0-beta.0" 12 | timeago: any 13 | less_dart: any 14 | browser: any 15 | html: any 16 | dart_to_js_script_rewriter: '^0.1.0' 17 | 18 | dev_dependencies: 19 | test: '^0.12.6' 20 | angular2_testing: any 21 | 22 | transformers: 23 | - less_dart: 24 | include_path: ../lib/components/ 25 | - angular2: 26 | entry_points: web/main.dart 27 | - dart_to_js_script_rewriter 28 | -------------------------------------------------------------------------------- /test/domain_pipe_spec.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:ng2_hackernews/pipes/domain_pipe.dart'; 3 | 4 | main () { 5 | DomainPipe pipe; 6 | setUp(() async { 7 | pipe = new DomainPipe(); 8 | }); 9 | 10 | group('DomainPipe', () { 11 | test('Should return correct domain for url', () { 12 | expect(pipe.transform('www.google.com/asd'), equals('google.com')); 13 | expect(pipe.transform('http://www.google.com/asd'), equals('google.com')); 14 | expect(pipe.transform('https://www.google.com/asd'), equals('google.com')); 15 | expect(pipe.transform('http://www.google.com?asd=qwe'), equals('google.com')); 16 | }); 17 | }); 18 | } -------------------------------------------------------------------------------- /test/hn_item_spec.dart: -------------------------------------------------------------------------------- 1 | // Because Angular is using dart:html, we need these tests to run on an actual 2 | // browser. This means that it should be run with `-p dartium` or `-p chrome`. 3 | @TestOn('browser') 4 | import 'package:angular2/angular2.dart' 5 | show 6 | Component, 7 | View, 8 | NgFor, 9 | provide, 10 | Inject, 11 | Injectable, 12 | Optional, 13 | Provider, 14 | DebugElement, 15 | Directive; 16 | 17 | import 'package:angular2/src/mock/mock_application_ref.dart'; 18 | 19 | import 'package:angular2/router.dart' show RouterLink; 20 | 21 | import 'package:test/test.dart'; 22 | import 'package:angular2_testing/angular2_testing.dart'; 23 | import 'package:ng2_hackernews/components/hn_item/hn_item.dart'; 24 | import 'package:ng2_hackernews/services/hn_api.dart'; 25 | import 'dart:html' as dom; 26 | 27 | @Injectable() 28 | class MockHNApi implements HNApi { 29 | fetchItem(itemId) async { 30 | int time = 31 | (new DateTime.now().millisecondsSinceEpoch / 1000 - (15 * 60)).round(); 32 | return {'type': 'comment', 'time': time, 'text': 'My dummy comment'}; 33 | } 34 | 35 | noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); 36 | const MockHNApi(); 37 | } 38 | 39 | void main() { 40 | initAngularTests(); 41 | 42 | setUpProviders(() => [provide(HNApi, useClass: MockHNApi)]); 43 | 44 | group('HNItem Component', () { 45 | ngTest('should use mock hacker news api', (HNApi api) async { 46 | expect(api, new isInstanceOf()); 47 | }); 48 | 49 | group('When component is initialized', () { 50 | HNItem hnItem; 51 | dom.Element hnItemElement; 52 | 53 | ngSetUp((TestComponentBuilder tcb) async { 54 | Uri uri = new Uri.file( 55 | 'packages/ng2_hackernews/components/hn_item/hn_item.html'); 56 | String template = await dom.HttpRequest.getString(uri.toString()); 57 | var rootTC = await tcb 58 | .overrideTemplate(HNItem, template) 59 | .overrideDirective(HNItem, RouterLink, Dummy) 60 | .createAsync(HNItem); 61 | 62 | hnItem = rootTC.debugElement.componentInstance; 63 | hnItemElement = rootTC.debugElement.nativeElement; 64 | await hnItem.ngOnInit(); 65 | rootTC.detectChanges(); 66 | }); 67 | 68 | ngTest('should set data/timeago/type', () async { 69 | expect(hnItem.topLevel, equals(false)); 70 | expect(hnItem.collapsed, equals(false)); 71 | expect(hnItem.loadChildren, equals(true)); 72 | expect(hnItem.timeAgo, equals('15 minutes ago')); 73 | expect(hnItem.type, equals(itemMap['comment'])); 74 | 75 | expect(hnItemElement.querySelector('.hnItem--coment-content').text.trim(), equals('My dummy comment')); 76 | }); 77 | 78 | 79 | }); 80 | }); 81 | } 82 | 83 | @Directive(selector: '[routerLink]') 84 | class Dummy {} 85 | -------------------------------------------------------------------------------- /test/parse_html_directive_spec.dart: -------------------------------------------------------------------------------- 1 | // Because Angular is using dart:html, we need these tests to run on an actual 2 | // browser. This means that it should be run with `-p dartium` or `-p chrome`. 3 | @TestOn('browser') 4 | import 'package:angular2/angular2.dart' 5 | show 6 | Component, 7 | View, 8 | NgFor, 9 | provide, 10 | Inject, 11 | Injectable, 12 | Optional, 13 | Provider; 14 | 15 | import 'package:test/test.dart'; 16 | import 'package:angular2_testing/angular2_testing.dart'; 17 | import 'package:ng2_hackernews/directives/parse_html/parse_html.dart'; 18 | 19 | // This is the component we will be testing. 20 | @Component(selector: 'test-cmp') 21 | @View(template: '
{{raw}}
', directives: const [ParseHtml]) 22 | class TestComponent { 23 | String raw; 24 | TestComponent() { 25 | this.raw = "Dummy
Dummy2
"; 26 | } 27 | } 28 | 29 | void main() { 30 | initAngularTests(); 31 | 32 | ngTest('should parse a raw html string', (TestComponentBuilder tcb) async { 33 | var rootTC = await tcb 34 | .createAsync(TestComponent); 35 | 36 | rootTC.detectChanges(); 37 | expect(rootTC.debugElement.nativeElement.text, equals('DummyDummy2')); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /test/timeago_pipe_spec.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:ng2_hackernews/pipes/timeago_pipe.dart'; 3 | 4 | main () { 5 | TimeAgoPipe pipe; 6 | setUp(() async { 7 | pipe = new TimeAgoPipe(); 8 | }); 9 | 10 | group('DomainPipe', () { 11 | test('Should return correct domain for url', () { 12 | int time = new DateTime.now().millisecondsSinceEpoch - (15 * 60 * 1000); 13 | expect(pipe.transform('$time'), equals('15 minutes ago')); 14 | }); 15 | }); 16 | } -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Angular2 Hackernews Dart 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /web/main.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, . All rights reserved. Use of this source code 2 | // is governed by a BSD-style license that can be found in the LICENSE file. 3 | 4 | import 'package:angular2/angular2.dart' show provide; 5 | import 'package:angular2/bootstrap.dart' show bootstrap; 6 | import 'package:angular2/router.dart' show APP_BASE_HREF, HashLocationStrategy, LocationStrategy, ROUTER_PROVIDERS; 7 | 8 | import 'package:ng2_hackernews/ng2_hackernews.dart' show App; 9 | import 'package:ng2_hackernews/services/hn_api.dart'; 10 | 11 | main() { 12 | 13 | bootstrap(App, [ 14 | ROUTER_PROVIDERS, 15 | provide(APP_BASE_HREF, useValue: '/'), 16 | provide(LocationStrategy, useClass: HashLocationStrategy), 17 | provide(HNApi) 18 | ]); 19 | } -------------------------------------------------------------------------------- /web/main.less: -------------------------------------------------------------------------------- 1 | 2 | input { font-family:Courier; font-size:10pt; color:#000000; } 3 | input[type=submit] { font-family:Verdana, Geneva, sans-serif; } 4 | textarea { font-family:Courier; font-size:10pt; color:#000000; } 5 | 6 | a:link { color:#000000; text-decoration:none; } 7 | a:visited { color:#828282; text-decoration:none; } 8 | 9 | .default { font-family:Verdana, Geneva, sans-serif; font-size: 10pt; color:#828282; } 10 | .admin { font-family:Verdana, Geneva, sans-serif; font-size:8.5pt; color:#000000; } 11 | 12 | .adtitle { font-family:Verdana, Geneva, sans-serif; font-size: 9pt; color:#828282; } 13 | .yclinks { font-family:Verdana, Geneva, sans-serif; font-size: 8pt; color:#828282; } 14 | .pagetop { font-family:Verdana, Geneva, sans-serif; font-size: 10pt; color:#222222; } 15 | .comhead { font-family:Verdana, Geneva, sans-serif; font-size: 8pt; color:#828282; } 16 | .comment { font-family:Verdana, Geneva, sans-serif; font-size: 9pt; } 17 | .dead { font-family:Verdana, Geneva, sans-serif; font-size: 9pt; color:#dddddd; } 18 | 19 | .comment a:link, .comment a:visited { text-decoration:underline;} 20 | .dead a:link, .dead a:visited { color:#dddddd; } 21 | .pagetop a:visited { color:#000000;} 22 | .topsel a:link, .topsel a:visited { color:#ffffff; } 23 | 24 | .comhead a:link, .subtext a:visited { color:#828282; } 25 | .comhead a:hover { text-decoration:underline; } 26 | 27 | .default p { margin-top: 8px; margin-bottom: 0px; } 28 | 29 | .pagebreak {page-break-before:always} 30 | 31 | pre { overflow: auto; padding: 2px; max-width:600px; } 32 | pre:hover {overflow:auto} 33 | 34 | 35 | /** 36 | * Real Styles 37 | */ 38 | 39 | .u-pointer { 40 | cursor: pointer; 41 | } 42 | 43 | body { 44 | font-family: Verdana, Geneva, sans-serif; 45 | font-size: 10pt; 46 | color: #828282; 47 | } 48 | 49 | .bodyContainer { 50 | margin: 0 auto; 51 | width: 85%; 52 | max-width: 1280px; 53 | background: #F6F6EF; 54 | } 55 | 56 | .headerBar { 57 | background: #FF6600; 58 | } 59 | 60 | @import "packages/ng2_hackernews/components/spinner.less"; 61 | @import "packages/ng2_hackernews/components/hn_item/hn_item.less"; 62 | @import "packages/ng2_hackernews/components/item/item_page.less"; 63 | @import "packages/ng2_hackernews/components/user_page/user_page.less"; --------------------------------------------------------------------------------