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

can.route crossbind to can.Map #752

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

can.route crossbind to can.Map #752

moschel opened this issue Feb 24, 2014 · 7 comments

Comments

@moschel
Copy link
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 self-assigned this Feb 24, 2014
@dispatchrabbi
Copy link
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.

@justinbmeyer
Copy link
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;
  }
})

@justinbmeyer
Copy link
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()

@moschel
Copy link
Contributor Author

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
Copy link
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.

@moschel
Copy link
Contributor Author

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
Copy link
Contributor Author

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
Projects
None yet
Development

No branches or pull requests

4 participants