1

In my code I am trying to get the result from this observable:

// Runs fine
obtenerOfertaPorId(id: number): Observable<Oferta> {
    return this.http.get<Oferta>(`${this.apiUrl}${this.base_url}/obtenerOfertaPorId/${id}`)
      .pipe(
        tap(resultado => console.log(resultado)),
        catchError(error => {
          console.log('Error fetching oferta by id');
          return throwError(() => new Error(error))
        })
      );
  }

On this component:

export class JobPostDetailPage {
  
  private activatedRoute = inject(ActivatedRoute);
  private id = this.activatedRoute.snapshot.paramMap.get('id')!;
  private ofertaService = inject(OfertaService);
  jobPost = toSignal(this.ofertaService.obtenerOfertaPorId(parseInt(this.id)));
  
}

To send the result as an input signal to the children:

<navbar-wrapper/>
    <job-posting-form [jobPostInput]="jobPost()"/>
<wkFooter/>

And then the job-posting-form (This component is also for creating new entities, which is why is too convoluted, but I am open to ideas to make it more clean)

export class JobPostingForm {

  private ofertaMapper = OfertaMapper;
  private fb = inject(NonNullableFormBuilder);
  private titleCasePipe = new TitleCasePipe();
  private contrataService = inject(ContrataService);
  
  formUtils = FormUtilsJobPosting;
  router = inject(Router);
  jobPostInput = input<Oferta>();

  tiposContratoOptions = Object.values(TipoContrato).map(value => ({
    label: this.titleCasePipe.transform(value),
    value: value
  }));
  
  tiposModalidadesOptions = Object.values(ModalidadTrabajo).map(value => ({
    label: this.titleCasePipe.transform(value), 
    value: value                               
  }));
  

  jobPostingForm: FormGroup<OfertaFormGroup> = this.fb.group({
    puesto: [this.jobPostInput()?.puesto || '', [Validators.required, Validators.pattern(this.formUtils.onlyCharactersRegex), Validators.maxLength(23)]],
    sector: [this.jobPostInput()?.sector || '', [Validators.required, Validators.pattern(this.formUtils.onlyCharactersRegex), Validators.maxLength(23)]],
    descripcion: [this.jobPostInput()?.descripcion || '', Validators.maxLength(23)],
    ciudad: [this.jobPostInput()?.ciudad || '', [Validators.required, Validators.pattern(this.formUtils.onlyCharactersRegex), Validators.maxLength(23)]],
    horas: [this.jobPostInput()?.horas || null as number | null, [Validators.required, Validators.pattern(this.formUtils.onlyNumbersRegex)]],
    salarioAnual: [this.jobPostInput()?.salarioAnual || null as number | null, [Validators.required, Validators.pattern(this.formUtils.onlyNumbersRegex)]],
    modalidadTrabajo: [this.jobPostInput()?.modalidadTrabajo || null as ModalidadTrabajo | null, Validators.required],
    tipoContrato: [this.jobPostInput()?.tipoContrato || null as TipoContrato | null, Validators.required],
  });

  submitForm () {
    this.jobPostingForm.markAllAsTouched();

    if (this.jobPostingForm.valid) {
      this.contrataService.uploadNewOferta(
        this.ofertaMapper.mapNewOfertaFormGroupToOferta(this.jobPostingForm)
      ).subscribe({
        next: () => this.router.navigate(['employerSection','myJobPostings'])
      })
    }

  }

  goBackEvent () {
    this.router.navigate(['employerSection','myJobPostings'])
  }

}

My problem is that the input does not register the changes when the observable resolves and thus the form does not render the values from the observable request.

I already tried using the async pipe sending the observable on the input and I also tried using ngOnChanges to change the input value, but that is not possible.

Can it be done without lifecycle hooks? I am using Angular 20.

EDIT: Naren Murali and Lavi Kumar responses didn´t work straight but they were helpful to figure out a solution combining both, so thanks to them for their contributions.

Apparently if you set the value of a field from an input, the value of the FormControl won´t update by itself even if the value of the input changes:

sector: [this.jobPostInput()?.sector || '', [Validators.required, Validators.pattern(this.formUtils.onlyCharactersRegex), Validators.maxLength(23)]],

// This FormControl value won´t update by itself even if jobPostInput changes

So I had to manage it by myself.

First I have changed the way I get my Observable result by getting it with a reactive resource to handle the Observable:

// job-post-detail-page.ts

export class JobPostDetailPage {
  
  private activatedRoute = inject(ActivatedRoute);
  private id = computed(() =>
    parseInt(this.activatedRoute.snapshot.paramMap.get('id')!)
  );
  private ofertaService = inject(OfertaService);

  jobPost = rxResource({
    params: () => this.id(),
    stream: ({ params: id }) => this.ofertaService.obtenerOfertaPorId(id),
    defaultValue: {} as Oferta, // Required 'as Oferta', otherwise would fail
  });
  
}

Then I asked on the html if the resource was resolved:

<!-- job-post-detail-page.html -->
<navbar-wrapper/>
    @if (jobPost.status() === 'resolved') {
        <job-posting-form [jobPostInput]="jobPost.value()"/>
    }
<wkFooter/>

And finally, I declared a horrendous ugly effect to manually change the form values with the updated ones from the input:

export class JobPostingForm {
  /** Same code **/

  jobPostInput = input<Oferta>();

  jobPostingForm: FormGroup<OfertaFormGroup> = this.fb.group({
    puesto: ['', [Validators.required, Validators.pattern(this.formUtils.onlyCharactersRegex), Validators.maxLength(23)]],
    sector: ['', [Validators.required, Validators.pattern(this.formUtils.onlyCharactersRegex), Validators.maxLength(23)]],
    descripcion: ['', Validators.maxLength(500)],
    ciudad: ['', [Validators.required, Validators.pattern(this.formUtils.onlyCharactersRegex), Validators.maxLength(23)]],
    horas: [null as number | null, [Validators.required, Validators.pattern(this.formUtils.onlyNumbersRegex)]],
    salarioAnual: [null as number | null, [Validators.required, Validators.pattern(this.formUtils.onlyNumbersRegex)]],
    modalidadTrabajo: [null as ModalidadTrabajo | null, Validators.required],
    tipoContrato: [null as TipoContrato | null, Validators.required],
  });

  private _patchFormEffect = effect(() => {


    const jobPost = this.jobPostInput();
    if (jobPost) {
      this.jobPostingForm.patchValue({
        puesto: jobPost.puesto,
        sector: jobPost.sector,
        descripcion: jobPost.descripcion ?? '',
        ciudad: jobPost.ciudad,
        horas: jobPost.horas,
        salarioAnual: jobPost.salarioAnual,
        modalidadTrabajo: jobPost.modalidadTrabajo,
        tipoContrato: jobPost.tipoContrato,
      });
    }
  });

  /** Same code **/
}

Unnecesary overengineered solution imo

3 Answers 3

1

Your child form only reads jobPostInput() once at creation time. When the parent’s toSignal emits later, the form isn’t updated.

Use an effect() in the child to react to input-signal changes — no lifecycle hooks needed.

Parent

jobPost = toSignal(
  this.ofertaService.obtenerOfertaPorId(+this.id),
  { initialValue: null }
);

Child

jobPostInput = input<Oferta | null>();

constructor() {
  effect(() => {
    const value = this.jobPostInput();
    if (value) this.jobPostingForm.patchValue(value);
  });
}

This ensures the form updates when the observable resolves.

Sign up to request clarification or add additional context in comments.

Comments

0

You should use an httpResource or rxResource to wrap the API call.

Http Resource (Simple):

export class JobPostDetailPage {
  rs = ResourceStatus; // <- contains resource API status Codes
  private activatedRoute = inject(ActivatedRoute);
  private id = toSignal(this.activatedRoute.paramMap.get('id'));
  private ofertaService = inject(OfertaService);

  jobPost = httpResource(() => `${this.apiUrl}${this.base_url}/obtenerOfertaPorId/${this.id()}`, {
    defaultValue: {},
  });
  
}

Rx Resource (We can use the observable itself):

export class JobPostDetailPage {
  rs = ResourceStatus; // <- contains resource API status Codes
  private activatedRoute = inject(ActivatedRoute);
  private id = toSignal(this.activatedRoute.paramMap.get('id'));
  private ofertaService = inject(OfertaService);

  jobPost = rxResource({
    params: () => this.id(),
    stream: ({ params: id }) => this.ofertaService.obtenerOfertaPorId(parseInt(this.id)),
    defaultValue: {},
  });
}

Now we have a way to guarantee the component loads after the API is completed, we also have the option of showing a loader and error block using @if and status codes of resource API.

<navbar-wrapper/>
@let jobPostStatus = jobPost.status();
@let jobPostData = jobPost.value();
@if (jobPostStatus === rs.Resolved) {
  <job-posting-form [jobPostInput]="jobPostData"/>
} @else if (jobPostStatus === rs.Error) {
  Unknown API Error
} @else {
  Loading...
}
<wkFooter/>

Now your component is guaranteed to load post the API call is complete, If you do not want to hide the component when the API is still loading. We need to use an effect to wait for the API to complete in the child component then patch the values into the form. But we need to pass the Resource directly to the child component.

HTML:

TS:

export class JobPostingForm {
  jobPostInput = input.required<ResourceRef<JobPost>>();
  ...

  ...
  constructor() {
    effect(() => {
      @if(jobPostInput.status() === ResourceStatus.Resolved) {
        const jobPostData = jobPostInput.value();
        this.jobPostingForm.patchValue(jobPostData);
      }
    });
  }

  ...

Comments

0

In Angular 21, this just got easier with Angular Signal Forms.

First we pass in the data directly.

<navbar-wrapper/>
  @let jobPostData= jobPost.value();
  <job-posting-form [jobPostInput]="jobPostData"/>
<wkFooter/>

In the child component we can change the input to a model and directly pass it into the `form:

export class JobPostingForm {
  jobPost = model<JobPost>();
  jobPostingForm = form(this.loginModel);
  ...

In the HTML, you just need to use the field directive (import it) and bind it to the controls.

<input type="text" [field]="jobPostingForm.puesto" />
<input type="text" [field]="jobPostingForm.sector" />
....

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.