#Best way to set initial values for a reactive form for updating items

69 messages ยท Page 1 of 1 (latest)

night gazelle
#

Is this the best way to do this? I'm using effect/constructor because that's what chat GPT spat out at me

Is there a way to do this using input.required<string>() or do I just need to use activeRoute.snapshot.params['id'] ?


export class BookUpdate {
  private bookService = inject(BookService)
  private router = inject(Router)
  private fb = inject(FormBuilder)
  id = input.required<string>()
  bookData = signal<any>(null);
  Using bookId = Number(this.activeRoute.snapshot.params['id'])
  book = rxResource({
    stream: () => this.bookService.fetchBookById(this.bookId)

  BookForm: FormGroup = this.fb.group({
    title: ['', Validators.required],
    author: ['', Validators.required],
    yearPublished: [null]
  })
  
  constructor() {
    const bookSignal = toSignal(this.bookService.fetchBookById(Number(this.id())))

    effect(() => {
      const data = bookSignal();
      if (data) {
        this.bookData.set(data);
        this.BookForm.setValue({
          title: data.title,
          author: data.author,
          yearPublished: data.yearPublished
        });
      }
    });
  }

  handleUpdate() {
    const book = this.BookForm.value as UpdateBookDto
    this.bookService.updateBook(Number(this.id()), book).subscribe({
      next: () => this.router.navigateByUrl('/books')
    })
  }

}```
brisk shadow
#

Could you format your code using triple backticks?

#

Also your code does not make a lot of sense as is.
You define a book property outside of a class so that'll be a compilation error?

night gazelle
brisk shadow
night gazelle
#

done

brisk shadow
#

For my own reference, how deep are you into angular/signals/reactive and declarative programming and how much do you want to understand that?

#

Also, is BookUpdate itself intended to be used directly in your routes.ts or is it a child-component?

night gazelle
#

I'd say I'm around an advanced beginner perhaps? I'm approaching the point to where I can sit down and memorize the total CRUD process and I have a reference for rxjs/signals for the addition of a cart to study. I would like to be employable, so as good an explanation as you want. If there's something I don't understand I'll look into it. BookUpdate is a child in the .routes.ts

book.routes.ts:

    { path: '', component: Book, pathMatch: 'full' },
    { path: 'create', component: BookCreate},
    { path: 'update/:id', component: BookUpdate},
    { path: ':id', component: BookDetails}
]

app.routes.ts

    {path: '', component: Home, pathMatch: 'full' },
    {path: 'books',  loadChildren: () =>
        import('./features/book/book.routes').then((m) => m.BookRoutes)
    },
    {path: 'checkout', loadChildren: () =>
        import('./features/cart/cart.routes').then((m) => m.CartRoutes)
    },
    { path: 'admin', loadChildren: () =>
        import('./features/admin/operations/books-management/book-management.routes').then((m) => m.BookManagementRoutes)
    }
]; 

not sure why the ```ts isn't working there

brisk shadow
#

Alright, so your BookUpdate is directly a component.
In that case for your first question:
Using input will not work for you - the component would need to be a child-component of another component that can pass in the data in question.

night gazelle
#

ah, I'm using provideRouter(routes, withComponentInputBinding()) in the config

brisk shadow
#

Note: Going forward when I speak of "reactive", it means observable or signal, anything that will update its own data as the value changes and force anything listening to it (so using computed, pipe-map or the like) to trigger or update.

However, if you need a signal, you can just make one from the reactive version of the id parameter from activatedRoute!
Notably, never use snapshot inside a component, you always want the reactive value, which in this case is the observable of param (I'm freeballing, iirc that's the property name)

#

The key here is that you can always swap between observables and signals via toSignal/toObservable that take in a reactive value of type A and spit out type B.

In your case, a reactive version of id (pseudocode):

private readonly id$ = this.activeRoute.params.pipe(
  map(params => Number(params['id'])
);
private readonly id = toSignal(this.id$);
brisk shadow
#

The alternative to just grab your reactive, automatically updating value from activatedRoute is just simpler imo

#

Of course there's always edge cases, but you're not running into one of those here

night gazelle
#

so activeRoute.params is by default an observable?

brisk shadow
#

yep, queryParams too

night gazelle
#

ok, so it'd be easier to just use observables in this case? what about inside the constructor since they're frowned upon? Is that the best way to do it?

brisk shadow
#

99% of the time you want to act upon the reactive version of data, snapshot is something you basically only ever want to see in resolvers, canActivateFn or similar.

brisk shadow
night gazelle
#

ok so it's better to leave it as just this:
private readonly id$ = this.activeRoute.params.pipe(
map(params => Number(params['id'])
);

from what you shared?

#

that was what chat GPT gave me since I didn't know how to do it, and I really don't trust chat GPT with anything angular... having a hard time finding reliable beginner resources

brisk shadow
night gazelle
#

ah, ok so you were saying I could use either the id$ (which is your preference in this case) or convert it to signal (which could make things a bit hard to find where they come from if done in excess)

brisk shadow
#

Yes, you can also immediately jump to "id", I just tend to to do that to do things more "step by step"

night gazelle
#

sounds reasonable

brisk shadow
#

By "jump to id" I mean you can wrap the entire assignment to "id$" in a toSignal immediately and bam, directly received the signal value

night gazelle
#

gotcha

brisk shadow
#

Ok, so, then let's go through this step by step
You have a component with a form that will always have an existing book dataset that depends upon the "id" param of the url and that should be used to prefill the form (and I assume potentially override existing form-data if the id parameter suddenly changes for some reason?).

So:

  • We need to have a resource that automatically updates itself as id changes => You do that via rxResource (never used that myself but it should do the trick, note that that one is experimental)
  • You already have your reactive form defined (note that you will need to associate that in your HTML with the corresponding form and input/textarea tags)
  • Now you just need to define your "side-effect" that updates your form every time your "book" updates (e.g. when it loads its data initially) and your form submission function
private readonly injector = inject(Injector);
private readonly destroyRef = inject(DestroyRef);
private readonly activatedRoute = inject(ActivatedRoute);
private readonly bookService = inject(BookService);

private readonly id = toSignal(... as we discussed ...);
private readonly book = rxResource({
  params: () => ({ bookId: this.id() }),
  stream: (params) => this.bookService.fetchBookById(params.bookId)
});

protected readonly bookForm = this.fb.group({...});

constructor(){
  this.setupSyncBooktoForm()
}

protected handleUpdate(){
  const updatedBookDto = this.BookForm.value as UpdateBookDto

  this.bookService.updateBook(this.id(), updatedBookDto)
    .pipe(takeUntilDestroyed(this.destroyRef))
    .subscribe(() => this.router.navigateByUrl('/books'))
}

private setupSyncBookToForm(){
  effect(() => {
    const initialBookData = this.book.value() // I think that's the signal property on an rxResource that gives you the value?
    if(!initialBookData) return;
    const newBookFormData = this.toFormData(initialBookData);
    this.bookForm.setValue(newBookFormData);
  }, { injector: this.injector})
}
#

For reference:

  • takeUntilDestroyed is there so that you abort the subscription if the component gets destroyed before the HTTP request can finish (e.g. user navigates away because the request takes a minute for some reason). In any other scenario it does not matter
  • I moved the effect into its own function because once you start having multiple side-effects it is cleaner to set up each side-effect individually and keep the constructor nice and easy to understand. If you do that though you typically might want to supply the injector that it would otherise grab implicitly when used in the constructor.
    Technically this isn't necessary here because the function is called inside the constructor, it's matter of taste, just note that if you remove the injector and call that outside of anywhere with an injection context, this will blow up
  • The book-resource will make an HTTP request every time id changes, which means book.value() will also change, which means that will also update your form accordingly
#

Also note: All of the above is pseudo-coded. Should get the idea across though

night gazelle
#

ok, thanks for the response! and a couple of questions.

how does params: () => work inside rxResource? I haven't used that yet
is this.destroyRef something I'm supposed to make? or is it just apart of the takeUntilDestroyed functionality?
this.book.value() is how you do it so you remembered correctly
how does !initialBookData work if it just ends the function early? why not have the rest of that stuff inside a { } block?

#

yeah it makes sense thanks

brisk shadow
# night gazelle ok, thanks for the response! and a couple of questions. how does params: () => ...

You might really want to just take a look at the angular docs for the individual things ๐Ÿ˜„
https://angular.dev/ecosystem/rxjs-interop#using-rxresource-for-async-data

params essentially is like a part of an effect that triggers the data-loading.
You can imagine that it works very strongly simplified something like this:

RxResource {
 ... properties and such ...
  constructor(){
    effect(() => {
      const params = this.config.params();
      ... trigger the rest of the data loading with this.config.stream(params) that also updates all the other variables and properties and such ...
    })
  }
#

is this.destroyRef something I'm supposed to make? or is it just apart of the takeUntilDestroyed functionality?
Angular provides a destroyRef for each component individually.
When you inject DestroyRef in a component, you always only get the destroyRef for that specific component.
All it does really is be something that fires when the component is about to get destroyed.
That allows you to react to the component getting destroyed, similar to ngOnDestroy but this allows for tools like takeUntilDestroyed to function ๐Ÿ˜„

#

how does !initialBookData work if it just ends the function early? why not have the rest of that stuff inside a { } block?
Keep in mind that the effect will always execute when book.value() changes.
That means initially it likely will execute while book.value() is undefined, right before it has requested any book.
That means, you have not loaded a book yet, and thus you don't really have anything to sync into the form, so it makes sense to return early aka abort doing anything

#

Once the book is loaded, book.value() will change, that will automatically re-execute the effect and sync the value into the form again

night gazelle
#

ah ok. thanks. Yeah I'll start looking at the angular docs when I see things I haven't messed with yet. appreciate the help

brisk shadow
# night gazelle ah ok. thanks. Yeah I'll start looking at the angular docs when I see things I h...

In general, strong recommendation:
You should generally be able to mark almost any property in a component "readonly", because the way angular wants you to code is declaratively.
That means, values inside reactive properties change, but typically not the properties themselves (aka never ever assign to id/id$ after the first initial assignment).

That means, any value that can change should be a signal or observable/subject.
Any value that is "derived" from them should be created via computed (signal) or .pipe (observable/subject)
You typically should not need effect or subscribe, or rather only need them rarely.
Most of the times it's needed if you need to sync data between various elements that contain their own observables, like how we sync from book to bookForm

#

If done right, typically nearly all of your code in a component will either be:

  • property assignments
  • private helper functions only used property assignments
  • private functions that set up side-effects you call in the constructor
  • functions that get called in an event-listener in your HTML

That's pretty much it

night gazelle
#

alright, thanks! really appreciate it

night gazelle
#

Hey! I actually have a question about the setupSyncBookToForm. What's the point in having an intermediary to .setValue the form? And then what's the point of the { injector: this.injector }?

    effect(() => {
      const book = this.book.value()
      if(!book) return;
      this.bookForm.setValue({
        title: book.title,
        author: book.author,
        yearPublished: book.yearPublished
      })
    }), { injector: this.injector}
  }

Is what I have right now, trying to get
const newBookFormData = this.toFormData(initialBookData);
this.bookForm.setValue(newBookFormData);

to work instead.
{ injector: this.injector} according to chatGPT is for the components injected context for when it isn't in the constructor, but when I move it into an ngOnInit it doesn't work.

Could you give me a bit more advice on those two?

brisk shadow
# night gazelle Hey! I actually have a question about the setupSyncBookToForm. What's the point ...

The "intermediary" step is mostly just my coding style, I like doing things step by step.
So:

  • Get book
  • Check if you have book
  • Transform book into data for form
  • update form with new data

With the transformation just moved into its own method (You'd need to define the method yourself), keeps that effect slightly smaller.

As for injector:
For effect and takeUntilDestroyed they both can only be defined where there is an injection context.
takeUntilDestroyed needs it to grab DestroyRef itself, effect needs it to grab the injector.
If you define either of them outside of the constructor or outside of a property assignment, you need to provide these things explicitly.

In this case it's strictly speaking not necessary, because you call your function from the constructor which IIRC should be enough to grant effect access to the constructor-injection-context to grab injector.
However, I dislike that this means you can only call this function from the constructor and if you call it elsewhere, it'll blow up with a runtime error, so I provide it explicitly.

night gazelle
#

alright! thanks again for explaining

empty timber
#

i'm loving this thread

night gazelle
#

"done" if interested:
app.config.ts : provideRouter(routes, withComponentInputBinding())
imports: [ReactiveFormsModule, RouterLink]

export class BookUpdate {
  private bookService = inject(BookService)
  private router = inject(Router)
  private fb = inject(FormBuilder)
  private readonly destroyRef = inject(DestroyRef)
  protected bookForm: FormGroup = this.fb.group({
    title: [''],
    author: [''],
    yearPublished: [null]
  })
  id = input.required<string>()

  protected book = rxResource({
    params: () => ({ id: Number(this.id())}),
    stream: ({ params }) => this.bookService.getBookById(params.id)
  })

  constructor() {
    this.syncData()
  }

  submitData() {
    const book = this.bookForm.value as BookUpdateDto
  // takeUntilDestroyed & destroyRef for edge case involving leaving the page due to slow load times causing the navigation to occur after closing the page, frustrating the user
    this.bookService.updateBook(Number(this.id()), book)
    .pipe(takeUntilDestroyed(this.destroyRef))
    .subscribe({
      next: () => this.router.navigateByUrl('/')
    })
  }

  syncData() {
    effect(() => {
      const book = this.book.value()
      if(!book) return
      this.bookForm.setValue({
        title: book.title,
        author: book.author,
        yearPublished: book.yearPublished})
    })
  }
}

with this being the HTML:

<form [formGroup]= "bookForm" (ngSubmit)="submitData()">
    <div>
        <label>Title
            <input type="text" placeholder="Enter book title" formControlName="title"/>
        </label>
    </div>
    <div>
        <label>Author
            <input type="text" placeholder="Enter author name" formControlName="author"/>
        </label>
    </div>
    <div>
        <label>Year Published
            <input type="text" placeholder="Enter year published" formControlName="yearPublished"/>
        </label>
    </div>
    <div>
        <button>Submit</button>
        <a routerLink="/">Cancel</a>
    </div>
</form>```
brisk shadow
#

If you want to do me a favor, please add a takeUntilDestroyed(this.destroyRef) do your subscription in submitData.
Otherwise you leave open an edge case where the user submits, request takes forever so they navigate away, response comes in after they left the site and suddenly they get navigated.

If the user navigates away from a page, they'd expect that to abort (I would claim)

night gazelle
brisk shadow
# night gazelle "done" if interested: app.config.ts : provideRouter(routes, withComponentInputBi...

I'd recommend looking into what valid, decent HTML looks like for forms and more generally:

  1. Inside forms, your input must either be inside a label tag (so that the input is associated with the label) or your label must have a "for" attribute with the id of the input-tag it is associated with.
  2. Never ever (unless you have some super special fancy case I have not seen yet in 5 years) use routerLink on a button.
    RouterLink belongs only on anchor tags. If you put it on a button, you are obscuring the fact that it will cause navigation to screenreaders (you also lose the right click context menu, opening in a new tab with ctrl+click, url preview at the bottom, context menu on mobile with long press on the button etc. etc.)
night gazelle
#

and actually when dealing with buttons you get a warning (maybe not exactly that) when you leave the button type as blank on that cancel button since it defaults to submit it warns you that you left without submitting. changing it to type="button" fixed that warning for me

brisk shadow
#

otherwise you will get some rude awakenings when you use a button tag in a component that somebody uses inside a form tag

#

Buttons are by default always type="submit", so any button in a form tag, regardless on how it is used, will trigger form submission if you click it.

night gazelle
#

Should I format it as a button then? Maybe I just need to go find some forms/google premade forms to see how other people do it

night gazelle
#

make it easily visible to the user that it's clickable

brisk shadow
#

"it" being the cancel-element ?

night gazelle
#

yes

brisk shadow
#

Yep, that is perfectly fine

night gazelle
#

updated the HTML so that the input was inside the label

#

ah, and changed the button to anchor. thanks for telling me about that stuff

brisk shadow
#

Check!
Strong recommendation from me to you if you want to strive for perfectionism, install yourself a screenreader, click inside the first form field, close your eyes and see if you can fill out the form without already knowing what the form looks like ๐Ÿ˜„

night gazelle
#

I'll check that out. that's a nice thought

brisk shadow
#

It's most likely not super necessary for you unless you live in the EU (and even then only in specific sectors), but using HTML properly has a decent amount of depth to it and knowing how to please screenreaders can be a challenge

#

If you ever have questions shoot them and feel free to ping, when it comes to accessibility I had to migrate entire social media products to be at least mostly barebones useable (didn't get the time for more) and Angular has quite a lot of tooling to help there (e.g. the angular/aria package)

#

In that regard, generally:
A button always represents performing an action on the current page, an anchor-tag always represents a navigation.
A button that only triggers a router-navigate call should be an anchor-tag with routerlink instead.

And if the user interacts with something via clicking it, 95% of the time it should be either a button or an anchor-tag, exception there only being if the functionality triggered by that click can already be triggered via a button/anchor-tag elsewhere on the same page and is just a "nicety" for mouse-users