Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Form with a dynamic array of objects inside plus validation #45

Closed
jacqui932 opened this issue Feb 9, 2018 · 17 comments
Closed

Form with a dynamic array of objects inside plus validation #45

jacqui932 opened this issue Feb 9, 2018 · 17 comments
Labels

Comments

@jacqui932
Copy link

Hi,

I have a model which is something like this:

Player {
ebuNumber: number;
firstName: string;
lastName: string
}

EntryForm{
comments: string;
heat: number;
players: Player[]
}

How would I go about creating the ngrx form to model this? I also need to be able to do the following:

  1. Dynamically add players to the players array - also default the size of this when the form is initially created to a known number of players which depends on the event they are entering (stored in another part of the store)

  2. Perform validation on each of the players - i.e., ebuNumber must be of a particular format and firstName/lastName are required fields for all players

  3. The heat is a required field based on the value which is stored in another part of the store - i..e, the event they are entering may or not have associated heats and only if the event has heats is the heat id mandatory

Any help you could give would be very much appreciated. :-)

@MrWolfZ
Copy link
Owner

MrWolfZ commented Feb 10, 2018

For 1. (initializing the state based on event information) and 3. (the heat being required or not) there are a couple of options.

If you can model your state so that the form state is a child of the state that contains the required event information then you can provide the additional information as parameters (see the second code example here for how this could be done).

However, I would recommend that you create a new action that contains all the required information. Then you can handle that action inside the reducer that contains the form state to initialize it properly. The initial player array can simply be set via setValue and the "heat required" information can be stored as a user defined property on the control. This value can then be used during validation.

Regarding the validation, you would simply create a three-level nested update function, i.e. an updateGroup for the whole EntryForm, then an updateArray for all Players, and inside that another updateGroup for each player.

PS: I am specifically not providing any code examples to try to enable you to figure this out by yourself. However, if you want more concrete examples let me know and I'll write the code for you. Just be aware that it is always better to write the code yourself :)

@jacqui932
Copy link
Author

Thank you :-). I'll go through all the info you have supplied and try to figure it out but I have tried unsuccessfully twice to get this working. The first time was about 2 months ago and I gave up after about 2 days and went back to using Angular reactive forms and the second time was yesterday when I tried for about 2 hours before writing the above question. I'm pretty new to Angular which is probably most of the problem :-)

@MrWolfZ
Copy link
Owner

MrWolfZ commented Feb 10, 2018

Alright, I've typed up some code that does what you need. Feel free to not look at it yet and try a bit more or ask questions. However, I understand that it is sometimes easier to learn from examples than to write everything yourself.

import { Action } from '@ngrx/store';
import {
  createFormGroupState,
  FormGroupState,
  updateGroup,
  validate,
  updateArray,
  formGroupReducer,
  setUserDefinedProperty,
  addArrayControl,
  cast,
} from 'ngrx-forms';
import { required, pattern, greaterThan } from 'ngrx-forms/validation';

export interface PlayerFormValue {
  ebuNumber: string;
  firstName: string;
  lastName: string
}

export interface EventFormValue {
  comments: string;
  heat: number;
  players: PlayerFormValue[]
}

export class InitializeEventFormAction implements Action {
  static readonly TYPE = 'my-actions/INITIALIZE_EVENT_FORM';
  readonly type = InitializeEventFormAction.TYPE;

  constructor(
    public heatIsRequired: boolean,
    public initialPlayers: PlayerFormValue[],
  ) { }
}

export class AddPlayerAction implements Action {
  static readonly TYPE = 'my-actions/ADD_PLAYER';
  readonly type = AddPlayerAction.TYPE;

  constructor(
    public newPlayer: PlayerFormValue,
  ) { }
}

type EventFormActions =
  | InitializeEventFormAction
  | AddPlayerAction
  ;

const HEAT_IS_REQUIRED_PROPERTY = 'heatIsRequired';
const EVENT_FORM_ID = 'EVENT_FORM';

const INIITIAL_STATE = createFormGroupState<EventFormValue>(EVENT_FORM_ID, {
  comments: '',
  heat: 0,
  players: [],
});

export function eventFormReducer(state = INIITIAL_STATE, action: EventFormActions) {
  const validateForm = updateGroup<EventFormValue>({
    heat: heat => heat.userDefinedProperties[HEAT_IS_REQUIRED_PROPERTY] ? validate([required, greaterThan(0)], heat) : heat,
    players: updateArray(
      updateGroup<PlayerFormValue>({
        ebuNumber: validate<string>([required, pattern(/^[0-9a-z]+$/)]),
        firstName: validate<string>(required),
        lastName: validate<string>(required),
      })
    ),
  });

  switch (action.type) {
    case InitializeEventFormAction.TYPE:
      state = createFormGroupState<EventFormValue>(EVENT_FORM_ID, {
        comments: '',
        heat: 0,
        players: action.initialPlayers,
      });

      state = setUserDefinedProperty(HEAT_IS_REQUIRED_PROPERTY, action.heatIsRequired, state);
      break;

    case AddPlayerAction.TYPE:
      state = updateGroup<EventFormValue>(state, {
        players: players => addArrayControl<PlayerFormValue>(action.newPlayer, cast(players)),
      });
      break;

    default:
      state = formGroupReducer(state, action);
  }

  return validateForm(state);
}

@jacqui932
Copy link
Author

Thank you so much :-). I really appreciate you taking the time out to help me like this.

@jacqui932
Copy link
Author

Another question (sorry :-)) - How would I go about updating the players array so that only the player referenced by a given index number is updated in the reducer? I have found the 'updateArray' function, but that seems to apply the given function to all elements of the array.

@MrWolfZ
Copy link
Owner

MrWolfZ commented Feb 11, 2018

That is indeed something that is still missing from the API. However, it is pretty easy to add this yourself. Have a look at this issue for the required code.

@jacqui932
Copy link
Author

Cool - thanks. One last question - is it possible to have userDefinedFields on each of the 'player' objects inside the array?

@MrWolfZ
Copy link
Owner

MrWolfZ commented Feb 14, 2018

Sure, just replace the players update with something like this:

players: updateArray(
  state => {
    state = setUserDefinedProperty('yourProperty', someValue, state);
    return updateGroup<PlayerFormValue>(state, {
      ebuNumber: validate<string>([required, pattern(/^[0-9a-z]+$/)]),
      firstName: validate<string>(required),
      lastName: validate<string>(required),
    });
  }
),

@jacqui932
Copy link
Author

Brill thanks - everything nearly working now! I just noticed a small issue though - on my form I have a 'ebuNumber' field and next to it a 'find player' button. The idea is that the person enters the ebu number, clicks the button and the first name/last name get loaded into two other fields.

It seems that what is happening is that when I click the button, the 'onBlur' event fires ok and the ebu number is updated in the form state (because I have [ngrxUpdateOn]="'blur'") but the 'Find player' action on the button is not fired. If I click the button again, the 'Find player' action is then fired. If I remove the [ngrxUpdateOn] from the field, every time a character is entered the control loses its focus and I have to click on it again to enter another character.

Any ideas on what I might be doing wrong?

@jacqui932
Copy link
Author

Also, I'm not sure where I should put the code you wrote above? I would like it to be part of the form initialization process:

I have the following at the moment:

// Inside the reducer but each player is an actual 'Player' object so need to do something after creating 'newState' maybe?....

const newState = createFormGroupState<EntryFormValue>(ENTRY_FORM_ID, {
        ... other entry form fields here
        players: initializePlayers(playerCount(action.payload.event)),  // each player in the array should have the user defined field set
      });

export function initializePlayers(numberOfPlayers: number): Player[] {
  return Array(numberOfPlayers).fill(initializePlayer()) as Player[];
}

export function initializePlayer(): Player {
  return {
    ebuNumber: null,
    firstName: '',
    lastName: ''
  };
}

@MrWolfZ
Copy link
Owner

MrWolfZ commented Feb 15, 2018

Regarding your second question you can simply perform an updateGroup after you have created the form state, i.e. you could do this:

let newState = createFormGroupState<EntryFormValue>(ENTRY_FORM_ID, {
  players: initializePlayers(playerCount(action.payload.event)),
});

newState = updateGroup<EventFormValue>(newState, {
  players: updateArray(setUserDefinedProperty('yourProperty', someValue)),
});

Regarding the first issue, do you by chance use *ngFor to iterate over the player group states and render each of them? If so, make sure you are using trackBy since otherwise the all elements inside the *ngFor will be rendered again whenever the state is updated, which happens on each keystroke. This would explain both why the focus is lost after each keystroke and why clicking the button does not work the first time.

@jacqui932
Copy link
Author

Brilliant - thank you :-)

@patricknazar
Copy link

@jacqui932 would you be kind enough to share some of the html you used to display this form? I'm having an issue where when I change an array item value control value it loses focus. Must do something with my ngFor loop.

@jacqui932
Copy link
Author

jacqui932 commented Feb 19, 2018

No problem.

In the HTML:

<div *ngFor="let player of playersFormState.controls;  trackBy: trackByIndex; let myIndex=index;">
... stuff to render the player here
</div>

In the component class:

export class EnterEventFormPlayersComponent {
trackByIndex(index, item) {
    return index;
  }
}

@patricknazar
Copy link

patricknazar commented Feb 19, 2018

Thanks, I was using trackBy: index which wasn't a function and then had to use [ngrxUpdateOn]="'blur'" and it fixed it.

@MrWolfZ
Copy link
Owner

MrWolfZ commented Feb 19, 2018

@patricknazar you shouldn't need to use update on "blur" for that unfocusing behaviour to go away. In general I recommend not using "blur" since it defers all validation etc to the blur event as well.

@patricknazar
Copy link

@MrWolfZ You're right. It was again an issue with my trackBy: typo!! Works a treat now thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants