Integrating Stripe OAuth in Open Event Frontend

Why is Stripe Oauth needed in frontend? Open event allows organizers to add tickets and accepts payments for tickets through various modes for example, Credit card, Debit card, Netbanking and offline payments. Stripe allows users to accept payments into their linked accounts on various online platforms after they provide client secret and publishable key. So to enable online payments in open event, organizers were required to authenticate their stripe account. This is done through Stripe OAuth.

Flow of OAuth

To allow organizers to link their stripe account admin has to enable stripe under payment gateway in admin settings. Admin provides his client ID and secret key. Admin also sets the redirect URL for his app on the stripe dashboard. After enabling these settings organizer will see an option to link their stripe account to open event when they are creating an event with paid tickets.

Here is what open event frontend does when we click connect to stripe button:

  1. Opens a popup to allow organizer to fill his stripe credentials and authorize open event app to access their secret and publishable key.
  2. Once the organizer fills his credentials and authorizes open event app, open event frontend fetches organizers auth code and saves it to server.
  3. Server on receiving auth code from frontend makes a request to stripe using the auth code to retrieve the publishable key and secret key.
  4. Once these are fetched server saves this information against the event so that all payments for that event can go to the linked stripe account.

Implementing the Frontend portion:

  • Choosing the library:

After looking at various libraries that support OAuth for Ember applications we decided to use Torii. Torii is the library that allows the addition of OAuth for various social apps such as Facebook, Google and Stripe too. It allows writing a custom provider for OAuth in case we do not want to use clients for which torii provides supports by default.

  • Implementing Stripe Provider:

Default provider for stripe given by torii fetched the client ID and redirect URL from environment.js file. But since in open event we have already saved client id of admin in our database so we will extend default stripe provider and modify its client Id so that it fetches client id from server. Code for extending default provider is given here:

import stripeConnect from 'torii/providers/stripe-connect';
import { alias } from '@ember/object/computed';
import { inject } from '@ember/service';
import { configurable } from 'torii/configuration';

function currentUrl() {
 let url = [window.location.protocol,
   '//',
   window.location.host].join('');
 if (url.substr(-1) !== '/') {
   url += '/';
 }
 return url;
}

export default stripeConnect.extend({

 settings: inject(),

 clientId: alias('settings.stripeClientId'),

 redirectUri: configurable('redirectUri', function() {
   return `${currentUrl()}torii/redirect.html`;
 })

});

 

We have fetched clientId from our settings service as alias(‘settings.stripeClientId’).

We have already defined settings in our services so we just need to inject the service here to be able to use it.

By default torii provides redirect url as {currentUrl}/torii/redirect.html. But in open event frontend we allow organizers to edit information on two routes and torii suggests in its docs to use {baseUrl}/torii/redirect.html as the redirect url to avoid potential vulnerability. So we also modified the default redirect url building method.

Saving information to server

Once we get the authorization token from stripe we send it to the server and save it to stripe-authorization model. The logic for the same is given below:

connectStripe() {
     this.get('data.event.stripeAuthorization.content') ? '' : this.set('data.event.stripeAuthorization', this.store.createRecord('stripe-authorization'));
     this.get('torii').open('stripe')
       .then(authorization => {
         this.set('data.event.stripeAuthorization.stripeAuthCode', authorization.authorizationCode);
       })
       .catch(error => {
         this.get('notify').error(this.get('l10n').t(`${error.message}. Please try again`));
       });
   },

 

This action gets called when we click on connect to stripe button. This action calls the stripe provider and opens a popup to enable the organizer to authenticate his stripe account.
Full code for this can be seen here.

In this way we connect the stripe service to open event to allow the organizer to receive payments for his events.

Resources
  • Stripe : Documentation on Stripe-Connect : Link
  • Torii: Library to implement Oauth. : Link
  • Implementation: Link to PR showing its implementation : Link

Open Event Frontend – Implement Access Event API via REST API

FOSSASIA‘s Open Event Frontend uses the Open Event Server as the REST API backend. The user can create an event using the Frontend. He can add sessions, tickets speakers etc. and all this updates the database tables in Open Event Server. The server provides certain endpoints for the user to access and/or update the information. It is important that the user is aware of the expected response from the server for his API request. Let’s see how this is displayed in the frontend.

In the event-view page of the frontend, which is accessible to the organizers, there is an Export tab, along with Overview, Tickets, Scheduler, Sessions, Speakers.

This tab has an Access Event Information via REST API section which displays the URL to be used by the user and the expected response. It looks as follows :

The user can choose between various options which he can include or exclude. The GET URL is modified accordingly and the appropriate response is shown to the user.

Example of this –

How is this implemented in Code?

We maintain two variables baseUrl and displayUrl to display the URL. baseUrl is the URL which is common in all requests, ie, till the include tag.

baseUrl: computed('eventId', function() {
 return `${`${ENV.APP.apiHost}/${ENV.APP.apiNamespace}/events/`}${this.get('eventId')}`;
})

displayUrl is the variable which stores the URL being displayed on the webpage. It is initialized to the same as baseUrl.

displayUrl: computed('eventId', function() {
 return `${`${ENV.APP.apiHost}/${ENV.APP.apiNamespace}/events/`}${this.get('eventId')}`;
})

To store the value of the toggle switches we use toggleSwitches as follows:

toggleSwitches: {
 sessions       : false,
 microlocations : false,
 tracks         : false,
 speakers       : false,
 sponsors       : false,
 tickets        : false
}

Whenever any of the switches are toggled, an action checkBox is called. This method updates the value of toggleSwitches, calls the method to update the displayUrl and make the corresponding API request to update the displayed response. The code looks like this :

makeRequest() {
 this.set('isLoading', true);
 this.get('loader')
   .load(this.get('displayUrl'), { isExternal: true })
   .then(json => {
     json = JSON.stringify(json, null, 2);
     this.set('json', htmlSafe(syntaxHighlight(json)));
   })
   .catch(() => {
     this.get('notify').error(this.get('l10n').t('Could not fetch from the server'));
     this.set('json', 'Could not fetch from the server');
   })
   .finally(() => {
     this.set('isLoading', false);
   });
},

buildDisplayUrl() {
 let newUrl = this.get('baseUrl');
 const include = [];

 for (const key in this.get('toggleSwitches')) {
   if (this.get('toggleSwitches').hasOwnProperty(key)) {
     this.get('toggleSwitches')[key] && include.push(key);
   }
 }

 this.set('displayUrl', buildUrl(newUrl, {
   include: include.length > 0 ? include : undefined
 }, true));
},

actions: {
 checkboxChange(data) {
   this.set(`toggleSwitches.${data}`, !this.get(`toggleSwitches.${data}`));
   this.buildDisplayUrl();
   this.makeRequest();
 }
}

The above code uses some utility methods such as buildUrl and this.get(‘loager’).load(). The complete codebase is available here -> Open Event Frontend Repository.

References

Implementing Event Image Size and Speaker Image Size APIs in Open Event Frontend

This blog article will illustrate how the Image Sizes APIs concerning event and speaker images are integrated in  Open Event Frontend, which allows for dynamic configurations of storing speaker and event images. The primary end points of Open Event API with which we are concerned with for fetching the event and speaker image sizes are

GET /v1/event-image-sizes

And

GET /v1/speaker-image-sizes

These endpoints are accessible only to a user with has administrator privileges as the customisation of image sizes is possible only on the admin dashboard. The image sizes are independent in regards to relationships and don’t have any related fields.

The model for the admin image settings route is defined as follows:

export default ModelBase.extend(CustomPrimaryKeyMixin, {
 thumbnailSizeQuality     : attr('number'),
 type                     : attr('string'),
 smallSizeWidthHeight     : attr('number'),
 smallSizeQuality         : attr('number'),
 iconSizeQuality          : attr('number'),
 iconSizeWidthHeight      : attr('number'),
 thumbnailSizeWidthHeight : attr('number')
});

The form which allows user to select image sizes, is in a separate component, and initially both the speaker and event image sizes are passed onto the component as a part of the entire model, so they can be separated later as per the requirement.

{{forms/admin/settings/images-form image=model save=’saveImages’ isLoading=isLoading}}

Most of the fields specify the units in which the numerical input concerning the image dimensions will be interpreted by the server and standard min and max validations are applied to the fields to ensure genuine and legitimate values can pass through the frontend.

<h3 class=”ui header”>{{t ‘Large Size’}}</h3>


{{input type=’number’ name=’large_width’ value=image.eventImageSize.fullWidth min=1}}

{{input type=’number’ name=’large_height’ value=image.eventImageSize.fullHeight min=1}}

{{input type=’number’ name=’large_quality’ value=image.eventImageSize.fullQuality min=1}}

</div>

{{ui-checkbox label=(t ‘Standard aspect ratio is 13:5. Tick to maintain aspect ratio.’) class=’checkbox’ name=’large_ratio’ checked=image.eventImageSize.fullAspect onChange=(action (mut image.eventImageSize.fullAspect))}}

{{t ‘Standard Size of the available area is 1300px X 500px’}}
<p>{{t ‘Found in :’}}</p>

{{t ‘Background Header Image in Public Event Page’}}

</div>

Furthermore, to ensure a user does not accidentally change the values inside the form, an action is triggered while transitioning away from the route which rollbacks any unsaved changes to the image sizes.

actions: {
willTransition() {
this.get('controller.model').forEach(image => {
image.rollbackAttributes();
});
}
}

Resources

Creating the View Route for Users in Open Event Frontend

This blog article will describe how the users view user route is created in Open Event Frontend, which allows the admin to view a user whenever view action button in the user table is clicked.

The primary end point of Open Event API with which we are concerned with for fetching the the users details is

GET /v1/users/{user_id}

The complete user information on the view route is displayed, which includes the name, email, details about the user. All these are the attributes of the model user. Thus the model for the route is defined as follows:

model(params) {
return this.store.findRecord('user', params.session_id);

The view route is located at app/admin/users/<user_id> and the parent route, app/users has another sub route within it called list. The list route shows all, active, deleted users. This list has a column of action buttons.

This list can only be accessed by the admin. Whenever the view button in the Actions column is clicked the admin gets redirected to the view users route.

actions: {
    moveToUserDetails(id) {
      this.transitionToRoute('admin.users.view', id);
    }
}

The user profile form is:

{{widgets/forms/image-upload
imageUrl=user.avatarUrl
needsCropper=true
label=(t ‘User Image’)
id=’user_image’
icon=’photo’
hint=(t ‘Select User Image’)
maxSizeInKb=10000
aspectRatio=(array 1 1)
helpText=(t ‘We recommend using at least a 1000x1000px (1:1 ratio) image’)}}


{{input type=’text’ id=’name’ value=user.firstName}}

{{input type=’text’ id=’last_name’ value=user.lastName}}

{{widgets/forms/rich-text-editor id=’details’ value=user.details}}

The view route shows the following information about the user: Name, Family name, Email, Image of the user, Details of the user. Thus the admin can view all the users registered in the application.

Resources

Adding Sessions and Events Statistics in the Admin Dashboard in Open Event Frontend

This blog article will illustrate how the admin statistics API for events and sessions is integrated and how the values of different types of sessions and events is added to the admin dashboard in   Open Event Frontend.

The primary end point of Open Event API with which we are concerned with for fetching the statistics are

GET /v1/admin/statistics/events

GET /v1/admin/statistics/events

import Route from '@ember/routing/route';

export default Route.extend({
 async model() {
   return {
     events: await this.get('store').queryRecord('admin-statistics-event', {
       filter: {
         name : 'id',
         op   : 'eq',
         val  : 1
       }
     })
sessions: await this.get('store').queryRecord('admin-statistics-session', {
       filter: {
         name : 'id',
         op   : 'eq',
         val  : 1
       }
     })
   };
 }
});

The route file helps to fetch the total count of each type of session and event through the queries written in the code. queryRecord is used instead of query because a single record is expected to be returned by the API. The view route is /admin.

The model needs to extend the base model class and all the attributes of the model will be number since the all the data obtained via these models from the API will be numerical statistics.

For Events:

import attr from 'ember-data/attr';
import ModelBase from 'open-event-frontend/models/base';export default ModelBase.extend({
draft     : attr('number'),
published : attr('number'),
past      : attr('number')
});

For Sessions:

import attr from 'ember-data/attr';
import ModelBase from 'open-event-frontend/models/base';export default ModelBase.extend({
confirmed : attr('number'),
accepted  : attr('number'),
submitted : attr('number'),
draft     : attr('number'),
rejected  : attr('number'),
pending   : attr('number')
});

Once we retrieve the values of the attributes from the queries written in the route file we display the values of pending, rejected, accepted sessions and published, draft, past events.

class="label"> {{t 'Accepted'}}
class="value">
class="ui teal label"> {{model.sessions.accepted}}
</div> </div>
class="ui small statistic">
class="label"> {{t 'Draft'}}
class="value">
class="ui yellow label"> {{model.sessions.pending}}
</div> </div>
class="ui small statistic">
class="label"> {{t 'Rejected'}}
class="value">
class="ui red label"> {{model.sessions.rejected}}

Resources

Creating the View Route for Sessions in Open Event Frontend

This blog article will illustrate how the creation of the view route for sessions is done and how the sessions API is integrated with it on Open Event Frontend, which allows for the sessions and their associated data to be displayed. Also, it illustrates how the template for my-sessions is modified to make it possible to reuse it for the view route with desired changes.

The primary end point of Open Event API with which we are concerned with for fetching the the session details is

GET /v1/sessions/{session_identifier}

For displaying the complete session information on the view route, the session type,  speakers and session track are also required. All of these extra attributes have a relationship with a given session and hence can be fetched in a single query. Thus the model for the route is defined as follows:

model(params) {
return this.store.findRecord(‘session’, params.session_id, {
include: ‘session-type,speakers,track’
});

The view route is located at app/routes/my-sessions/view and the parent route, app/routes/my-sessions has another sub route within it called list. The list route shows upcoming and past sessions to the user based on the params passed to it. Thus a navigation is required to alternate between those two routes. However, this navigation should not be present in the view route. Thus the template my-sessions.hbs is modified as follows:

{{#if (and (not-includes session.currentRouteName ‘my-sessions.view’))}}
<h1 class=”ui header”>{{t ‘My Sessions’}}</h1>

{{#tabbed-navigation}}
{{#link-to ‘my-sessions.list’ ‘upcoming’ class=’item’}}
{{t ‘Upcomming Sessions’}}
{{/link-to}}
{{#link-to ‘my-sessions.list’ ‘past’ class=’item’}}
{{t ‘Past Sessions’}}
{{/link-to}}
{{/tabbed-navigation}}

</div>
{{outlet}}
</div>
{{else}}
{{outlet}}
{{/if}}

The session.currentRouteName property allows conditional rendering of the navigation component.

Finally the template for the view route is created:

   

{{#if (eq model.state ‘accepted’)}}

{{t ‘Accepted’}}

{{else if (eq model.state ‘pending’)}}

{{t ‘Pending’}}

{{else if (eq model.state ‘rejected’)}}

{{t ‘Rejected’}}

{{/if}}
</div>

{{t ‘From ‘}}{{moment-format model.startAt ‘ddd, MMM DD HH:mm A’}}{{t ‘ to ‘}}{{moment-format model.endsAt ‘ddd, MMM DD HH:mm A’}}

</div>

{{#if model.shortAbstract}}
<p> <i>{{model.shortAbstract}}</i> </p>
{{/if}}
{{#if model.sessionType.name}}
<h3 class=”ui left aligned header”>Session Type</h3>
<p>{{model.sessionType.name}}</p>
{{/if}}
{{#if model.track.name}}
<h3 class=”ui left aligned header”>Track</h3>
<p>{{model.track.name}}</p>
{{/if}}
{{#if model.slidesUrl}}
<h3 class=”ui left aligned header”>Slide</h3>
<a href=”{{model.slidesUrl}}”>{{t ‘Download’}}</a>
{{/if}}
</div>

Based on the state property of the session, the label displaying the status is appropriately coloured wherein green, yellow and red colours denote accepted, pending and rejected sessions respectively. Since most of the fields of the session model are optional in nature, all of the them are subjected to conditional checks for existence.

Resources

 

Implementing dynamic forms to edit a speaker using Custom Forms in Open Event Frontend

Open Event Frontend allows the organizer of an event to customise the sessions and speakers form using the custom form API. While creating the event the organiser can select the form fields which he wants to place in the sessions and speakers form. This blog will illustrate how a form to edit the details of a speaker is created. Only those fields are included which were included by the person who created the event during the sessions and speakers creation section.

The first step is to retrieve the fields of the form. Each event has custom form fields which can be enabled on the sessions-speakers page, where the organiser can include/exclude the fields for speakers & session forms.

A query is written in the javascript file of the route admin/speakers/edit to retrieve the required details and create a form. The second query helps to determine the speaker id and include the model of speaker and the attribute values of the speaker with that specific id.

import Route from '@ember/routing/route';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';

export default Route.extend(AuthenticatedRouteMixin, {
 titleToken(model) {
   var speakerName = model.get('name');
   return this.get('l10n').t('Edit Speaker-'.concat(speakerName));
 },
 async model(params) {
   const eventDetails = this.modelFor('events.view');
   return {
     event : eventDetails,
     form  : await eventDetails.query('customForms', {
       'page[size]' : 50,
       sort         : 'id'
     }),
     speaker: await this.get('store').findRecord('speaker', params.speaker_id)
   };
 }
});

In the frontend we call the form of session and speakers. With the speaker-id being passed from the route, a form is created with the values entered by the user during the speaker creation and the other attributes marked included in the session-speakers wizard.

{{forms/session-speaker-form fields=model.form data=model save=(action 'save') isSpeaker=true includeSpeaker=true isSessionSpeaker=true isLoading=isLoading}}

Finally whenever user edits a speaker and clicks on the save button patch endpoint of the speakers API is called and the new details are saved.

Resources

  • Official Ember Model Table docs: http://onechiporenko.github.io/ember-models-table/v.1
  • Official Ember Data documentation: https://github.com/emberjs/data

How forms are created and the validations are added in Open Event Frontend

This blog article will illustrate how validations are added to a form  in Open Event Frontend, in a standard format. The form to Edit a Speaker is created in the route /event/<event_identifier>/speakers/edit. For the creation of a form an ember component is generated.

$ ember g component forms/events/view/edit-speaker

This command results in the generation of:

  1. An ember component edit-speaker.js to add the validation rules of the form.
  2. A handlebar edit-speaker.hbs where the HTML code is written.

First the form for editing a speaker is created. All the fields are added.

<form class=”ui form {{if isLoading ‘loading’}}” {{action ‘submit’ on=’submit’}}>


{{input type=’text’ id=’title’ value=data.name}}

{{input type=’text’ id=’email’ value=data.email}}

{{widgets/forms/image-upload
label=(t ‘Photo’)
imageUrl=data.photoUrl
icon=’image’
hint=(t ‘Select Photo’)
maxSizeInKb=1000}}

{{input type=’text’ id=’organisation’ value=data.organisation}}

{{input type=’text’ id=’position’ value=data.position}}

{{input type=’text’ id=’country’ value=data.country}}

{{widgets/forms/rich-text-editor value=data.shortBiography name=’shortBiography’}}

{{input type=’text’ id=’website’ value=data.website}}

{{input type=’text’ id=’twitter’ value=data.twitter}}

<button type=”submit” class=”ui teal submit button update-changes”>
{{t ‘Save Speaker’}}
</button>
</form>

Then validation rules for the fields included in the form are added in the component. The validations of a form are stored as objects, where the  identifier attribute determines which field to apply the validation conditions to. The rules array contains all the rules to be applied to the determined object. Within rules, the type represents the kind of validation, whereas the prompt attribute determines what message shall be displayed in case there is a violation of the validation rule. These validations are in turn implemented by the FormMixin.

import { protocolLessValidUrlPattern, validTwitterProfileUrlPattern } from ‘open-event-frontend/utils/validators’;

export default Component.extend(FormMixin, {

getValidationRules() {
return {
inline : true,
delay  : false,
on     : ‘blur’,
fields : {
email: {
identifier : ’email’,
rules      : [
{
type   : ’empty’,
prompt : this.get(‘l10n’).t(‘Please enter your email ID’)
},
{
type   : ’email’,
prompt : this.get(‘l10n’).t(‘Please enter a valid email ID’)
}
]
},
twitter: {
identifier : ‘twitter’,
optional   : true,
rules      : [
{
type   : ‘regExp’,
value  : validTwitterProfileUrlPattern,
prompt : this.get(‘l10n’).t(‘Please enter a valid twitter url’)
},
{
type   : ‘regExp’,
value  : protocolLessValidUrlPattern,
prompt : this.get(‘l10n’).t(‘Please enter a valid url’)
}
]
},
website: {
identifier : ‘website’,
optional   : ‘true’,
rules      : [
{
type   : ‘regExp’,
value  : protocolLessValidUrlPattern,
prompt : this.get(‘l10n’).t(‘Please enter a valid url’)
}
]
}
}
};
}

Then for adding the validation for the URLs of the speaker’s website and his twitter account regular expressions are used. They are used to perform pattern-matching.

export const  validTwitterProfileUrlPattern = new RegExp(
‘^twitter\\.com\\/([a-zA-Z0-9_]+)$’
);

 

export const protocolLessValidUrlPattern = new RegExp(
‘^’
// user:pass authentication
+ ‘(?:\\S+(?::\\S*)[email protected])?’
+ ‘(?:’
// IP address exclusion
// private & local networks
+ ‘(?!(?:10|127)(?:\\.\\d{1,3}){3})’
+ ‘(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})’
+ ‘(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})’
// IP address dotted notation octets
// excludes loopback network 0.0.0.0
// excludes reserved space >= 224.0.0.0
// excludes network & broacast addresses
// (first & last IP address of each class)
+ ‘(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])’
+ ‘(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}’
+ ‘(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))’
+ ‘|’
// host name
+ ‘(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)’
// domain name
+ ‘(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*’
// TLD identifier
+ ‘(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))’
// TLD may end with dot
+ ‘\\.?’
+ ‘)’
// port number
+ ‘(?::\\d{2,5})?’
// resource path
+ ‘(?:[/?#]\\S*)?’
+ ‘$’, ‘i’
);

Resources

  • EmberJS Mixins–Official ember documentation: https://guides.emberjs.com/v2.2.0/models
  • Kravvitz, Regular Expression Rules: http://forums.devshed.com/javascript-development/493764-regexp-match-url-pattern-post1944160.html#post1944160

Open Event Frontend – Updating Ember Models Table from V1 to V2

FOSSASIA‘s Open Event Frontend uses the Ember Models Table for rendering all its tables. This provides features like easy sorting, pagination etc. Another major feature is that it can be modified to meet our styling needs. As we use Semantic UI for styling, we added the required CSS classes to our table.

In version 1 this was done by overriding the classes, as shown below :

const defaultMessages = {
  searchLabel            : 'Search:',
  searchPlaceholder      : 'Search',


  ..... more to follow 
};

const defaultIcons = {
  sortAsc         : 'caret down icon',
  sortDesc        : 'caret up icon',
  columnVisible   : 'checkmark box icon',
  
  ..... more to follow  
};

const defaultCssClasses = {
  outerTableWrapper              : 'ui ui-table',
  innerTableWrapper              : 'ui segment column sixteen wide inner-table-wrapper',
  table                          : 'ui tablet stackable very basic table',
  globalFilterWrapper            : 'ui row',

 ... more to follow
};

const assign = Object.assign || assign;

export default TableComponent.extend({
  layout,

  _setupMessages: observer('customMessages', function() {
    const customIcons = getWithDefault(this, 'customMessages', {});
    let newMessages = {};
    assign(newMessages, defaultMessages, customIcons);
    set(this, 'messages', O.create(newMessages));
  }),

  _setupIcons() {
    const customIcons = getWithDefault(this, 'customIcons', {});
    let newIcons = {};
    assign(newIcons, defaultIcons, customIcons);
    set(this, 'icons', O.create(newIcons));
  },

  _setupClasses() {
    const customClasses = getWithDefault(this, 'customClasses', {});
    let newClasses = {};
    assign(newClasses, defaultCssClasses, customClasses);
    set(this, 'classes', O.create(newClasses));
  },

  simplePaginationTemplate: 'components/ui-table/simple-pagination',

  ........
});

And was used in the template as follows:

<div class="{{classes.outerTableWrapper}}">
  <div class="{{classes.globalFilterDropdownWrapper}}">

But in version 2, some major changes were introduced as follows:

  1. All partials inside a models-table were replaced with components
  2. models-table can now be used with block content
  3. New themes mechanism introduced for styling

Here, I will talk about how the theming mechanism has been changed. As I mentioned above, in version 1 we used custom classes and icons. In version 2 the idea itself has changed. A new type called Theme was added. It provides four themes out of the box – SemanticUI, Bootstrap4, Bootstrap3, Default.

We can create our custom theme based on any of the predefined themes. To suit our requirements we decided to modify the SemanticUI theme. We created a separate file to keep our custom theme so that code remains clean and short.

import Default from 'ember-models-table/themes/semanticui';

export default Default.extend({
 components: {
   'pagination-simple'    : 'components/ui-table/simple-pagination',
   'numericPagination'    : 'components/ui-table/numeric-pagination',
   .....  
 },

 classes: {
   outerTableWrapper              : 'ui ui-table',
   innerTableWrapper              : 'ui segment column sixteen wide inner-table-wrapper',
   .....
 },

 icons: {
   sortAsc         : 'caret down icon',
   sortDesc        : 'caret up icon',
   ......
 },

 messages: {
   searchLabel            : 'Search:',
   .....
 }
});

So a theme mostly consists of four main parts:

  • Components
  • Classes
  • Icons
  • Messages

The last three are same as customClasses and customIcons and customMessages in version 1. Components is the map for components used internally in the models-table. In case you need to use a custom component, that can be done as follows:

Make a new JavaScript file and provide its path in your theme file.

import DefaultDropdown from '../../columns-dropdown';
import layout from 'your layout file path';
export default DefaultDropdown.extend({
  layout
});

Now just create the theme file object and pass it to themeInstance in the ui-table file (can also be passed in the template and the controller, but this has to be done for each table individually).

import TableComponent from 'ember-models-table/components/models-table';
import layout from 'open-event-frontend/templates/components/ui-table';
import Semantic from 'open-event-frontend/themes/semantic';

export default TableComponent.extend({
 layout,

 themeInstance: Semantic.create()
});

Hence, version 2 introduces many new styling options and requires some refactoring for those who were using version 1. It is totally worth it though considering how easy and well managed it is now.

References

Adding the Edit Session route in Open Event Frontend

This blog article will illustrate how to edit a Session in Open Event Frontend, for which a nested route /sessions/edit/<session_id> had to be created. Further an Edit Session Form was created on that route to use the patch end-point of the sessions API of Open Event API.

To get started with it, we simply use the ember CLI to generate the routes

$  ember generate route events/view/sessions/edit

Now router.js file is changed to add the session_id. The router.js file should be looking like this now.

this.route(‘sessions’, function() {
this.route(‘list’, { path: ‘/:session_status’ });
this.route(‘create’);
this.route(‘edit’, { path: ‘/edit/:session_id’ });
});

This means that our routes and sub routes are in place. We are using ember CLI to generate these routes, that means, the template files for them generate automatically. Now these routes exist and we need to write the data in the templates of these routes which will get displayed to the end user.

The routes are nested, the content of the parent route can be made available to all the children routes via the outlet in ember js.

Next, we go to the template file of sessions/edit route which is at templates/events/view/sessions/edit.hbs which displays an Edit Session form.

<form class=”ui form {{if isLoading ‘loading’}}” {{action ‘submit’ on=’submit’}}>


{{input type=’text’ id=’title’ value=data.title}}

{{widgets/forms/rich-text-editor value=data.shortAbstract name=’shortAbstract’}}

{{widgets/forms/rich-text-editor value=data.comments name=’comments’}}

{{input type=’text’ value=data.slidesUrl}}

<button type=”submit” class=”ui teal submit button update-changes”>
{{t ‘Save Session’}}
</button>
</form>

After the editing of different attributes is done Save Session button is clicked for which the action is defined in the controller /controllers/events/view/sessions/edit.js. The patch endpoint was called and the details of the attributes changes were saved.

save() {
this.set(‘isLoading’, true);
this.get(‘model’).save()
.then(() => {
this.get(‘notify’).success(this.get(‘l10n’).t(‘Session details have been saved’));
this.transitionToRoute(‘events.view.sessions’);
})
.catch(() => {
this.get(‘notify’).error(this.get(‘l10n’).t(‘Oops something went wrong. Please try again’));
})
.finally(() => {
this.set(‘isLoading’, false);
});
}
}

Thus after editing the attributes of sessions in the Edit Session form and clicking on the Save Session button the session details are edited.

Resources

  • EmberJS Controllers: https://guides.emberjs.com/v2.12.0/controllers
  • Open Event Server, API docs: https://open-event-api.herokuapp.com