Skip to content

Instantly share code, notes, and snippets.

@linebreaker
Created October 12, 2017 22:27
Show Gist options
  • Save linebreaker/72270a4d4a51ce1103fb4e1a5b7e7b69 to your computer and use it in GitHub Desktop.
Save linebreaker/72270a4d4a51ce1103fb4e1a5b7e7b69 to your computer and use it in GitHub Desktop.
Angular CMS/Editor
<body ng-app="backoffice" ng-controller="AppCtrl as app">
<div class="container">
<!-- Info -->
<h1>Angular CMS/Editor</h1>
<p><b>Problem:</b> Currently, we create PHP constants, spend some time to create a new space and then deploy it. It's just a block of HTML content and the frontend developer has to try and put that into different places on the page.</p>
<p><b>Solution:</b> Make templates within the CMS. The frontend developer can creates templates for the content team to populate, easily. No PHP constants,
deployments or dev time needed.</p>
<ol>
<li ng-class="{ strong: !app.space }">Frontend Developer creates a template</li>
<li ng-class="{ strong: app.space }">Content team create a space by populating space previously defined</li>
</ol>
<hr />
<!-- List of templates -->
<div ng-if="!app.space.template">
<h2>Templates</h2>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>ID</th>
<th>Name (slug)</th>
<th>Created</th>
<th>Updated</th>
<th>Fields</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<tr ng-if="!app.templates.length">
<td colspan="6">There are no templates defined</td>
</tr>
<tr ng-repeat="template in app.templates">
<td>{{ template.id }}</td>
<td>{{ template.name }} ({{template.slug}})</td>
<td>{{ template.createdOn | date }}</td>
<td>{{ template.updatedOn | date }}</td>
<td>
<span class="label label-default" title="{{field.type}} - {{field.description}}" ng-repeat="field in template.fields">{{ field.label }}</span>
</td>
<td class="text-right">
<button ng-click="app.templates.splice($index, 1)" class="btn btn-danger"><i class="fa fa-remove"></i> Delete</button>
<button ng-click="app.template = template; app.creatingTemplate = true" class="btn btn-primary"><i class="fa fa-pencil"></i> Edit</button>
<button ng-click="app.createSpace(template)" class="btn btn-primary"><i class="fa fa-plus"></i> New space</button>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="6">
<div ng-if="app.creatingTemplate">
<form name="tForm" ng-submit="app.saveTemplate(tForm)" novalidate>
<!-- Mandatory identifiers -->
<fieldset>
<legend>Identifiers</legend>
<p class="fieldset-info">Fields required for identification and deep-linking within system</p>
<div class="field">
<div class="inner">
<div class="row">
<div class="col-xs-3">
<div class="form-group" ng-class="{ 'has-error': (tForm.name.$invalid && tForm.$submitted) || (tForm.name.$invalid && tForm.name.$dirty) }">
<label class="control-label">Name</label>
<input type="text" class="form-control" name="name" placeholder="My Template" ng-model="app.template.name" required />
</div>
</div>
<div class="col-xs-3">
<div class="form-group" ng-class="{ 'has-error': (tForm.slug.$invalid && tForm.$submitted) || (tForm.slug.$invalid && tForm.slug.$dirty) }">
<label class="control-label">Slug</label>
<input type="text" class="form-control" name="slug" placeholder="my-template" ng-model="app.template.slug" required />
</div>
</div>
</div>
</div>
</div>
</fieldset>
<!-- Fields -->
<fieldset>
<legend>Fields</legend>
<div class="field" ng-repeat="field in app.template.fields">
<div class="btn-tabs">
<button type="button" class="btn-tab btn-danger" title="Remove" ng-if="app.template.fields.length > 1" ng-click="app.template.fields.splice($index, 1)">
<i class="fa fa-remove"></i> Kill
</button>
<button type="button" class="btn-tab btn-primary" title="Add" ng-if="$last" ng-click="app.template.fields.push({ type: 'text', required: true, multiple: false })">
<i class="fa fa-plus"></i> Add
</button>
</div>
<div class="inner">
<div class="row">
<div class="col-xs-3">
<div class="form-group" ng-class="{ 'has-error': (tForm['field-label'].$invalid && tForm.$submitted) || (tForm['field-label'].$invalid && tForm['field-label'].$dirty) }">
<label class="control-label">Label</label>
<input type="text" class="form-control" name="field-label" placeholder="My Field" ng-model="app.template.fields[$index].label" required />
</div>
</div>
<div class="col-xs-3">
<div class="form-group" ng-class="{ 'has-error': (tForm['field-key'].$invalid && tForm.$submitted) || (tForm['field-key'].$invalid && tForm['field-key'].$dirty) }">
<label class="control-label">Key</label>
<input type="text" class="form-control" name="field-key" placeholder="my-field" ng-model="app.template.fields[$index].key" required />
</div>
</div>
<div class="col-xs-2">
<div class="form-group" ng-class="{ 'has-error': (tForm['field-type'].$invalid && tForm.$submitted) || (tForm['field-type'].$invalid && tForm['field-type'].$dirty) }">
<label class="control-label">Type</label>
<select class="form-control" name="field-type" ng-model="app.template.fields[$index].type" ng-options="o.value as o.label for o in app.options.types" required></select>
</div>
</div>
<div class="col-xs-2">
<div class="form-group" ng-class="{ 'has-error': (tForm['field-required'].$invalid && tForm.$submitted) || (tForm['field-required'].$invalid && tForm['field-required'].$dirty) }">
<label class="control-label">Required</label>
<select class="form-control" name="field-required" ng-model="app.template.fields[$index].required" ng-options="o.value as o.label for o in app.options.booleans" required></select>
</div>
</div>
<div class="col-xs-2">
<div class="form-group" ng-class="{ 'has-error': (tForm['field-multiple'].$invalid && tForm.$submitted) || (tForm['field-multiple'].$invalid && tForm['field-multiple'].$dirty) }">
<label class="control-label">Multiple</label>
<select class="form-control" name="field-multiple" ng-model="app.template.fields[$index].multiple" ng-options="o.value as o.label for o in app.options.booleans" required></select>
</div>
</div>
</div>
</div>
</div>
</fieldset>
<hr />
<div class="text-right">
<button type="button" class="btn btn-danger" ng-click="app.creatingTemplate = false">Cancel</button>
<button type="submit" class="btn btn-primary" ng-disabled="dForm.$invalid"><i class="fa fa-check"></i> Save template</button>
</div>
</form>
</div>
<div class="text-right" ng-if="!app.creatingTemplate">
<button type="button" class="btn btn-primary" ng-click="app.creatingTemplate = !app.creatingTemplate"><i class="fa fa-plus"></i> New template</button>
</div>
</td>
</tr>
</tfoot>
</table>
<!-- List of spaces -->
<div ng-if="app.spaces.length">
<hr />
<h2>Spaces</h2>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>ID</th>
<th>Name (slug)</th>
<th>Template (slug)</th>
<th>Created</th>
<th>Updated</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="space in app.spaces">
<td>{{ space.id }}</td>
<td>{{ space.name }} ({{ space.slug }})</td>
<td>{{ space.template.name }} ({{ space.template.slug }})</td>
<td>{{ space.createdOn | date }}</td>
<td>{{ space.updatedOn | date }}</td>
<td class="text-right">
<button ng-click="app.spaces.splice($index, 1)" class="btn btn-danger"><i class="fa fa-remove"></i> Delete</button>
<button ng-click="app.space = space" class="btn btn-primary"><i class="fa fa-pencil"></i> Edit</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Space creation/editing -->
<div class="row" ng-if="app.space.template">
<div class="col-md-6">
<h2>{{app.space.template.name}} ({{app.space.template.slug}})</h2>
<form name="sForm" ng-submit="app.saveSpace(sForm)" novalidate>
<!-- Mandatory fields for identification -->
<fieldset>
<legend>
Identifiers
<span class="text-danger" title="Required">*</span>
</legend>
<p class="fieldset-info">Fields required for identification and deep-linking within system</p>
<div class="field">
<div class="inner">
<div class="row">
<div class="col-xs-6">
<div class="form-group" ng-class="{ 'has-error': (sForm.name.$invalid && sForm.$submitted) || (sForm.name.$invalid && sForm.name.$dirty) }">
<label class="control-label">Name</label>
<input type="text" class="form-control" name="name" placeholder="My Space" ng-model="app.space.name" required />
</div>
</div>
<div class="col-xs-6">
<div class="form-group" ng-class="{ 'has-error': (sForm.slug.$invalid && sForm.$submitted) || (sForm.slug.$invalid && sForm.slug.$dirty) }">
<label class="control-label">Slug</label>
<input type="text" class="form-control" name="slug" placeholder="my-slug" ng-model="app.space.slug" required />
</div>
</div>
</div>
</div>
</div>
</fieldset>
<!-- Template data fields -->
<fieldset ng-repeat="definition in app.space.template.fields" ng-class="{ 'has-error': (sForm['data-' + definition.key].$invalid && sForm.$submitted) || (sForm['data-' + definition.key].$invalid && sForm['data-' + definition.key].$dirty) }">
<legend>
{{ definition.label }}
<span class="text-danger" title="Required" ng-if="definition.required !== false">*</span>
</legend>
<!-- Singular -->
<field-data class="field"
ng-if="!definition.multiple"
data="app.space.data[definition.key]"
definition="definition"></field-data>
<!-- Multiple:
have to pass in data like this or else ng-model doesn't trigger changes:
data="app.space.data[definition.key][$index]"
-->
<field-data class="field"
ng-if="definition.multiple"
ng-repeat="d in app.space.data[definition.key] track by $index"
data="app.space.data[definition.key][$index]"
definition="definition"
on-add="app.addData(definition, app.space.data[definition.key])"
on-remove="app.removeData(definition, $index)"
item-length="app.space.data[definition.key].length"
last="$last"></field-data>
</fieldset>
<div class="text-right">
<button type="button" ng-click="app.space = null" class="btn btn-danger">Cancel</button>
<button type="submit" class="btn btn-primary"><i class="fa fa-check"></i> Save space</button>
</div>
</form>
<br />
</div>
<!-- Debug -->
<div class="col-md-6">
<h2>Live Preview</h2>
<p>Template HTML/CSS is shared between frontend and CMS for live-previews and only need to be coded once.</p>
<template-space data="app.space.data" slug="{{app.space.template.slug}}"></template-space>
<h2>Raw JSON</h2>
<pre>{{app.space | json}}</pre>
</div>
</div>
</div>
</body>
<!-- Imported from FE/Common -->
<script type="text/ng-template" id="components/template-space/banner.tpl.html">
<div class="carousel banner">
<div class="carousel-inner">
<div class="item active">
<img ng-src="{{tSpace.data.image}}">
<h3 ng-repeat="title in tSpace.data.titles">{{title}}</h3>
<div class="bottom">
<button ng-repeat="cta in tSpace.data.ctas" ng-href="{{cta.url}}" class="btn btn-{{cta.size}} btn-{{cta.style}}">{{cta.text}}</button>
{{tSpace.title}}
</div>
<div class="flip-content" ng-bind-html="tSpace.data.flipContent"></div>
</div>
</div>
</div>
</script>
angular
.module('backoffice', [
'ngAnimate',
'textAngular',
'ngFileUpload'
])
.controller('AppCtrl', AppCtrl)
.directive('fieldData', fieldData)
// Imported from FE/Common
.directive('templateSpace', templateSpace);
function AppCtrl() {
var vm = this;
vm.creatingTemplate = false;
vm.template = {
slug: '',
name: '',
fields: [
{
key: '',
label: '',
type: 'text',
required: true,
multiple: false
}
]
};
vm.templates = [
{
"id": 1,
"name": "HTML Space",
"slug": "html-space",
"createdOn": new Date("Sun Feb 07 2016 10:40:38 GMT+0100 (CET)"),
"updatedOn": new Date("Sun Feb 08 2016 10:40:38 GMT+0100 (CET)"),
"fields": [
{
"key": "title",
"type": "text",
"label": "Title",
"multiple": false,
"required": true
}, {
"key": "content",
"type": "html",
"label": "Content",
"multiple": false,
"required": true
}, {
"key": "ctas",
"type": "button",
"label": "Calls to action",
"multiple": true,
"required": true
}
]
},
{
"id": 2,
"name": "Banner",
"slug": "banner",
"createdOn": new Date("Friday Feb 12 2016 10:40:38 GMT+0100 (CET)"),
"updatedOn": new Date("Saturday Feb 13 2016 10:40:38 GMT+0100 (CET)"),
"fields": [
{
"key": "titles",
"type": "text",
"label": "Titles",
"multiple": true,
"required": true
}, {
"key": "image",
"type": "text",
"label": "Image URL",
"default": "https://i.ytimg.com/vi/tntOCGkgt98/maxresdefault.jpg",
"multiple": false,
"required": true
}, {
"key": "flipContent",
"type": "html",
"label": "Flip Content",
"multiple": false,
"required": true
}, {
"key": "ctas",
"type": "button",
"label": "Calls to action",
"multiple": true,
"required": true
}
]
}
];
vm.options = {
types: [
{
value: 'text',
label: 'Text'
}, {
value: 'button',
label: 'Button'
}, {
value: 'html',
label: 'HTML'
}, {
value: 'file',
label: 'File'
}
],
booleans: [
{
value: true,
label: 'Yes'
}, {
value: false,
label: 'No'
}
]
}
vm.spaces = [];
vm.space = null;
vm.saveTemplate = saveTemplate;
vm.createSpace = createSpace;
vm.saveSpace = saveSpace;
// add/remove of field data
vm.addData = addData;
vm.removeData = removeData;
function saveTemplate(form) {
if (form.$valid) {
var now = new Date;
vm.template.updatedOn = now;
if (!vm.template.id) {
vm.template.id = vm.templates.length + 1;
vm.template.createdOn = new Date;
vm.templates.push(vm.template);
}
vm.creatingTemplate = false;
}
}
function createSpace(template) {
// init field data
var spaceData = {};
template.fields.forEach(function(definition) {
var data = getDefaultData(definition);
if (definition.multiple) {
spaceData[definition.key] = [data];
} else {
spaceData[definition.key] = data;
}
});
vm.space = {
template: template,
data: spaceData
};
}
function saveSpace(form) {
if (form.$valid) {
var now = new Date;
vm.space.updatedOn = now;
if (!vm.space.id) {
vm.space.id = vm.spaces.length + 1;
vm.space.createdOn = now;
vm.spaces.push(vm.space);
}
vm.space = null;
}
}
function getDefaultData(definition) {
if (definition.type === 'button') {
return {
text: definition.default || 'Submit',
href: '',
size: 'md',
style: 'default'
};
}
return definition.default || '';
}
function addData(definition, spaceData) {
var defaultData = getDefaultData(definition);
spaceData.push(defaultData);
}
function removeData(definition, index) {
vm.space.data[definition.key].splice(index, 1);
}
}
function fieldData() {
return {
restrict: 'E',
bindToController: true,
scope: {
definition: '=',
data: '=',
last: '=',
itemLength: '=',
onAdd: '&',
onRemove: '&'
},
controllerAs: 'field',
controller: SpaceFieldController,
template: `
<div class="btn-tabs">
<button type="button" class="btn-tab btn-danger" title="Remove" ng-if="field.itemLength > 1" ng-click="field.onRemove()">
<i class="fa fa-remove"></i> Kill
</button>
<button type="button" class="btn-tab btn-primary" title="Add" ng-if="field.last" ng-click="field.onAdd()">
<i class="fa fa-plus"></i> Add
</button>
</div>
<div class="inner" ng-switch on="field.definition.type">
<div class="form-group" ng-switch-when="text">
<input type="text" class="form-control" name="data-{{field.definition.key}}" placeholder="Some text content" ng-required="field.definition.required !== false" ng-model="field.data" />
</div>
<div class="form-group" ng-switch-when="file">
<section class="draggable" ngf-max-size="{{field.maxUploadSize}}" ngf-pattern="'{{field.allowedFiletypes}}'" ng-model="field.data" ngf-drop="field.uploadFiles($files);" ngf-drag-over-class="'drag-over'">
<div class="drop-txt off" translate>Drag file here to upload</div>
<div class="drop-txt on" translate>Drop file to upload</div>
<div class="or">or</div>
<div>
<button type="button" name="data-{{field.definition.key}}" class="btn btn-primary" ngf-select="field.uploadFiles($files);" ng-required="field.definition.required !== false">Select files on your computer</button>
</div>
</section>
<section class="uploading" ng-show="field.thinking">
<div class="col-md-3">
<i class="fa fa-2x fa-file-image-o"></i>
</div>
<div class="col-md-6 text-center">
<i class="fa fa-2x fa-circle-o-notch fa-spin"></i> <span>uploading</span>
</div>
<div class="col-md-3">
<button class="btn btn-sm btn-block btn-default">Cancel</button>
</div>
</section>
</div>
<div class="form-group" ng-switch-when="html">
<text-angular name="data-{{field.definition.key}}" ng-model="field.data" ng-required="field.definition.required !== false"></text-angular>
</div>
<div ng-switch-when="button">
<!-- Text and URL -->
<div class="row">
<div class="col-xs-4">
<div class="form-group">
<label>Text</label>
<input type="text" class="form-control" name="data-{{field.definition.key}}" ng-model="field.data.text" ng-required="field.definition.required !== false">
</div>
</div>
<div class="col-xs-8">
<div class="form-group">
<label>URL</label>
<input type="url" class="form-control" name="data-{{field.definition.key}}" ng-model="field.data.href" placeholder="https://www.url.com" ng-required="field.definition.required !== false">
</div>
</div>
</div>
<!-- Style and size -->
<div class="row">
<div class="col-xs-4">
<div class="form-group">
<label>Size</label>
<div>
<div class="btn-group">
<button type="button"
ng-repeat="size in field.sizes"
ng-click="field.data.size = size.key"
ng-class="{ active: field.data.size == size.key }"
title="{{size.title}}"
class="btn btn-sm btn-default">{{size.name}}</button>
</div>
</div>
</div>
</div>
<div class="col-xs-8">
<div class="form-group">
<label>Style</label>
<div>
<div class="btn-group">
<button type="button"
ng-repeat="style in field.styles"
ng-click="field.data.style = style.key"
ng-class="{ active: field.data.style == style.key }"
title="{{style.title}}"
class="btn btn-sm btn-{{style.key}}">{{style.name}}</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`
};
}
function SpaceFieldController($scope, $timeout) {
var vm = this;
vm.maxUploadSize = 5000;
vm.allowedFiletypes = '.jpg, .jpeg, .png, .gif';
vm.sizes = [
{
key: 'xs',
title: 'Extra Small',
name: 'XS'
}, {
key: 'sm',
title: 'Small',
name: 'SM'
}, {
key: 'md',
title: 'Medium',
name: 'MD'
}, {
key: 'lg',
title: 'Large',
name: 'LG'
}
];
vm.styles = [
{
key: 'default',
name: 'Default',
title: 'Default'
}, {
key: 'primary',
name: 'Pri',
title: 'Primary'
}, {
key: 'success',
name: 'Suc',
title: 'Success'
}, {
key: 'info',
name: 'Info',
title: 'Info'
}, {
key: 'warning',
name: 'Warn',
title: 'Warning'
}, {
key: 'danger',
name: 'Dan',
title: 'Danger'
}, {
key: 'link',
name: 'Link'
}
];
vm.uploadFiles = uploadFiles;
function uploadFiles($files) {
console.log('TODO: Upload', $files);
}
}
function templateSpace() {
return {
restrict: 'E',
templateUrl: function(elem, attrs) {
// Harcoded for now... attrs.slug doesn't get compiled yet :S
return 'components/template-space/banner.tpl.html';
// return 'components/template-space/' + attrs.slug + '.tpl.html';
},
scope: {
slug: '@',
data: '=?'
},
controller: TemplateSpaceCtrl,
controllerAs: 'tSpace',
bindToController: true
};
}
/* @ngInject */
function TemplateSpaceCtrl() {
var vm = this;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.14/angular.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.14/angular-animate.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/textAngular/1.5.0/textAngular-rangy.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/textAngular/1.5.0/textAngular-sanitize.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/textAngular/1.5.0/textAngular.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/danialfarid-angular-file-upload/12.0.1/ng-file-upload-shim.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/danialfarid-angular-file-upload/12.0.1/ng-file-upload.min.js"></script>
body { padding: 0 50px 50px; }
.table {
> tbody {
> tr {
> td {
line-height: 35px;
}
}
}
> tfoot {
form {
padding: 20px;
}
}
.label {
margin-right: 2px;
margin-bottom: 2px;
}
}
.btn-tabs {
z-index: 1;
position: absolute;
top: 10px;
right: 0;
transition: transform .1s;
transform: translateX(8px);
}
.btn-tab {
display: block;
width: 100%;
margin-bottom: 2px;
padding: 5px 8px;
border: 0;
border-radius: 0 3px 3px 0;
text-transform: uppercase;
transition: color .1s, transform .1s;
&:hover {
transform: translateX(5%);
}
}
.field {
position: relative;
margin: 0 0 3px;
transition: opacity .2s, transform .2s;
> .inner {
z-index: 2;
position: relative;
padding: 10px 10px 1px;
min-height: 80px;
border-radius: 0;
background: #f4f4f4;
}
&.ng-enter {
opacity: 0;
transform: translateY(-50px);
&.ng-enter-active {
opacity: 1;
transform: translateY(0);
}
}
&.ng-leave {
opacity: 1;
transform: translateY(0);
&.ng-leave-active {
opacity: 0;
transform: translateY(-50px);
}
}
&:hover {
.btn-tabs {
transform: translateX(95%);
}
}
}
.form-group {
margin-bottom: 10px;
}
fieldset {
margin-bottom: 10px;
}
.fieldset-info {
margin: -15px 0 20px;
color: #9c9c9c;
}
field-data {
display: block;
}
.strong {
font-weight: bold;
}
.btn-group {
box-shadow: 1px 0 5px rgba(0, 0, 0, 0.2);
border-radius: 5px;
}
// Imported from FE/Common
template-space {
display: block;
}
.carousel {
&.banner {
.item {
height: 250px;
padding: 10px;
background: #eee;
color: #fff;
}
h1, h2, h3, h4 {
margin: 0 0 5px;
}
img {
z-index: -1;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.bottom {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 10px;
text-align: right;
background: rgba(0, 0, 0, 0.3);
}
}
.btn {
margin-left: 5px;
}
.flip-content {
position: absolute;
top: 0;
right: 0;
bottom: 54px;
left: 0;
padding: 10px;
background: rgba(#000, .8);
color: #fff;
opacity: 0;
transition: opacity .1s;
}
&:hover {
.flip-content {
opacity: 1;
}
}
}
.draggable {
border: 5px dashed #ccc;
padding: 15px;
text-align: center;
&.drag-over {
border-color: #000;
.drop-txt {
&.off {
display: none;
}
&.on {
display: block;
}
}
}
.drop-txt {
font-weight: bold;
&.on {
display: none;
}
}
.or {
margin: 5px 0;
color: #333;
}
}
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet" />
<link href="//cdnjs.cloudflare.com/ajax/libs/textAngular/1.5.0/textAngular.css" rel="stylesheet" />
<link href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.5.0/css/font-awesome.min.css" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment