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

Settable can.computes #824

Closed
justinbmeyer opened this issue Mar 24, 2014 · 17 comments
Closed

Settable can.computes #824

justinbmeyer opened this issue Mar 24, 2014 · 17 comments
Assignees
Milestone

Comments

@justinbmeyer
Copy link
Contributor

I would like a can.compute API that could set itself.

For example, the following would update its value with a new todo list everytime makeId changes.

var todos = can.compute(new Todo.List(), function(oldVal, setVal){
  Todo.findAll({makeId: map.attr("makeId")}, function(todos){
    setVal(todos);
  })
})

The following would update the list every time makeId changes.

var todos = can.compute(new Todo.List(), function(oldVal, setVal){
  Todo.findAll({makeId: map.attr("makeId")}, function(todos){
    oldVal.replace(todos);
  })
})

Essentially, we are using the "rerun when a source value changes" ability of compute, but not for an immediate value. Instead when a source value changes, we run the function with a setVal function that will be called back with the final value.

The signature as proposed will not work as it conflicts with another signature of can.compute. Please share any proposals that might work.

@justinbmeyer justinbmeyer added this to the 2.1.0 milestone Mar 24, 2014
@justinbmeyer
Copy link
Contributor Author

This relates to #819. I'd like a getter API that used this:

{
  define: {
    todos: {
      get: function(oldValue, setValue){
          Todo.findAll({makeId: map.attr("makeId")}, function(todos){
            setValue(todos);
          })
      }
    }
  }
}

@matthewp
Copy link
Contributor

I like it, can we figure out a way to do this and #816 as one signature though?

@matthewp
Copy link
Contributor

What if it were one of the existing signatures, but we added a function to the compute that could be used in this way. For example:

var todos = can.compute(new Todo.List()).later(function(oldVal, setVal) {

});

That way any existing signature could be used async. (later might not be the right name).

@justinbmeyer
Copy link
Contributor Author

@matthewp Ha! I forgot I created the other issue. Closing that one.

@justinbmeyer
Copy link
Contributor Author

@matthewp I like the idea. It feels deferred ish.

A small issue with that is it would involve creating 2 computes ... the initial one and the one that gets called when there are changes. One would be setup to change the other.

This 2x the number of change events being fired. I'm not sure about performance.

@justinbmeyer
Copy link
Contributor Author

I think a signature like:

var todos = can.compute(new Todo.List(), function(newVal, oldVal, setVal){
  Todo.findAll({makeId: map.attr("makeId")}, function(todos){
    setVal(todos);
  })
})

will work now.

@justinbmeyer
Copy link
Contributor Author

That will not work. I was thinking something like:

var obj = can.compute({}, {
        value: {},
    fn: function( curVal, setVal ){
        if(a()) {
            curVal.a = a();
        } else {
            delete curVal.a;
        }
        if(b()) {
            curVal.b = b();
        } else {
            delete curVal.b;
        }
    }
});

But the problem with all of this ... what is the value of an unbound compute? It will not be correct.

@justinbmeyer
Copy link
Contributor Author

So, I think this should not be through the compute constructor because it is returning a compute that you can read but might not have the current best value. Only a "bound" async compute will have the current value.

can.compute.async

@function can.compute.async

Create a compute that can set its value after the computed function has been called.

@Signature can.compute.async(initialValue, computed(currentValue, setValue(newValue) )

@param {*} The initial value of the compute.
@param {can.compute.asyncComputer} computed(currentValue, setValue) A function that returns the current value of the compute and can optionally later call its setValue callback to update the value.

@return {can.computed} Returns a compute, but a compute that will possibly not have the correct value unless it is bound to.

@Body

Use

The following compute is a live list of todos for a given userId. todos value would alternate between null and a Todo.List as userId changes.

var userId = can.compute(5)

var todos = can.compute.async(null, function(oldTodoList, setValue){
  Todo.findAll({ userId: userId() }, function(todos){
    setValue(todos)
  });
  return null;
})

The following replaces the list in place:

var userId = can.compute(5)

var todos = can.compute.async(new Todo.List(), function(todoList, setValue){
  todoList.replace( Todo.findAll({ userId: userId() })
  return todoList;
})

can.compute.asyncComputer

@typedef {function(*,function)} can.compute.asyncComputer(currentVal, setVal)

A function that determines a value for an [can.compute.async async compute].

@option {function(*,function)} can.compute.asyncComputer(currentVal, setVal)

@param {*} currentVal The current value of the compute. This should be returned
if you are doing an in-place compute.

@param {function(*)} setVal(newVal) Called to update the value of the compute at a later time.

@return The immediate value of the compute.

Thoughts?

@justinbmeyer
Copy link
Contributor Author

I am not going to document this for 2.1. can.Map define will use it, but I'm not sure it's super useful outside maps. I've added the documentation but hidden it. We can unhide it for 2.2 if people crave it.

@rjgotten
Copy link

rjgotten commented May 5, 2014

I'm not sure it's super useful outside maps

It's useful beyond can.Map to support things like lazy loading certain content in views. E.g. displaying a spinner until an image has finished downloading. If the code is there and has to be maintained anyway, then I'd definitely like it to be sanctioned and available officially in its 'raw' can.compute.async form for those particular one-off scenarios.

@justinbmeyer
Copy link
Contributor Author

How would you use async for a spinner image on its own?

@rjgotten
Copy link

rjgotten commented May 5, 2014

How would you use async for a spinner image on its own?

Write a component that has a can.compute.async.

Inside said compute, create an img element, hook up its img.onload and img.onerror to call setValue with the img element, then set the img.src property to start downloading the image and finally return null synchronously to indicate the 'loading' state.

Listen to the compute for changes. When a change occurs, wipe the existing children of the component's containing element and either append the newly resolved element or, if the resolved element is null, set a background image spinner GIF (probably as a data-uri to ensure it is available synchronously).

The value for the img.src property can be sourced from an accessor function brought in as a constructor option. In that way the component can be tied generically to whatever concrete can.Map subtype you may want.

Sure, you can hack this today as well; but it involves a substantial amount of book-keeping you have to perform manually. Having can.compute.async directly available would remove that overhead.

@justinbmeyer
Copy link
Contributor Author

I would not do it that way. I would probably do it like:

can.Component({
  tag: "img-spinner",
  template: "<img {{^ready}}style='display:none'{{/ready}} src='{{src}}' can-load='loaded'/>"+
     "{{^ready}}<img src='{{spinnerSrc}}'/>{{/ready}}"
  scope: {
    loaded: function(){
      this.attr("ready", true);
    }
  }
})

stache:

<img-spinner src="cat.png" spinner-src="dog.png"></img-spinner>

@rjgotten
Copy link

rjgotten commented May 6, 2014

I would not do it that way. I would probably do it like

Depending on the order in which the templating system creates the img element, sets its src property and binds the load event, you may or may not run into a race condition where load is never triggered for images arriving from cache.

Also; you're somewhat conveniently discarding the error handling case, which requires also incorporating a timeout somewhere for IE, as it does not reliably fire events for timed out image requests...

Loading images async reliably is not something you can solve trivially from simple template markup like that. ;)

@justinbmeyer
Copy link
Contributor Author

As all of that happens in the same "turn", it should not miss load.

Sent from my iPhone

On May 6, 2014, at 9:19 AM, rjgotten notifications@github.com wrote:

I would not do it that way. I would probably do it like

Depending on the order at which the templating system creates image element, sets its src property and binds its load event (you're discarding the error handling case, btw.) , you may or may not run into a race condition where load is never triggered for images arriving from cache.


Reply to this email directly or view it on GitHub.

@rjgotten
Copy link

rjgotten commented May 6, 2014

As all of that happens in the same "turn", it should not miss load.

It could in IE, which resolves images from cache and executes any bound load event handlers synchronously; without deferring to the next tick. But anyway; this is already veering off-topic, the example was only meant to illustrate that there are still use cases where having can.compute.async directly exposed for use may be a useful thing.

@justinbmeyer
Copy link
Contributor Author

Can you write out how you would use async explicitly for image loading.

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

No branches or pull requests

3 participants