<!DOCTYPE html>
<html ng-app="myApp">
<head>
<!-- bootstrap: only css needed -->
<link data-require="bootstrap-css@3.*" data-semver="3.2.0" rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" />
<!-- angular -->
<script data-require="angular.js@1.2.*" data-semver="1.2.25" src="https://code.angularjs.org/1.2.25/angular.js"></script>
<!-- libs needed for this demonstration -->
<script data-require="showdown@0.3.*" data-semver="0.3.1" src="//cdnjs.cloudflare.com/ajax/libs/showdown/0.3.1/showdown.min.js"></script>
<script data-require="angular-markdown-text@0.0.2" data-semver="0.0.2" src="https://cdn.rawgit.com/McNull/angular-markdown-text/v0.0.2/dist/angular-markdown-text.min.js"></script>
<script data-require="angular-sanitize@1.2.25" data-semver="1.2.25" src="https://code.angularjs.org/1.2.25/angular-sanitize.js"></script>
<link data-require="angular-block-ui@*" data-semver="0.1.1" rel="stylesheet" href="https://cdn.rawgit.com/McNull/angular-block-ui/v0.1.1/dist/angular-block-ui.min.css" />
<script data-require="angular-block-ui@*" data-semver="0.1.1" src="https://cdn.rawgit.com/McNull/angular-block-ui/v0.1.1/dist/angular-block-ui.min.js"></script>
<!-- angular-form-gen -->
<link data-require="angular-form-gen@*" data-semver="0.0.2" rel="stylesheet" href="https://cdn.rawgit.com/McNull/angular-form-gen/v0.0.2/dist/angular-form-gen.min.css" />
<script data-require="angular-form-gen@*" data-semver="0.0.2" src="https://cdn.rawgit.com/McNull/angular-form-gen/v0.0.2/dist/angular-form-gen.min.js"></script>
<!-- demonstration code -->
<link href="style.css" rel="stylesheet" />
<script src="script.js"></script>
</head>
<body>
<div ng-include="'header.html'"></div>
<div ng-include="'example.html'"></div>
</body>
</html>
var myApp = angular.module('myApp', ['fg', 'ngSanitize', 'markdown', 'blockUI']);
myApp.controller('MyController', function ($scope, myWizardPages, queueBlockUI, $q) {
// Expose state on scope.
$scope.myWizard = {
data: {},
pages: myWizardPages,
pageIndex: 0
};
// Called whenever the next button is pressed
$scope.onNext = function (data) {
// Grab the current page schema
var page = $scope.myWizard.pages[$scope.myWizard.pageIndex],
result = true;
if (page.name === 'Zork') {
// Because we'll perfom an async action we'll need to return
// a promise object to the wizard and resolve it when we're done.
var promise, defer = $q.defer();
switch (data.zorkAction) {
case 'east':
promise = queueBlockUI(['You can\'t go that way!!']);
// By resolving or returning false we'll tell the wizard that we can't go
// to the next page.
result = false;
break;
case 'west':
promise = queueBlockUI(['You go west']);
break;
default:
promise = queueBlockUI(['You\'ve killed the mailbox']);
break;
}
// When the promise of queueBlockUI is resolved we resolve our promise so
// the wizard can continue.
promise.then(function () {
defer.resolve(result);
});
return defer.promise;
}
return result;
};
// Executed when the finish button has been pressed.
$scope.onFinish = function (data) {
queueBlockUI([
'Processing your data ...',
'Feeding results to server ...',
'Parsing server response ...',
'Trying to make sense of it all ...',
'Comprehension failed ... ',
'Resetting Form ...'
]).then(function () {
$scope.myWizard.pageIndex = 0;
$scope.myWizard.data = {};
});
};
});
// The wizard directive
myApp.directive('myWizard', function ($q, $timeout) {
return {
restrict: 'EA',
templateUrl: 'my-wizard.html',
scope: {
pages: '=myWizardPages',
pageIndex: '=myWizardPageIndex',
data: '=myWizardData',
onFinish: '&myWizardOnFinish',
onNext: '&myWizardOnNext'
},
link: {
pre: function ($scope) {
// This needs to be done in the prelink, otherwise the form element
// on the template cannot access this property on the scope.
$scope.wizard = { form: {} };
},
post: function ($scope) {
var transition;
// Track the current pageIndex and update the currentPage property
// when it has changed.
$scope.$watch('pageIndex', function () {
// - - - - 8-< - - - - - - - - - - -
// Something silly to do animations
if(transition) {
$timeout.cancel(transition);
}
$scope.pageActive = false;
transition = $timeout(function(){
$scope.pageActive = true;
}, 1);
// - - - - 8-< - - - - - - - - - - -
// Grab the currentPage based on the index
$scope.currentPage = $scope.pages[$scope.pageIndex];
});
// Executed when the user presses the previous button
// Note that I didn't implement a callback here.
$scope.prevPage = function () {
$scope.pageIndex = Math.max($scope.pageIndex - 1, 0);
};
// Executed when the user presses the previous button
$scope.nextPage = function () {
// Execute the callback (see MyController) and if that
// resolves to true we can continue to the next page.
var x = $scope.onNext({ data: $scope.data });
$q.when(x).then(function (result) {
if (result) {
$scope.pageIndex = Math.min($scope.pageIndex + 1, $scope.pages.length);
}
});
};
$scope.prevAllowed = function () {
return $scope.pageIndex > 0;
};
$scope.nextAllowed = function () {
return $scope.wizard.form.$valid && $scope.pageIndex <= $scope.pages.length;
};
}
}
};
});
// The schemas used to display the wizard pages
myApp.value('myWizardPages', [
{
name: 'Who are YOU?',
schema: {
"fields": [
{
"type": "text",
"name": "firstName",
"displayName": "First name",
"validation": {
"messages": {},
"required": true
},
"placeholder": "Enter your first name here",
"tooltip": "Enter your first name here"
},
{
"type": "text",
"name": "lastName",
"displayName": "Last name",
"validation": {
"messages": {},
"required": false
},
"placeholder": "Enter your last name here",
"tooltip": "Enter your last name here"
}
]
}
},
{
name: "Zork",
description: "You are standing in an open field west of a white house, with a boarded front door. There is a small mailbox here.",
schema: {
"fields": [
{
"type": "radiobuttonlist",
"name": "zorkAction",
"displayName": "Action",
"options": [
{
"value": "openMailbox",
"text": "Open mailbox"
},
{
"value": "breakMailbox",
"text": "Kick mailbox"
},
{
"value": "tortureMailbox",
"text": "Torture mailbox"
},
{
"value": "insultMailbox",
"text": "Insult mailbox"
},
{
"value": "allTheAbove",
"text": "All the above"
},
{
"value": "west",
"text": "Go west"
},
{
"value": "east",
"text": "Go east"
}
],
"value": "openMailbox"
}
]
}
},
{
name: 'Something something',
schema: {
"fields": [
{
"type": "text",
"name": "magicNumber",
"validation": {
"maxlength": 15,
"messages": {
"pattern": "The value {{ field.state.$viewValue }} is {{ field.state.$viewValue < 7 && 'too low' || field.state.$viewValue > 7 && 'too high' || 'not a number' }}"
},
"pattern": "^7$",
"required": true
},
"displayName": "Magic Number",
"placeholder": "Guess the magic number"
},
{
"type": "password",
"name": "secret",
"displayName": "Secret",
"validation": {
"messages": {}
},
"placeholder": "Tell me your secret"
},
{
"type": "checkboxlist",
"name": "checkboxList",
"displayName": "Checkbox List",
"options": [
{
"value": "1",
"text": "Option 1"
},
{
"value": "2",
"text": "Option 2"
},
{
"value": "3",
"text": "Option 3"
}
],
"value": {
"1": true,
"2": true
}
}
]
}
}
]);
// Something silly to display a little bit of interaction between the pages.
myApp.factory('queueBlockUI', function (blockUI, $timeout, $q) {
return function (messages) {
var x = 0, defer = $q.defer();
function next() {
var msg = messages[x];
if (!x) {
blockUI.start(msg);
} else if (x < messages.length) {
blockUI.message(msg);
} else {
blockUI.stop();
defer.resolve();
}
if (x++ < messages.length) {
$timeout(next, Math.floor(Math.random() * 500 + 1000));
}
}
next();
return defer.promise;
}
});
/* Styles go here */
body {
padding-bottom: 100px;
}
html {
overflow-y: scroll;
}
.needs-some-more-space {
margin-top: 30px;
}
.form-group .jsonify {
margin-top: 0;
}
.my-wizard-page-slide, .my-wizard .control-label {
transition: all 0s;
opacity: 0;
}
.my-wizard-page-slide-left {
margin-left: 200px;
}
.my-wizard .control-label {
transform: translateX(-100px);
}
.my-wizard-page-slide-right {
margin-left: -200px;
}
.my-wizard-page-active .my-wizard-page-slide, .my-wizard-page-active .control-label {
transition: all 1s;
opacity: 1;
}
.my-wizard-page-active .my-wizard-page-slide-left,
.my-wizard-page-active .my-wizard-page-slide-right {
margin-left: 0px;
}
.my-wizard-page-active .control-label {
transform: translateX(0px);
}
# Wizard Example
This plunker displays how a custom wizard can be created based on the form schemas created with the
<a href="http://plnkr.co/edit/8erjmp?p=info" target="_blank">schema editor</a>.
<div ng-controller="MyController" class="container">
<div markdown markdown-src="'README.md'" class="text-center"></div>
<div my-wizard
my-wizard-page-index="myWizard.pageIndex"
my-wizard-pages="myWizard.pages"
my-wizard-data="myWizard.data"
my-wizard-on-next="onNext(data)"
my-wizard-on-finish="onFinish(data)"
class="needs-some-more-space"></div>
</div>
<header class="container">
<div class="jumbotron text-center">
<h1>angular-form-gen</h1>
<p>Design Bootstrap based form schemas for AngularJS in a drag and drop WYSIWYG environment.</p>
<a href="http://angular-form-gen.nullest.com" target="_blank">
Homepage
</a> -
<a href="https://github.com/McNull/angular-form-gen" target="_blank">
GitHub Project
</a>
</div>
</header>
<form novalidate class="form-horizontal my-wizard" name="wizard.form" ng-class="{ 'my-wizard-page-active': pageActive }">
<fieldset ng-if="pageIndex < pages.length">
<legend>
<div class="row">
<div class="col-sm-offset-3 col-sm-9">
<span class="my-wizard-page-slide my-wizard-page-slide-left">{{ currentPage.name }}</span>
</div>
</div>
</legend>
<div class="row">
<div class="col-sm-offset-3 col-sm-9">
<p class="my-wizard-page-slide my-wizard-page-slide-right">{{ currentPage.description }}</p>
</div>
</div>
<!-- Magic starts here -->
<div fg-form
fg-form-data="data"
fg-schema="currentPage.schema">
</div>
<!-- Magic ends here -->
</fieldset>
<fieldset ng-if="pageIndex === pages.length">
<legend>
<div class="row">
<div class="col-sm-offset-3 col-sm-3">Summary</div>
</div>
</legend>
<div class="form-group">
<label class="control-label col-sm-3">Form data</label>
<div class="col-sm-9">
<div jsonify="data"></div>
</div>
</div>
</fieldset>
<div class="row">
<div class="col-sm-offset-3 col-sm-3">
<button type="button" class="btn btn-default" ng-class="{ disabled: !prevAllowed() }" ng-click="prevPage()">
Previous
</button>
<button ng-if="pageIndex < pages.length" type="submit" class="btn btn-default" ng-class="{ disabled: !nextAllowed() }" ng-click="nextPage()">
Next
</button>
<button ng-if="pageIndex === pages.length" type="submit" class="btn btn-primary" ng-class="{ disabled: !nextAllowed() }" ng-click="onFinish({data: data})">
Finish
</button>
</div>
</div>
</form>