<!DOCTYPE html>
<html>

<head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Aurelia TypeScript Template</title>
    <!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <link rel="stylesheet" href="style.css" />
</head>

<body aurelia-app="main">
    <h1>Loading...</h1>
    <!--Import jQuery before materialize.js-->
    <script type="text/javascript" src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <!-- Latest compiled and minified JavaScript -->
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/0.20.19/system.js"></script>
    <script src="systemjs.config.js"></script>
    <script>
        System.import('aurelia-bootstrapper');
    </script>
</body>

</html>
export function configure(aurelia) {
  aurelia.use.basicConfiguration()
  .plugin('aurelia-validation');
  aurelia.start().then(() => aurelia.setRoot('app'));
}
System.config({
  map: {
    'i18next': 'https://unpkg.com/i18next/i18next.min',
    
    'aurelia-binding': 'https://unpkg.com/aurelia-binding/dist/commonjs/aurelia-binding.js',
    'aurelia-bootstrapper': 'https://unpkg.com/aurelia-bootstrapper/dist/commonjs/aurelia-bootstrapper.js',
    'aurelia-dependency-injection': 'https://unpkg.com/aurelia-dependency-injection/dist/commonjs/aurelia-dependency-injection.js',
    'aurelia-dialog': 'https://unpkg.com/aurelia-dialog/dist/commonjs',
    'aurelia-event-aggregator': 'https://unpkg.com/aurelia-event-aggregator/dist/commonjs/aurelia-event-aggregator.js',
    'aurelia-fetch-client': 'https://unpkg.com/aurelia-fetch-client/dist/commonjs/aurelia-fetch-client.js',
    'aurelia-framework': 'https://unpkg.com/aurelia-framework/dist/commonjs/aurelia-framework.js',
    'aurelia-history': 'https://unpkg.com/aurelia-history/dist/commonjs/aurelia-history.js',
    'aurelia-history-browser': 'https://unpkg.com/aurelia-history-browser/dist/commonjs/aurelia-history-browser.js',
    'aurelia-i18n': 'https://unpkg.com/aurelia-i18n/dist/commonjs',
    'aurelia-loader': 'https://unpkg.com/aurelia-loader/dist/commonjs/aurelia-loader.js',
    'aurelia-loader-default': 'https://unpkg.com/aurelia-loader-default/dist/commonjs/aurelia-loader-default.js',
    'aurelia-logging': 'https://unpkg.com/aurelia-logging/dist/commonjs/aurelia-logging.js',
    'aurelia-logging-console': 'https://unpkg.com/aurelia-logging-console/dist/commonjs/aurelia-logging-console.js',
    'aurelia-metadata': 'https://unpkg.com/aurelia-metadata/dist/commonjs/aurelia-metadata.js',
    'aurelia-pal': 'https://unpkg.com/aurelia-pal/dist/commonjs/aurelia-pal.js',
    'aurelia-pal-browser': 'https://unpkg.com/aurelia-pal-browser/dist/commonjs/aurelia-pal-browser.js',
    'aurelia-path': 'https://unpkg.com/aurelia-path/dist/commonjs/aurelia-path.js',
    'aurelia-polyfills': 'https://unpkg.com/aurelia-polyfills/dist/commonjs/aurelia-polyfills.js',
    'aurelia-router': 'https://unpkg.com/aurelia-router/dist/commonjs/aurelia-router.js',
    'aurelia-route-recognizer': 'https://unpkg.com/aurelia-route-recognizer/dist/commonjs/aurelia-route-recognizer.js',
    'aurelia-task-queue': 'https://unpkg.com/aurelia-task-queue/dist/commonjs/aurelia-task-queue.js',
    'aurelia-templating': 'https://unpkg.com/aurelia-templating/dist/commonjs/aurelia-templating.js',
    'aurelia-templating-binding': 'https://unpkg.com/aurelia-templating-binding/dist/commonjs/aurelia-templating-binding.js',
    'aurelia-templating-resources': 'https://unpkg.com/aurelia-templating-resources/dist/commonjs',
    'aurelia-templating-router': 'https://unpkg.com/aurelia-templating-router/dist/commonjs',
    'aurelia-validation': 'https://unpkg.com/aurelia-validation/dist/commonjs',
  },
  packages: {
    '.': {},
    'aurelia-templating-resources': {
      main: 'aurelia-templating-resources.js'
    },
    'aurelia-templating-router': {
      main: 'aurelia-templating-router.js'
    },
    'aurelia-validation': {
      main: 'aurelia-validation.js'
    },
    'aurelia-dialog': {
      main: 'aurelia-dialog.js'
    },
    'aurelia-i18n': {
      main: 'aurelia-i18n.js'
    },
  }
});
{
  "compilerOptions": {
    "target": "ESNEXT",
    "module": "commonjs",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}
import { Aurelia, PLATFORM } from 'aurelia-framework';

export class App {
  message = "App";
  
  public formName = "Support";
  public formTemplate = {
    "types": [
      "text",
      "file",
      "number",
      "email"
    ],
    "Support": {
      "description": "This will generate form for Operational Support request",
      "fields": [
        {
          "name": "title",
          "element": "input",
          "type": "text",
          "label": "Title",
          "placeHolder": "Provide Short Discription",
          "classes": "form-control",
          "value": "",
          "validation": {
            "isValidate": true,
            "validationRule": {
              "required": true,
              "maxLength": 250
            }
          }
        },
        {
          "name": "email",
          "element": "custom-email",
          "type": "email",
          "label": "Email",
          "placeHolder": "Provide email addresses",
          "classes": "form-control",
          "value": "",
          "validation": {
            "isValidate": true,
            "validationRule": {
              "required": true,
              "email": true
            },
            "errors": []
          }
        }
      ]
    }
  };
  
  public processForm() {
    //this.controller.validate();
    console.log(this.formTemplate[this.formName].fields);
    console.log(this);
  }
}
<template>
  <require from="dynamicform"></require>
  <div class="panel panel-primary">
    <div class="panel-heading">${pageTitle}</div>
    <div class="panel-body">
      <dynamic-form form-template.two-way="formTemplate" form-name.two-way="formName" callback.call="processForm()"></dynamic-form>
    </div>
  </div>
</template>


import { customElement, useView, bindable, bindingMode, inject } from 'aurelia-framework';
import { ValidationRules, ValidationControllerFactory, ValidateBindingBehavior } from 'aurelia-validation';

@inject(ValidationControllerFactory)

@customElement('custom-input')
@useView('./custominput.html')
export class CustomInput {
  @bindable public callback;
  @bindable public formItem;

  controller = null;

  constructor(factory) {
    this.controller = factory.createForCurrentScope();
  }
  
  bind() {
    this.controller.validate();
    this.formItem.validation.errors = this.controller.errors;
    this.controller.subscribe(this.callback);
  }


}
<template>
  <input
    class="${formItem.classes}"
    type="text"  
    value.two-way="formItem.value & validateOnChangeOrBlur"
    placeholder="${formItem.placeHolder}"
    name="${formItem.name}" />

  <ul if.bind="controller.errors.length > 0" style="list-style-type:none;">
    <li class="text-danger" repeat.for="error of controller.errors">${error.message}</li>
  </ul>
</template>
import { bindable, bindingMode, inject } from 'aurelia-framework';
import { FormHelper } from './formhelper'; 


export class DynamicForm {

  @bindable public formName: string;
  @bindable public formTemplate: Object;
  @bindable public callback;
  inputItem: HTMLInputElement;

  public processFormRequest() {
    console.log(this);
  }
  
  public bind() {
    const forms = Object.keys(this.formTemplate)
      .filter(key => key !== "types")
      .map(key => this.formTemplate[key]);
      
    for (const form of forms) {
      FormHelper.initializeFormRules(form);
    };
  }
  
  private errorCount: number = 0;

  public onValidate = (event: ValidateEvent) => {
    this.errorCount = 0;
    const forms = Object.keys(this.formTemplate)
      .filter(key => key !== "types")
      .map(key => this.formTemplate[key]);
      
    for (const form of forms) {
      for (const field of form.fields) {
        this.errorCount += field.validation.errors.length;
      }
    }
  }
}
<template>
  <require from="./custominput"></require>
  <require from="./customemail"></require>
  <form class="form-horizontal">
    <template repeat.for="item of formTemplate[formName].fields">
      <div form-name.bind="formName" class="form-group">
        <label for="${item.name}" class="col-sm-2 control-label">${item.label}</label>
        <div class="col-sm-10" if.bind="item.type === 'text' && item.element === 'input'">
          <custom-input form-item.bind="item" callback.bind="onValidate">
          </custom-input>
        </div>
        <div class="col-sm-10" if.bind="item.type === 'email' && item.element === 'custom-email'">
            <custom-email form-item.bind="item" callback.bind="onValidate"></custom-email>
          </div>
      </div>
    </template>
    <div class="form-group">
      <div class="col-sm-12">
        <button type="submit" class="btn btn-default pull-right" disabled.bind="errorCount" click.delegate="processFormRequest()">Submit</button>
      </div>
    </div>
  </form>
</template>
import { ValidationRules } from 'aurelia-validation';
export class FormHelper {
  private static initializedForms = [];
  
  public static initializeFormRules(form) {
    if (this.initializedForms.indexOf(form) > -1) {
      return;
    }
    this.initializedForms.push(form);
    for (const field of form.fields) {
      if (field.validation.isValidate) {
        field.validation.errors = [];
        let ruleBuilder = ValidationRules
          .ensure("value")
          .displayName(field.label);
        
        const rules = Object.keys(field.validation.validationRule)
          .map(key => ({key, value: field.validation.validationRule[key]}));
          
        for (const rule of rules) {
          ruleBuilder = ruleBuilder[rule.key](rule.value);
        }
        
        ruleBuilder.on(field);
      }
    }
  }
}
import { customElement, useView, bindable, bindingMode, inject } from 'aurelia-framework';
import { ValidationRules, ValidationControllerFactory, Validator } from 'aurelia-validation';

@inject(ValidationControllerFactory)
@customElement('custom-email')
@useView('./customemail.html')
export class CustomEmail {
  @bindable public formItem;
  @bindable public callback;
  controller = null;

  constructor(factory) {
    this.controller = factory.createForCurrentScope();
  }

  bind() {
    this.controller.validate();
    this.formItem.validation.errors = this.controller.errors;
    this.controller.subscribe(this.callback);
  }

}
<template>
  <require from="./email"></require>
  <div class="input-group">
    <input readonly class="${formItem.classes}" type="text" value.two-way="formItem.value & validateOnChangeOrBlur" placeholder="${formItem.placeHolder}" name="${formItem.name}" />
    <span class="input-group-btn">
        <!-- Button trigger modal -->
  <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#emailModal">
    +
  </button>
    </span>
  </div>
  <ul if.bind="controller.errors.length > 0" style="list-style-type:none;">
    <li class="text-danger" repeat.for="error of controller.errors">${error.message}</li>
  </ul>
  <email modal-name.bind="'emailModal'" email-address.two-way="formItem.value" modal-value.bind="'+'"></email>

</template>
import { customElement, useView, bindable, bindingMode, inject } from 'aurelia-framework';
import { ValidationRules, ValidationControllerFactory, Validator } from 'aurelia-validation';

@inject(ValidationControllerFactory)
@customElement('email')
@useView('./email.html')
export class Email {
  @bindable public modalName: string;
  @bindable public modalValue: string;
  @bindable public emailAddress: string;
  public emailAddresses = [];
  public setEmail: string;
  public errorMessage: string;
  emailController = null;

  constructor(factory) {
    this.setEmail = '';
    this.emailController = factory.createForCurrentScope();
    ValidationRules.ensure('setEmail').displayName('Email').required().email().on(this);
  }

  public bind() {
    this.emailController.validate();
  }
  
  private joinEmails() {
    this.emailAddress = this.emailAddresses.join(";");
  }
  
  private isUniqueEmail = (email: string) => {
    return (this.emailAddresses.indexOf(email) > -1)
  }

  public addEmail() {
    if(this.isUniqueEmail(this.setEmail))
    {
      this.errorMessage = "You must provide unique email address.";
      return;
    }
    this.emailAddresses.push(this.setEmail);
    this.joinEmails();
    this.setEmail = "";
  }
  
  public removeEmail(index) {
    this.emailAddresses.splice(index, 1);
    this.joinEmails();
  }
}
<template>
  <!-- Modal -->
  <div class="modal fade" id="${modalName}" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
    <div class="modal-dialog" role="document">
      <div class="modal-content">
        <div class="modal-header">
          <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
          <h4 class="modal-title" id="myModalLabel">Add Email Address</h4>
        </div>
        <div class="modal-body">
          <div class="input-group">
            <input type="text" id="setEmail" name="setEmail" class="form-control" value.bind="setEmail & validateOnChangeOrBlur" />
            <span class="input-group-btn">
              <button class="btn btn-primary" class.bind="emailController.errors.length > 0 ? 'disabled' : ''" disabled.bind="emailController.errors.length > 0" click.delegate="addEmail()">Add</button>
            </span>
          </div>
          <input type="text" value.bind="emailAddress" hidden />
          <span class="text-danger" repeat.for="error of emailController.errors">${error.message}</span>
          <span class="text-danger" if.bind="errorMessage">${errorMessage}</span>
          <div>
            <ul class="list-group" if.bind="emailAddresses.length > 0" style="margin-top: 10px;">
              <li class="list-group-item" repeat.for="e of emailAddresses">
                ${e} <span class="glyphicon glyphicon-remove text-danger pull-right" style="cursor: pointer;" click.delegate="removeEmail($index)"></span>
              </li>
            </ul>
          </div>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
        </div>
      </div>
    </div>
  </div>
</template>