8

I seem to have gotten stuck on this matter for the last couple of days.

We're working on an Angular 2 application, and I need to create a wizard for users to fill out a form.

I've successfully managed to make the data flow through each step of the wizard, and save it in order to freely move back and forth. However, I can't seem to be able to reset it once the form is submitted.

I should add that each component is behind a wall. Maybe a better solution would be a singleton service injected directly at the AppModule. But I can't seem to make it work.

Here's my code so far:

Step 1

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { EventOption } from '../../../events/shared/event-option.model';
import { Store } from '@ngrx/store';
import { NewEventService } from '../shared/new-event.service';
import { Event } from '../../../events/shared/event.model';
import { FriendService } from '../../../friends/shared/friend.service';

@Component({
  selector: 'app-upload-images',
  templateUrl: './upload-images.component.html',
  styleUrls: ['../../../events/new-event/new-event.component.css']
})
export class UploadImagesComponent implements OnInit {
  form: FormGroup;
  private event;
  private images = [];

  constructor(
    private _store: Store<any>,
    private formBuilder: FormBuilder,
    private router: Router,
    private newEventService: NewEventService,
    private friendService: FriendService
  ) {
    _store.select('newEvent').subscribe(newEvent => {
      this.event = newEvent;
    })
  }

  ngOnInit() {
    this.initForm(this.event);
    if (this.event.counter === 0) {
      let friends = this.friendService.getFriends('58aaf6304fabf427e0acc08d');
      for (let friend in friends) {
        this.event.userIds.push(friends[friend]['id']);
      }
    }
  }

  initForm(event: Event) {
    this.images.push({ imageUrl: 'test0', voteCount: 0 });
    this.images.push({ imageUrl: 'test1', voteCount: 0 });
    this.images.push({ imageUrl: 'test2', voteCount: 0 });
    this.images.push({ imageUrl: 'test3', voteCount: 0 });
    this.form = this.formBuilder.group({
      firstImage: [this.event.length > 0 ? this.event.eventOption[0].imageUrl : null],
      secondImage: [this.event.length > 0 ? this.event.eventOption[1].imageUrl : null],
      thirdImage: [this.event.length > 0 ? this.event.eventOption[2].imageUrl : null],
      fourthImage: [this.event.length > 0 ? this.event.eventOption[3].imageUrl : null],
    })
  }

  next() {
    this.event.eventOptions = this.images;
    this.newEventService.updateEvent(this.event);
    this.router.navigate(['events/new-event/choose-friends']);
  }

}

Step 2

import { Component, OnInit, Input } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { EventOption } from '../../../events/shared/event-option.model';
import { Store } from '@ngrx/store';
import { Event } from '../../shared/event.model';
import { NewEventService } from '../shared/new-event.service';
import { FriendService } from '../../../friends/shared/friend.service';
import { SearchPipe } from '../../../core/search.pipe';

@Component({
  selector: 'app-choose-friends',
  templateUrl: './choose-friends.component.html',
  styleUrls: ['../../../events/new-event/new-event.component.css', './choose-friends.component.css']
})
export class ChooseFriendsComponent implements OnInit {

  private searchTerm = '';
  private event;
  private friends = [];
  private friendsError = false;

  constructor(
    private _store: Store<any>,
    private formBuilder: FormBuilder,
    private router: Router,
    private newEventService: NewEventService,
    private friendService: FriendService
  ) {
    _store.select('newEvent').subscribe(newEvent => {
      this.event = newEvent;
    })
  }

  ngOnInit() {
    this.friends = this.friendService.getFriends('58aaf6304fabf427e0acc08d');
  }

  selectedFriend(friendId: string) {
    return this.friendService.selectedFriend(friendId, this.event.userIds);
  }

  toggleFriend(friendId: string) {
    return this.friendService.toggleFriend(friendId, this.event.userIds);
  }

  toggleAllFriends() {
    return this.friendService.toggleAllFriends(this.friends, this.event.userIds);
  }

  submit() {
    if (this.event.userIds.length > 0) {
      this.newEventService.resetEvent();
      this.router.navigate(['events/vote-events']);
    } else {
      this.friendsError = true;
    }
  }

  back() {
    this.newEventService.updateEvent(this.event);
    this.router.navigate(['events/new-event/upload-images']);
  }

}

Event Service

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Store, Action } from '@ngrx/store';
import { Event } from '../../../events/shared/event.model';
import { EventOption } from '../../../events/shared/event-option.model';
import { newEvent, newEventModel } from './new-event.reducer';
import 'rxjs/add/operator/take';
import 'rxjs/add/operator/find';
import { Subject } from 'rxjs/Subject';

@Injectable()
export class NewEventService {
  public newEvent$: Observable<newEventModel>;

  constructor(private store: Store<newEventModel>) {
    this.newEvent$ = this.store.select('newEvent');
  }

  getEvent(event) {
    return this.store.dispatch({
      type: 'GET_EVENT',
      payload: event
    })
  }

  updateEvent(event) {
    return this.store.dispatch({
      type: 'UPDATE_EVENT',
      payload: event
    })
  }

  resetEvent() {
    return this.store.dispatch({
      type: 'RESET_EVENT',
    })
  }

}

Event Reducer

import { EventOption } from '../../shared/event-option.model';
import { EventType } from '../../shared/event-type.model';
import { ActionReducer, Action } from '@ngrx/store';
import { Event } from '../../shared/event.model';
import { FriendService } from '../../../friends/shared/friend.service';

export interface newEventModel {
  eventOptions: EventOption[];
  eventTypeId: number,
  duration: number,
  comment: string,
  privacyId: number,
  isGlobal: boolean,
  id: string,
  userIds: string[],
  counter: number
}

let blankState: newEventModel = {
  eventOptions: [],
  eventTypeId: null,
  duration: 1440,
  comment: '',
  privacyId: 0,
  isGlobal: false,
  id: '',
  userIds: [],
  counter: 0
}

let initialState: newEventModel = {
  eventOptions: [],
  eventTypeId: null,
  duration: 1440,
  comment: '',
  privacyId: 0,
  isGlobal: false,
  id: '',
  userIds: [],
  counter: 0
}

export const newEvent: ActionReducer<newEventModel> = (state: newEventModel = initialState, action: Action) => {
  // return new state
  switch (action.type) {
    case 'GET_EVENT':
      return state;
    case 'UPDATE_EVENT':
      action.payload.counter = action.payload.counter + 1;
      return action.payload;
    case 'RESET_EVENT':
      return Object.assign({}, state, {
        eventOptions: [],
        eventTypeId: null,
        duration: 1440,
        comment: '',
        privacyId: 0,
        isGlobal: false,
        id: '',
        userIds: [],
        counter: 0
      });
    default:
      return state;
  }
}

I could provide a working plunkr if needed, but I need to create it first.

TLDR: How can I reset the state on @ngrx/store?

Thanks for any help provided!

abullrich
  • 259
  • 1
  • 3
  • 13

5 Answers5

9

there is much easier way, you just need to set the initialState instead of state:

  const reducer = createReducer(initialState,
  on(proofActions.cleanAdditionalInsuredState, (state, action) => ({
    ...initialState
  })),
noy levi
  • 113
  • 2
  • 9
  • This makes total sense. The initial state is a goods way to reset the state with so few lines of code. No new Object.assign or other workaround necessary. Just simple and clean. This should be the accepted answer! – wmehanna Oct 01 '22 at 04:02
8

Noy Levi had the right thinking in her answer to this question, which assigns initialState back into state, however, there is a way to assign initialState for each reducer automatically.

The key concept to understand is that if the value of 'state' passed into a reducer is 'undefined' (not 'null', it needs to be 'undefined') then the reducer will automatically assign into 'state' the initialState provided to the reducer when it was created. Because of this default behavior, you can create a 'metareducer' that recognizes an action, say 'logout', and then passes a state of 'undefined' into all the subsequent reducers called.

This behavior is described well in this article about redux, this article about NgRx, and also in this answer about NgRx.

The relevant code would look like this:

export function logoutClearState(reducer) {
    return function (state, action) {
        if (action.type === ActionTypes.LOGOUT) {
            state = undefined;
        }
        return reducer(state, action);
    };
}

@NgModule({
    imports: [
        StoreModule.forRoot(reducers, { metaReducers: [logoutClearState] }),
    ],
    declarations: [],
    providers: [],
})
John Q
  • 1,262
  • 2
  • 13
  • 19
7

You can reset the state to initialState in your reducer by using Object.assign to copy all properties of initialState to a new object.

export const newEvent: ActionReducer<newEventModel> = (state: newEventModel = initialState, action: Action) => {
  // return new state
  switch (action.type) {
    // ...
    case 'RESET_EVENT':
      return Object.assign({}, initialState);
    // ...
  }
}

A note on the reducer

The reducer should be a pure function, so should not modify the arguments. Your UPDATE_EVENT requires a little tweak:

case 'UPDATE_EVENT':
  let counter = { counter: action.payload.counter + 1 };
  return Object.assign({}, action.payload, counter);

The pattern to follow is Object.assign({}, source1, source2, ...) where source1, source2 etc contain properties to be assigned. Properties in source1 are overwritten by duplicate properties in source2 etc.

clinton3141
  • 4,751
  • 3
  • 33
  • 46
  • Tried this solution, but unfortunately it didn't work. For whatever reason I get sent my base route (As if I'd made a redirect to an invalid URL). Will give a try to the note... – abullrich Feb 22 '17 at 19:54
  • @abullrich that would suggest that `this.router.navigate(['events/vote-events']);` is not correct. Do you mean `this.router.navigateByUrl("events/vote-events")`? – clinton3141 Feb 22 '17 at 20:00
  • Allow me to provide a working plunker if possible. I really appreciate your help though. – abullrich Feb 22 '17 at 20:08
  • Ok, working example has been provided. Once you're able to get it running. You need to click the **Click Me** button, **Next**, and the **Reset** button calls the theorethical reset that doesn't work. Let me know if you need any help. – abullrich Feb 22 '17 at 20:56
  • @abullrich there are a number of issues. [Something's modified `initialState`](https://d3uepj124s5rcx.cloudfront.net/items/3l070d0F1g041d1M3y40/Screen%20Shot%202017-02-22%20at%2021.27.46.png?v=b8ebb1c7) (see note in answer about pure functions - *this is important*), the button is submitting the form (so `next()` as well as `reset()` is running). Fix those and you'll be in a better position to debug the state. – clinton3141 Feb 22 '17 at 21:32
  • I'll mark your answer as correct, because you gave me some further insight on the topic. But readers should know it's not the answer that solved the problem. – abullrich Feb 24 '17 at 12:22
1

I'm assuming your RESET_EVENT is suppose to return a fresh object. Though you are filling in the object with your state data and another object:

case 'RESET_EVENT':
  return Object.assign({}, state, {
    eventOptions: [],
    eventTypeId: null,
    duration: 1440,
    comment: '',
    privacyId: 0,
    isGlobal: false,
    id: '',
    userIds: [],
    counter: 0
  });

The syntax for Object.assign is Object.assign(target, ...sources) and your providing two items as sources: state and the object containing eventOptions, eventTypeId, etc.

Instead you'll want to return Object.assign({}, initialState);

Tyler Jennings
  • 8,761
  • 2
  • 44
  • 39
  • Sadly it didn't work either. I get either sent to the previous navigated URL or my default route. I've seen the `...state` notation, maybe that's what I'm missing? – abullrich Feb 22 '17 at 19:56
  • `...state` syntax is called the spread operator. It allows multiple arguments to be expanded from a single argument. ie if a function takes 3 arguments and you have an array of 3 items, you can call that function using the spread operator `myFunction(...args)` – Tyler Jennings Feb 22 '17 at 20:00
  • If you preserve the log in your developer tools(Chrome), what error does it log before it reroutes to the default route? – Tyler Jennings Feb 22 '17 at 20:04
  • There doesn't seem to be any error. I'm either rerouted to the third step of the wizard, or to the default route... – abullrich Feb 22 '17 at 20:15
1

sorry, I took a day off in order to study for some exams. I ended up "solving" it by doing the following:

....
case 'RESET_EVENT':
  action.payload.eventOptions = blankState.eventOptions;
  action.payload.eventTypeId = blankState.eventTypeId;
  action.payload.duration = blankState.duration;
  action.payload.comment = blankState.comment;
  action.payload.privacyId = blankState.privacyId;
  ....
  return action.payload;
....

It might not be the prettiest or best solution, but at least it works. Thanks for all the help @iblamefish and everyone.

abullrich
  • 259
  • 1
  • 3
  • 13