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

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

How Sanitizer Service Works in Open Event Frontend

This blog article will illustrate how the sanitizer service works  in Open Event Frontend, which allows the frontend to sanitize or clean any piece of text which can cause potential vulnerabilities in the application. To quote the official ember guides:

“An Ember.Service is a long-lived Ember object that can be made available in different parts of your application.”

Thus the chief advantage the service offers is, that the service once injected in the application can be called at  any place in the templates without an explicit import.

For instance, in the route `coc`, the templates make use of the servcie as

{{sanitize model.codeOfConduct}}

The core of the sanitizer service is comprised of npm sanitize-html module. The sanitize-html is a powerful module which offers an extensive set of features and customisations to deal with cleaning of html embedded text. The service efficiently exposes these features to the app, and wraps its features in various functions to offer several methods which can be used to handle html text.

export default Service.extend({

sanitize: null,

options: {
allowedTags       : [‘b’, ‘strong’, ‘i’, ’em’, ‘u’, ‘ol’, ‘ul’, ‘li’, ‘a’, ‘p’],
allowedAttributes : {
‘a’: [‘href’, ‘rel’, ‘target’]
},
selfClosing           : [‘br’],
allowedSchemes        : [‘http’, ‘https’, ‘ftp’, ‘mailto’],
allowedSchemesByTag   : {},
allowProtocolRelative : false,
transformTags         : {
‘i’ : ’em’,
‘b’ : ‘strong’,
‘a’ : sanitizeHtml.simpleTransform(‘a’, { rel: ‘nofollow’, target: ‘_blank’ })

},

purify(string) {
return sanitizeHtml(string, this.options);
},

strip(string) {
return sanitizeHtml(string, {
allowedTags       : [],
allowedAttributes : []
});
}
});

The options parameter of the service allows properties like allowedTags and and allowedAttributes which remove the specified tags and attributes from the text respectively. The transformTags property replaces the specified left hand side tags to the corresponding right hand side tags.

These are the generic requirements of the app and hence are exposed via tha purify method of the service. The purify method returns the sanitized string based on the configuration in the options parameter. Alternatively the strip method does not make use of the options parameter and rather passes a custom set of options with empty values for allowedTags and allowedAttributes. This method is used to completely remove any html tags or attributes from the text.

The service can be used outside of the templates in a controller or a component.

For instance the css helper uses the service to clean any text of html tags passed as a css property. The CSS helper is defined as follows.

export default Helper.extend({
sanitizer: service(),

compute(params, hash) {
let style = ”;
forOwn(hash, (value, key) => {
style += `${key}: ${value};`;
});
return htmlSafe(this.get(‘sanitizer’).strip(style));

});

Resources

  • Ember Services: https://guides.emberjs.com/v2.1.0/applications/services/
  • Sanitize-HTML docs: https://www.npmjs.com/package/sanitize-html

 

Implementing Speaker Table on Open Event Frontend by Integrating Speakers API

This blog article will illustrate how the Speakers API is integrated in Open Event Frontend, which allows for the speakers and their associated data to be displayed in the speakers table. Also, it illustrates how the basic semantic UI table is converted into an ember model table to accommodate the fetched data from the API. Our discussion primarily will involve the app/routes/events/view/speakers  route to illustrate the process.

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

GET /v1/events/{event_identifier}/speakers

First we need to formulate the filters options while making the API call. There are several tabs present which display all the sessions in a particular state i.e. pending, rejected, accepted and all. Thus the formulated filter options stored in the variable filterOptions are as follows:

let filterOptions = [];
if (params.speakers_sessions_state === 'pending') {
filterOptions = [
{
name : 'state',
op : 'eq',
val : 'pending'
}
];
} else if (params.speakers_sessions_state === 'accepted') {
filterOptions = [
{
name : 'state',
op : 'eq',
val : 'accepted'
}
];
} else if (params.speakers_sessions_state === 'rejected') {
filterOptions = [
{
name : 'state',
op : 'eq',
val : 'rejected'
}
];
} else {
filterOptions = [];
}

Next we need to formulate the required query to actually make the API call. It is important to note here that there is a column for sessions of the speaker in  the speaker table. Sessions are a related field for a user and hence we will make use of the include clause while generating the query, Similarly we will make use of the filter clause to pass on the filter options we generated above in filterOptions.

Since the speakers are being fetched for a particular event, and we are in that event’s dashboard, the speakers in question have a hasMany relationship with the aforementioned event. Thus we can make use of the model already fetched in the view route. So the final query call will be as follows:

 return this.modelFor('events.view').query('speakers', {
include      : 'sessions,event',
filter       : filterOptions,
'page[size]' : 10
});

The page[size] option in the query is for implementing pagination, it ensures that at max 10 speakers are returned at once.

Next, we need to convert the existing table to an ember model table. For this the first step is to delete the entire original table along with the dummy data in it. The events-table ember table component will be re-used to form the base structure of the table as follows:

{{events/events-table columns=columns data=model
useNumericPagination=true
showGlobalFilter=true
showPageSize=true
}}

Final steps of conversion table to ember table involve defining the columns of the table. They will be defined in the usual manner, with mandatory title and name attributes. In case the display requirements of the data are in mere simple text, calling a template for displaying the text is not required, however for more complex natured values in the columns, it is advisable to make use of the component, and the technical logic can be handled in the component template itself. For instance, one such field on the speakers table is sessions which are related records and were included especially in the query call. They are not directly accessible by their field names. Thus they must be referred as a child of the record object.

{{#each record.sessions as |session|}}
       {{session.title}}
{{/each}}

Similarly the template for the actions column will have to be created as it requires complex logic to implement actions on those buttons. After defining all the columns in the controller, the final columns are as follows:

columns: [
{
propertyName : 'name',
title       : 'Name'
},
{
propertyName : 'email',
title       : 'Email'
},
{
propertyName : 'mobile',
title       : 'Phone'
},
{
propertyName   : 'sessions',
title         : 'Submitted Sessions',
template       : 'components/ui-table/cell/events/view/speakers/cell-simple-sessions',
disableSorting : true
},
{
propertyName : '',
title       : 'Actions',
template     : 'components/ui-table/cell/events/view/speakers/cell-buttons'
}
]

After performing all these steps, the static table which was holding dummy data has been converted into an ember table and thus features like inbuilt pagination, searching etc. can be used.

Resources