Refreshing virtual async properties

I often define service calls in my view models like this:

define: {
  person: {
    get(lastVal, setVal) {
      PersonModel.findOne({ id: this.attr('personId') }).then(setVal);
    }
  }
}

A person binding will cause the initial service call but there is no way to update the person if a user action requires it to be refreshed. What do people recommend for this? I have two approaches I am currently using depending on the situation:

Using a method with the events object

Here I define the service call in the method getPerson() and call it in events.inserted(). I can then call the method anytime I want to refresh the person.

can.Component.extend({
  //...
  viewModel: {
    getPerson() {
      PersonModel.findOne({ id: this.attr('personId') }).then(person => {
        this.attr('person', person);
      });
    }
  },
  events: {
    inserted(el, ev) {
      this.viewModel.getPerson();
    }
  }
});

It’s simple and straight forward but maybe we could avoid using the events object.

Making extra bindings

We can do it all in the view model so that simply a person binding triggers the initial service call. By adding an extra binding within the person virtual property, we can change the depended-upon property to trigger a refresh.

can.Component.extend({
  //...
  viewModel: {
    define: {
      person: {
        get(lastVal, setVal) {
          // bind to extra property
          this.attr('extraProp');
          PersonModel.findOne({ id: this.attr('personId') }).then(setVal);
        }
      }
    },
    extraProp: 1,
    refreshPerson() {
      // increment extra property to cause change
      this.attr('extraProp', this.attr('extraProp') + 1);
    }
  }
});

Thoughts

I’m not sure I like either way. Does anyone have an alternative?

1 Like

If you are using models … the instance should be in the instance store. Couldn’t you just have a method called refreshPerson like:

define: {
  person: {
    get(lastVal, setVal) {
      PersonModel.findOne({ id: this.attr('personId') }).then(setVal);
    }
  }
},
refreshPerson: function(){
  PersonModel.findOne({ id: this.attr('personId') })
}

If something can call refreshPerson() the model should do the ajax request and update the Person instance in the store.

1 Like

Hmm… good thinking. Both of my use cases are actually lists but that should work as long as I have ids, correct? One of them does not really have ids but does have a unique field which should suffice if I define that field as the id for the model.

I’m thinking of doing this to avoid repeating the service call code:

define: {
  person: {
    get(lastVal, setVal) {
      this.getPerson().then(setVal);
    }
  }
},
getPerson() {
  return PersonModel.findOne({ id: this.attr('personId') });
}

Looks good to me. Thanks @justinbmeyer!

That works fine when the list is the same and only properties of the instances change, but if an item should no longer be in the list after the refresh, the item remains on the screen because that’s not how the bindings are set up (using an #each).

This seems to work as long as you don’t have a default:

define: {
  people: {
    get(lastVal) {
      if (lastVal) {
        return lastVal;
      }
      this.getPeople();
    }
  }
},
getPeople() {
  PersonModel.findAll({}).then(people => {
    this.attr('people', people);
  });
}

One of my data sets does have a default, though, so part of the problem is differentiating between the default value and the actual value from the service. I thought of comparing the value to the map’s cached defaults but lastVal is already a map so I can’t test for equality.

So I basically have 3 requirements:

  • default value for people
  • async fetch when people is bound
  • method that will refresh people on demand

This should work with can.connect which is able to add/remove items from a list in the list store.