diff --git a/src/app/app.component.html b/src/app/app.component.html
index 282cbeb..e49e79c 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -2,4 +2,8 @@
Material Comments
-
\ No newline at end of file
+
+
+
\ No newline at end of file
diff --git a/src/app/app.component.scss b/src/app/app.component.scss
index e69de29..14dea18 100644
--- a/src/app/app.component.scss
+++ b/src/app/app.component.scss
@@ -0,0 +1,5 @@
+.app-body {
+ display: flex;
+ justify-content: center;
+ margin-top: 1rem;
+}
\ No newline at end of file
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index d5fda4e..6e40470 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -4,22 +4,37 @@ import { EffectsModule } from '@ngrx/effects';
import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
+import { MatToolbarModule } from '@angular/material/toolbar';
+import { MatCardModule } from '@angular/material/card';
+import { MatButtonModule } from '@angular/material/button';
+import { MatIconModule } from '@angular/material/icon';
+
import { AppComponent } from './app.component';
import { reducers, metaReducers } from './reducers';
+import { CommentStoreModule } from './store';
import { CommentCardComponent } from './comment-card/comment-card.component';
+import { CommentsListComponent } from './comments-list/comments-list.component';
+import { PipesModule } from './pipes/pipes.module';
@NgModule({
declarations: [
- AppComponent
+ AppComponent,
+ CommentCardComponent,
+ CommentsListComponent
],
imports: [
BrowserModule,
+ BrowserAnimationsModule,
StoreModule.forRoot(reducers, {
metaReducers
}),
EffectsModule.forRoot([]),
- BrowserAnimationsModule,
- MatToolbarModule
+ CommentStoreModule,
+ MatToolbarModule,
+ MatCardModule,
+ MatButtonModule,
+ MatIconModule,
+ PipesModule
],
providers: [],
bootstrap: [AppComponent]
diff --git a/src/app/comment-card/comment-card.component.html b/src/app/comment-card/comment-card.component.html
new file mode 100644
index 0000000..949de2e
--- /dev/null
+++ b/src/app/comment-card/comment-card.component.html
@@ -0,0 +1,17 @@
+
\ No newline at end of file
diff --git a/src/app/comment-card/comment-card.component.scss b/src/app/comment-card/comment-card.component.scss
new file mode 100644
index 0000000..bbc4d0f
--- /dev/null
+++ b/src/app/comment-card/comment-card.component.scss
@@ -0,0 +1,11 @@
+:host {
+ display: block;
+}
+
+:host:not(:first-of-type) {
+ margin-top: 1rem;
+}
+
+.comment-card {
+ max-width: 420px;
+}
\ No newline at end of file
diff --git a/src/app/comment-card/comment-card.component.spec.ts b/src/app/comment-card/comment-card.component.spec.ts
new file mode 100644
index 0000000..eb93cf8
--- /dev/null
+++ b/src/app/comment-card/comment-card.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CommentCardComponent } from './comment-card.component';
+
+describe('CommentCardComponent', () => {
+ let component: CommentCardComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ CommentCardComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CommentCardComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/comment-card/comment-card.component.ts b/src/app/comment-card/comment-card.component.ts
new file mode 100644
index 0000000..0bd8343
--- /dev/null
+++ b/src/app/comment-card/comment-card.component.ts
@@ -0,0 +1,25 @@
+import { Component, Input } from '@angular/core';
+
+import { CommentFacade, Comment } from '../store/comment';
+
+@Component({
+ selector: 'app-comment-card',
+ templateUrl: './comment-card.component.html',
+ styleUrls: ['./comment-card.component.scss']
+})
+export class CommentCardComponent {
+ @Input() data: Comment;
+
+ constructor(
+ private readonly commentFacade: CommentFacade
+ ) { }
+
+ toggleEdit(): void {
+ // todo
+ }
+
+ delete(): void {
+ this.commentFacade.delete(this.data.id);
+ }
+
+}
diff --git a/src/app/comments-list/comments-list.component.html b/src/app/comments-list/comments-list.component.html
new file mode 100644
index 0000000..aa11efb
--- /dev/null
+++ b/src/app/comments-list/comments-list.component.html
@@ -0,0 +1,9 @@
+
+
+
+ Loading...
+
\ No newline at end of file
diff --git a/src/app/comments-list/comments-list.component.scss b/src/app/comments-list/comments-list.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/comments-list/comments-list.component.spec.ts b/src/app/comments-list/comments-list.component.spec.ts
new file mode 100644
index 0000000..7d816e5
--- /dev/null
+++ b/src/app/comments-list/comments-list.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CommentsListComponent } from './comments-list.component';
+
+describe('CommentsListComponent', () => {
+ let component: CommentsListComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ CommentsListComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CommentsListComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/comments-list/comments-list.component.ts b/src/app/comments-list/comments-list.component.ts
new file mode 100644
index 0000000..764c4a2
--- /dev/null
+++ b/src/app/comments-list/comments-list.component.ts
@@ -0,0 +1,23 @@
+import { Component, OnInit } from '@angular/core';
+
+import { CommentFacade } from '../store/comment';
+
+@Component({
+ selector: 'app-comments-list',
+ templateUrl: './comments-list.component.html',
+ styleUrls: ['./comments-list.component.scss']
+})
+export class CommentsListComponent implements OnInit {
+ list$ = this.commentFacade.list$;
+
+ constructor(
+ private readonly commentFacade: CommentFacade
+ ) { }
+
+ ngOnInit(): void {
+ // load comments, typically would do this with an effect watching the route,
+ // but for simplicity I'm just calling it here.
+ this.commentFacade.getAll();
+ }
+
+}
diff --git a/src/app/pipes/join.pipe.spec.ts b/src/app/pipes/join.pipe.spec.ts
new file mode 100644
index 0000000..1999698
--- /dev/null
+++ b/src/app/pipes/join.pipe.spec.ts
@@ -0,0 +1,23 @@
+import { JoinPipe } from './join.pipe';
+
+describe('JoinPipe', () => {
+ let pipe: JoinPipe;
+
+ beforeEach(() => {
+ pipe = new JoinPipe();
+ });
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('should transform array into string with \',\' separator', () => {
+ const array = ['one', 'two', 'three', 'four'];
+ expect(pipe.transform(array)).toBe('one, two, three, four');
+ });
+
+ it('should transform array into string with \'::\' separator', () => {
+ const array = ['one', 'two', 'three', 'four'];
+ expect(pipe.transform(array, '::')).toBe('one::two::three::four');
+ });
+});
\ No newline at end of file
diff --git a/src/app/pipes/join.pipe.ts b/src/app/pipes/join.pipe.ts
new file mode 100644
index 0000000..430c385
--- /dev/null
+++ b/src/app/pipes/join.pipe.ts
@@ -0,0 +1,12 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'join'
+})
+export class JoinPipe implements PipeTransform {
+
+ transform(value: any[], separator = ', '): any {
+ return Array.isArray(value) ? value.join(separator) : value;
+ }
+
+}
\ No newline at end of file
diff --git a/src/app/pipes/pipes.module.ts b/src/app/pipes/pipes.module.ts
new file mode 100644
index 0000000..85fd07f
--- /dev/null
+++ b/src/app/pipes/pipes.module.ts
@@ -0,0 +1,22 @@
+import { NgModule } from '@angular/core';
+
+import { JoinPipe } from './join.pipe';
+import { SafeHTMLPipe } from './safehtml.pipe';
+
+const exportedPipes = [
+ JoinPipe,
+ SafeHTMLPipe
+];
+
+@NgModule({
+ declarations: [
+ ...exportedPipes,
+ ],
+ providers: [
+ ...exportedPipes
+ ],
+ exports: [
+ ...exportedPipes
+ ]
+})
+export class PipesModule { }
diff --git a/src/app/pipes/safehtml.pipe.ts b/src/app/pipes/safehtml.pipe.ts
new file mode 100644
index 0000000..f9fc3e5
--- /dev/null
+++ b/src/app/pipes/safehtml.pipe.ts
@@ -0,0 +1,17 @@
+import { Pipe, PipeTransform } from '@angular/core';
+import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
+
+@Pipe({
+ name: 'safeHTML'
+})
+export class SafeHTMLPipe implements PipeTransform {
+
+ constructor(
+ private readonly _sanitizer: DomSanitizer
+ ) { }
+
+ transform(value: string): SafeHtml {
+ return this._sanitizer.bypassSecurityTrustHtml(value);
+ }
+
+}
\ No newline at end of file
diff --git a/src/app/reducers/index.ts b/src/app/reducers/index.ts
index 4f57bb0..c1c6ed1 100644
--- a/src/app/reducers/index.ts
+++ b/src/app/reducers/index.ts
@@ -1,18 +1,13 @@
import {
- ActionReducer,
ActionReducerMap,
- createFeatureSelector,
- createSelector,
MetaReducer
} from '@ngrx/store';
import { environment } from '../../environments/environment';
export interface State {
-
}
export const reducers: ActionReducerMap = {
-
};
diff --git a/src/app/store/comment/comment.actions.ts b/src/app/store/comment/comment.actions.ts
new file mode 100644
index 0000000..cadb73c
--- /dev/null
+++ b/src/app/store/comment/comment.actions.ts
@@ -0,0 +1,37 @@
+import { createAction, props } from '@ngrx/store';
+import { Update } from '@ngrx/entity';
+
+import { Comment } from './comment.model';
+
+export const loadComments = createAction(
+ '[Comment/API] Load Comments'
+);
+
+export const loadCommentsSuccess = createAction(
+ '[Comment/API] Load Comments Success',
+ props<{ comments: Comment[] }>()
+);
+
+export const loadCommentsFailure = createAction(
+ '[Comment/API] Load Comments Failure'
+);
+
+export const addComment = createAction(
+ '[Comment/API] Add Comment',
+ props<{ comment: Comment }>()
+);
+
+export const upsertComment = createAction(
+ '[Comment/API] Upsert Comment',
+ props<{ comment: Comment }>()
+);
+
+export const updateComment = createAction(
+ '[Comment/API] Update Comment',
+ props<{ comment: Update }>()
+);
+
+export const deleteComment = createAction(
+ '[Comment/API] Delete Comment',
+ props<{ id: number }>()
+);
\ No newline at end of file
diff --git a/src/app/store/comment/comment.effects.ts b/src/app/store/comment/comment.effects.ts
new file mode 100644
index 0000000..fe173d9
--- /dev/null
+++ b/src/app/store/comment/comment.effects.ts
@@ -0,0 +1,30 @@
+import { Injectable } from '@angular/core';
+import { Actions, createEffect, ofType } from '@ngrx/effects';
+
+import { of } from 'rxjs';
+import { map, switchMap, catchError } from 'rxjs/operators';
+
+import { CommentService } from './comment.service';
+import * as CommentActions from './comment.actions';
+
+@Injectable()
+export class CommentEffects {
+
+ loadComments$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(CommentActions.loadComments),
+ switchMap(_ =>
+ this.api.getAll().pipe(
+ map(data => CommentActions.loadCommentsSuccess({ comments: data as any })), // debug type issue here
+ catchError(_ => of(CommentActions.loadCommentsFailure()))
+ )
+ )
+ )
+ );
+
+ constructor(
+ private api: CommentService,
+ private actions$: Actions
+ ) { }
+
+}
diff --git a/src/app/store/comment/comment.facade.ts b/src/app/store/comment/comment.facade.ts
new file mode 100644
index 0000000..aa732a6
--- /dev/null
+++ b/src/app/store/comment/comment.facade.ts
@@ -0,0 +1,23 @@
+import { Injectable } from '@angular/core';
+import { Store, select } from '@ngrx/store';
+
+import * as commentActions from './comment.actions';
+import * as query from './comment.selectors'
+
+@Injectable()
+export class CommentFacade {
+ list$ = this.store.pipe(select(query.getAll))
+
+ constructor(
+ private readonly store: Store
+ ) { }
+
+ getAll(): void {
+ this.store.dispatch(commentActions.loadComments());
+ }
+
+ delete(id: number): void {
+ this.store.dispatch(commentActions.deleteComment({ id }))
+ }
+
+}
diff --git a/src/app/store/comment/comment.model.ts b/src/app/store/comment/comment.model.ts
new file mode 100644
index 0000000..688ba96
--- /dev/null
+++ b/src/app/store/comment/comment.model.ts
@@ -0,0 +1,19 @@
+/**
+ * from spec
+{
+id: “1”,
+title: “This is an item”,
+text: “This is a description of the item, it might describe a bug/task/comment, it
+can also display Links”,
+tags: [“bug”, “issue”, “etc”]
+}
+ */
+
+export interface Comment {
+ id: number;
+ // created: Date;
+ // updated: Date;
+ title: string;
+ text: string;
+ tags: string[];
+}
diff --git a/src/app/store/comment/comment.module.ts b/src/app/store/comment/comment.module.ts
new file mode 100644
index 0000000..86df8f1
--- /dev/null
+++ b/src/app/store/comment/comment.module.ts
@@ -0,0 +1,22 @@
+import { NgModule } from '@angular/core';
+import { HttpClientModule } from '@angular/common/http';
+import { StoreModule } from '@ngrx/store';
+import { EffectsModule } from '@ngrx/effects';
+
+import { commentsFeatureKey, reducer } from './comment.reducer';
+import { CommentService } from './comment.service';
+import { CommentEffects } from './comment.effects';
+import { CommentFacade } from './comment.facade';
+
+@NgModule({
+ imports: [
+ HttpClientModule,
+ StoreModule.forFeature(commentsFeatureKey, reducer),
+ EffectsModule.forFeature([CommentEffects])
+ ],
+ providers: [
+ CommentService,
+ CommentFacade
+ ]
+})
+export class CommentStoreModule { }
diff --git a/src/app/store/comment/comment.reducer.spec.ts b/src/app/store/comment/comment.reducer.spec.ts
new file mode 100644
index 0000000..bc2ef74
--- /dev/null
+++ b/src/app/store/comment/comment.reducer.spec.ts
@@ -0,0 +1,13 @@
+import { reducer, initialState } from './comment.reducer';
+
+describe('Comment Reducer', () => {
+ describe('unknown action', () => {
+ it('should return the previous state', () => {
+ const action = {} as any;
+
+ const result = reducer(initialState, action);
+
+ expect(result).toBe(initialState);
+ });
+ });
+});
diff --git a/src/app/store/comment/comment.reducer.ts b/src/app/store/comment/comment.reducer.ts
new file mode 100644
index 0000000..d32ee96
--- /dev/null
+++ b/src/app/store/comment/comment.reducer.ts
@@ -0,0 +1,44 @@
+import { Action, createReducer, on } from '@ngrx/store';
+import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity';
+import { Comment } from './comment.model';
+import * as CommentActions from './comment.actions';
+
+export const commentsFeatureKey = 'comments';
+
+export interface State extends EntityState {
+ // additional entities state properties
+}
+
+export const adapter: EntityAdapter = createEntityAdapter();
+
+export const initialState: State = adapter.getInitialState({
+ // additional entity state properties
+});
+
+
+export const reducer = createReducer(
+ initialState,
+ on(CommentActions.addComment,
+ (state, action) => adapter.addOne(action.comment, state)
+ ),
+ on(CommentActions.upsertComment,
+ (state, action) => adapter.upsertOne(action.comment, state)
+ ),
+ on(CommentActions.updateComment,
+ (state, action) => adapter.updateOne(action.comment, state)
+ ),
+ on(CommentActions.deleteComment,
+ (state, action) => adapter.removeOne(action.id, state)
+ ),
+ on(CommentActions.loadCommentsSuccess,
+ (state, action) => adapter.setAll(action.comments, state)
+ ),
+);
+
+
+export const {
+ selectIds,
+ selectEntities,
+ selectAll,
+ selectTotal,
+} = adapter.getSelectors();
diff --git a/src/app/store/comment/comment.selectors.ts b/src/app/store/comment/comment.selectors.ts
new file mode 100644
index 0000000..de0c16b
--- /dev/null
+++ b/src/app/store/comment/comment.selectors.ts
@@ -0,0 +1,8 @@
+import { createFeatureSelector, createSelector } from '@ngrx/store';
+
+import { Comment } from './comment.model';
+import * as fromComment from './comment.reducer';
+
+export const getState = createFeatureSelector(fromComment.commentsFeatureKey);
+export const getAll = createSelector(getState, fromComment.selectAll);
+export const getEntities = createSelector(getState, fromComment.selectEntities);
\ No newline at end of file
diff --git a/src/app/store/comment/comment.service.ts b/src/app/store/comment/comment.service.ts
new file mode 100644
index 0000000..1cd211d
--- /dev/null
+++ b/src/app/store/comment/comment.service.ts
@@ -0,0 +1,16 @@
+import { Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+
+import { map } from 'rxjs/operators'
+
+@Injectable()
+export class CommentService {
+
+ getAll() {
+ return this.http.get('./assets/data.json').pipe(
+ map(response => response as Comment[])
+ )
+ }
+
+ constructor(private http: HttpClient) { }
+}
diff --git a/src/app/store/comment/index.ts b/src/app/store/comment/index.ts
new file mode 100644
index 0000000..55cd3dd
--- /dev/null
+++ b/src/app/store/comment/index.ts
@@ -0,0 +1,2 @@
+export * from './comment.facade';
+export * from './comment.model';
\ No newline at end of file
diff --git a/src/app/store/index.ts b/src/app/store/index.ts
new file mode 100644
index 0000000..b6c3f72
--- /dev/null
+++ b/src/app/store/index.ts
@@ -0,0 +1 @@
+export * from './comment/comment.module'
\ No newline at end of file
diff --git a/src/assets/data.json b/src/assets/data.json
new file mode 100644
index 0000000..4e9d42b
--- /dev/null
+++ b/src/assets/data.json
@@ -0,0 +1,32 @@
+[
+ {
+ "id": 1,
+ "title": "Comment One",
+ "text": "This is a description of the item, it might describe a bug/task/comment, it can also display Links",
+ "tags": [
+ "bug",
+ "issue",
+ "etc"
+ ]
+ },
+ {
+ "id": 2,
+ "title": "Comment Two",
+ "text": "This is a description of the item, it might describe a bug/task/comment, it can also display Links",
+ "tags": [
+ "bug",
+ "issue",
+ "etc"
+ ]
+ },
+ {
+ "id": 3,
+ "title": "Comment Three",
+ "text": "This is a description of the item, it might describe a bug/task/comment, it can also display Links",
+ "tags": [
+ "bug",
+ "issue",
+ "etc"
+ ]
+ }
+]
\ No newline at end of file