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

Comments

Projects
None yet
3 participants
@justinbmeyer
Contributor

justinbmeyer commented Mar 24, 2014

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

This comment has been minimized.

Show comment
Hide comment
@justinbmeyer

justinbmeyer Mar 24, 2014

Contributor

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);
          })
      }
    }
  }
}
Contributor

justinbmeyer commented Mar 24, 2014

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

This comment has been minimized.

Show comment
Hide comment
@matthewp

matthewp Mar 24, 2014

Contributor

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

Contributor

matthewp commented Mar 24, 2014

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

@matthewp

This comment has been minimized.

Show comment
Hide comment
@matthewp

matthewp Mar 24, 2014

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).

Contributor

matthewp commented Mar 24, 2014

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

This comment has been minimized.

Show comment
Hide comment
@justinbmeyer

justinbmeyer Mar 24, 2014

Contributor

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

Contributor

justinbmeyer commented Mar 24, 2014

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

@justinbmeyer

This comment has been minimized.

Show comment
Hide comment
@justinbmeyer

justinbmeyer Mar 24, 2014

Contributor

@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.

Contributor

justinbmeyer commented Mar 24, 2014

@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

This comment has been minimized.

Show comment
Hide comment
@justinbmeyer

justinbmeyer Mar 25, 2014

Contributor

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.

Contributor

justinbmeyer commented Mar 25, 2014

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

This comment has been minimized.

Show comment
Hide comment
@justinbmeyer

justinbmeyer Mar 25, 2014

Contributor

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.

Contributor

justinbmeyer commented Mar 25, 2014

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

This comment has been minimized.

Show comment
Hide comment
@justinbmeyer

justinbmeyer Mar 25, 2014

Contributor

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?

Contributor

justinbmeyer commented Mar 25, 2014

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

This comment has been minimized.

Show comment
Hide comment
@justinbmeyer

justinbmeyer Apr 27, 2014

Contributor

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.

Contributor

justinbmeyer commented Apr 27, 2014

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

This comment has been minimized.

Show comment
Hide comment
@rjgotten

rjgotten 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.

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

This comment has been minimized.

Show comment
Hide comment
@justinbmeyer

justinbmeyer May 5, 2014

Contributor

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

Contributor

justinbmeyer commented May 5, 2014

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

@rjgotten

This comment has been minimized.

Show comment
Hide comment
@rjgotten

rjgotten 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.

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

This comment has been minimized.

Show comment
Hide comment
@justinbmeyer

justinbmeyer May 5, 2014

Contributor

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>
Contributor

justinbmeyer commented May 5, 2014

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

This comment has been minimized.

Show comment
Hide comment
@rjgotten

rjgotten 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. ;)

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

This comment has been minimized.

Show comment
Hide comment
@justinbmeyer

justinbmeyer May 6, 2014

Contributor

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.

Contributor

justinbmeyer commented May 6, 2014

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

This comment has been minimized.

Show comment
Hide comment
@rjgotten

rjgotten 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.

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

This comment has been minimized.

Show comment
Hide comment
@justinbmeyer

justinbmeyer May 6, 2014

Contributor

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

Contributor

justinbmeyer commented May 6, 2014

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