Commit d544db65 by Ben McMorran

Merge pull request #8513 from edx/benmcmorran/topic-card

TNL-1893 Add teams topic card
parents 390e2c18 c02d09a1
/**
* Model for a topic.
*/
(function (define) {
'use strict';
define(['backbone'], function (Backbone) {
var Topic = Backbone.Model.extend({
defaults: {
name: '',
description: '',
team_count: 0,
id: ''
}
});
return Topic;
})
}).call(this, define || RequireJS.define);
define(['jquery',
'underscore',
'teams/js/views/topic_card',
'teams/js/models/topic'],
function ($, _, TopicCardView, Topic) {
describe('topic card view', function () {
var view;
beforeEach(function () {
spyOn(TopicCardView.prototype, 'action');
view = new TopicCardView({
model: new Topic({
'id': 'renewables',
'name': 'Renewable Energy',
'description': 'Explore how changes in <ⓡⓔⓝⓔⓦⓐⓑⓛⓔ> ʎƃɹǝuǝ will affect our lives.',
'team_count': 34
}),
});
});
it('can render itself', function () {
expect(view.$el).toHaveClass('square-card');
expect(view.$el.find('.card-title').text()).toContain('Renewable Energy');
expect(view.$el.find('.card-description').text()).toContain('changes in <ⓡⓔⓝⓔⓦⓐⓑⓛⓔ> ʎƃɹǝuǝ');
expect(view.$el.find('.card-meta-details').text()).toContain('34 Teams');
expect(view.$el.find('.action').text()).toContain('View');
});
it('navigates when action button is clicked', function () {
view.$el.find('.action').trigger('click');
// TODO test actual navigation once implemented
expect(view.action).toHaveBeenCalled();
});
});
}
);
/**
* View for a topic card. Displays a Topic.
*/
;(function (define) {
'use strict';
define(['backbone', 'underscore', 'gettext', 'js/components/card/views/card'],
function (Backbone, _, gettext, CardView) {
var TeamCountDetailView = Backbone.View.extend({
tagName: 'p',
className: 'team-count',
initialize: function () {
this.render();
},
render: function () {
var team_count = this.model.get('team_count');
this.$el.html(_.escape(interpolate(
ngettext('%(team_count)s Team', '%(team_count)s Teams', team_count),
{team_count: team_count},
true
)));
return this;
}
});
var TopicCardView = CardView.extend({
initialize: function () {
this.detailViews = [new TeamCountDetailView({ model: this.model })];
CardView.prototype.initialize.apply(this, arguments);
},
action: function (event) {
event.preventDefault();
// TODO implement actual navigation
},
configuration: 'square_card',
cardClass: 'topic-card',
title: function () { return this.model.get('name'); },
description: function () { return this.model.get('description'); },
details: function () { return this.detailViews; },
actionClass: 'action-view',
actionContent: _.escape(gettext('View')) + ' <span class="icon fa-arrow-right"></span>'
});
return TopicCardView;
});
}).call(this, define || RequireJS.define);
/**
* A generic card view class.
*
* Subclasses can override any of the following:
* - configuration (string or function): Sets the display style of the card as square or list. Can take values of
* "square_card" or "list_card". Defaults to "square_card".
* - action (function): Action to take when the action button is clicked. Defaults to a no-op.
* - cardClass (string or function): Class name for this card's DOM element. Defaults to the empty string.
* - title (string or function): Title of the card. Defaults to the empty string.
* - description (string or function): Description of the card. Defaults to the empty string.
* - details (array or function): Array of child views to be rendered as details of this card. The class "meta-detail"
* is automatically added to each rendered child view to ensure appropriate styling. Defaults to an empty list.
* - actionClass (string or function): Class name for the action DOM element. Defaults to the empty string.
* - actionUrl (string or function): URL to navigate to when the action button is clicked. Defaults to the empty string.
* - actionContent: Content of the action button. This may include HTML. Default to the empty string.
*/
;(function (define) {
'use strict';
define(['jquery',
'backbone',
'text!templates/components/card/square_card.underscore',
'text!templates/components/card/list_card.underscore'],
function ($, Backbone, squareCardTemplate, listCardTemplate) {
var CardView = Backbone.View.extend({
events: {
'click .action' : 'action'
},
/**
* constructor is needed in addition to initialize because of Backbone's initialization order.
* initialize seems to run last in the initialization process, after className. However, className
* depends on this.configuration being set to pick the appropriate class. Therefore, configuration
* is set in the constructor, but the rest of the initialization happens in initialize.
*/
constructor: function (options) {
if (!this.configuration) {
this.configuration = (options && options.configuration) ? options.configuration : 'square_card';
}
Backbone.View.prototype.constructor.apply(this, arguments);
},
initialize: function () {
this.template = this.switchOnConfiguration(
_.template(squareCardTemplate),
_.template(listCardTemplate)
);
this.render();
},
switchOnConfiguration: function (square_result, list_result) {
return this.callIfFunction(this.configuration) === 'square_card' ?
square_result : list_result;
},
callIfFunction: function (value) {
if ($.isFunction(value)) {
return value.call(this);
} else {
return value;
}
},
className: function () {
return 'card ' +
this.switchOnConfiguration('square-card', 'list-card') +
' ' + this.callIfFunction(this.cardClass);
},
render: function () {
this.$el.html(this.template({
title: this.callIfFunction(this.title),
description: this.callIfFunction(this.description),
action_class: this.callIfFunction(this.actionClass),
action_url: this.callIfFunction(this.actionUrl),
action_content: this.callIfFunction(this.actionContent)
}));
var detailsEl = this.$el.find('.card-meta-details');
_.each(this.callIfFunction(this.details), function (detail) {
// Call setElement to rebind event handlers
detail.setElement(detail.el).render();
detail.$el.addClass('meta-detail');
detailsEl.append(detail.el);
});
return this;
},
action: function () { },
cardClass: '',
title: '',
description: '',
details: [],
actionClass: '',
actionUrl: '',
actionContent: ''
});
return CardView;
});
}).call(this, define || RequireJS.define);
(function (define) {
'use strict';
define(['jquery',
'underscore',
'backbone',
'js/components/card/views/card'
],
function($, _, Backbone, CardView) {
describe('card component view', function () {
it('can render itself as a square card', function () {
var view = new CardView({ configuration: 'square_card' });
expect(view.$el).toHaveClass('square-card');
expect(view.$el.find('.card-meta-wrapper .action').length).toBe(1);
});
it('can render itself as a list card', function () {
var view = new CardView({ configuration: 'list_card' });
expect(view.$el).toHaveClass('list-card');
expect(view.$el.find('.card-core-wrapper .action').length).toBe(1);
});
it('can render child views', function () {
var testChildView = new (Backbone.View.extend({ className: 'test-view' }))();
spyOn(testChildView, 'render');
var view = new (CardView.extend({ details: [testChildView] }))();
expect(testChildView.render).toHaveBeenCalled();
expect(view.$el.find('.test-view')).toHaveClass('meta-detail');
});
it('calls action when clicked', function () {
spyOn(CardView.prototype, 'action');
var view = new CardView({ configuration: 'square_card' });
view.$el.find('.action').trigger('click');
expect(view.action).toHaveBeenCalled();
});
var verifyContent = function (view) {
expect(view.$el).toHaveClass('test-card');
expect(view.$el.find('.card-title').text()).toContain('A test title');
expect(view.$el.find('.card-description').text()).toContain('A test description');
expect(view.$el.find('.action')).toHaveClass('test-action');
expect(view.$el.find('.action')).toHaveAttr('href', 'www.example.com');
expect(view.$el.find('.action').text()).toContain('A test action');
};
it('can have strings for cardClass, title, description, and action', function () {
var view = new (CardView.extend({
cardClass: 'test-card',
title: 'A test title',
description: 'A test description',
actionClass: 'test-action',
actionUrl: 'www.example.com',
actionContent: 'A test action'
}))();
verifyContent(view);
});
it('can have functions for cardClass, title, description, and action', function () {
var view = new (CardView.extend({
cardClass: function () { return 'test-card'; },
title: function () { return 'A test title'; },
description: function () { return 'A test description'; },
actionClass: function () { return 'test-action'; },
actionUrl: function () { return 'www.example.com'; },
actionContent: function () { return 'A test action'; }
}));
verifyContent(view);
});
});
});
}).call(this, define || RequireJS.define);
......@@ -583,8 +583,10 @@
define([
// Run the LMS tests
'lms/include/teams/js/spec/teams_factory_spec.js',
'lms/include/teams/js/spec/topic_card_spec.js',
'lms/include/js/spec/components/header/header_spec.js',
'lms/include/js/spec/components/tabbed/tabbed_view_spec.js',
'lms/include/js/spec/components/card/card_spec.js',
'lms/include/js/spec/photocapture_spec.js',
'lms/include/js/spec/staff_debug_actions_spec.js',
'lms/include/js/spec/views/notification_spec.js',
......
......@@ -93,11 +93,13 @@ fixture_paths:
- templates/file-upload.underscore
- templates/components/header
- templates/components/tabbed
- templates/components/card
- js/fixtures/edxnotes
- js/fixtures/search
- templates/search
- templates/discovery
- common/templates
- teams/templates
requirejs:
paths:
......
<div class="card-core-wrapper">
<div class="card-core">
<h3 class="card-title"><%- title %></h3>
<p class="card-description"><%- description %></p>
</div>
<div class="card-actions">
<a class="action <%= action_class %>" href="<%= action_url %>"><%= action_content %></a>
</div>
</div>
<div class="card-meta-wrapper">
<div class="card-meta-details">
</div>
</div>
<div class="card-core-wrapper">
<div class="card-core">
<h3 class="card-title"><%- title %></h3>
<p class="card-description"><%- description %></p>
</div>
</div>
<div class="card-meta-wrapper has-actions">
<div class="card-meta-details">
</div>
<div class="card-actions">
<a class="action <%= action_class %>" href="<%= action_url %>"><%= action_content %></a>
</div>
</div>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment