Sometimes you might find yourself working with a backend that expects a single string field to represent multiple pieces of information that need to be entered/validated individually. For example, a string that contains both date and time, or address lines separated with special characters. In such an instance, one solution is to change the backend, but sometimes that is simply not an option (due to budget, stubbornness, etc), causing you to work around it on the front end.
For illustration purposes, let’s say the server is going to provide (or will expect) a payload like this:
{
"id": 1,
"name": "Bob Smith",
"interviewDateTime": "2015-11-24 09:30:00"
}
That might be fine if the user has one text box for interview date/time, but what if the design calls for separate date and time fields on the screen and different validation rules for each smaller piece?
There are a few ways you could approach this, but regardless of the method, you will need to take care of 2 things:
- distributing the combined value into multiple fields when provided by the server
- combining the various pieces before sending them back to the server
We can take advantage of can.Map
's define plugin to do this fairly easily, using set
, type
or Type
for the distribution step, and serialize
for the combination step.
Using Additional Properties
The straight-forward approach is to create additional properties on your model for each distinct piece, use type
(or set
but type
seems most applicable) to distribute the combined value to the new properties, and serialize
to combine them again. The following examples defines interviewDate
and interviewTime
for this purpose.
var Candidate = can.Map.extend({
define: {
interviewDateTime: {
type: function(str) {
// parse the input string
var dateTime = str.split(' ');
// distribute the distinct pieces to the extra props
this.attr({
interviewDate: dateTime[0],
interviewTime: dateTime[1]
});
// keep the original
return str;
},
serialize: function(str) {
// combine both props into a single string for saving
return this.attr('interviewDate') + ' ' + this.attr('interviewTime');
}
},
// don't send these to the server because it doesn't know about them
interviewDate: { serialize: false },
interviewTime: { serialize: false }
}
});
You would bind it in your template like this:
<date-picker {($value)}="myCandidate.interviewDate"></date-picker>
<time-picker {($value)}="myCandidate.interviewTime"></time-picker>
That takes care of it. When interviewDateTime
is set such as when the map is created, it will distribute its pieces out to interviewDate
and interviewTime
, both of which are distinct fields that can be bound and validated individually. When the candidate is saved, interviewDateTime
will be serialized as a single string.
While this solution works and might be fine for a one-off field, what if you need this functionality for multiple fields on multiple models? It would get pretty annoying having to define all those additional properties with unique names, which brings me to the next approach…
Using Nested Properties
You can avoid defining additional properties by nesting them under the original in a child map. This can be done by using type
to return an object in place of the string.
interviewDateTime: {
type: function(str) {
var dateTime = str.split(' ');
// interviewDateTime will become a map
return new can.Map({
date: dateTime[0],
time: dateTime[1]
});
},
serialize: function(dateTime) {
return dateTime.attr('date') + ' ' + dateTime.attr('time');
}
}
You would bind it in your template like this:
<date-picker {($value)}="myCandidate.interviewDateTime.date"></date-picker>
<time-picker {($value)}="myCandidate.interviewDateTime.time"></time-picker>
Okay, cool, but what’s the simplest way to provide this functionality to multiple fields and modules? Creating a global type
in can.define.types
won’t cover the serialization (and you might have dependency problems if you aren’t careful), so we need a way to share the entire definition object like this:
mapDefinitions.js
export var dateTimeDefinition = {
type: function(str) {
var dateTime = str.split(' ');
return new can.Map({
date: dateTime[0],
time: dateTime[1]
});
},
serialize: function(dateTime) {
return dateTime.attr('date') + ' ' + dateTime.attr('time');
}
};
Then we might have to merge that object with a custom definition in Candidate
.
candidate.js
import { dateTimeDefinition } from 'mapDefinitions';
var Candidate = can.Map.extend({
define: {
interviewDateTime: can.extend({}, dateTimeDefinition , {
// custom stuff for this specific field
value: function() {/* today, 1 hour from now */}
}
}
});
This works and the pattern might come in handy elsewhere but we can simplify it further.
Nesting Properties with a Custom Type
Leveraging the flexibility of can.Map
, we can create a custom Map type that overrides setup
and serialize
to encapsulate all of the required functionality into a single construct.
util/dateTime.js
export var DateTime = can.Map.extend({
setup: function(str) {
var dateTime = str.split(' ');
this._super({
date: dateTime[0],
time: dateTime[1]
});
},
serialize: function() {
return this.attr('date') + ' ' + this.attr('time')
}
});
You’ll need the construct/super plugin to do this._super
. If you don’t, use can.Map.prototype.setup.call(this, {...})
.
candidate.js
import { DateTime } from 'util/dateTime';
var Candidate = can.Map.extend({
define: {
interviewDateTime: {
Type: DateTime,
value: function() {/* today, 1 hour from now */}
}
}
});
Notice the capital Type
in Candidate
. This is the constructor version of type
that coerces any set value through that constructor if the new value is not already an instance. For our purposes, it’s basically taking care of running new DateTime(str)
for us when we set a string.
Overriding serialize
is great because when it’s called on a can.Map
, it’s also called on all child maps recursively. By overriding the method, we can change the behavior so that it returns a string instead of the usual object.
Note: This date-time string example is simplified. You probably want to handle cases such as when the date-time string is incomplete or blank. You could also use a library like Moment to handle parsing.