diff --git a/package.json b/package.json
index d217319e1a486296432fc0071546c99bcbe92c78..807b7759c7418539b1702214e8017096def58f4e 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,8 @@
   "author": "AMIV IT team",
   "license": "ISC",
   "dependencies": {
+    "ajv": "^5.5.0",
+    "email-validator": "^1.1.1",
     "babel-core": "^6.26.0",
     "babel-cli": "^6.26.0",
     "babel-loader": "^7.1.2",
@@ -30,4 +32,4 @@
     "webpack": "^3.8.1",
     "webpack-dev-server": "^2.9.3"
   }
-}
\ No newline at end of file
+}
diff --git a/src/models/event.js b/src/models/events.js
similarity index 78%
rename from src/models/event.js
rename to src/models/events.js
index 53b46845c9b576b2a3efbfbed27b686d41520845..197931b89bc7bf4cf036c6801ffe44845be93f8e 100644
--- a/src/models/event.js
+++ b/src/models/events.js
@@ -45,11 +45,18 @@ export function checkCurrentSignup() {
   }).then((result) => {
     [this.currentSignup] = result._items;
     this.currentSignupLoaded = true;
-    log(this.currentSignup);
   });
 }
 
-export function signupCurrent(email = '') {
+export function signupCurrent(additionalFields, email = '') {
+  let additionalFieldsString;
+  if (this.current.additional_fields === undefined ||
+    additionalFields === null || typeof additionalFields !== 'object') {
+    additionalFieldsString = undefined;
+  } else {
+    additionalFieldsString = JSON.stringify(additionalFields);
+  }
+
   if (isLoggedIn()) {
     log(`UserId: ${getUserId()}`);
     m.request({
@@ -57,6 +64,7 @@ export function signupCurrent(email = '') {
       url: `${apiUrl}/eventsignups`,
       data: {
         event: this.current._id,
+        additional_fields: additionalFieldsString,
         user: getUserId(),
       },
       headers: getToken() ? {
@@ -64,19 +72,19 @@ export function signupCurrent(email = '') {
       } : {},
     }).then(() => { this.checkCurrentSignup(); });
   } else if (this.current.allow_email_signup) {
-    if (email.length > 0) {
-      m.request({
-        method: 'POST',
-        url: `${apiUrl}/eventsignups`,
-        data: {
-          event: this.current._id,
-          email,
-        },
-        headers: getToken() ? {
-          Authorization: `Token ${getToken()}`,
-        } : {},
-      }).then(() => { this.checkCurrentSignup(); });
-    }
+    log(`Email: ${email}`);
+    m.request({
+      method: 'POST',
+      url: `${apiUrl}/eventsignups`,
+      data: {
+        event: this.current._id,
+        additional_fields: additionalFieldsString,
+        email,
+      },
+      headers: getToken() ? {
+        Authorization: `Token ${getToken()}`,
+      } : {},
+    }).then(() => { this.checkCurrentSignup(); });
   }
 }
 
diff --git a/src/views/eventDetails.js b/src/views/eventDetails.js
index d873f8e32aec764fa720feaf4653551378673aff..996290b761139fce556dc5a0ae62bc77c97ea941 100644
--- a/src/views/eventDetails.js
+++ b/src/views/eventDetails.js
@@ -1,39 +1,83 @@
-import * as events from '../models/event';
+import * as EmailValidator from 'email-validator';
+import * as events from '../models/events';
+import { log } from '../models/log';
 import { isLoggedIn } from '../models/auth';
+import { inputGroup, submitButton } from './formFields';
+import JSONSchemaForm from './jsonSchemaForm';
 
 const m = require('mithril');
 
-let signupEmail = '';
-
-class EventSignupForm {
-  static oninit() {
+class EventSignupForm extends JSONSchemaForm {
+  oninit(vnode) {
+    super.oninit(vnode);
+    this.emailErrors = [];
+    this.emailValid = false;
     if (isLoggedIn()) {
       events.checkCurrentSignup();
     }
   }
 
-  static view() {
-    if (typeof events.getCurrent() === 'undefined') {
-      return m('div');
+  submit() {
+    if (isLoggedIn()) {
+      events.signupCurrent(super.getValue());
+    } else {
+      events.signupCurrent(super.getValue(), this.email);
     }
+  }
+
+  view() {
+    // do not render anything if there is no data yet
+    if (typeof events.getCurrent() === 'undefined') return m();
+
     if (isLoggedIn()) {
-      if (!events.currentSignupHasLoaded()) {
-        return m('span', 'Loading...');
-      } else if (typeof events.getCurrentSignup() === 'undefined') {
-        return m('button', { onclick() { events.signupCurrent(); } }, 'signup');
+      // do not render form if there is no signup data of the current user
+      if (!events.currentSignupHasLoaded()) return m('span', 'Loading...');
+      if (typeof events.getCurrentSignup() === 'undefined') {
+        const elements = this.renderFormElements();
+        elements.push(m(submitButton, {
+          active: super.isValid(),
+          args: {
+            onclick: () => this.submit(),
+          },
+          text: 'Signup',
+        }));
+        return m('form', elements);
       }
+      return m('div', 'You have already signed up for this event.');
     } else if (events.getCurrent().allow_email_signup) {
-      return m('div', [
-        m('input', {
+      const elements = this.renderFormElements();
+      elements.push(m(inputGroup, {
+        name: 'email',
+        title: 'Email',
+        args: {
           type: 'text',
-          placeholder: 'Email',
-          oninput: m.withAttr('value', (value) => { signupEmail = value; }),
-          value: signupEmail,
-        }),
-        m('button', { onclick() { events.signupCurrent(signupEmail); } }, 'signup'),
-      ]);
+        },
+        onchange: (e) => {
+          // bind changed data
+          this.email = e.target.value;
+
+          // validate if email address has the right structure
+          if (EmailValidator.validate(this.email)) {
+            this.emailValid = true;
+            this.emailErrors = [];
+          } else {
+            this.emailValid = false;
+            this.emailErrors = ['Not a valid email address'];
+          }
+        },
+        getErrors: () => this.emailErrors,
+        value: this.email,
+      }));
+      elements.push(m(submitButton, {
+        active: this.emailValid && super.isValid(),
+        args: {
+          onclick: () => this.submit(),
+        },
+        text: 'Signup',
+      }));
+      return m('form', elements);
     }
-    return m('div');
+    return m('div', 'This event is for AMIV members only.');
   }
 }
 
@@ -44,7 +88,27 @@ export default class EventDetails {
 
   static view() {
     if (typeof events.getCurrent() === 'undefined') {
-      return m('div');
+      return m();
+    }
+    log(events.getCurrent());
+    let eventSignupForm;
+    const now = new Date();
+    const registerStart = new Date(events.getCurrent().time_register_start);
+    const registerEnd = new Date(events.getCurrent().time_register_end);
+    log(`Now: ${now}`);
+    log(`Start: ${registerStart}`);
+    log(`End: ${registerEnd}`);
+    if (registerStart <= now) {
+      if (registerEnd >= now) {
+        eventSignupForm = m(EventSignupForm, {
+          schema: events.getCurrent().additional_fields === undefined ?
+            undefined : JSON.parse(events.getCurrent().additional_fields),
+        });
+      } else {
+        eventSignupForm = m('div', 'The registration period is over.');
+      }
+    } else {
+      eventSignupForm = m('div', `The registration starts at ${registerStart}`);
     }
     return m('div', [
       m('h1', events.getCurrent().title_de),
@@ -52,7 +116,7 @@ export default class EventDetails {
       m('span', events.getCurrent().signup_count),
       m('span', events.getCurrent().spots),
       m('p', events.getCurrent().description_de),
-      m(EventSignupForm),
+      eventSignupForm,
     ]);
   }
 }
diff --git a/src/views/eventList.js b/src/views/eventList.js
index 30024e0a6b221234dde8687a6f7fd13e85c8b468..6943b18352b60335ef9edcbb855b07ce802e6948 100644
--- a/src/views/eventList.js
+++ b/src/views/eventList.js
@@ -1,4 +1,4 @@
-import * as events from '../models/event';
+import * as events from '../models/events';
 
 const m = require('mithril');
 
@@ -16,6 +16,13 @@ export default class EventList {
     });
   }
 
+  static onbeforeupdate(vnode, old) {
+    // when attrs are different it means we changed route
+    if (vnode.attrs.id !== old.attrs.id) {
+      events.reload();
+    }
+  }
+
   static view() {
     return m('table', [
       m('thead', [
diff --git a/src/views/formFields.js b/src/views/formFields.js
new file mode 100644
index 0000000000000000000000000000000000000000..b411a78ebb30f0f759e4bf306115ced56334d380
--- /dev/null
+++ b/src/views/formFields.js
@@ -0,0 +1,133 @@
+const m = require('mithril');
+
+export class inputGroup {
+  constructor(vnode) {
+    // Link the error-getting function from the binding
+    this.getErrors = () => [];
+    if (vnode.attrs.getErrors) {
+      this.getErrors = vnode.attrs.getErrors;
+    }
+  }
+
+  view(vnode) {
+    // set display-settings accoridng to error-state
+    let errorField = null;
+    let groupClasses = vnode.attrs.classes ? vnode.attrs.classes : '';
+    const errors = this.getErrors();
+    if (errors.length > 0) {
+      errorField = m('span', `Error: ${errors.join(', ')}`);
+      groupClasses += ' has-error';
+    }
+
+    let { args } = vnode.attrs;
+    if (args === undefined) {
+      args = {};
+    }
+    args.value = vnode.attrs.value;
+    args.onchange = vnode.attrs.onchange;
+
+    if (['radio', 'checkbox'].includes(args.type)) {
+      return m('div', { class: groupClasses }, [
+        m(`input[name=${vnode.attrs.name}][id=${vnode.attrs.name}]`, args),
+        m(`label[for=${vnode.attrs.name}]`, vnode.attrs.title),
+        errorField,
+      ]);
+    }
+    return m('div', { class: groupClasses }, [
+      m(`label[for=${vnode.attrs.name}]`, vnode.attrs.title),
+      m(`input[name=${vnode.attrs.name}][id=${vnode.attrs.name}]`, args),
+      errorField,
+    ]);
+  }
+}
+
+export class selectGroup {
+  oninit() {
+    this.value = [];
+  }
+
+  view(vnode) {
+    switch (vnode.attrs.args.type) {
+      case 'buttons': {
+        if (vnode.attrs.args.multipleSelect) {
+          return m('div', { class: vnode.attrs.classes }, [
+            m(`label[for=${vnode.attrs.name}]`, vnode.attrs.title),
+            m('div', vnode.attrs.args.options.map(option =>
+              m(inputGroup, {
+                name: vnode.attrs.name,
+                title: option,
+                value: option,
+                onchange: (e) => {
+                  if (e.target.checked) {
+                    this.value.push(e.target.value);
+                  } else {
+                    this.value = this.value.filter(item => item !== e.target.value);
+                  }
+                  vnode.attrs.onchange({ target: { name: e.target.name, value: this.value } });
+                },
+                args: { type: 'checkbox' },
+              }))),
+          ]);
+        }
+        return m('div', { class: vnode.attrs.classes }, [
+          m('div', vnode.attrs.options.map(option =>
+            m(inputGroup, {
+              name: vnode.attrs.name,
+              title: option,
+              onchange: vnode.attrs.onchange,
+              args: { type: 'radio' },
+            }))),
+          m(`label[for=${vnode.attrs.name}]`, vnode.attrs.title),
+        ]);
+      }
+      case 'select':
+      default: {
+        if (vnode.attrs.args.multipleSelect) {
+          return m('div', { class: vnode.attrs.classes }, [
+            m(`label[for=${vnode.attrs.name}]`, vnode.attrs.title),
+            m(
+              `select[name=${vnode.attrs.name}][id=${vnode.attrs.name}]`,
+              {
+                onchange: (e) => {
+                  const value = [];
+                  let opt;
+                  for (let i = 0; i < e.target.options.length; i += 1) {
+                    opt = e.target.options[i];
+                    if (opt.selected) {
+                      value.push(opt);
+                    }
+                  }
+                  vnode.attrs.onchange(e);
+                },
+                multiple: true,
+              },
+              vnode.attrs.options.map(option => m('option', option)),
+            ),
+          ]);
+        }
+        return m('div', { class: vnode.attrs.classes }, [
+          m(`label[for=${vnode.attrs.name}]`, vnode.attrs.title),
+          m(
+            `select[name=${vnode.attrs.name}][id=${vnode.attrs.name}]`,
+            {
+              value: vnode.attrs.value,
+              onchange: vnode.attrs.onchange,
+              multiple: false,
+            },
+            vnode.attrs.args.options.map(option => m('option', option)),
+          ),
+        ]);
+      }
+    }
+  }
+}
+
+export class submitButton {
+  static view(vnode) {
+    const { args } = vnode.attrs;
+    if (!vnode.attrs.active) {
+      args.disabled = 'disabled';
+    }
+    return m('button[type=button]', args, vnode.attrs.text);
+  }
+}
diff --git a/src/views/jsonSchemaForm.js b/src/views/jsonSchemaForm.js
new file mode 100644
index 0000000000000000000000000000000000000000..aa07e8f31cf1391845b447261742bba72d146faf
--- /dev/null
+++ b/src/views/jsonSchemaForm.js
@@ -0,0 +1,129 @@
+import Ajv from 'ajv';
+import { inputGroup, selectGroup } from './formFields';
+
+const m = require('mithril');
+const jsonSchemaDraft04 = require('ajv/lib/refs/json-schema-draft-04.json');
+
+export default class JSONSchemaForm {
+  constructor() {
+    this.data = {};
+    this.errors = {};
+
+    this.ajv = new Ajv({
+      missingRefs: 'ignore',
+      errorDataPath: 'property',
+      allErrors: true,
+    });
+    this.ajv.addMetaSchema(jsonSchemaDraft04);
+  }
+
+  oninit(vnode) {
+    this.schema = vnode.attrs.schema;
+    if (this.schema === null || typeof this.schema !== 'object') {
+      this.schema = undefined;
+    } else {
+      this.ajv.addSchema(this.schema, 'schema');
+    }
+  }
+
+  // bind form-fields to the object data and validation
+  bind(attrs) {
+    // initialize error-list for every bound field
+    if (!this.errors[attrs.name]) this.errors[attrs.name] = [];
+
+    const boundFormelement = {
+      onchange: (e) => {
+        // bind changed data
+        this.data[e.target.name] = e.target.value;
+
+        // validate against schema
+        const validate = this.ajv.getSchema('schema');
+        this.valid = validate(this.data);
+
+        if (this.valid) {
+          Object.keys(this.errors).forEach((field) => {
+            this.errors[field] = [];
+          });
+        } else {
+          // get errors for respective fields
+          Object.keys(this.errors).forEach((field) => {
+            const errors = validate.errors.filter(error =>
+              `.${field}` === error.dataPath);
+            this.errors[field] = errors.map(error => error.message);
+          });
+        }
+      },
+      getErrors: () => this.errors[attrs.name],
+      value: this.data[attrs.name],
+    };
+    // add the given attributes
+    Object.keys(attrs).forEach((key) => { boundFormelement[key] = attrs[key]; });
+
+    return boundFormelement;
+  }
+
+  view() {
+    const elements = this.renderFormElements();
+    return m('form', elements);
+  }
+
+  isValid() {
+    return this.schema === undefined || this.valid;
+  }
+
+  getValue() {
+    return this.data;
+  }
+
+  // render all schema properties to an array of form-fields
+  renderFormElements() {
+    const elements = [];
+    if (this.schema !== undefined) {
+      Object.keys(this.schema.properties).forEach((key) => {
+        elements.push(this._renderProperty(key, this.schema.properties[key]));
+      });
+    }
+    return elements;
+  }
+
+  // render schema property to form-fields
+  _renderProperty(key, item) {
+    if ('enum' in item) {
+      return m(selectGroup, this.bind({
+        name: key,
+        title: item.description,
+        args: {
+          options: item.enum,
+          type: 'select',
+          multipleSelect: false,
+        },
+      }));
+    }
+    switch (item.type) {
+      case 'integer': {
+        return m(inputGroup, this.bind({ name: key, title: item.description, args: { type: 'number', step: 1 } }));
+      }
+      case 'number': {
+        return m(inputGroup, this.bind({ name: key, title: item.description, args: { type: 'number' } }));
+      }
+      case 'boolean': {
+        return m(inputGroup, this.bind({ name: key, title: item.description, args: { type: 'checkbox' } }));
+      }
+      case 'array': {
+        return m(selectGroup, this.bind({
+          name: key,
+          title: item.description,
+          args: {
+            options: item.items.enum,
+            type: item.items.enum.length > 8 ? 'select' : 'buttons',
+            multipleSelect: true,
+          },
+        }));
+      }
+      case 'string':
+      default: {
+        return m(inputGroup, this.bind({ name: key, title: item.description, args: { type: 'text' } }));
+      }
+    }
+  }
+}