Schema Form Developer Panel

A developer panel that allows you to edit schema forms in real-time,
with multiple examples. Also, you can view that same form in a
standard Horizon modal to preview what the form will look like. Based
on http://schemaform.io/examples/bootstrap-example.html

Also fixes some bugs in the schema-form implementation:
- Arrays cleaned up and fixed
- Tab arrays cleaned up
- Improved modal form to use schema.title if available

Co-Authored-By: Rob Cresswell <robert.cresswell@outlook.com>
Change-Id: Ia75a18d4c0c064ae618ee923cd6d602b1ef6e66e
This commit is contained in:
Tyr Johanson 2016-07-27 12:57:26 -06:00 committed by Richard Jones
parent 22d907cb76
commit 9ed4acd4ff
35 changed files with 1035 additions and 13 deletions

View File

@ -0,0 +1,59 @@
/**
* (c) Copyright 2016 Cisco Systems
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use self file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
(function() {
'use strict';
/**
* @ngdoc directive
* @name horizon.framework.widgets.contenteditable
* @description
* Allows the use of contenteditable with ng-model. Altered from
* https://docs.angularjs.org/api/ng/type/ngModel.NgModelController
*/
angular
.module('horizon.framework.widgets.contenteditable')
.directive('contenteditable', contenteditable);
function contenteditable() {
var directive = {
restrict: 'A',
require: '?ngModel', // get a hold of NgModelController
link: link
};
return directive;
function link(scope, element, attrs, ngModel) {
if (!ngModel) { return; } // do nothing if no ng-model
// Specify how UI should be updated
ngModel.$render = function() {
element.html(ngModel.$viewValue || '');
};
// Listen for change events to enable binding
element.on('blur keyup change', function() {
scope.$evalAsync(read);
});
read();
function read() {
ngModel.$setViewValue(element.html());
}
}
}
})();

View File

@ -0,0 +1,67 @@
/**
* Copyright 2016 Cisco Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
describe('contenteditable directive', function() {
var $compile, $scope;
beforeEach(module('horizon.framework.widgets'));
beforeEach(module('horizon.framework.widgets.contenteditable'));
beforeEach(inject(function ($injector) {
$compile = $injector.get('$compile');
$scope = $injector.get('$rootScope').$new();
$scope.testData = '';
}));
describe('using a model', function() {
var element;
beforeEach(function before() {
element = $compile('<pre contenteditable ng-model="testData"></pre>')($scope);
$scope.$digest();
});
it('should update the model when content is edited', function () {
element.triggerHandler('focus');
element.html('foo');
$scope.$digest();
element.triggerHandler('blur');
expect($scope.testData).toBe('foo');
});
it('should update the view when model is changed', function () {
element.triggerHandler('focus');
$scope.testData = 'spam';
$scope.$digest();
expect(element.html()).toBe('spam');
});
});
it('should not do anything without an accompanying ng-model', function() {
var element = $compile('<pre contenteditable></pre>')($scope);
$scope.$digest();
element.triggerHandler('focus');
element.html('bar');
$scope.$digest();
element.triggerHandler('blur');
expect($scope.testData).toBe('');
});
});
})();

View File

@ -0,0 +1,25 @@
/*
* Copyright 2016 Cisco Systems, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
/**
* @ngdoc overview
* @name horizon.framework.widgets.contenteditable
*/
angular
.module('horizon.framework.widgets.contenteditable', []);
})();

View File

@ -2,8 +2,8 @@
sf-field-model="sf-new-array" sf-field-model="sf-new-array"
sf-new-array> sf-new-array>
<label class="control-label" ng-show="showTitle()">{$:: form.title $}</label> <label class="control-label" ng-show="showTitle()">{$:: form.title $}</label>
<ol class="list-group" sf-field-model ui-sortable="form.sortOptions"> <ol class="list-unstyled" sf-field-model ui-sortable="form.sortOptions">
<li class="list-group-item {$::form.fieldHtmlClass$}" <li class="{$::form.fieldHtmlClass$}"
schema-form-array-items schema-form-array-items
sf-field-model="ng-repeat" sf-field-model="ng-repeat"
ng-repeat="item in $$value$$ track by $index"> ng-repeat="item in $$value$$ track by $index">

View File

@ -1,4 +1,4 @@
<fieldset ng-disabled="form.readonly" class="hz-fieldset {$::form.htmlClass$}"> <fieldset ng-disabled="form.readonly" class="hz-fieldset {$::form.htmlClass$}">
<legend ng-class="{'sr-only': !showTitle() }">{$:: form.title $}</legend> <legend ng-show="showTitle()" ng-class="{'sr-only': !showTitle() }">{$:: form.title $}</legend>
<div class="help-block" ng-show="form.description" ng-bind-html="form.description"></div> <div class="help-block" ng-show="form.description" ng-bind-html="form.description"></div>
</fieldset> </fieldset>

View File

@ -2,10 +2,10 @@
ng-model="modelArray" schema-validate="form" ng-model="modelArray" schema-validate="form"
sf-field-model="sf-new-array" sf-field-model="sf-new-array"
sf-new-array sf-new-array
class="clearfix hz-tabarray schema-form-tabarray-{$form.tabType || 'left'$} {$form.htmlClass$}"> class="clearfix hz-tabarray schema-form-tabarray-{$form.tabType || 'left'$} {::$form.htmlClass$}">
<div ng-if="!form.tabType || form.tabType !== 'right'" <div ng-if="!form.tabType || form.tabType !== 'right'"
ng-class="{'col-xs-3': !form.tabType || form.tabType === 'left'}"> ng-class="{'col-xs-3': !form.tabType || form.tabType === 'left'}">
<ul class="nav nav-tabs" <ul class="nav nav-pills nav-stacked"
ng-class="{ 'tabs-left': !form.tabType || form.tabType === 'left'}"> ng-class="{ 'tabs-left': !form.tabType || form.tabType === 'left'}">
<li sf-field-model="ng-repeat" <li sf-field-model="ng-repeat"
ng-repeat="item in $$value$$ track by $index" ng-repeat="item in $$value$$ track by $index"
@ -18,14 +18,14 @@
ng-click="$event.preventDefault() || (selected.tab = appendToArray().length - 1)"> ng-click="$event.preventDefault() || (selected.tab = appendToArray().length - 1)">
<a href="#"> <a href="#">
<span class="fa fa-plus"></span> <span class="fa fa-plus"></span>
<span class="hz-tabarray-add">{$ form.add || 'Add'$}</span> <span class="hz-tabarray-add">{$:: form.add || 'Add'$}</span>
</a> </a>
</li> </li>
</ul> </ul>
</div> </div>
<div ng-class="{'col-xs-9': !form.tabType || form.tabType === 'left' || form.tabType === 'right'}"> <div ng-class="{'col-xs-9': !form.tabType || form.tabType === 'left' || form.tabType === 'right'}">
<div class="tab-content {$form.fieldHtmlClass$}"> <div class="tab-content {$::form.fieldHtmlClass$}">
<div class="tab-pane clearfix tab{$selected.tab$} index{$$index$}" <div class="tab-pane clearfix tab{$selected.tab$} index{$$index$}"
sf-field-model="ng-repeat" sf-field-model="ng-repeat"
ng-repeat="item in $$value$$ track by $index" ng-repeat="item in $$value$$ track by $index"
@ -37,7 +37,7 @@
ng-click="selected.tab = deleteFromArray($index).length - 1" ng-click="selected.tab = deleteFromArray($index).length - 1"
ng-disabled="form.schema.minItems >= modelArray.length" ng-disabled="form.schema.minItems >= modelArray.length"
type="button" type="button"
class="btn {$ form.style.remove || 'btn-default' $} pull-right"> class="btn {$:: form.style.remove || 'btn-danger' $} pull-right">
<span class="fa fa-trash"></span> <span class="fa fa-trash"></span>
<span class="hz-tabarray-remove">{$::form.remove || 'Remove'$}</span> <span class="hz-tabarray-remove">{$::form.remove || 'Remove'$}</span>
</button> </button>
@ -50,7 +50,7 @@
</div> </div>
<div ng-if="form.tabType === 'right'" class="col-xs-3"> <div ng-if="form.tabType === 'right'" class="col-xs-3">
<ul class="nav nav-tabs tabs-right"> <ul class="nav nav-pills tabs-right">
<li sf-field-model="ng-repeat" <li sf-field-model="ng-repeat"
ng-repeat="item in $$value$$ track by $index" ng-repeat="item in $$value$$ track by $index"
ng-click="$event.preventDefault() || (selected.tab = $index)" ng-click="$event.preventDefault() || (selected.tab = $index)"

View File

@ -42,7 +42,7 @@
function controller($uibModalInstance, context) { function controller($uibModalInstance, context) {
var ctrl = this; var ctrl = this;
ctrl.formTitle = context.title; ctrl.formTitle = context.schema.title || context.title;
ctrl.form = context.form; ctrl.form = context.form;
ctrl.schema = context.schema; ctrl.schema = context.schema;
ctrl.model = context.model; ctrl.model = context.model;

View File

@ -50,6 +50,7 @@
function open(config) { function open(config) {
var modalConfig = { var modalConfig = {
backdrop: 'static', backdrop: 'static',
size: 'lg',
resolve: { resolve: {
context: function() { context: function() {
return { return {

View File

@ -20,6 +20,7 @@
angular angular
.module('horizon.framework.widgets', [ .module('horizon.framework.widgets', [
'horizon.framework.widgets.headers', 'horizon.framework.widgets.headers',
'horizon.framework.widgets.contenteditable',
'horizon.framework.widgets.details', 'horizon.framework.widgets.details',
'horizon.framework.widgets.form', 'horizon.framework.widgets.form',
'horizon.framework.widgets.help-panel', 'horizon.framework.widgets.help-panel',

View File

@ -0,0 +1,19 @@
# (c) Copyright 2016 Hewlett Packard Enterprise Development Company LP
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
PANEL = 'form_builder'
PANEL_GROUP = 'default'
PANEL_DASHBOARD = 'developer'
ADD_PANEL = \
'openstack_dashboard.contrib.developer.form_builder.panel.FormBuilder'

View File

@ -0,0 +1,22 @@
# (c) Copyright 2016 Hewlett Packard Enterprise Development Company LP
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from django.utils.translation import ugettext_lazy as _
import horizon
class FormBuilder(horizon.Panel):
name = _("Form Builder")
slug = 'form_builder'

View File

@ -0,0 +1,20 @@
# (c) Copyright 2016 Hewlett Packard Enterprise Development Company LP
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from django.conf.urls import url
from openstack_dashboard.contrib.developer.form_builder import views
urlpatterns = [
url('', views.IndexView.as_view(), name='index'),
]

View File

@ -0,0 +1,19 @@
# (c) Copyright 2016 Hewlett Packard Enterprise Development Company LP
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from django.views import generic
class IndexView(generic.TemplateView):
template_name = 'angular.html'

View File

@ -25,9 +25,10 @@
*/ */
angular angular
.module('horizon.dashboard.developer', [ .module('horizon.dashboard.developer', [
'horizon.dashboard.developer.theme-preview', 'horizon.dashboard.developer.form-builder',
'horizon.dashboard.developer.profiler',
'horizon.dashboard.developer.resource-browser', 'horizon.dashboard.developer.resource-browser',
'horizon.dashboard.developer.profiler' 'horizon.dashboard.developer.theme-preview'
]) ])
.config(config); .config(config);

View File

@ -1,3 +1,4 @@
// Top level file for Developer dashboard SCSS // Top level file for Developer dashboard SCSS
@import "theme-preview/theme-preview"; @import "form-builder/form-builder";
@import "profiler/profiler"; @import "profiler/profiler";
@import "theme-preview/theme-preview";

View File

@ -0,0 +1,60 @@
{
"schema": {
"type": "object",
"title": "Add Ons, Required and Feedback",
"properties": {
"first": {
"type": "string"
},
"second": {
"type": "string"
},
"third": {
"type": "string"
},
"fourth": {
"type": "string",
"pattern": "^YES$"
},
"fifth": {
"type": "string",
"pattern": "^NO$"
}
}
},
"form": [
{
"type": "help",
"helpvalue": "This is helpful <b><em><u>HTML</u></em></b> help text. Don't you feel better now?"
},
{
"key": "first",
"type": "text",
"title": "Shows 'fieldAddonLeft'",
"fieldAddonLeft": "With <b><em><u>HTML</u></em></b> added on left"
},
{
"key": "second",
"type": "text",
"title": "Shows 'fieldAddonRight",
"fieldAddonRight": "With <b><em><u>HTML</u></em></b> added on right"
},
{
"key": "third",
"type": "text",
"title": "Title of a 'required' field",
"required": "true"
},
{
"key": "fourth",
"type": "text",
"title": "This requires the pattern 'YES'"
},
{
"key": "fifth",
"type": "text",
"title": "This requires the pattern 'NO', but does NOT provide the feedback icon",
"feedback": false
}
]
}

View File

@ -0,0 +1,31 @@
{
"schema": {
"title": "Array",
"type": "object",
"properties": {
"arrayKey": {
"type": "array",
"title": "Array Item",
"items": {
"type": "object",
"properties": {
"one": {
"type": "string"
},
"two": {
"type": "string"
}
}
}
}
}
},
"form": [
{
"key": "arrayKey",
"type": "array",
"title": "The 'array' title",
"description": "This is the 'array' description"
}
]
}

View File

@ -0,0 +1,21 @@
{
"schema": {
"title": "Buttons",
"type": "object",
"properties": {
"none": "null"
}
},
"form": [
{
"key": "singleButton",
"type": "button",
"title": "Button"
},
{
"key": "submitButton",
"type": "submit",
"title": "A Submit Button"
}
]
}

View File

@ -0,0 +1,16 @@
{
"schema": {
"type": "object",
"title": "Delete 'Foo'?",
"properties": {
"confirm": {}
}
},
"form": [
{
"key": "confirm",
"type": "help",
"helpvalue": "Are you sure you wish to delete 'Foo'? This action cannot be undone."
}
]
}

View File

@ -0,0 +1,3 @@
<H1>Helpy McHelp Text</H1>
<p>This is some snazzy HTML help text.</p>
<p>You are welcome!</p>

View File

@ -0,0 +1,143 @@
{
"schema": {
"type": "object",
"title": "Radios, Checkboxes, and Select",
"properties": {
"checkBox": {
"type": "boolean"
},
"checkBoxes": {
"type": "array",
"items": {
"type": "string",
"enum": [
"one",
"two",
"three"
]
}
},
"select": {
"type": "string"
},
"radios": {
"type": "string",
"enum": [
"one",
"two",
"three"
]
},
"radioInline": {
"type": "string",
"enum": [
"one",
"two",
"three"
]
},
"radioButtons": {
"type": "string",
"enum": [
"one",
"two",
"three"
]
}
}
},
"form": [
{
"key": "checkBox",
"type": "checkbox",
"title": "This is a 'checkbox' title",
"description": "This is the 'checkbox' description"
},
{
"key": "checkBoxes",
"type": "checkboxes",
"title": "This is a 'checkboxes' title",
"description": "This is the 'checkboxes' description"
},
{
"key": "select",
"type": "select",
"title": "This is a 'select' title",
"description": "This is the 'select' description",
"titleMap": [
{
"value": "one",
"name": "1 (One)"
},
{
"value": "two",
"name": "2 (Two)"
},
{
"value": "three",
"name": "3 (Three)"
}
]
},
{
"key": "radios",
"type": "radios",
"title": "This is a 'radios' title",
"description": "This is the 'radios' description",
"titleMap": [
{
"value": "one",
"name": "1 (One)"
},
{
"value": "two",
"name": "2 (Two)"
},
{
"value": "three",
"name": "3 (Three)"
}
]
},
{
"key": "radioInline",
"type": "radios-inline",
"title": "This is a 'radios-inline' title",
"description": "This is the 'radios-inline' description",
"titleMap": [
{
"value": "one",
"name": "1 (Alpha)"
},
{
"value": "two",
"name": "2 (Beta)"
},
{
"value": "three",
"name": "3 (Gamma)"
}
]
},
{
"key": "radioButtons",
"type": "radiobuttons",
"title": "This is a 'radiobuttons' title",
"description": "This is the 'radiobuttons' description",
"titleMap": [
{
"value": "one",
"name": "1 (I)"
},
{
"value": "two",
"name": "2 (II)"
},
{
"value": "three",
"name": "3 (III)"
}
]
}
]
}

View File

@ -0,0 +1,53 @@
{
"schema": {
"type": "object",
"title": "Sections and Fieldsets",
"properties": {
"first": {
"type": "string"
},
"second": {
"type": "string"
},
"third": {
"type": "string"
},
"fourth": {
"type": "string"
},
"fifth": {
"type": "string"
},
"sixth": {
"type": "string"
}
}
},
"form": [
{
"type": "section",
"items": [
"first",
"second"
]
},
{
"type": "fieldset",
"title": "A 'fieldset' title",
"description": "A 'fieldset' description",
"items": [
"third",
"fourth"
]
},
{
"type": "fieldset",
"title": "A second 'fieldset' title",
"description": "A second 'fieldset' description",
"items": [
"fifth",
"sixth"
]
}
]
}

View File

@ -0,0 +1,30 @@
{
"schema": {
"type": "object",
"title": "Tab Array",
"properties": {
"tabArrayItems": {
"type": "array",
"items": {
"type": "object",
"properties": {
"one": {
"type": "string"
},
"two": {
"type": "string"
}
}
}
}
}
},
"form": [
{
"key": "tabArrayItems",
"type": "tabarray",
"title": "{$ 'Tab '+$index $}",
"description": "The 'tabarray' description"
}
]
}

View File

@ -0,0 +1,43 @@
{
"schema": {
"type": "object",
"title": "Tabs",
"properties": {
"first": {
"type": "string"
},
"second": {
"type": "string"
},
"third": {
"type": "string"
},
"fourth": {
"type": "string"
}
}
},
"form": [
{
"type": "tabs",
"tabs": [
{
"title": "Tab One",
"help": "static/dashboard/developer/form-builder/example-forms/example-help.html",
"items": [
"first",
"second"
]
},
{
"title": "Tab Two",
"help": "static/dashboard/developer/form-builder/example-forms/example-help.html",
"items": [
"third",
"fourth"
]
}
]
}
]
}

View File

@ -0,0 +1,29 @@
{
"schema": {
"type": "object",
"title": "Text Inputs",
"properties": {
"aString": {
"type": "string"
},
"aTextArea": {
"type": "string"
}
}
},
"form": [
{
"key": "aString",
"title": "This is a 'string' input ",
"description": "This is the 'string' input description",
"placeholder": "Prompt for input"
},
{
"key": "aTextArea",
"type": "textarea",
"title": "This is a 'textarea' input ",
"description": "This is the 'textarea' input description",
"placeholder": "Prompt for input"
}
]
}

View File

@ -0,0 +1,189 @@
/*
* (c) Copyright 2016 Hewlett Packard Enterprise Development Company LP
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
angular
.module('horizon.dashboard.developer.form-builder')
.controller('horizon.dashboard.developer.form-builder.FormBuilderController', controller);
controller.$inject = [
'$http',
'$scope',
'horizon.framework.widgets.form.ModalFormService',
'horizon.dashboard.developer.form-builder.basePath',
'horizon.framework.util.i18n.gettext',
];
/**
* @ngdoc controller
* @name horizon.dashboard.developer.form-builder:FormBuilderController
* @description
* This controller allows the launching of any actions registered for resource types
*/
function controller($http, $scope, modalFormService, basePath, gettext) {
var ctrl = this;
ctrl.schemaParses = true;
ctrl.formParses = true;
ctrl.availableForms = [
{
name: gettext("Text Inputs"),
data: basePath + 'example-forms/text-inputs.json'
},
{
name: gettext("Buttons"),
data: basePath + 'example-forms/buttons.json'
},
{
name: gettext("Radios, Checkboxes and Select"),
data: basePath + 'example-forms/radios-checkboxes-select.json'
},
{
name: gettext("Sections and Fieldsets"),
data: basePath + 'example-forms/sections-fieldsets.json'
},
{
name: gettext("Tabs"),
data: basePath + 'example-forms/tabs.json'
},
{
name: gettext("Add Ons, Required and Feedback"),
data: basePath + 'example-forms/addons-required-feedback.json'
},
{
name: gettext("Array"),
data: basePath + 'example-forms/array.json'
},
{
name: gettext("Tab Array"),
data: basePath + 'example-forms/tabarray.json'
},
{
name: gettext("A Confirmation Dialog"),
data: basePath + 'example-forms/confirmation-dialog.json'
}
];
ctrl.selectedForm = ctrl.availableForms[0];
ctrl.model = {};
ctrl.formJson = JSON.stringify(ctrl.form, undefined, 2);
ctrl.schemaJson = JSON.stringify(ctrl.schema, undefined, 2);
ctrl.launchCurrentFormAsModal = launchCurrentFormAsModal;
ctrl.viewFormJavascript = viewFormJavascript;
// Update if user selects a new form example
$scope.$watch('ctrl.selectedForm',function(item) {
if (angular.isDefined(item.data)) {
$http.get(item.data).then(function(result) {
setNewForm(result.data);
});
}
});
// Update if user edits schema JSON
$scope.$watch('ctrl.schemaJson',function(val, old) {
if (val && val !== old) {
try {
ctrl.schema = JSON.parse(ctrl.schemaJson);
ctrl.schemaParses = true;
} catch (e) {
ctrl.schemaParses = false;
ctrl.schemaError = e.message;
}
}
});
// Update if the user edits the form JSON
$scope.$watch('ctrl.formJson', function(val, old){
if (val && val !== old) {
try {
ctrl.form = JSON.parse(ctrl.formJson);
ctrl.formParses = true;
} catch (e) {
ctrl.formParses = false;
ctrl.formError = e.message;
}
}
});
// Format the current form model for readability
ctrl.prettyModelData = function(){
return typeof ctrl.model === 'string' ? ctrl.model : JSON.stringify(ctrl.model, undefined, 2);
};
// Format the current form, schema and model and a single javascript string
function prettyFormJavascript(){
// Put the user's form into a JavaScript object, including any current model values
var currentFormAsObject = {
schema: JSON.parse(ctrl.schemaJson),
form: JSON.parse(ctrl.formJson),
model: ctrl.model
};
// Convert that to a string so we can use it as input to a schema form model
return "var formConfig = " + JSON.stringify(currentFormAsObject, undefined, 2) + ";";
}
// Set the builder to loaded form data
function setNewForm(data) {
ctrl.schema = data.schema;
ctrl.form = data.form;
ctrl.schemaJson = JSON.stringify(ctrl.schema, undefined, 2);
ctrl.formJson = JSON.stringify(ctrl.form, undefined, 2);
ctrl.model = data.model || {};
}
// Show the user what the current form looks like as a modal
function launchCurrentFormAsModal() {
var config = {
schema: ctrl.schema,
form: ctrl.form,
model: ctrl.model
};
return modalFormService.open(config);
}
// Show the user the current form as javascript for copy-paste
function viewFormJavascript() {
// Build a schema form to display the user's form
var viewJsSchema = {
"type": "object",
"properties": {
"formJs": {
"type": "string"
}
}
};
var viewJsForm = [
{
"key": "formJs",
"type": "template",
"templateUrl": basePath + "form-config-modal.html"
}
];
var viewJsModel = {
"formJS": prettyFormJavascript(),
};
var config = {
schema: viewJsSchema,
form: viewJsForm,
model: viewJsModel,
title: gettext("Your Form as JavaScript")
};
return modalFormService.open(config);
}
}
})();

View File

@ -0,0 +1,44 @@
/*
* (c) Copyright 2016 Hewlett Packard Enterprise Development Company LP
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function () {
'use strict';
angular
.module('horizon.dashboard.developer.form-builder')
.directive('formBuilder', directive);
directive.$inject = ['horizon.dashboard.developer.form-builder.basePath'];
/**
* @ngdoc directive
* @name form-builder
* @description
* Show a schema form builder
*/
function directive(path) {
var directive = {
restrict: 'E',
templateUrl: path + 'form-builder.html',
scope: {},
controller: 'horizon.dashboard.developer.form-builder.FormBuilderController as ctrl'
};
return directive;
}
})();

View File

@ -0,0 +1,58 @@
<h1 translate>Live Edit Schema Form Examples</h1>
<p translate>This page contains examples of the different fields provided by Schema Form in Horizon. The <code>Schema</code> and <code>Form</code> sections below can be edited inline to customise the form output. For further documentation on Schema Form, see <a href="http://schemaform.io/">http://schemaform.io/</a>.</p>
<div class="row">
<div class="col-sm-6">
<h2 class="h3">
<span translate>Form Output</span>
<button class="btn btn-default"
type="button"
ng-disabled="!(ctrl.formParses && ctrl.schemaParses)"
ng-click="ctrl.launchCurrentFormAsModal()">
<span translate>View As Modal</span>
</button>
<button class="btn btn-default"
type="button"
ng-disabled="!(ctrl.formParses && ctrl.schemaParses)"
ng-click="ctrl.viewFormJavascript()">
<span translate>View Form JSON</span>
</button>
</h2>
<div class="modal-content form-builder-modal">
<div class="modal-header">
<div class="h4 modal-title">{$ ctrl.schema.title $}</div>
</div>
<div class="modal-body">
<form name="schemaForm"
sf-model="ctrl.model"
sf-form="ctrl.form"
sf-schema="ctrl.schema"
ng-submit="ctrl.submitForm(schemaForm, ctrl.model)">
</form>
</div>
</div>
<h2 class="h3" translate>Model</h2>
<pre>{$ ctrl.prettyModelData() $}</pre>
</div>
<div class="col-sm-6">
<h2 class="h3" translate>Select Example</h2>
<select class="form-control"
ng-model="ctrl.selectedForm"
ng-options="item.name for item in ctrl.availableForms">
</select>
<h2 class="h3" translate>Schema</h2>
<p translate>Use the schema to describe the data model and valid input patterns. A schema is always JSON. NOTE: A schema element is not needed for items that have no data model, like sections and fieldsets.</p>
<pre class="code-input" contenteditable ng-keydown="ctrl.onKeydown($event)" ng-model="ctrl.schemaJson"></pre>
<div class="alert alert-danger" role="alert" ng-hide="ctrl.schemaParses">{{ ctrl.schemaError }}</div>
<h2 class="h3" translate>Form</h2>
<p translate>Use the form to set titles, descriptions and choose specific UI element types for each schema item. In the form builder, the form must be JSON, but you can use JavaScript in production code, such as <code>gettext</code> for translation.</p>
<pre class="code-input" contenteditable ng-keydown="ctrl.onKeydown($event)" ng-model="ctrl.formJson"></pre>
<div class="alert alert-danger" role="alert" ng-hide="ctrl.formParses">{{ ctrl.formError }}</div>
</div>
</div>

View File

@ -0,0 +1,46 @@
/*
* (c) Copyright 2016 Hewlett Packard Enterprise Development Company LP
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function () {
'use strict';
/**
* @ngdoc module
* @ngname horizon.dashboard.developer.form-builder
* @description
* Dashboard module for the form-builder panel.
*/
angular
.module('horizon.dashboard.developer.form-builder', [], config)
.constant('horizon.dashboard.developer.form-builder.BASE_ROUTE', '/developer/form_builder/');
config.$inject = [
'$provide',
'$routeProvider',
'$windowProvider',
'horizon.dashboard.developer.form-builder.BASE_ROUTE'];
function config($provide, $routeProvider, $windowProvider, baseRoute) {
$routeProvider
.when(baseRoute, {
templateUrl: $windowProvider.$get().STATIC_URL +
'dashboard/developer/form-builder/index.html'
});
var path = $windowProvider.$get().STATIC_URL + 'dashboard/developer/form-builder/';
$provide.constant('horizon.dashboard.developer.form-builder.basePath', path);
}
})();

View File

@ -0,0 +1,9 @@
// Since this is inline on the form-builder demo page,
// override the max-height / scrolling hack, and the shadow
.form-builder-modal {
box-shadow: none;
.modal-body {
max-height: none;
}
}

View File

@ -0,0 +1,3 @@
<p translate>This JavaScript object contains your new form and can be used with <code>horizon.framework.widgets.form.ModalFormService</code></p>
<pre>{$::model.formJS$}</pre>

View File

@ -0,0 +1 @@
<form-builder></form-builder>

View File

@ -0,0 +1,7 @@
// Workarounds for some browsers inserting <div> elements when editing
.code-input {
display: inline-block;
width: 100%;
color: $kbd-color;
background-color: $kbd-bg;
}

View File

@ -19,6 +19,7 @@
@import "components/breadcrumbs"; @import "components/breadcrumbs";
@import "components/charts"; @import "components/charts";
@import "components/checkboxes"; @import "components/checkboxes";
@import "components/code";
@import "components/datepicker"; @import "components/datepicker";
@import "components/dl_lists"; @import "components/dl_lists";
@import "components/dropdowns"; @import "components/dropdowns";