问题 如何处理Angular 4 + Redux设置中的表单


我成功实施了 @angular-redux/store 和 redux 在我最小的Angular 4样板中。 我确实理解Redux循环背后的想法,但我很难让我超越简单的计数器增量按钮示例。

目前我已经构建了一个简单的登录表单作为需要从我的API中获取JWT令牌的组件。 我不想让它太复杂,所以现在我不想将表单状态存储在商店中,因为表单组件不会影响其他组件,对吧?

所以,当我点击提交按钮时,我的 login.component.ts 将处理对我的API的验证和http请求。 但表单提交也是一个动作,所以Redux什么时候进来玩?


5518
2017-08-12 13:53


起源

我不认为将redux用于登录表单(这与状态无关)是有意义的,而是使用反应式表单,以便您可以使用内置功能轻松验证输入: angular.io/guide/reactive-forms - enf0rcer
它已经是一种反应形式。但登录和注销状态是对的吗? - user3411864
我对redux没有多少经验,但似乎这篇文章可能以某种方式回答你的问题。 brianflove.com/2017/04/10/angular-reactive-authentication - trungk18
@ user3411864除非您希望用户每次访问应用程序时都登录,我建议您将JWT存储在本地存储中。然后,您可以使用服务来检查用户是否仍然登录。您可以使用此服务来设置我猜的loggedIn状态。 - enf0rcer


答案:


没有必要在商店中保持表单状态。使用被动形式来处理验证和更改。你不必把所有东西都留在商店里。

当需要采取行动时,您将按照以下方式发送:

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

这个动作会触发一个 影响,它将调用登录API。成功或错误后,您需要发送新操作来处理它。 如果您的呼叫成功,您将从此效果中分配一个动作,例如 LOGIN_SUCCESS 您将在另一个效果中处理,例如将获取您将在商店中保存的“获取用户数据”API。至于 jwt,在效果中你可以保存它 localStorage 所以应用程序会记住登录状态。在您的代码中,您可能有一个 AuthenticationService 有可观察的财产 status,告诉用户是否已登录。

如果发生错误,您需要将其保存在商店的某些错误属性中并将其显示在表单上。


3
2017-08-21 14:29



所以发送与存储不一样?因为您将完整的表单有效负载发送到商店? - user3411864
@ user3411864 Dispatch仅启动操作。您必须在效果中处理此操作,并最终在reducer中处理。 reducer返回新状态,而示例的效果则调用API。行动本身什么都不做。有效载荷是任意的,你可以传递你所需要的任何动作。 - Matsura
我在墙上有这个图像 uploads.toptal.io/blog/image/121874/... 但可能它不再准确。效果在哪里适合这种模式?你指的是 @ngrx/effects ?从Udemy课程中我了解到我需要在component.ts文件中的onSubmit()函数中调度LOGIN_BUSY操作,对API执行异步调用,并在成功调度时调用第二次调用LOGIN_SUCCEED。这种方法有什么问题? - user3411864
@ user3411864没有错。是的,我指的是@ngrx / effects。它只是减速器和动作之间的一层,它为你提取了一些工作。正如我所说,您可以将它们用于API调用并分发新操作。您在课程中看到的内容是相同的,只有您在效果中执行异步调用,并且在成功时您将调度LOGIN_SUCCEED,然后您将在reducer中处理。 - Matsura
那么在架构图像中,效果库基本上是“DISPATCHER”块?它接收所有应用程序操作。将操作传递给不适用于效果的减速器,或采取旨在作为效果处理的操作并吐出一个或多个操作。我接近了吗? - user3411864


表单是收集用户数据的基本要素,所有验证都类似于您对该数据执行的检查,以确保它可用于您的应用程序以供将来处理。

对于这种情况,反应形式是您可以使用的最佳形式,它将为您提供在表单中添加验证器和逻辑并将所有内容保存在一个位置所需的所有灵活性。

只有在您点击提交后完成所有验证和检查后,您才能将整个表单数据作为有效负载发送到状态作为对象。

喜欢这个 this.store.dispatch({ type: Form_Data , payload : this.form.value});

然后,您将在您的应用程序中移动。进一步处理。作为你国家的一部分。

有关使用反应形式的更多信息,请检查此 链接

更多关于ngrx 链接

整个应用程序构建在ngrx v4工作  它的回购 链接


4
2017-08-15 18:22





我知道现在已有几个月了,但我恭敬地不同意以前的答案,因为使用Reactive Forms方法来管理表单会产生很多挑战,因为那时你并没有将你的商店用作真相的核心和单一位置 - 你将Redux哲学与Angular哲学相结合,以管理国家,无论何时你这样做,它都会变得混乱。

如果您没有将表单状态保留在商店中,那么将表单状态恢复为更具挑战性,例如,如果用户处于流程中并且前进并返回到他们刚刚访问的页面。保存表单状态后,您只需读取表单状态即可将其恢复。否则,您必须提出一些其他方式来存储状态,这意味着通过父组件和子组件传递数据(假设首先是单页应用程序样式) - 这也是值得注意的,与您已经管理的redux商店分开。

如果你要使用redux,我经过实战测试的建议是专注于让组件变得愚蠢,让你的商店驱动所有东西[尽可能纯粹的减少];意思是,所有组件都是从商店读取,分配本地变量和调用服务(其中一些是调度操作)。您可以使用Reactive Forms(例如FormBuilder和FormGroups),但具有由redux和您的商店驱动的表单验证(例如,您可以实现@ angular-redux / form并拦截 @@angular-redux/form/FORM_CHANGED 用你自己的减速机打电话 action.payload.path 区分正在调用哪个表单来执行验证等 - 这也存储在每个用户操作的商店中,并不意味着如果没有按下完成/提交按钮则不存储任何内容,这将发生在拉胡尔的案子)。

虽然之前给出的答案可行,但我认为它们并不能提供最佳的开发人员或用户体验。如果你使用redux,那么在管理状态和用户输入的时候,坚持使用redux哲学优于Angular,你会发现Angular和Redux之间的结合是一个非常神奇的联盟。

直接回答您的具体问题:“所以,当我点击提交按钮时,我的login.component.ts将处理验证和http请求到我的API。但表单提交也是一个动作,所以Redux什么时候进入在这玩吗?“我构建这个的方法是让你的登录组件进行一个服务调用,然后进行一个http调用,返回一个observable然后处理你的商店的响应,就像一个服务调用一起调度一个带有效负载的动作(更改页面状态或存储返回的信息)。下面的极简主义模拟示例[注意:请记住您使用https的安全做法,在适当时清除商店,等等,如果您正在实施真正的登录]:

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});
  }
}

登录-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,
    )
  })

2
2018-04-16 15:59