Merge pull request #115 from jharding/61-configurable-value-key

Make value key configurable
This commit is contained in:
Jake Harding
2013-03-14 21:52:10 -07:00
6 changed files with 116 additions and 168 deletions

View File

@@ -21,7 +21,8 @@ var Dataset = (function() {
this.limit = o.limit || 5;
this.header = o.header;
this.footer = o.footer;
this.template = compileTemplate(o.template, o.engine);
this.valueKey = o.valueKey || 'value';
this.template = compileTemplate(o.template, o.engine, this.valueKey);
// used in #initialize
this.local = o.local;
@@ -95,18 +96,25 @@ var Dataset = (function() {
},
_processData: function(data) {
var itemHash = {}, adjacencyList = {};
var that = this, itemHash = {}, adjacencyList = {};
utils.each(data, function(i, item) {
var id;
utils.each(data, function(i, datum) {
var value = utils.isString(datum) ? datum : datum[that.valueKey],
tokens = datum.tokens || utils.tokenizeText(value),
item = { value: value, tokens: tokens },
id;
// convert string datums to datum objects
if (utils.isString(item)) {
item = { value: item, tokens: utils.tokenizeText(item) };
if (utils.isString(datum)) {
item.datum = {};
item.datum[that.valueKey] = datum;
}
else {
item.datum = datum;
}
// filter out falsy tokens
item.tokens = utils.filter(item.tokens || [], function(token) {
item.tokens = utils.filter(item.tokens, function(token) {
return !utils.isBlankString(token);
});
@@ -197,41 +205,6 @@ var Dataset = (function() {
return suggestions;
},
_compareItems: function(a, b, areLocalItems) {
var aScoreBoost = !a.score_boost ? 0 : a.score_boost,
bScoreBoost = !b.score_boost ? 0 : b.score_boost,
aScore = !a.score ? 0 : a.score,
bScore = !b.score ? 0 : b.score;
if(areLocalItems) {
return (b.weight + bScoreBoost) - (a.weight + aScoreBoost);
} else {
return (bScore + bScoreBoost) - (aScore + aScoreBoost);
}
},
_ranker: function(a, b) {
if (this._customRanker) {
return this._customRanker(a, b);
} else {
// Anything local should always be first (anything with a non-zero weight) and remote results (non-zero scores), and sort by weight/score within each category
var aIsLocal = a.weight && a.weight !== 0;
var bIsLocal = b.weight && b.weight !== 0;
if (aIsLocal && !bIsLocal) {
return -1;
} else if (bIsLocal && !aIsLocal) {
return 1;
} else {
return (aIsLocal && bIsLocal) ? this._compareItems(a, b, true) : this._compareItems(a, b, false);
}
}
},
_processRemoteSuggestions: function(callback, matchedItems) {
var that = this;
},
// public methods
// ---------------
@@ -256,9 +229,7 @@ var Dataset = (function() {
getSuggestions: function(query, cb) {
var that = this,
terms = utils.tokenizeQuery(query),
suggestions = this._getLocalSuggestions(terms)
.sort(this._ranker)
.slice(0, this.limit);
suggestions = this._getLocalSuggestions(terms).slice(0, this.limit);
cb && cb(suggestions);
@@ -301,7 +272,7 @@ var Dataset = (function() {
return Dataset;
function compileTemplate(template, engine) {
function compileTemplate(template, engine, valueKey) {
var wrapper = '<div class="tt-suggestion">%body</div>',
compiledTemplate;
@@ -314,7 +285,7 @@ var Dataset = (function() {
else {
compiledTemplate = {
render: function(context) {
return wrapper.replace('%body', '<p>' + context.value + '</p>');
return wrapper.replace('%body', '<p>' + context[valueKey] + '</p>');
}
};
}

View File

@@ -52,7 +52,7 @@ var DropdownView = (function() {
_handleSelection: function($e) {
var $suggestion = $($e.currentTarget);
this.trigger('suggestionSelected', getSuggestionData($suggestion));
this.trigger('suggestionSelected', extractSuggestion($suggestion));
},
_show: function() {
@@ -94,7 +94,7 @@ var DropdownView = (function() {
}
$underCursor = $suggestions.eq(nextIndex).addClass('tt-is-under-cursor');
this.trigger('cursorMoved', getSuggestionData($underCursor));
this.trigger('cursorMoved', extractSuggestion($underCursor));
},
_getSuggestions: function() {
@@ -166,16 +166,16 @@ var DropdownView = (function() {
.filter('.tt-is-under-cursor')
.first();
return $suggestion.length > 0 ? getSuggestionData($suggestion) : null;
return $suggestion.length > 0 ? extractSuggestion($suggestion) : null;
},
getFirstSuggestion: function() {
var $suggestion = this._getSuggestions().first();
return $suggestion.length > 0 ? getSuggestionData($suggestion) : null;
return $suggestion.length > 0 ? extractSuggestion($suggestion) : null;
},
renderSuggestions: function(query, dataset, suggestions) {
renderSuggestions: function(dataset, suggestions) {
var datasetClassName = 'tt-dataset-' + dataset.name,
$suggestionsList,
$dataset = this.$menu.find('.' + datasetClassName),
@@ -204,7 +204,7 @@ var DropdownView = (function() {
fragment = document.createDocumentFragment();
utils.each(suggestions, function(i, suggestion) {
elBuilder.innerHTML = dataset.template.render(suggestion);
elBuilder.innerHTML = dataset.template.render(suggestion.datum);
$el = $(elBuilder.firstChild)
.css(css.suggestion)
@@ -251,7 +251,7 @@ var DropdownView = (function() {
// helper functions
// ----------------
function getSuggestionData($el) {
function extractSuggestion($el) {
return $el.data('suggestion');
}
})();

View File

@@ -217,32 +217,25 @@ var TypeaheadView = (function() {
byClick && utils.isMsie() ?
utils.defer(this.dropdownView.close) : this.dropdownView.close();
this.eventBus.trigger('selected', suggestion);
this.eventBus.trigger('selected', suggestion.datum);
}
},
_getSuggestions: function() {
var that = this,
query = this.inputView.getQuery();
var that = this, query = this.inputView.getQuery();
if (utils.isBlankString(query)) {
return;
}
if (utils.isBlankString(query)) { return; }
utils.each(this.datasets, function(i, dataset) {
dataset.getSuggestions(query, function(suggestions) {
that._renderSuggestions(query, dataset, suggestions);
// only render the suggestions if the query hasn't changed
if (query === that.inputView.getQuery()) {
that.dropdownView.renderSuggestions(dataset, suggestions);
}
});
});
},
_renderSuggestions: function(query, dataset, suggestions) {
if (query !== this.inputView.getQuery()) { return; }
suggestions = suggestions.slice(0, dataset.limit);
this.dropdownView.renderSuggestions(query, dataset, suggestions);
},
_autocomplete: function(e) {
var isCursorAtEnd, ignoreEvent, query, hint, suggestion;

View File

@@ -1,20 +1,27 @@
describe('Dataset', function() {
var fixtureData = ['grape', 'coconut', 'cake', 'tea', 'coffee'],
var fixtureStrings = ['grape', 'coconut', 'cake', 'tea', 'coffee'],
fixtureDatums = [
{ value: 'grape' },
{ value: 'coconut' },
{ value: 'cake' },
{ value: 'tea' },
{ value: 'coffee' }
],
expectedAdjacencyList = {
g: ['grape'],
c: ['coconut', 'cake', 'coffee'],
t: ['tea']
},
expectedItemHash = {
grape: { tokens: ['grape'], value: 'grape' },
coconut: { tokens: ['coconut'], value: 'coconut' },
cake: { tokens: ['cake'], value: 'cake' },
tea: { tokens: ['tea'], value: 'tea' },
coffee: { tokens: ['coffee'], value: 'coffee' }
grape: createItem('grape'),
coconut: createItem('coconut'),
cake: createItem('cake'),
tea: createItem('tea'),
coffee: createItem('coffee')
},
prefetchResp = {
status: 200,
responseText: JSON.stringify(fixtureData)
responseText: JSON.stringify(fixtureStrings)
},
mockStorageFns = {
getMiss: function() {
@@ -56,7 +63,7 @@ describe('Dataset', function() {
describe('#constructor', function() {
it('should initialize persistent storage', function() {
expect(new Dataset({ local: fixtureData }).storage).toBeDefined();
expect(new Dataset({ local: fixtureStrings }).storage).toBeDefined();
expect(PersistentStorage).toHaveBeenCalled();
});
@@ -82,7 +89,7 @@ describe('Dataset', function() {
describe('when called with no template', function() {
beforeEach(function() {
this.dataset = new Dataset({ local: fixtureData });
this.dataset = new Dataset({ local: fixtureStrings });
});
it('should compile default template', function() {
@@ -94,7 +101,7 @@ describe('Dataset', function() {
describe('when called with a template and engine', function() {
beforeEach(function() {
this.dataset = new Dataset({
local: fixtureData,
local: fixtureStrings,
template: 't',
engine: { compile: this.spy = jasmine.createSpy().andReturn('boo') }
});
@@ -113,7 +120,7 @@ describe('Dataset', function() {
it('should return Deferred instance', function() {
var returnVal;
this.dataset = new Dataset({ local: fixtureData });
this.dataset = new Dataset({ local: fixtureStrings });
returnVal = this.dataset.initialize();
// eh, have to rely on duck typing unfortunately
@@ -124,13 +131,18 @@ describe('Dataset', function() {
describe('when called with local', function() {
beforeEach(function() {
this.dataset = new Dataset({ local: fixtureData });
this.dataset.initialize();
this.dataset1 = new Dataset({ local: fixtureStrings });
this.dataset2 = new Dataset({ local: fixtureDatums });
this.dataset1.initialize();
this.dataset2.initialize();
});
it('should process and merge the data', function() {
expect(this.dataset.itemHash).toEqual(expectedItemHash);
expect(this.dataset.adjacencyList).toEqual(expectedAdjacencyList);
expect(this.dataset1.itemHash).toEqual(expectedItemHash);
expect(this.dataset1.adjacencyList).toEqual(expectedAdjacencyList);
expect(this.dataset2.itemHash).toEqual(expectedItemHash);
expect(this.dataset2.adjacencyList).toEqual(expectedAdjacencyList);
});
});
@@ -158,9 +170,7 @@ describe('Dataset', function() {
describe('if filter was passed in', function() {
var filteredAdjacencyList = { f: ['filter'] },
filteredItemHash = {
filter: { tokens: ['filter'], value: 'filter' }
};
filteredItemHash = { filter: createItem('filter') };
beforeEach(function() {
this.dataset = new Dataset({
@@ -181,7 +191,7 @@ describe('Dataset', function() {
expect(this.request).not.toBeNull();
});
it('should process and merge filtered data', function() {
it('should process and merge fileered data', function() {
expect(this.dataset.adjacencyList).toEqual(filteredAdjacencyList);
expect(this.dataset.itemHash).toEqual(filteredItemHash);
});
@@ -251,31 +261,9 @@ describe('Dataset', function() {
});
});
describe('Datasource options', function() {
describe('Matching, combining, returning results', function() {
beforeEach(function() {
this.dataset = new Dataset({ local: fixtureData });
this.dataset.initialize();
});
it('allow for a custom ranking function to be defined', function() {
this.dataset._customRanker = function(a, b) {
return a.value.length > b.value.length ?
1 : a.value.length === b.value.length ? 0 : -1;
};
this.dataset.getSuggestions('c', function(items) {
expect(items).toEqual([
{ tokens: ['cake'], value: 'cake' },
{ tokens: ['coffee'], value: 'coffee' },
{ tokens: ['coconut'], value: 'coconut' }
]);
});
});
});
describe('Matching, ranking, combining, returning results', function() {
beforeEach(function() {
this.dataset = new Dataset({ local: fixtureData, remote: '/remote' });
this.dataset = new Dataset({ local: fixtureStrings, remote: '/remote' });
this.dataset.initialize();
});
@@ -283,9 +271,9 @@ describe('Dataset', function() {
this.dataset.limit = 3;
this.dataset.getSuggestions('c', function(items) {
expect(items).toEqual([
{ tokens: ['coconut'], value: 'coconut' },
{ tokens: ['cake'], value: 'cake' },
{ tokens: ['coffee'], value: 'coffee' }
createItem('coconut'),
createItem('cake'),
createItem('coffee')
]);
});
@@ -294,9 +282,9 @@ describe('Dataset', function() {
this.dataset.limit = 100;
this.dataset.getSuggestions('c', function(items) {
expect(items).toEqual([
{ tokens: ['coconut'], value: 'coconut' },
{ tokens: ['cake'], value: 'cake' },
{ tokens: ['coffee'], value: 'coffee' }
createItem('coconut'),
createItem('cake'),
createItem('coffee')
]);
});
@@ -306,9 +294,9 @@ describe('Dataset', function() {
it('matches', function() {
this.dataset.getSuggestions('c', function(items) {
expect(items).toEqual([
{ tokens: ['coconut'], value: 'coconut' },
{ tokens: ['cake'], value: 'cake' },
{ tokens: ['coffee'], value: 'coffee' }
createItem('coconut'),
createItem('cake'),
createItem('coffee')
]);
});
});
@@ -350,22 +338,6 @@ describe('Dataset', function() {
expectedItemHash.grape
]);
});
it('sorts results: local first, then remote, sorted by graph weight / score within each local/remote section', function() {
expect([
{ id: 1, weight: 1000, score: 0 },
{ id: 2, weight: 500, score: 0 },
{ id: 3, weight: 1500, score: 0 },
{ id: 4, weight: 0, score: 100000 },
{ id: 5, weight: 0, score: 250000 }
].sort(this.dataset._ranker)).toEqual([
{ id: 3, weight: 1500, score: 0 },
{ id: 1, weight: 1000, score: 0 },
{ id: 2, weight: 500, score: 0 },
{ id: 5, weight: 0, score: 250000 },
{ id: 4, weight: 0, score: 100000 }
]);
});
});
describe('tokenization', function() {
@@ -379,27 +351,22 @@ describe('Dataset', function() {
it('normalizes capitalization to match items', function() {
this.dataset.getSuggestions('Cours', function(items) {
expect(items)
.toEqual([{ tokens: ['course', '106'], value: 'course-106' }]);
expect(items).toEqual([createItem('course-106')]);
});
this.dataset.getSuggestions('cOuRsE 106', function(items) {
expect(items)
.toEqual([{ tokens: ['course', '106'], value: 'course-106' }]);
expect(items).toEqual([createItem('course-106')]);
});
this.dataset.getSuggestions('one two', function(items) {
expect(items)
.toEqual([{ tokens: ['one', 'two'], value: 'One-Two' }]);
expect(items).toEqual([createItem('One-Two')]);
});
this.dataset.getSuggestions('THREE TWO', function(items) {
expect(items)
.toEqual([{ tokens: ['two', 'three'], value: 'two three' }]);
expect(items).toEqual([createItem('two three')]);
});
});
it('matches items with dashes', function() {
this.dataset.getSuggestions('106 course', function(items) {
expect(items)
.toEqual([{ tokens: ['course', '106'], value: 'course-106' }]);
expect(items).toEqual([createItem('course-106')]);
});
this.dataset.getSuggestions('course-106', function(items) {
expect(items).toEqual([]);
@@ -408,15 +375,14 @@ describe('Dataset', function() {
it('matches items with underscores', function() {
this.dataset.getSuggestions('user name', function(items) {
expect(items)
.toEqual([{ tokens: ['user', 'name'], value: 'user_name' }]);
expect(items).toEqual([createItem('user_name')]);
});
});
});
});
describe('with datum objects', function() {
var fixtureData = [{ value: 'course-106', tokens: ['course-106'] }];
var fixtureData = [{ value: 'course-106' }];
beforeEach(function() {
this.dataset = new Dataset({ local: fixtureData });
@@ -425,13 +391,24 @@ describe('Dataset', function() {
it('matches items with dashes', function() {
this.dataset.getSuggestions('106 course', function(items) {
expect(items).toEqual([]);
expect(items).toEqual([createItem('course-106')]);
});
this.dataset.getSuggestions('course-106', function(items) {
expect(items)
.toEqual([{ value: 'course-106', tokens: ['course-106'] }]);
expect(items).toEqual([]);
});
});
});
// helper functions
// ----------------
function createItem(val) {
return {
value: val,
tokens: utils.tokenizeText(val),
datum: { value: val }
};
}
});

View File

@@ -78,7 +78,7 @@ describe('DropdownView', function() {
it('should trigger suggestionSelected', function() {
expect(this.spy).toHaveBeenCalledWith({
type: 'suggestionSelected',
data: { value: 'one' }
data: { value: 'one', tokens: ['one'], datum: { value: 'one' } }
});
});
});
@@ -431,7 +431,11 @@ describe('DropdownView', function() {
suggestion = this.dropdownView.getSuggestionUnderCursor();
expect(suggestion).toEqual({ value: 'one' });
expect(suggestion).toEqual({
value: 'one',
tokens: ['one'],
datum: { value: 'one' }
});
});
});
});
@@ -444,7 +448,11 @@ describe('DropdownView', function() {
it('should return obj with data about suggestion under the cursor',
function() {
var suggestion = this.dropdownView.getFirstSuggestion();
expect(suggestion).toEqual({ value: 'one' });
expect(suggestion).toEqual({
value: 'one',
tokens: ['one'],
datum: { value: 'one' }
});
});
});
@@ -471,7 +479,7 @@ describe('DropdownView', function() {
describe('if new dataset', function() {
beforeEach(function() {
this.dropdownView.renderSuggestions('query', mockNewDataset, []);
this.dropdownView.renderSuggestions(mockNewDataset, []);
});
it('should render the header', function() {
@@ -498,7 +506,7 @@ describe('DropdownView', function() {
spyOn(this.dropdownView, 'clearSuggestions');
this.dropdownView.renderSuggestions('query', mockOldDataset, []);
this.dropdownView.renderSuggestions(mockOldDataset, []);
});
it('should call clearSuggestions', function() {
@@ -518,20 +526,19 @@ describe('DropdownView', function() {
spyOn(this.dropdownView, 'clearSuggestions').andCallThrough();
this.dropdownView.renderSuggestions(
'query',
mockOldDataset,
[{ value: 'i am a value' }]
[{ datum: { value: 'i am a value' } }]
);
});
it('should overwrite previous suggestions', function() {
var $suggestions = this.$testDataset.children(),
$suggestion = $suggestions.first(),
suggestion = $suggestion.data('suggestion');
datum = $suggestion.data('suggestion').datum;
expect($suggestions.length).toBe(1);
expect($suggestion).toHaveText('i am a value');
expect(suggestion).toEqual({ value: 'i am a value' });
expect(datum).toEqual({ value: 'i am a value' });
});
it('should trigger suggestionsRendered', function() {
@@ -563,8 +570,7 @@ describe('DropdownView', function() {
// ----------------
function renderTestDataset(view, open) {
var mockQuery = 'test q',
mockDataset = {
var mockDataset = {
name: 'test' ,
template: {
render: function(c) {
@@ -573,12 +579,12 @@ describe('DropdownView', function() {
}
},
mockSuggestions = [
{ value: 'one' },
{ value: 'two' },
{ value: 'three' }
{ value: 'one', tokens: ['one'], datum: { value: 'one' } },
{ value: 'two', tokens: ['two'], datum: { value: 'two' } },
{ value: 'three', tokens: ['three'], datum: { value: 'three' } }
];
view.renderSuggestions(mockQuery, mockDataset, mockSuggestions);
view.renderSuggestions(mockDataset, mockSuggestions);
open && view.open();
return $('#jasmine-fixtures .tt-dataset-test > .tt-suggestions');

View File

@@ -51,6 +51,7 @@
<script>
$('.states').typeahead({
valueKey: 'state',
local: [
"Alabama",
"Alaska",