Skip to content

Instantly share code, notes, and snippets.

@peterbsmyth
Last active May 1, 2018 08:44
Show Gist options
  • Save peterbsmyth/ad520397da70c0d726cd8d326e6fe23d to your computer and use it in GitHub Desktop.
Save peterbsmyth/ad520397da70c0d726cd8d326e6fe23d to your computer and use it in GitHub Desktop.
Editable Card Recipe

Editable Card Recipe

Introduction

The follwing example of an editable card has 4 components:

  1. Home Page
  2. Editable Try Card
  3. Try Card Form
  4. Try Card

In context a 'try' standards for 'try this...', with a suggestion for handling a rhetorical situation.

Recipe

Home Page
.ts

import { Component, OnInit } from '@angular/core';
import * as fromRoot from '../../reducers';
import * as tryActions from '../../actions/try.actions';
import { map } from 'rxjs/operators';

import { Store, select } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
@Component({
  selector: 'arg-home-page',
  templateUrl: './home-page.component.html',
  styleUrls: ['./home-page.component.css']
})
export class HomePageComponent implements OnInit {
  complete$: Observable<any>;
  randomTry$: Observable<any>;
  editing$: Observable<any>;

  constructor(
    private store: Store<fromRoot.State>,
    private actions$: PopActions,
  ) {
    this.randomTry$ = store.pipe(select(fromRoot.getSelectedTry));
    this.editing$ = store.pipe(select(fromRoot.selectTryEditing));
  }


  ngOnInit() {
    this.store.dispatch(new tryActions.Get());
  }

  onSave(data) {
    this.store.dispatch(new tryActions.Put(data));
  }

  onEdit() {
    this.store.dispatch(new tryActions.Edit());
  }

  onRefresh() {
    this.store.dispatch(new tryActions.SetRandom());
  }

}

.html

<arg-navigation title="Home">
  <arg-editable-try-card [try]="randomTry$ | async" [editing]="editing$ | async" (save)="onSave($event)" (edit)="onEdit()" (refresh)="onRefresh()"></arg-editable-try-card>
</arg-navigation>

Editable Try Card
.ts

import { Component, OnInit, Input, Output, EventEmitter, } from '@angular/core';

@Component({
  selector: 'arg-editable-try-card',
  templateUrl: './editable-try-card.component.html',
  styleUrls: ['./editable-try-card.component.css']
})
export class EditableTryCardComponent implements OnInit {
  @Input() try;
  @Input() editing;
  @Output() save: EventEmitter<any> = new EventEmitter();
  @Output() edit: EventEmitter<any> = new EventEmitter();
  @Output() refresh: EventEmitter<any> = new EventEmitter();

  constructor() { }

  ngOnInit() {
  }

}

.html

<arg-try-card [try]="try" (edit)="edit.emit()" (refresh)="refresh.emit()" *ngIf="!editing; else editForm"></arg-try-card>
<ng-template #editForm>
  <arg-try-card-form [try]="try" (cancel)="edit.emit()" (save)="save.emit($event)" ></arg-try-card-form>
</ng-template>

Try Card
.ts

import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core';

@Component({
  selector: 'arg-try-card',
  templateUrl: './try-card.component.html',
  styleUrls: ['./try-card.component.css']
})
export class TryCardComponent implements OnInit {
  @Input() try;
  @Output() edit: EventEmitter<any> = new EventEmitter();
  @Output() refresh: EventEmitter<any> = new EventEmitter();

  constructor(
  ) {
  }

  ngOnInit() {
  }

}

.html

<section>
  <mat-card>
    <mat-card-header>
      <mat-card-title-group>
        <mat-card-title>Try This...</mat-card-title>
        <mat-card-subtitle>{{ try?.title }}</mat-card-subtitle>
      </mat-card-title-group>
      <mat-card-title-group>
        <button mat-icon-button (click)="refresh.emit()">
          <mat-icon>refresh</mat-icon>
        </button>
        <button mat-icon-button (click)="edit.emit()">
          <mat-icon>edit</mat-icon>
        </button>
      </mat-card-title-group>
    </mat-card-header>
    <mat-card-content>
      <div class="mat-body-1" [innerHTML]="try?.text | markdown"></div>
    </mat-card-content>
    <mat-card-actions>
    </mat-card-actions>
    <div class="footer">
      <span class="mat-body-1">#{{try?.id}}</span>
      <span class="mat-body-1">Chapter {{ try?.chapter?.id }}, Page {{ try?.page }}</span>
    </div>
  </mat-card>
</section>

Try Card Form
.ts

import { Component, OnInit, Input, EventEmitter, Output, NgZone } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';

@Component({
  selector: 'arg-try-card-form',
  templateUrl: './try-card-form.component.html',
  styleUrls: ['./try-card-form.component.css']
})
export class TryCardFormComponent implements OnInit {
  @Input() set try(data) {
    this.form.get('title').patchValue(data.title);
    this.form.get('text').patchValue(data.text);
    this.form.get('page').patchValue(data.page);
    this.form.get('id').patchValue(data.id);
    this.chapter = data.chapter.id;
  }

  @Output() save: EventEmitter<any> = new EventEmitter();
  @Output() cancel: EventEmitter<any> = new EventEmitter();
  form: FormGroup;
  chapter: number;
  id: number;

  constructor(
    private fb: FormBuilder,
    private zone: NgZone
  ) {
    this.form = this.fb.group({
      id: '',
      title: '',
      text: '',
      page: ''
    });
  }

  ngOnInit() {
  }

}

.html

<section>
  <form [formGroup]="form">
    <mat-card>
      <mat-card-header>
        <mat-card-title-group>
          <mat-card-title>Try This...</mat-card-title>
          <mat-card-subtitle>
            <mat-form-field>
              <mat-placeholder>Title</mat-placeholder>
              <input matInput formControlName="title">
            </mat-form-field>
          </mat-card-subtitle>
        </mat-card-title-group>
        <mat-card-title-group>
          <button mat-icon-button (click)="cancel.emit()">
            <mat-icon>cancel</mat-icon>
          </button>
          <button mat-icon-button (click)="save.emit(form.value)">
            <mat-icon>done</mat-icon>
          </button>
        </mat-card-title-group>
      </mat-card-header>
      <mat-card-content>
        <mat-form-field id="text">
          <mat-placeholder>Text</mat-placeholder>
          <textarea matInput formControlName="text"></textarea>
        </mat-form-field>
      </mat-card-content>
      <mat-card-actions>
      </mat-card-actions>
      <div class="footer">
        <span class="mat-body-1">#{{form.get('id').value}}</span>
        <span class="mat-body-1">
          <mat-form-field id="chapter">
            <mat-placeholder>Chapter</mat-placeholder>
            <input matInput [value]="chapter" disabled>
          </mat-form-field>,
          <mat-form-field id="page">
            <mat-placeholder>Page</mat-placeholder>
            <input matInput formControlName="page">
          </mat-form-field>
        </span>

      </div>
    </mat-card>
  </form>
</section>

Try Model
.ts

import { Chapter } from './chapter.model';

export interface Try {
  id: string;
  page: string;
  title: string;
  text: string;
  chapter: Chapter;
  createdAt?: string;
  updatedAt?: string;
}

Try Effects
.ts

import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { Action } from '@ngrx/store';
import { ApiService } from '../api.service';
import { TryActionTypes, Get, GetComplete, Put, PutComplete, SetRandom } from '../actions/try.actions';

import {
  map,
  mergeMap,
  switchMap,
  skip,
  takeUntil,
  catchError,
} from 'rxjs/operators';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class TryEffects {
  @Effect()
  getTry$: Observable<Action> = this.actions$.pipe(
    ofType<Get>(TryActionTypes.GET),
    switchMap(() => {
      return this.api.getTry()
        .pipe(
          mergeMap((tries) => [
            new GetComplete(tries),
            new SetRandom()
          ])
        );
    })
  );

  @Effect()
  putTry$: Observable<Action> = this.actions$.pipe(
    ofType<Put>(TryActionTypes.PUT),
    switchMap((action) => {
      return this.api.putTry(action.payload)
        .pipe(
          map(data => new PutComplete(data))
        );
    })
  );

  constructor(
    private actions$: Actions,
    private api: ApiService,
  ) { }
}

Try Reducer
.ts

import { Action } from '@ngrx/store';
import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity';
import { Try } from '../models/try.model';
import { TryActionTypes, TryActions } from '../actions/try.actions';
import { ChapterActionTypes, ChapterActions } from '../actions/chapter.actions';

export interface State extends EntityState<Try> {
  selectedId: string | number;
  editing: boolean;
}

export const adapter: EntityAdapter<Try> = createEntityAdapter<Try>({

});

export const initialState: State = adapter.getInitialState({
  selectedId: null,
  editing: false
});

export function reducer(state = initialState, action: TryActions | ChapterActions): State {
  switch (action.type) {
    case TryActionTypes.GET_COMPLETE:
      return adapter.addMany(action.payload, state);

    case TryActionTypes.SET_RANDOM:
      const randomIndex = Math.floor(Math.random() * (state.ids.length));
      const randomId = state.ids[randomIndex];
      return {
        ...state,
        selectedId: randomId
      };

    case TryActionTypes.EDIT:
      return {
        ...state,
        editing: !state.editing
      };

    case TryActionTypes.PUT_COMPLETE:
      const changes = {
        id: action.payload.id,
        changes: {
          text: action.payload.text,
          title: action.payload.title,
          page: action.payload.page,
        }
      };
      return adapter.updateOne(changes, {
        ...state,
        editing: false
      });

    default:
      return state;
  }
}

export const {
  selectIds: selectTryIds,
  selectEntities: selectTryEntities
} = adapter.getSelectors();

export const selectedId = (state: State) => state.selectedId;
export const editing = (state: State) => state.editing;

Selectors
.ts

export const selectTry = (state: State) => state.try;

export const selectTryIds = createSelector(
  selectTry,
  fromTry.selectTryIds
);

export const selectTryEntities = createSelector(
  selectTry,
  fromTry.selectTryEntities
);

export const selectTrySelectedId = createSelector(
  selectTry,
  fromTry.selectedId
);

export const selectTryEditing = createSelector(
  selectTry,
  fromTry.editing
);

export const getSelectedTry = createSelector(
  selectTryEntities,
  selectTrySelectedId,
  (entities, id) => entities[id]
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment