Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Howto handle forms in a Angular 4 + Redux setup

I successfully implemented @angular-redux/store and redux in my minimal Angular 4 boilerplate. I do understand the idea behind the Redux loop, but I have a hard time to get me past the simple counter increment button examples.

At the moment I have build a simple login form as a component that needs to grab a JWT token from my API. I don't want to make it too complex, so for now I don't want to store the form state in the store, since a form component doesn't effect other components, right?

So when I hit the submit button, my login.component.ts will handle the validation and http request to my API. But a form submit is also an action, so when does Redux comes in to play here?

like image 653
user3411864 Avatar asked Aug 12 '17 13:08

user3411864


People also ask

How forms are handled in Angular explain both types of forms?

Angular provides two different approaches to handling user input through forms: reactive and template-driven. Both capture user input events from the view, validate the user input, create a form model and data model to update, and provide a way to track changes.

Should I store form data in Redux?

There is no “right” answer for this. Some users prefer to keep every single piece of data in Redux, to maintain a fully serializable and controlled version of their application at all times.

What are the two ways to build forms in angular6?

Let us now create our form in the app. We have created a simple form with input tags having email id, password and the submit button. We have assigned type, name, and placeholder to it. In template driven forms, we need to create the model form controls by adding the ngModel directive and the name attribute.


2 Answers

I know this is several months old now, but I respectfully disagree with both previous answers in that using the Reactive Forms methods for managing your forms creates a lot of challenges because then you're not using your store as the central and single location of truth -- you're mixing Redux philosophy with Angular philosophy for managing state and whenever you do that, it can get messy.

If you do not keep the form state in the store, then it is more challenging to bring that form state back if, for example, the user is in a flow and goes forward and comes back to the page they were just on. With the form state saved, you simply read the form state to bring it back. Otherwise, you have to come up with some other way to store the state, which will mean some janky passing around of data through parent and child components (assuming a Single Page Application style in the first place) -- which will also, notably, be separate from the redux store you're already managing.

If you're going redux, my battle-tested suggestion is to focus on making components dumb and having your store drive everything [as pure redux as possible]; meaning, all the components do is read from the store, assign local variables, and call services (some of which dispatch actions). You can use Reactive Forms (e.g. FormBuilder and FormGroups), but have the form validation driven by redux and your store (e.g. you can implement @angular-redux/form and intercept the @@angular-redux/form/FORM_CHANGED call in your own reducer using the action.payload.path to distinguish which form is being called to perform validation, etc. -- this also stores to the store on every user action and doesn't mean you store nothing if a finish/submit button isn't pressed, which is what would happen in Rahul's case).

While the previously given answers can work, I don't think they provide the best developer or user experiences. If you use redux, then stick with redux philosophy over Angular when managing state and user input whenever possible and you'll find the marriage between Angular and Redux to be a really fantastic union.

To respond directly to your specific question: "So when I hit the submit button, my login.component.ts will handle the validation and http request to my API. But a form submit is also an action, so when does Redux comes in to play here?" The way I would construct this is to have your login component make a service call that then makes an http call that returns an observable and then handle the response with your store, like with a service call to dispatch an action with a payload (changing the state of the page or storing information that is returned). Minimalist mock example below [Note: please remember your security practices of using https, clearing the store when appropriate, etc. if you're implementing a real login]:

login.component.ts:

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  stylesUrls: ['./login.component.css']
}
export class LoginComponent implements OnInit {
  form: FormGroup;
  username: string;
  password: string;
  login_failed: boolean;
  login_failed_message: string;
  @select(['main','login']) getLogin Observable<ILogin>;

  constructor(private loginService: LoginService, private fb: FormBuilder) {
  }

  ngOnInit() {
    // Watch Changes on state
    this.getLogin.subscribe(login => {
      // Read Login
      if (login) {
        this.username = login.username;
        this.password = login.password;
        this.login_failed = login.failed;
        this.login_failed_message = login.message;
      }
    }

    // Initialize Login Form
    this.form = this.fb.group({
      username: [this.username],
      password: [this.password],
    });
  }

  onSubmit() {
    this.loginService.submitLogin(this.login, this.password)
    .map(res => res.json())
    .subscribe(res => {
      if (res.status === 'success') {
        this.loginService.loadHomePage();
      } else {
        this.loginService.setLoginFailed();
      }
    },
    error => {
       this.loginService.setLoginServerCommunicationFailed(error);
    });
  }
}

login.service.ts:

export class LoginService {

  constructor(private ngRedux: NgRedux<IAppState>, private http: Http) {
  }

  submitLogin(login, password) {
    const jsonBody = {
      login: login,
      password: password,
    }
    return this.http.post(`/myApiAddress`, jsonBody);
  }

  loadHomePage() {
    this.ngRedux.dispatch({ type: 'LOGIN::HOME_PAGE'});
  }

  setLoginFailed() {
    this.ngRedux.dispatch({ type: 'LOGIN::LOGIN_FAILED'});
  }

  setLoginServerCommunicationFailed(message: string) {
    this.ngRedux.dispatch({ type: 'LOGIN::LOGIN_SERVER_UNREACHABLE', payload: message});
  }
}

login-form.reducer.ts:

import * as _ from 'lodash';
export function loginFormReducer(state: ILoginState, action: any) {
  switch (action.type) {
    case '@@angular-redux/form/FORM_CHANGED':
      // only acts on login form actions sent from @angular-redux/form
      if (_.isEqual(action.payload.path, ['login']) {
         // Real-time login validation
      }
      return state;
    default:
      return state;
  }
}

login.reducer.ts:

export function loginReducer(state: ILoginState, action: any) {
  switch (action.type) {
    case 'LOGIN::LOGIN_FAILED':
      return {
        ...state,
        login: {
          ...state!.login,
          failed: true,
          message: action.payload,
        }
      }
    case 'LOGIN::LOGIN_SERVER_UNREACHABLE':
      return {
        ...state,
        login: {
          ...state!.login,
          failed: true,
          message: 'Server Unreachable',
        }
      }
    default:
      return state;
  }
}

navigation.reducer.ts:

export function navigationReducer(state: IAppState, action: any) {
  switch (action.type) {
    case 'LOGIN::HOME_PAGE':
      return {
        ...state,
        router: '/home',
      }
    default:
      return state;
  }
}

login.interface.ts:

export interface ILogin {
  username: string;
  password: string;
  failed: boolean;
  message: string;
}

store.ts

// Other Reducers
import { routerReducer } from '@angular-redux/router';
import { navigationReducer } from '...';
import { mainReducer } from '...';
// Redux Imports
import { composeReducers, defaultFormReducer } from '@angular-redux/form';
import { combineReducers, applyMiddleware } from 'redux';
import { routerReducer } from '@angular-redux/router';
export const rootReducer = composeReducers(
  defaultFormReducer(),
  navigationReducer,
  combineReducers({
    router: routerReducer,
    main: mainReducer,
  })
);

main.reducer.ts

import { loginReducer } from '...';
import { loginFormReducer } from '...';
export const mainReducer =
  combineReducers({
    login: composeReducers(
      loginReducer,
      loginFormReducer,
    )
  })
like image 171
MapLion Avatar answered Sep 17 '22 12:09

MapLion


Forms are essentials a means to collect user data and all the validations are like checks that you perform on that data to make sure it is usable for your application for future processing.

For such a scenario a reactive form is the best you can use, it will give you all the flexibility you need to add validators and logic to the form and maintaining everything in a single spot.

Only after all validations and checks are done when you hit the submit you can dispatch the whole form data as a payload into the state as object.

like this this.store.dispatch({ type: Form_Data , payload : this.form.value});

Which will then move around your application. for further processing. as a part of your state.

For more info on using reactive forms check this link.

More on ngrx Link

A whole app build on ngrx v4 working example its repo link

like image 44
Rahul Singh Avatar answered Sep 17 '22 12:09

Rahul Singh