Search code examples
angularjparxjsobservablesubscription

Angular, RxJs, Initializing a Component's Child Variable


What is the best way to initialize a JavaScript variable user.person in the UserEditComponent? I read that nested subscribes are not recommended. How else would I get the person fields firstName and lastName? Below is all the Angular code related to my question.

I have created a component, a service, and JPA entities.

The {{user.person.firstName}} is throwing an error since user.person is null.

The call to http://localhost:8080/users/ returns:

[{"id":"951bcd7e-a69c-4143-abc7-c2475ee249d2","version":0,"password":null,"username":null,"isActive":false,"createTime":1598588747997,"deactivatedTime":0,"person":{"id":"4340c6fc-8288-434c-a5ea-2497b72034a0","version":0,"firstName":"Amar","middleName":null,"lastName":"Patel","ssn":null}}]

The call to the userService.get(id) or http://localhost:8080/users/951bcd7e-a69c-4143-abc7-c2475ee249d2 returns:

{
  "password" : null,
  "username" : null,
  "isActive" : false,
  "createTime" : 1598588747997,
  "deactivatedTime" : 0,
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/users/951bcd7e-a69c-4143-abc7-c2475ee249d2"
    },
    "user" : {
      "href" : "http://localhost:8080/users/951bcd7e-a69c-4143-abc7-c2475ee249d2"
    },
    "person" : {
      "href" : "http://localhost:8080/users/951bcd7e-a69c-4143-abc7-c2475ee249d2/person"
    }
  }
}

This is what the call to http://localhost:8080/users/951bcd7e-a69c-4143-abc7-c2475ee249d2/person returns:

{
  "firstName" : "Amar",
  "middleName" : null,
  "lastName" : "Patel",
  "ssn" : null,
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/persons/4340c6fc-8288-434c-a5ea-2497b72034a0"
    },
    "person" : {
      "href" : "http://localhost:8080/persons/4340c6fc-8288-434c-a5ea-2497b72034a0"
    },
    "user" : {
      "href" : "http://localhost:8080/persons/4340c6fc-8288-434c-a5ea-2497b72034a0/user"
    }
  }
}

The Angular component:

@Component({
  selector: 'app-user-edit',
  templateUrl: './user-edit.component.html',
  styleUrls: ['./user-edit.component.css']
})
export class UserEditComponent implements OnInit, OnDestroy {

  user: any = {};

  sub: Subscription;

  constructor(private route: ActivatedRoute,
              private router: Router,
              private userService: UserService) { 

  }

  ngOnInit(): void {
    this.sub = this.route.params.subscribe(params => {
      const id = params.id;
      if (id) {
        this.userService.get(id).subscribe((user: any) => {
          if(user) {
            this.user = user;
            this.user.href = user._links.self.href;
          } else {
            console.log(`User with id '${id}' not found, returning to list`);
            this.gotoList();
          }
        });
      }
    });
  }

The Angular service:

@Injectable({
  providedIn: 'root'
})
export class UserService {
  public API = '//localhost:8080';
  public USER_API = this.API + '/users';

  constructor(private http: HttpClient) {
  }

  getAll(): Observable<any> {
    return this.http.get(this.API + '/users');
  }

  get(id: string) {
    return this.http.get(this.USER_API + '/' + id);
  }

  save(user: any): Observable<any> {
    let result: Observable<any>;
    if(user.href) {
      result = this.http.put(user.href, user);
    } else {
      result = this.http.post(this.USER_API, user);
    }

    return result;
  }

  remove(href: string) {
    return this.http.delete(href);
  }
}

The JPA User object:

@Entity
@NoArgsConstructor
public class User extends BaseEntity {

    @Basic
    private String password;

    @Basic
    @Column(unique = true, length = 100, columnDefinition = "VARCHAR(100)")
    private String username;

    @OneToOne
    @JoinColumn(name = "person_id")
    private Person person;

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Person getPerson() {
        return person;
    }

    public void setPerson(Person person) {
        this.person = person;
    }

}

The JPA Person object:

@Entity
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
public class Person extends BaseEntity {

    @Basic(optional = false)
    @Column(nullable = false, columnDefinition = "VARCHAR(255)")
    private String firstName;

    @Basic(optional = false)
    @Column(nullable = false, columnDefinition = "VARCHAR(255)")
    private String lastName;

    @OneToOne(mappedBy = "person", cascade = CascadeType.ALL)
    @JsonIgnore
    private User user;

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }
}

Solution

  • There are some fundamental decisions about the architecture that have to be made.

    First of all, you should introduce type definitions for User and Person. To name 3 possible definitions for User:

    1. A user object always contains a person object:
    interface User {
      ...
      person: Person;
      ...
    }
    
    1. A user object may contain a person object:
    interface User {
      ...
      person?: Person;
      ...
    }
    
    1. A user object contains the ID of a person object:
    interface User {
      ...
      _links: {
        person: string
      };
      ...
    }
    

    The design of the User type affects the question about who has the responsibility to fetch the person data, that means, to call http://localhost:8080/users/{id}/person. If you choose option 1, then it should be the task of the UserService and the UserEditComponent will never see a User object without an initialized Person. With option 3, the UserEditComponent is going to have two separate properties for the user and the person and it is the task of the component to explicitly fetch the person. Option 2 lies in the middle and both ways of initializing would be reasonable.

    Furthermore, this.user.href = user._links.self.href; does not seem to be something that a component should do. Services are responsible for transforming the data received from the backend.

    I hope that this answer goes in the right direction. Please feel free to further discuss it in the comments.