Hello Aleksey,
We’d like to provide open source modular theming examples accessible on GitHub sometime in the future, but hopefully this will answer your question in the short term.
The contents of the community-posts
module’s partials.js
file are as follows:
const communityPostsListItem = `
<div class="community-posts-list-item">
<div class="title-container">
<i class="product-logo">{{{productSVG}}}</i> <a class="title" id="topic" href="{{topic.html_url}}">{{topic.name}}</a> > <a class="title" id="post" href="{{html_url}}">{{title}}</a>
{{#if status}}
<span class="status {{status}}">{{statusText}}</span>
{{/if}}
</div>
<div class="user">
<img class="user-photo" src="{{user.photo.content_url}}"/>
<span class="name">{{user.name}}</span>
<span class="date">{{date}}</span>
</div>
<div class="details">
<div class="votes">
<div class="count">{{vote_count}}</div>
<div class="text">{{votes}}</div>
</div>
<div class="comments">
<div class="count">{{comment_count}}</div>
<div class="text">{{comments}}</div>
</div>
</div>
</div>
`
Handlebars.registerPartial('communityPostsListItem', communityPostsListItem);
const communityPostsContainerList = `
<div>
{{#if posts}}
{{#each posts}}
{{> communityPostsListItem this}}
{{/each}}
{{else}}
<p class="none">{{none}}</p>
{{/if}}
</div>
`
Handlebars.registerPartial('communityPostsContainerList', communityPostsContainerList);
const communityPosts = `
<div class="community-posts">
<h1 class="title">
{{title}}
</h1>
<div class="tabs">
{{#each tabs}}
<span class="tab" data-id="{{panel}}">
<div class="icon">
{{{icon}}}
</div>
<span>{{label}}</span>
</span>
{{/each}}
</div>
<div class="panels-container">
<div class="panel topics shadow-blocks__layout">
{{#each topics}}
{{> shadowCard this}}
{{/each}}
</div>
<div class="panel posts">
<div class="filters">
{{#each filters}}
<span class="filter {{id}}" data-id="{{id}}">
{{#if icon}}
<img class="icon" src="{{icon}}"/>
{{/if}}
<span class="name">{{name}}</span>
</span>
{{/each}}
</div>
<div class="posts-container">
{{#each filters}}
<div class="posts {{id}}">
<img class="loading" src="{{../loadingGif}}" />
</div>
{{/each}}
</div>
</div>
<div class="panel eaps shadow-blocks__layout">
{{#each eaps}}
{{> shadowCard this}}
{{/each}}
</div>
</div>
</div>
`
Handlebars.registerPartial('communityPosts', communityPosts);
This file is loaded into the module’s index.js
file along with other dependencies using ECMA import statements. Webpack resolves the import
statements and allows us to build a single asset JS file (modules.js
) and CSS file (modules.css
) for theme deployment.
The module JS & CSS is loaded and runs like any other Guide theme asset.
Top of index.js
:
import './partials'
import './style.scss'
In document_head.hbs
:
<script src={{asset 'handlebars-1-0-rc-1.js'}}></script>
... much later in the file ...
<link rel="stylesheet" href="{{asset "modules.css"}}">
<script src="{{asset "modules.js"}}"></script>
Note: we load Handlebars as an asset in document_head.hbs
and it’s globally accessible to the module code for legacy purposes.
Within index.js
, the community-posts constructor compiles the templates and stores references on the instance for use in render methods that run the compiled templates and insert the results into the DOM.
export default class CommunityPosts {
constructor() {
const config = window.Modules.communityPosts
this.state = {
config,
/* Compile the templates once on load. */
communityPostsTemplate: Handlebars.compile('{{> communityPosts}}'),
communityPostsContainerListTemplate: Handlebars.compile('{{> communityPostsContainerList}}'),
/* Community Post Filters */
filters: config.filters,
posts: {},
/* jQuery Element References */
$communityPostRoot: $('#community-posts')
}
/* Instance Bindings */
this.render = this.render.bind(this)
this.renderPosts = this.renderPosts.bind(this)
this.fetchPosts = this.fetchPosts.bind(this)
this.selectorClicked = this.selectorClicked.bind(this)
/* Initialization */
if (this.state.$communityPostRoot.length > 0) {
this.initialize()
}
}
initialize() {
this.state.filters.map(filter => {
this.fetchPosts(filter)
.then(data => {
const posts = data.posts.map(post => {
const { postProduct, topic } = parseProductFromTopic(post.topic)
post.topic = topic
post.productSVG = productIcons[postProduct]
return post
})
this.state.posts[filter.id] = posts
})
.then(() => this.renderPosts(filter))
.catch(e => console.error(`Could not fetch posts for filter "${filter.id}"!`, e)) //eslint-disable-line
})
this.render()
}
// ...
// NOTE:
// MANY METHODS OMITTED FOR CLARITY
// ...
/* Convenience method to set the contents of a jQuery element. */
setHTML($element, html) {
$element[0].innerHTML = html
}
renderPosts(filter) {
const posts = this.state.posts[filter.id]
const context = {
none: this.state.config.none,
posts: (posts && posts.length > 0 ? posts : false)
}
/* Render posts panel. */
const $container = this.state.$communityPostRoot.find(`.panels-container .posts.${filter.id}`)
this.setHTML($container, this.state.communityPostsContainerListTemplate(context))
}
render() {
this.setHTML(this.state.$communityPostRoot,
this.state.communityPostsTemplate({
...window.Modules.communityPosts
})
)
/* Bindings and Initialization */
const selectors = ['.tabs .tab', '.filters .filter']
this.state.$communityPostRoot.find(selectors.join(', ')).click(this.selectorClicked)
this.state.$communityPostRoot.find(selectors.map(selector => `${selector}:first`).join(', ')).click()
}
}
This module is instantiated once on every page but will only run the initialize()
method and subsequent fetching and render methods to decorate the page if the $communityPostRoot
element is found.
Finally, it should be noted the data used for rendering the module is assembled partially from Zendesk server side rendered Curlybars templating and partially from REST requests to endpoints. This is the content of community-posts.hbs
which actually gets appended to the Guide footer.hbs
Curlybars template when we build the theme.
Note: we use the {{dc }}
helper extensively to load localized strings dynamically. This string data is substituted on the server side for use in browser by the module. Use with caution in your own theme.
<!-- community-posts.hbs -->
<script>
(function() {
"use strict";
const ICONS = window.Modules.GardenIcons
const PRODUCT = getCurrentProduct();
const tabs = [
{
panel: 'posts',
icon: ICONS['duplicate-stroke'],
label: "{{t 'posts'}}"
}
]
const FEATURED_POSTS = {
support: "{{dc 'community_posts_featured_posts_support'}}",
chat: "{{dc 'community_posts_featured_posts_chat'}}",
explore: "{{dc 'community_posts_featured_posts_explore'}}",
connect: "{{dc 'community_posts_featured_posts_connect'}}"
}
const filters = [
{
id: "recent",
icon: false,
name: "{{dc 'community_posts_f_recent'}}",
url: '/api/v2/community/posts.json?per_page=100&sort_by=updated_at&include=users,topics'
},
{
id: "trending",
icon: false,
name: "{{dc 'community_posts_f_trending'}}",
url: '/api/v2/community/posts.json?per_page=100&sort_by=updated_at&include=users,topics'
},
{
id: "featured",
icon: false,
name: "{{dc 'community_posts_f_featured'}}",
url: '/api/v2/community/posts/ID_PLACEHOLDER.json?include=users,topics',
ids: FEATURED_POSTS[PRODUCT] || FEATURED_POSTS['support']
},
{
id: "unanswered",
icon: false,
name: "{{dc 'community_posts_f_unanswered'}}",
url: '/api/v2/community/posts.json?per_page=100&sort_by=created_at&include=users,topics'
}
]
/* Parse JSON Configs */
const jsonConfigs = {
topics: window.CARD_DATA['topics']['en-us'],
eaps: window.CARD_DATA['eaps']['en-us']
}
Object.values(jsonConfigs).forEach(function(config) {
config.forEach(function(item) {
if(item && (item.icon in window.Modules.ProductIcons || item.icon in window.Modules.GardenIcons)) {
item.icon = window.Modules.ProductIcons[item.icon] || window.Modules.GardenIcons[item.icon]
}
})
})
if(jsonConfigs.topics.length > 0){
tabs.unshift({
panel: 'topics',
icon: ICONS['speech-bubble-conversation-stroke'],
label: "{{t 'topics'}}"
})
}
if(jsonConfigs.eaps.length > 0){
tabs.push({
panel: 'eaps',
icon: ICONS['clipboard-list-stroke'],
label: 'EAPs'
})
}
window.Modules = Object.assign({}, window.Modules || {}, {
communityPosts: {
title: "{{dc 'community_posts_title'}}",
none: "{{dc 'community_posts_none'}}",
comment: "{{t 'comment' count=1}}",
comments: "{{t 'comment' count=2}}",
vote: "{{t 'vote' count=1}}",
votes: "{{t 'vote' count=2}}",
statusMap: {
planned: "{{dc 'community_posts_planned'}}",
not_planned: "{{dc 'community_posts_not_planned'}}",
answered: "{{dc 'community_posts_answered'}}",
completed: "{{dc 'community_posts_completed'}}"
},
tabs: tabs,
filters: filters,
topics: jsonConfigs.topics,
eaps: jsonConfigs.eaps,
loadingGif: "{{asset 'loading.gif'}}"
}
})
})()
</script>
This is awesome and very helpful, thank you for the info!