can.route crossbind to can.Map #752

Closed
moschel opened this Issue Feb 24, 2014 · 7 comments

Comments

Projects
None yet
4 participants
@moschel
Contributor

moschel commented Feb 24, 2014

A common pattern in our apps is binding a can.Map object (representing application state) to the can.route, and vice versa. We need a cleaner API for this. Often the code looks like:

// Change the route when the app state changes.
appState.bind('change', function(ev) {
  var currentState = appState.attr();
  can.route.attr({
    searchTerm: currentState.searchTerm,
    flags: currentState.flags.join('')
  }, true);
});

// Change the app state when the route changes.
can.route.bind('change', function() {
  appState.attr('searchTerm', can.route.attr('searchTerm') || '');
  appState.attr('flags', can.route.attr('flags') ? can.route.attr('flags').split('') : []);
});

But with additional logic accounting for ev.batchNum being unique.

This issue proposes an API like:

can.route.data( appState )

We'll possibly need options for that method to map appState properties to what they'll be called in can.route.

@moschel moschel added this to the 2.1.0 milestone Feb 24, 2014

@moschel moschel added the Feature label Feb 24, 2014

@moschel moschel self-assigned this Feb 24, 2014

@dispatchrabbi

This comment has been minimized.

Show comment
Hide comment
@dispatchrabbi

dispatchrabbi Feb 24, 2014

Contributor

We probably want the ability to do generic serializer/deserializer functions instead of just a property name map, for when your state is something like {search: 'bar', options: ['a', 'b', 'c']} and you want your route to look like #!q=bar&opts=abc.

As a note on serializer functions, can.param is not all that helpful here, because can.param({foo: [1, 2, 3]}) === 'foo[]=1&foo[]=2&foo[]=3', which I don't think is a great default.

Contributor

dispatchrabbi commented Feb 24, 2014

We probably want the ability to do generic serializer/deserializer functions instead of just a property name map, for when your state is something like {search: 'bar', options: ['a', 'b', 'c']} and you want your route to look like #!q=bar&opts=abc.

As a note on serializer functions, can.param is not all that helpful here, because can.param({foo: [1, 2, 3]}) === 'foo[]=1&foo[]=2&foo[]=3', which I don't think is a great default.

@justinbmeyer

This comment has been minimized.

Show comment
Hide comment
@justinbmeyer

justinbmeyer Feb 24, 2014

Contributor

Other things to consider:

Passing a Map constructor function

AppState = can.Map.extend({ ... })
can.route.data(AppState)

Passing can.route an object of prototype properties

can.route.data({
  setSearchTerm: function(){ ... }
})

Serializing and de-serializing

AppState = can.Map.extend({
  setSearchTerm: function(newTerm){
    return newTerm || ""
  },
  setFlags: function(newValue){
    return typeof newValue === "string" ? newValue.split("") : newValue || [] )
  },
  serialize: function(){
    var s = this.attr();
    s.flags = s.flags.join("");
    return s;
  }
})
Contributor

justinbmeyer commented Feb 24, 2014

Other things to consider:

Passing a Map constructor function

AppState = can.Map.extend({ ... })
can.route.data(AppState)

Passing can.route an object of prototype properties

can.route.data({
  setSearchTerm: function(){ ... }
})

Serializing and de-serializing

AppState = can.Map.extend({
  setSearchTerm: function(newTerm){
    return newTerm || ""
  },
  setFlags: function(newValue){
    return typeof newValue === "string" ? newValue.split("") : newValue || [] )
  },
  serialize: function(){
    var s = this.attr();
    s.flags = s.flags.join("");
    return s;
  }
})
@justinbmeyer

This comment has been minimized.

Show comment
Hide comment
@justinbmeyer

justinbmeyer Feb 24, 2014

Contributor

@moschel

We'll possibly need options for that method to map appState properties to what they'll be called in can.route.

I assume you mean if I do something like:

AppState = can.Map.extend({
  doSomething: function(){ ... }
})
can.route.data(AppState);

You want someone to be able to write:

can.route.doSomething()

?

Imo, Modules should not call can.route directly, they should instead be passed an observable and listen / make changes to it. That means, can.route.doSomething()is not something we really need to support. If someone must do that, they can do:

can.route.data().doSomething()

Contributor

justinbmeyer commented Feb 24, 2014

@moschel

We'll possibly need options for that method to map appState properties to what they'll be called in can.route.

I assume you mean if I do something like:

AppState = can.Map.extend({
  doSomething: function(){ ... }
})
can.route.data(AppState);

You want someone to be able to write:

can.route.doSomething()

?

Imo, Modules should not call can.route directly, they should instead be passed an observable and listen / make changes to it. That means, can.route.doSomething()is not something we really need to support. If someone must do that, they can do:

can.route.data().doSomething()

@moschel

This comment has been minimized.

Show comment
Hide comment
@moschel

moschel Apr 4, 2014

Contributor

As stated above, serialize and de-serialize are handled with setters (or the new can/map/define) and the serialize method on the can.Map instance. So creating an appState and binding it should involve:

a) define a can.Map constructor, with added serialize helpers (used to convert this object into a can.route params string) and deserialize helpers (used to convert the other direction)
b) set the default route pattern matcher
c) call this method to bind can.route to this can.Map
d) call can.route.ready

Here's a proposed API for binding some can.Map state object as can.route.data. I'm not sure about the name, a few options are:

  • can.route.Map - I think makes the most sense, similar to can.List.Map although that is a property and this is a function
  • can.route.data - kinda similar to jQuery.fn.data
  • can.route.setMap
  • can.route.bindMap
  • can.route.applicationState
  • can.route.scope - since this is similar to component scope

can.route.Map

@function can.route.Map

Assign a can.Map instance that acts as can.route's internal can.Map. The purpose for this is to cross-bind a top level state object (Application State) to the can.route.

@Signature can.route.Map(mapConstructor)

@param {can.Map} mapConstructor A can.Map constructor function. A new can.Map instance will be created and used as the can.Map internal to can.route.

@Signature can.route.Map(mapInstance)

@param {can.Map} mapInstance A can.Map instance, used as the can.Map internal to can.route.

@Signature can.route.Map(func(attrs))

@param {function(attrs)} func A method, which will be called after can.route.ready is called. It will be passed attrs, an object representing the deparameterized URL. This function should create and return a can.Map instance, which will be used as the internal can.Map for can.route.

Use

var AppState = can.Map.extend({
    // return an object with string friendly formats
    serialize: function(){
        return {
            searchTerm: this.attr('searchTerm'),
            flags: this.attr('flags').join(',')
        }
    },
    // convert a stringified object into the javascript friendly format
    setFlags: function(val){
        if(val === ""){
            return [];
        }
        var arr = val;
        if(typeof val === "string"){
            arr = val.split(',')
        }
        return arr;
    }
});

var appState = new AppState;

can.route("", {
    searchTerm: '',
    flags: ''
});

can.route.Map(appState);

Loading data on application start

A common use case is loading some metadata related to the Application State when the application begins, which must be loaded as part of the Application State before we can start up all the components.

To implement this functionality, load this data, call the AppState constructor with the data and save it in the init method, then call can.route.Map and can.route.ready to set the application init process in motion.

For example:

var AppState = can.Map.extend({
    init: function(locations){
        // a can.List of {name: "Chicago", id: 3}
        this.attr('locations', locations);
    },
    // return an object with string friendly formats
    serialize: function(){
        return {
            locationIds: this.attr('locations').filter(function(location){
                return this.location.attr('selected');
            }),
            searchTerm: this.attr('searchTerm')
        }
    },
    setLocationIds: function(val){
        if(val === ""){
            return [];
        }
        var arr = val;
        if(typeof val === "string"){
            arr = val.split(',')
        }
        this.attr('locations').forEach(function(location){
            if(arr.indexOf(location.attr('id')) !== -1){
                location.attr('selected', true);
            }
        })
    }
});

Locations.findAll({}, function(locations){
    var appState = new AppState(locations);
    can.route.Map(appState);
    can.route.ready();
})

can.route("", {
    searchTerm: '',
    locationIds: ''
});
Contributor

moschel commented Apr 4, 2014

As stated above, serialize and de-serialize are handled with setters (or the new can/map/define) and the serialize method on the can.Map instance. So creating an appState and binding it should involve:

a) define a can.Map constructor, with added serialize helpers (used to convert this object into a can.route params string) and deserialize helpers (used to convert the other direction)
b) set the default route pattern matcher
c) call this method to bind can.route to this can.Map
d) call can.route.ready

Here's a proposed API for binding some can.Map state object as can.route.data. I'm not sure about the name, a few options are:

  • can.route.Map - I think makes the most sense, similar to can.List.Map although that is a property and this is a function
  • can.route.data - kinda similar to jQuery.fn.data
  • can.route.setMap
  • can.route.bindMap
  • can.route.applicationState
  • can.route.scope - since this is similar to component scope

can.route.Map

@function can.route.Map

Assign a can.Map instance that acts as can.route's internal can.Map. The purpose for this is to cross-bind a top level state object (Application State) to the can.route.

@Signature can.route.Map(mapConstructor)

@param {can.Map} mapConstructor A can.Map constructor function. A new can.Map instance will be created and used as the can.Map internal to can.route.

@Signature can.route.Map(mapInstance)

@param {can.Map} mapInstance A can.Map instance, used as the can.Map internal to can.route.

@Signature can.route.Map(func(attrs))

@param {function(attrs)} func A method, which will be called after can.route.ready is called. It will be passed attrs, an object representing the deparameterized URL. This function should create and return a can.Map instance, which will be used as the internal can.Map for can.route.

Use

var AppState = can.Map.extend({
    // return an object with string friendly formats
    serialize: function(){
        return {
            searchTerm: this.attr('searchTerm'),
            flags: this.attr('flags').join(',')
        }
    },
    // convert a stringified object into the javascript friendly format
    setFlags: function(val){
        if(val === ""){
            return [];
        }
        var arr = val;
        if(typeof val === "string"){
            arr = val.split(',')
        }
        return arr;
    }
});

var appState = new AppState;

can.route("", {
    searchTerm: '',
    flags: ''
});

can.route.Map(appState);

Loading data on application start

A common use case is loading some metadata related to the Application State when the application begins, which must be loaded as part of the Application State before we can start up all the components.

To implement this functionality, load this data, call the AppState constructor with the data and save it in the init method, then call can.route.Map and can.route.ready to set the application init process in motion.

For example:

var AppState = can.Map.extend({
    init: function(locations){
        // a can.List of {name: "Chicago", id: 3}
        this.attr('locations', locations);
    },
    // return an object with string friendly formats
    serialize: function(){
        return {
            locationIds: this.attr('locations').filter(function(location){
                return this.location.attr('selected');
            }),
            searchTerm: this.attr('searchTerm')
        }
    },
    setLocationIds: function(val){
        if(val === ""){
            return [];
        }
        var arr = val;
        if(typeof val === "string"){
            arr = val.split(',')
        }
        this.attr('locations').forEach(function(location){
            if(arr.indexOf(location.attr('id')) !== -1){
                location.attr('selected', true);
            }
        })
    }
});

Locations.findAll({}, function(locations){
    var appState = new AppState(locations);
    can.route.Map(appState);
    can.route.ready();
})

can.route("", {
    searchTerm: '',
    locationIds: ''
});
@justinbmeyer

This comment has been minimized.

Show comment
Hide comment
@justinbmeyer

justinbmeyer Apr 4, 2014

Contributor

Map would break convention as we only call those functions with new.

Sent from my iPhone

On Apr 4, 2014, at 2:05 AM, Brian Moschel notifications@github.com wrote:

As stated above, serialize and de-serialize are handled with setters (or the new can/map/define) and the serialize method on the can.Map instance. So creating an appState and binding it should involve:

a) define a can.Map constructor, with added serialize helpers (used to convert this object into a can.route params string) and deserialize helpers (used to convert the other direction)
b) set the default route pattern matcher
c) call this method to bind can.route to this can.Map
d) call can.route.ready

Here's a proposed API for binding some can.Map state object as can.route.data. I'm not sure about the name, a few options are:

can.route.Map - I think makes the most sense, similar to can.List.Map although that is a property and this is a function
can.route.data - kinda similar to jQuery.fn.data
can.route.setMap
can.route.bindMap
can.route.applicationState
can.route.scope - since this is similar to component scope
can.route.Map

@function can.route.Map

Assign a can.Map instance that acts as can.route's internal can.Map. The purpose for this is to cross-bind a top level state object (Application State) to the can.route.

@Signature can.route.Map(mapConstructor)

@param {can.Map} mapConstructor A can.Map constructor function. A new can.Map instance will be created and used as the can.Map internal to can.route.

@Signature can.route.Map(mapInstance)

@param {can.Map} mapInstance A can.Map instance, used as the can.Map internal to can.route.

@Signature can.route.Map(func(attrs))

@param {function(attrs)} func A method, which will be called after can.route.ready is called. It will be passed attrs, an object representing the deparameterized URL. This function should create and return a can.Map instance, which will be used as the internal can.Map for can.route.

Use

var AppState = can.Map.extend({
// return an object with string friendly formats
serialize: function(){
return {
searchTerm: this.attr('searchTerm'),
flags: this.attr('flags').join(',')
}
},
// convert a stringified object into the javascript friendly format
setFlags: function(val){
if(val === ""){
return [];
}
var arr = val;
if(typeof val === "string"){
arr = val.split(',')
}
return arr;
}
});

var appState = new AppState;

can.route("", {
searchTerm: '',
flags: ''
});

can.route.Map(appState);
Loading data on application start

A common use case is loading some metadata related to the Application State when the application begins, which must be loaded as part of the Application State before we can start up all the components.

To implement this functionality, load this data, call the AppState constructor with the data and save it in the init method, then call can.route.Map and can.route.ready to set the application init process in motion.

For example:

var AppState = can.Map.extend({
init: function(locations){
// a can.List of {name: "Chicago", id: 3}
this.attr('locations', locations);
},
// return an object with string friendly formats
serialize: function(){
return {
locationIds: this.attr('locations').filter(function(location){
return this.location.attr('selected');
}),
searchTerm: this.attr('searchTerm')
}
},
setLocationIds: function(val){
if(val === ""){
return [];
}
var arr = val;
if(typeof val === "string"){
arr = val.split(',')
}
this.attr('locations').forEach(function(location){
if(arr.indexOf(location.attr('id')) !== -1){
location.attr('selected', true);
}
})
}
});

Locations.findAll({}, function(locations){
var appState = new AppState(locations);
can.route.Map(appState);
can.route.ready();
})

can.route("", {
searchTerm: '',
locationIds: ''
});

Reply to this email directly or view it on GitHub.

Contributor

justinbmeyer commented Apr 4, 2014

Map would break convention as we only call those functions with new.

Sent from my iPhone

On Apr 4, 2014, at 2:05 AM, Brian Moschel notifications@github.com wrote:

As stated above, serialize and de-serialize are handled with setters (or the new can/map/define) and the serialize method on the can.Map instance. So creating an appState and binding it should involve:

a) define a can.Map constructor, with added serialize helpers (used to convert this object into a can.route params string) and deserialize helpers (used to convert the other direction)
b) set the default route pattern matcher
c) call this method to bind can.route to this can.Map
d) call can.route.ready

Here's a proposed API for binding some can.Map state object as can.route.data. I'm not sure about the name, a few options are:

can.route.Map - I think makes the most sense, similar to can.List.Map although that is a property and this is a function
can.route.data - kinda similar to jQuery.fn.data
can.route.setMap
can.route.bindMap
can.route.applicationState
can.route.scope - since this is similar to component scope
can.route.Map

@function can.route.Map

Assign a can.Map instance that acts as can.route's internal can.Map. The purpose for this is to cross-bind a top level state object (Application State) to the can.route.

@Signature can.route.Map(mapConstructor)

@param {can.Map} mapConstructor A can.Map constructor function. A new can.Map instance will be created and used as the can.Map internal to can.route.

@Signature can.route.Map(mapInstance)

@param {can.Map} mapInstance A can.Map instance, used as the can.Map internal to can.route.

@Signature can.route.Map(func(attrs))

@param {function(attrs)} func A method, which will be called after can.route.ready is called. It will be passed attrs, an object representing the deparameterized URL. This function should create and return a can.Map instance, which will be used as the internal can.Map for can.route.

Use

var AppState = can.Map.extend({
// return an object with string friendly formats
serialize: function(){
return {
searchTerm: this.attr('searchTerm'),
flags: this.attr('flags').join(',')
}
},
// convert a stringified object into the javascript friendly format
setFlags: function(val){
if(val === ""){
return [];
}
var arr = val;
if(typeof val === "string"){
arr = val.split(',')
}
return arr;
}
});

var appState = new AppState;

can.route("", {
searchTerm: '',
flags: ''
});

can.route.Map(appState);
Loading data on application start

A common use case is loading some metadata related to the Application State when the application begins, which must be loaded as part of the Application State before we can start up all the components.

To implement this functionality, load this data, call the AppState constructor with the data and save it in the init method, then call can.route.Map and can.route.ready to set the application init process in motion.

For example:

var AppState = can.Map.extend({
init: function(locations){
// a can.List of {name: "Chicago", id: 3}
this.attr('locations', locations);
},
// return an object with string friendly formats
serialize: function(){
return {
locationIds: this.attr('locations').filter(function(location){
return this.location.attr('selected');
}),
searchTerm: this.attr('searchTerm')
}
},
setLocationIds: function(val){
if(val === ""){
return [];
}
var arr = val;
if(typeof val === "string"){
arr = val.split(',')
}
this.attr('locations').forEach(function(location){
if(arr.indexOf(location.attr('id')) !== -1){
location.attr('selected', true);
}
})
}
});

Locations.findAll({}, function(locations){
var appState = new AppState(locations);
can.route.Map(appState);
can.route.ready();
})

can.route("", {
searchTerm: '',
locationIds: ''
});

Reply to this email directly or view it on GitHub.

@moschel

This comment has been minimized.

Show comment
Hide comment
@moschel

moschel Apr 4, 2014

Contributor

Ok, then can.route.scope is probably the next best option, since it has all the same signatures: map instance, map constructor, function.

We want this to be a function though, whereas component's scope is a property.

can.route.scope(AppState);

Agree?

Contributor

moschel commented Apr 4, 2014

Ok, then can.route.scope is probably the next best option, since it has all the same signatures: map instance, map constructor, function.

We want this to be a function though, whereas component's scope is a property.

can.route.scope(AppState);

Agree?

@moschel

This comment has been minimized.

Show comment
Hide comment
@moschel

moschel Apr 4, 2014

Contributor

Decided on can.route.map. I'll work on adding this and getting some docs in shortly.

Contributor

moschel commented Apr 4, 2014

Decided on can.route.map. I'll work on adding this and getting some docs in shortly.

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