Adding a picture to the MEAN.JS sample with Angular-file-upload - meanjs

I am using MEAN.JS (https://github.com/meanjs/mean) and angular-file-upload (https://github.com/danialfarid/angular-file-upload).
The "Article" sample provided by MEAN.JS contains two fields named "title" and "content". I want to modify that and add a "picture" field allowing a user to upload a picture.
I understand that I have to modify 3 files in MEAN.JS:
~myproject/app/models/article.server.model.js
~myproject/public/modules/articles/controllers/articles.client.controller.js
~myproject/public/modules/articles/views/create-article.client.view.html
However, I can not modify them successfully.

My solution uses angular-file-upload on the client and uses connect-multiparty to handle the file upload.
The images are stored directly in the database which limits their size. I have not included the required code to check the image size.
set up
bower install ng-file-upload --save
bower install ng-file-upload-shim --save
npm i connect-multiparty
npm update
all.js
add angular-file-upload scripts
...
'public/lib/ng-file-upload/FileAPI.min.js',
'public/lib/ng-file-upload/angular-file-upload-shim.min.js',
'public/lib/angular/angular.js',
'public/lib/ng-file-upload/angular-file-upload.min.js',
...
config.js
Inject angular-file-upload dependency
...
var applicationModuleVendorDependencies = ['ngResource', 'ngAnimate', 'ui.router', 'ui.bootstrap', 'ui.utils', 'angularFileUpload'];
...
article.client.controller.js
Use angular-file-upload dependency
angular.module('articles').controller('ArticlesController', ['$scope', '$timeout', '$upload', '$stateParams', '$location', 'Authentication', 'Articles',
function($scope, $timeout, $upload, $stateParams, $location, Authentication, Articles) {
$scope.fileReaderSupported = window.FileReader !== null;
// Create new Article
$scope.create = function(picFile) {
console.log('create');
console.log(picFile);
var article = new Articles({
title: this.title,
content: this.content,
image: null
});
console.log(article);
$upload.upload({
url: '/articleupload',
method: 'POST',
headers: {'Content-Type': 'multipart/form-data'},
fields: {article: article},
file: picFile,
}).success(function (response, status) {
$location.path('articles/' + response._id);
$scope.title = '';
$scope.content = '';
}).error(function (err) {
$scope.error = err.data.message;
});
};
$scope.doTimeout = function(file) {
console.log('do timeout');
$timeout( function() {
var fileReader = new FileReader();
fileReader.readAsDataURL(file);
console.log('read');
fileReader.onload = function(e) {
$timeout(function() {
file.dataUrl = e.target.result;
console.log('set url');
});
};
});
};
$scope.generateThumb = function(file) {
console.log('generate Thumb');
if (file) {
console.log('not null');
console.log(file);
if ($scope.fileReaderSupported && file.type.indexOf('image') > -1) {
$scope.doTimeout(file);
}
}
};
}
create-article.client.view.html
update the create view to handle file selection and upload
<section data-ng-controller="ArticlesController">
<div class="page-header">
<h1>New Article</h1>
</div>
<div class="col-md-12">
<form name="articleForm" class="form-horizontal" data-ng-submit="create(picFile)" novalidate>
<fieldset>
<div class="form-group" ng-class="{ 'has-error': articleForm.title.$dirty && articleForm.title.$invalid }">
<label class="control-label" for="title">Title</label>
<div class="controls">
<input name="title" type="text" data-ng-model="title" id="title" class="form-control" placeholder="Title" required>
</div>
</div>
<div class="form-group">
<label class="control-label" for="content">Content</label>
<div class="controls">
<textarea name="content" data-ng-model="content" id="content" class="form-control" cols="30" rows="10" placeholder="Content"></textarea>
</div>
</div>
<div class="form-group">
<label class="control-label" for="articleimage">Article Picture</label>
<div class="controls">
<input id="articleimage" type="file" ng-file-select="" ng-model="picFile" name="file" accept="image/*" ng-file-change="generateThumb(picFile[0], $files)" required="">
<br/>
<img ng-show="picFile[0].dataUrl != null" ng-src="{{picFile[0].dataUrl}}" class="img-thumbnail" height="50" width="100">
<span class="progress" ng-show="picFile[0].progress >= 0">
<div style="width:{{picFile[0].progress}}%" ng-bind="picFile[0].progress + '%'" class="ng-binding"></div>
</span>
<span ng-show="picFile[0].result">Upload Successful</span>
</div>
</div>
<div class="form-group">
<input type="submit" class="btn btn-default" ng-disabled="!articleForm.$valid" ng-click="uploadPic(picFile)">
</div>
<div data-ng-show="error" class="text-danger">
<strong data-ng-bind="error"></strong>
</div>
</fieldset>
</form>
</div>
</section>
view-article.client.view.html
update list view to include image
<img ng-src="data:image/jpeg;base64,{{article.image}}" id="photo-id" width="200" height="200"/>
list-articles.client.view.html
update view to include imgae
<img ng-src="data:image/jpeg;base64,{{article.image}}" id="photo-id" width="40" height="40"/>
article.server.model.js
Add image to database model
...
image: {
type: String,
default: ''
},
...
article.server.routes.js
Add new route for upload use connect-multiparty
...
multiparty = require('connect-multiparty'),
multipartyMiddleware = multiparty(),
...
app.route('/articleupload')
.post(users.requiresLogin, multipartyMiddleware, articles.createWithUpload);
...
article.server.controller.js
Handle new route for upload require fs
...
fs = require('fs'),
...
/**
* Create a article with Upload
*/
exports.createWithUpload = function(req, res) {
var file = req.files.file;
console.log(file.name);
console.log(file.type);
console.log(file.path);
console.log(req.body.article);
var art = JSON.parse(req.body.article);
var article = new Article(art);
article.user = req.user;
fs.readFile(file.path, function (err,original_data) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
}
// save image in db as base64 encoded - this limits the image size
// to there should be size checks here and in client
var base64Image = original_data.toString('base64');
fs.unlink(file.path, function (err) {
if (err)
{
console.log('failed to delete ' + file.path);
}
else{
console.log('successfully deleted ' + file.path);
}
});
article.image = base64Image;
article.save(function(err) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.json(article);
}
});
});
};
...

Thanks Charlie Tupman for this last solution which works very well.
I simply add the dependencies :
...
multiparty = require('multiparty'),
uuid = require('uuid'),
...
before the exports function and changed two lines to improve the behavior:
...
var destPath = './public/uploads/' + fileName;
article.image = '/uploads/' + fileName;
...
which resolve in:
exports.createWithUpload = function(req, res) {
var form = new multiparty.Form();
form.parse(req, function(err, fields, files) {
var file = req.files.file;
console.log(file.name);
console.log(file.type);
console.log(file.path);
console.log(req.body.article);
var art = JSON.parse(req.body.article);
var article = new Article(art);
article.user = req.user;
var tmpPath = file.path;
var extIndex = tmpPath.lastIndexOf('.');
var extension = (extIndex < 0) ? '' : tmpPath.substr(extIndex);
var fileName = uuid.v4() + extension;
var destPath = './public/uploads/' + fileName;
article.image = '/uploads/' + fileName;
var is = fs.createReadStream(tmpPath);
var os = fs.createWriteStream(destPath);
if(is.pipe(os)) {
fs.unlink(tmpPath, function (err) { //To unlink the file from temp path after copy
if (err) {
console.log(err);
}
});
article.save(function(err) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.jsonp(article);
}
});
} else
return res.json('File not uploaded');
});
};

My working solution in MEAN.js
server model:
image:{
type: String,
default: ''
},
server controller:
var item = new Item(JSON.parse(req.body.item));
item.user = req.user;
if(req.files.file)
item.image=req.files.file.name;
else
item.image='default.jpg';
//item.image=
item.save(function(err) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.jsonp(item);
}
});
server route: (requires multer: "npm install multer --save")
var multer = require('multer');
app.use(multer({ dest: './public/uploads/'}));
frontend angular controller:
$scope.image='';
$scope.uploadImage = function(e){
console.log(e.target.files[0]);
$scope.image=e.target.files[0];
};
// Create new Item
$scope.create = function() {
// Create new Item object
var item = new Items ({
name: this.name,
bought: this.bought,
number: this.number,
description: this.description,
warranty: this.warranty,
notes: this.notes
});
ItemsService.saveItem(item,$scope.image);
};
service which send's the request:
.factory('ItemsService', ['$http','$rootScope', function($http, $rootScope)
{
var service={};
service.saveItem = function(item, image)
{
var fd = new FormData();
fd.append('file', image);
fd.append('item', JSON.stringify(item));
$http.post('items/', fd, {
transformRequest: angular.identity,
headers: {'Content-Type': undefined}
})
.success(function(){
console.log('success add new item');
})
.error(function(e){
console.log('error add new item', e);
});
};
return service;
}
]);
html view:
<div class="form-group">
<label class="control-label" for="name">Image</label>
<div class="controls">
<input type="file" data-ng-model="image" id="image" my-file-upload="uploadImage" required>
{{selectedFile.name}}
</div>
</div>

#john prunell Thank you so much for this, I finally figured it out (took me over a week but i am now much more comfortable with the stack) I have forked it to get it to upload the file to a folder:
'exports.createWithUpload = function(req, res) {
var form = new multiparty.Form();
form.parse(req, function(err, fields, files) {
var file = req.files.file;
console.log(file.name);
console.log(file.type);
console.log(file.path);
console.log(req.body.article);
var art = JSON.parse(req.body.article);
var article = new Article(art);
article.user = req.user;
var tmpPath = file.path;
var extIndex = tmpPath.lastIndexOf('.');
var extension = (extIndex < 0) ? '' : tmpPath.substr(extIndex);
var fileName = uuid.v4() + extension;
var destPath = './uploads/' + fileName;
article.image = fileName;
var is = fs.createReadStream(tmpPath);
var os = fs.createWriteStream(destPath);
if(is.pipe(os)) {
fs.unlink(tmpPath, function (err) { //To unlink the file from temp path after copy
if (err) {
console.log(err);
}
});
article.save(function(err) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.jsonp(article);
}
});
}else
return res.json('File not uploaded');
});
};
What I would like to do now is fork your solution to allow uploading of multiple images in the same way, if you have any insight into how to do this that would be amazing, I will let you know how I get on.

Related

Upload file to public folder of vuejs

would like to know if there is a way to upload files at public folder using vuejs coding
I do have a code but it is build to move the file at laravel public folder. do vuejs have that kind of function?
here are what i have so far on my function. hope you guy's can help me
form code
<!-- Form Upload -->
<div class="row">
<div class="col-sm-6 offset-3">
<div class="form-group">
<label for="exampleFormControlFile1">Upload</label>
<input
type="file"
ref="file"
class="form-control-file"
#change="onFileChange"
>
<small class="text-muted">
for slideshow images
<br />
size: 1280 x 630 pixel for good quality
</small>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-primary"
data-dismiss="modal"
#click="uploadFile"
:disabled="disableBtn"
>Upload</button>
</div>
method code
onFileChange(e) {
this.file = this.$refs.file.files[0];
},
uploadFile() {
if (this.file == "") {
this.alertClass = "alert alert-danger";
this.alertMessage = "Please select a file";
this.showAlert = true;
} else {
this.disableBtn = true;
this.$parent.showLoading();
let requestUrl =
this.baseUrl + "/media";
let formData = new FormData();
formData.append("file", this.file,);
formData.append("mediatype", this.Mediatype);
let headers = {
headers: {
"Content-Type": "multipart/form-data"
}
};
this.$http
.post(requestUrl, formData, headers)
.then(response => {
this.alertClass = "alert alert-success";
this.alertMessage = response.data.message;
this.$refs.file.value = ''
this.showAlert = true;
this.$parent.hideLoading();
this.disableBtn = false;
this.$parent.getGallery();
})
.catch(() => {
this.disableBtn = false;
this.$parent.hideLoading();
this.alertClass = "alert alert-danger";
this.alertMessage =
"There is a problem in the request.";
this.showAlert = true;
});
}
}
//Follow this instruction
File
<input
type="file"
ref="image2"
v-on:change="handleFilesUpload()"
/>
In methods properties
handleFilesUpload() {
let uploadedFiles = this.$refs.image2.files;
let fileExtension = uploadedFiles[0].name.replace(/^.*\./, "");
//console.log("fileExtension", fileExtension);
let allowedExtensions = /(\.jpg|\.JPG|\.jpeg|\.JPEG|\.png|\.PNG|\.pdf|\.PDF|\.doc|\.docx)$/i;
if (!allowedExtensions.exec(uploadedFiles[0].name)) {
var message = "You can upload jpg, jpeg, png, pdf and docx file only";
this.$refs.image2.value = "";
this.documentFiles = [];
} else {
//console.log("uploadedFiles[i] = ", uploadedFiles[0]);
//Upload for single file
this.documentFiles = uploadedFiles;
//Upload for multiple file
/*
for (var i = 0; i < uploadedFiles.length; i++) {
this.documentFiles.push(uploadedFiles[i]);
}
*/
}
},
// After submit form
validateBeforeSubmit(e) {
let formData = new FormData();
for (var i = 0; i < this.documentFiles.length; i++) {
let file = this.documentFiles[i];
formData.append("files[" + i + "]", file);
}
axios.post('laravel-api-url', formData)
.then(res => {
console.log({res});
}).catch(err => {
console.error({err});
});
},
--In Laravel controller function--
public function save($request){
$total = #count($_FILES['files']['name']);
if ($total>0)
{
$allFiles = $this->uploadFiles($request);
$data['document_file_name'] = ($allFiles) ? $allFiles[0] :
$result = CreditMaster::create($data);
if ($result) {
return response()->json(array('status' => 1,
'message' => 'Data saved successfully!'));
} else {
return response()->json(array('status' => 0, 'message' => 'Save failed!'));
}
}
}
public function uploadFiles($request)
{
$storeFileName = [];
$total = #count($_FILES['files']['name']);
$diretory = '/save_directory_name/';
$path = public_path() . $diretory;
$fileForMate = time() . '_';
for ($i = 0; $i < $total; $i++) {
$tmpFilePath = $_FILES['files']['tmp_name'][$i];
if ($tmpFilePath != "") {
$fileName = $fileForMate . $_FILES['files']['name'][$i];
$newFilePath = $path . $fileName;
if (move_uploaded_file($tmpFilePath, $newFilePath)) {
array_push($storeFileName, $diretory.$fileName);
}
else
{
return $_FILES["files"]["error"];
}
}
}
return $storeFileName;
}

The value is not sent from dropdown to Express.js

I want to send the values of a form to Express.js to then save them in database. But the only value that is not reaching Express.js is the value in the select element. The form sends two things it sends an excel file and the value inside the select element. I even tried to console.log the req.body to see if the value is sent in the request body but it returns a void {} value. Here is the HTML.
<div class="w3-card-2 w3-display-middle" class="margin:auto">
<header class="w3-container w3-blue"><h4><b>Subir documento de Excel</b></h4></header>
<form id="uploadForm" enctype="multipart/form-data" action="/upload" method="post">
<br>
<input type="file" name="file" />
<br>
<br>
<label>Entidad financiera: </label>
<select name="bancos" class="w3-select w3-border"> /* <---------- */
<option value="noAsignado">No asignado</option>
<option value="bhdLeon">BHD Leon</option>
<option value="asociacionNacional">ASOCIACION NACIONAL DE AHORROS Y PRESTAMOS</option>
<option value="pucmm">PUCMM</option>
<option value="grupoAltus">GRUPO ALTUS</option>
</select>
<br>
<br>
<br>
<br>
<input class="w3-display-bottommiddle" type="submit" value="Subir" name="submit">
</form>
Here the Node code:
app.post('/upload', function(req, res){
console.log(req.body); // <---------------------
var exceltojson;
upload(req, res, function(err){
if (err) {
res.json({error_code:1,err_desc:err});
return;
}
if(!req.file){
res.json({error_code:1, err_desc:"No file passed"});
return;
}
if(req.file.originalname.split('.')[req.file.originalname.split('.').length-1] === 'xlsx'){
exceltojson = xlsxtojson;
} else {
exceltojson = xlstojson;
}
try {
exceltojson({
input: req.file.path,
output: "./outPutJSON/output.json",
lowerCaseHeaders: true
}, function(err, result){
if(err){
return res.json({error_code:1, err_desc:err, data: null});
}
res.json({datos:"Los datos fueron agregados exitosamente"});
fs.readFile("./outPutJSON/output.json", 'utf8', async (err, fileContents) => {
if (err) {
console.error(err);
return;
}
try {
let data = JSON.parse(fileContents);
console.log(data); //--------------HERE THE EXCEL FILE WHEN IS PARSED
io.emit('test event', 'Se han subido ' + data.length + ' casos' );
for(let cantidad = 0; cantidad < data.length; cantidad++){
var documento = data[cantidad];
if(documento.nombre === '' || documento.cedula === '' || documento.direccion === '') {
console.log('No se puede guardar este documento');
} else {
var mostrar = await incrementar();
documento.caso = mostrar;
documento.montoApoderado = data[cantidad]['monto apoderado'];
documento.numeroCliente = data[cantidad]['no.cliente'];
documento.numeroProducto = data[cantidad]['no.producto'];
let today = moment().format('YYYY M D');
documento.fechaCreado = today;
var mod = new model(documento);
await mod.save(documento);
}
}
} catch(err) {
console.error(err);
}
})
});
var fs = require('fs');
try {
fs.unlinkSync(req.file.path)
}catch(e){
}
} catch (e) {
res.json({error_code:1, err_desc:"Corrupted excel file"});
}
});
});
1-) Check that your express.js use the next sentences before calling the router methods:
app.use(bodyParser);
app.use(bodyParser.json()); // support json encoded bodies
app.use(bodyParser.urlencoded({ extended: false })); // support encoded bodies
2-) You don't have an ID attribute attached to your tag and NAME attributes to select options (although the name is the one necessary for the server-side... :/)
You need to re-write your js code:
<select id="banks" name="bancos" class="w3-select w3-border"> /* <---------- */
<option name="noAsignado" value="noAsignado">No asignado</option>
<option name="bhdLeon" value="bhdLeon">BHD Leon</option>
<option name="asociacionNacional" value="asociacionNacional">ASOCIACION NACIONAL DE AHORROS Y PRESTAMOS</option>
<option name="pucmm" value="pucmm">PUCMM</option>
<option name="grupoAltus" value="grupoAltus">GRUPO ALTUS</option>
</select>
3-) Maybe you are checking the body before going through the next() function. Try before executing the handler and there, check the req.body again. :)
Best regards.

Get the data after redirect

I've a page '/order/new' that include 2 dropdown menues for customer & its address & a button that redirect to choose product to add to the order and I get the products in an array and pass the array to the '/order/new'..but when i redirect I don't get the selected customer & its address..I save the products which will be added in order in global array called hh which its get the data from ajax
I want to get the customer & its address after adding the product array
global.hh = new Array();
module.exports = {
getProduct: (req , res ) => {
var products = req.query.selectedProduct;
var total = req.query.sumTotal;
var btekh = hh.push(products)
}
getOrderProduct: (req , res) => {
let query1 = "SELECT * FROM `product`";
getConnection().query(query1 ,(err , result) => {
if(err) {
return res.status(500).send(err);
}
res.render('productOrder.ejs' , {
products: result
});
});
},
addOrderProduct: (req, res) => {
res.redirect('/order/new')
}
postOrder: (req ,res) => {
var products = req.session.products;
getConnection().query('INSERT INTO `order` ( clientId , addressId,orderStatusId) VALUES (?,?,?)', [req.body.selectUser, req.body.selectAddress,req.body.selectedStatus], (err,result) => {
if (err) {
console.log(err);
}
hh.slice(-1)[0].forEach(function (item) {
let insertedOrder = result.insertId;
let query = "INSERT INTO `orderProduct`( `orderIdProduct`, `productId`, `orderProductName`,`orderProductPrice`, `orderProductQuantity`, `orderProductTotal`) VALUES (?,?,?,?,?,?)";
getConnection().query( query , [insertedOrder,item.check,item.name,item.p,item.q,item.total] , (err , result)=>{
if (err) {
console.log(err);
}
res.end();
})
})
})
}
res.redirect('/')
}
app.get('/order/new' , addOrderPage)
app.use('/postOrder' , postOrder);
app.use('/order/product' , getProduct);
my ajax code:
////////////////add users's address///////////////////////////
$('#user').change(function(){
var item = $('#user').val();
$.ajax({
type:'GET',
data: { selectedId: item },
url:'/users/address',
success: function(data){
$('#address').empty();
$('address').append("<option disabled selected> Select Address..</option>");
$.each(data, function (index, addressObj) {
$('#address').append("<option value = '" + addressObj.addressId + "' > " + addressObj.addressName + " </option > ");
});
}
});
})
;
//////////////get the product to order/////////////
$('#save').click(function () {
var orderProduct = new Array();
$("#table input[type=checkbox]:checked").each(function () {
var row = $(this).closest("tr");
var message0 = row.find('#check').val();
var message1 = row.find('.name').text().trim();
var message3 = row.find('.currency').text().trim();
var message4 = row.find(".foo").val();
const result = {check: message0,name: message1,p: message3,q: message4 ,total: message3 * message4}
var hh = orderProduct.push(result);
});
console.log(orderProduct);
var totalPrice = 0;
orderProduct.forEach(function (item , index) {
totalPrice = totalPrice + item.total
})
$.ajax({
type: 'GET',
data: {selectedProduct: orderProduct , sumTotal: totalPrice},
url: '/order/product',
success: function (data) {
console.log(data);
}
})
})
My ejs for'/new/order':
<header>
<h2>New Order : </h2>
</header>
<form action="/postOrder" method="post" id="newForm" >
<label> User Name:</label>
<select name="selectUser" id="user" >
<option disabled selected> Select User..</option>
<% users.forEach((users) => { %>
<option value="<%= users.id %>" > <%=users.name %> </option>
<% }) %>
</select>
<br>
<label>Address :</label>
<select name="selectAddress" id="address">
<option disabled selected> Select Address..</option>
</select>
<br>
<label>Product</label>
add product
<br>
<label>Status</label>
<select name="selectedStatus">
<% status.forEach((status) => { %>
<option value="<%= status.statusId %>"> <%= status.statusText %></option>
<% }) %>
</select>
<br>
<input type="submit" value="Save">
</form>
in these lines,
res.render('productOrder.ejs' , {
products: result
});
are you successfully getting all products from your DB back?
because in your AJAX call,
$.ajax({
type: 'GET',
data: {selectedProduct: orderProduct , sumTotal: totalPrice},
url: '/order/product',
success: function (data) {
console.log(data);
}
})
you're sending data, but not using it in your 'query1' here,
let query1 = "SELECT * FROM `product`";
I'm trying to get the flow of your program.
So the user goes to your webpage, they select a product, your webpage pings your REST server with the product information, your REST server pings your database looking for that product, then that data is rendered to this template here?
res.render('productOrder.ejs' , {
products: result
});
EDIT: You have to handle the user interaction with event listeners and the user input with some storage method.
Your_client-side_JS_script.js
function handle_submit(e) {
e.preventDefault();
if (e.target.id === 'addProduct') {
// When they click add product, you need to either pass their user details to the route and send them with the $.ajax GET request
//... and keep track of the data like that
// OR
// track that data in a cookie,
read up on cookies https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie
} else if (e.target.id === 'newForm') {
console.log("Submit Form");
}
};
$('#addProduct').on('click', handle_submit);
$('#newForm').on('submit', handle_submit);
I recommend the cookie method. You set the cookie once, use it when you need it, then delete it when you're done.
With the first method, you have to send the data in your fetch (which doesn't need that data to get the products), then you have to manage that user_data throughout your back-end making sure it doesn't get lost. It's too much work. Use the cookie method instead.

How do i test my custom angular schema form field

I've just started developing with Angular schema form and I'm struggling to write any tests for my custom field directive.
I've tried compiling the schema form html tag which runs through my directives config testing it's display conditions against the data in the schema. However it never seems to run my controller and I can't get a reference to the directives HTML elements. Can someone give me some guidance on how to get a reference to the directive? Below is what I have so far:
angular.module('schemaForm').config(['schemaFormProvider',
'schemaFormDecoratorsProvider', 'sfPathProvider',
function(schemaFormProvider, schemaFormDecoratorsProvider, sfPathProvider) {
var date = function (name, schema, options) {
if (schema.type === 'string' && schema.format == 'date') {
var f = schemaFormProvider.stdFormObj(name, schema, options);
f.key = options.path;
f.type = 'date';
options.lookup[sfPathProvider.stringify(options.path)] = f;
return f;
}
};
schemaFormProvider.defaults.string.unshift(date);
schemaFormDecoratorsProvider.addMapping('bootstrapDecorator', 'date',
'app/modules/json_schema_form/schema_form_date_picker/schema_form_date_picker.html');
}]);
var dateControllerFunction = function($scope) {
$scope.isCalendarOpen = false;
$scope.showCalendar = function () {
$scope.isCalendarOpen = true;
};
$scope.calendarSave = function (date) {
var leaf_model = $scope.ngModel[$scope.ngModel.length - 1];
var formattedDate = $scope.filter('date')(date, 'yyyy-MM-dd');
leaf_model.$setViewValue(formattedDate);
$scope.isCalendarOpen = false;
};
};
angular.module('schemaForm').directive('schemaFormDatePickerDirective', ['$filter', function($filter) {
return {
require: ['ngModel'],
restrict: 'A',
scope: false,
controller : ['$scope', dateControllerFunction],
link: function(scope, iElement, iAttrs, ngModelCtrl) {
scope.ngModel = ngModelCtrl;
scope.filter = $filter
}
};
}]);
<div ng-class="{'has-error': hasError()}">
<div ng-model="$$value$$" schema-form-date-picker-directive>
<md-input-container>
<!-- showTitle function is implemented by ASF -->
<label ng-show="showTitle()">{{form.title}}</label>
<input name="dateTimePicker" ng-model="$$value$$" ng-focus="showCalendar()" ng-disabled="isCalendarOpen">
</md-input-container>
<time-date-picker ng-model="catalogue.effectiveFrom" ng-if="isCalendarOpen" on-save="calendarSave($value)" display-mode="date"></time-date-picker>
</div>
<!-- hasError() defined by ASF -->
<span class="help-block" sf-message="form.description"></span>
</div>
And the spec:
'use strict'
describe('SchemaFormDatePicker', function() {
var $compile = undefined;
var $rootScope = undefined;
var $scope = undefined
var scope = undefined
var $httpBackend = undefined;
var elem = undefined;
var html = '<form sf-schema="schema" sf-form="form" sf-model="schemaModel"></form>';
var $templateCache = undefined;
var directive = undefined;
beforeEach(function(){
module('app');
});
beforeEach(inject(function(_$compile_, _$rootScope_, _$templateCache_, _$httpBackend_) {
$compile = _$compile_
$rootScope = _$rootScope_
$httpBackend = _$httpBackend_
$templateCache = _$templateCache_
}));
beforeEach(function(){
//Absorb call for locale
$httpBackend.expectGET('assets/locale/en_gb.json').respond(200, {});
$templateCache.put('app/modules/json_schema_form/schema_form_date_picker/schema_form_date_picker.html', '');
$scope = $rootScope.$new()
$scope.schema = {
type: 'object',
properties: {
party: {
title: 'party',
type: 'string',
format: 'date'
}}};
$scope.form = [{key: 'party'}];
$scope.schemaModel = {};
});
describe("showCalendar", function () {
beforeEach(function(){
elem = $compile(html)($scope);
$scope.$digest();
$httpBackend.flush();
scope = elem.isolateScope();
});
it('should set isCalendarOpen to true', function(){
var result = elem.find('time-date-picker');
console.log("RESULT: "+result);
));
});
});
});
If you look at the below example taken from the project itself you can see that when it uses $compile it uses angular.element() first when setting tmpl.
Also, the supplied test module name is 'app' while the code sample has the module name 'schemaForm'. The examples in the 1.0.0 version of Angular Schema Form repo all use sinon and chai, I'm not sure what changes you would need to make if you do not use those.
Note: runSync(scope, tmpl); is a new addition for 1.0.0 given it is now run through async functions to process $ref includes.
/* eslint-disable quotes, no-var */
/* disabling quotes makes it easier to copy tests into the example app */
chai.should();
var runSync = function(scope, tmpl) {
var directiveScope = tmpl.isolateScope();
sinon.stub(directiveScope, 'resolveReferences', function(schema, form) {
directiveScope.render(schema, form);
});
scope.$apply();
};
describe('sf-array.directive.js', function() {
var exampleSchema;
var tmpl;
beforeEach(module('schemaForm'));
beforeEach(
module(function($sceProvider) {
$sceProvider.enabled(false);
exampleSchema = {
"type": "object",
"properties": {
"names": {
"type": "array",
"description": "foobar",
"items": {
"type": "object",
"properties": {
"name": {
"title": "Name",
"type": "string",
"default": 6,
},
},
},
},
},
};
})
);
it('should not throw needless errors on validate [ノಠ益ಠ]ノ彡┻━┻', function(done) {
tmpl = angular.element(
'<form name="testform" sf-schema="schema" sf-form="form" sf-model="model" json="{{model | json}}"></form>'
);
inject(function($compile, $rootScope) {
var scope = $rootScope.$new();
scope.model = {};
scope.schema = exampleSchema;
scope.form = [ "*" ];
$compile(tmpl)(scope);
runSync(scope, tmpl);
tmpl.find('div.help-block').text().should.equal('foobar');
var add = tmpl.find('button').eq(1);
add.click();
$rootScope.$apply();
setTimeout(function() {
var errors = tmpl.find('.help-block');
errors.text().should.equal('foobar');
done();
}, 0);
});
});
});

Compile mustache partials using external json data

I'm using mustache partials for our internal documentation and for testing - as described in this other SO question how to have grunt task render mustache partials to static HTML, and I'd now like to make the partials data driven using external json files. The json files would be named the same as the partials and will contain mock data that would compile with the partials.
Any suggestions on how to get this working?
I've updated the hogan.js file to read in data.json files that have the same name as the partials:
module.exports = function(grunt) {
// Grunt utilities.
var task = grunt.task,
file = grunt.file,
utils = grunt.util,
log = grunt.log,
verbose = grunt.verbose,
fail = grunt.fail,
option = grunt.option,
config = grunt.config,
template = grunt.template,
_ = utils._
// external dependencies
var fs = require('fs'),
hogan = require('hogan');
// ==========================================================================
// TASKS
// ==========================================================================
grunt.registerMultiTask('hogan', 'Compile mustache files to HTML with hogan.js', function() {
var data = this.data,
src = grunt.file.expandFiles(this.file.src),
dest = grunt.template.process(data.dest),
// Options are set in gruntfile
defaults = {
production: false,
docs: false,
title: 'Sellside',
setAccount: 'NA',
setSiteId: 'NA',
layout: 'docs/templates/layout.mustache',
paths: {},
partials: {},
partialsData: {},
},
options = _.extend(defaults, this.data.options || {})
!src && grunt.warn('Missing src property.')
if(!src) return false
!dest && grunt.warn('Missing dest property')
if(!dest) return false
var done = this.async()
var srcFiles = file.expandFiles(src)
if(options.paths.partials) {
var partials = grunt.file.expandFiles(options.paths.partials);
log.writeln('Compiling Partials...');
partials.forEach(function(filepath) {
var filename = _.first(filepath.match(/[^\\\/:*?"<>|\r\n]+$/i)).replace(/\.mustache$/, '');
log.writeln(filename.magenta);
var dataFilepath = filepath.replace(/\.mustache$/, '.json');
var partial = fs.readFileSync(filepath, 'utf8');
options.partials[filename] = hogan.compile(partial);
// if a data file exists, read in the data
if(fs.existsSync(dataFilepath)) {
options.partialsData[filename] = grunt.file.readJSON(dataFilepath);
}
});
log.writeln();
}
try {
options.layout = fs.readFileSync(options.layout, 'utf8')
options.layout = hogan.compile(options.layout, {
sectionTags: [{
o: '_i',
c: 'i'
}]
})
} catch(err) {
grunt.warn(err) && done(false)
return
}
srcFiles.forEach(function(filepath) {
var filename = _.first(filepath.match(/[^\\\/:*?"<>|\r\n]+$/i)).replace(/\.mustache$/, '')
grunt.helper('hogan', filepath, filename, options, function(err, result) {
err && grunt.warn(err) && done(false)
if(err) return
file.write(dest.replace('FILE', filename), result)
})
})
done()
})
// ==========================================================================
// HELPERS
// ==========================================================================
grunt.registerHelper('hogan', function(src, filename, options, callback) {
log.writeln('Compiling ' + filename.magenta);
var page = fs.readFileSync(src, 'utf8'),
html = null,
layout = options.layout,
context = {};
context[filename] = 'active';
context._i = true;
context.production = options.production;
context.docs = options.docs;
context.setAccount = options.setAccount;
context.setSiteId = options.setSiteId;
var title = _.template("<%= page == 'Index' ? site : page + ' · ' + site %>")
context.title = title({
page: _(filename).humanize().replace('css', 'CSS'),
site: options.title
})
try {
page = hogan.compile(page, {
sectionTags: [{
o: '_i',
c: 'i'
}]
})
context = _.extend(context, options.partialsData);
options.partials.body = page;
page = layout.render(context, options.partials)
callback(null, page)
} catch(err) {
callback(err)
return
}
})
};
A simple example of an alert.json file:
{
"modifier": "alert-warning",
"alertType": "Warning!",
"message": "Best check yo self, you're not looking too good."
}
alert.mustache
alerts
<div class="row-fluid">
<div class="alert {{alert.modifier}}">
<button type="button" class="close" data-dismiss="alert">×</button>
<strong>{{alert.alertType}}</strong>
{{alert.message}}
</div>
</div>
</div><!-- /container -->
I like this next example better because you can create different data to use
in the same partials:
alert.json
{
"default": {
"modifier": "alert-warning",
"alertType": "Warning!",
"message": "Best check yo self, you're not looking too good.!"
},
"error": {
"modifier": "alert-error",
"alertType": "Error!!!",
"message": "You did something horribily wrong!!!"
},
"success": {
"modifier": "alert-success",
"alertyType": "Success!!!",
"message": "The record has been successfully saved..."
}
}
alert.mustache
<!-- partial -->
<div class="container">
<h1>alerts</h1>
<div class="row-fluid">
<div class="alert {{modifier}}">
<button type="button" class="close" data-dismiss="alert">×</button>
<strong>{{alertType}}</strong>
{{message}}
</div>
</div>
</div><!-- /container -->
partials.mustache --- this is where the alert partial is referenced
{{#alert.default}}
{{> alert }}
{{/alert.default}}
{{#alert.error}}
{{> alert }}
{{/alert.error}}
{{#alert.success}}
{{> alert }}
{{/alert.success}}
Let me know if this helps.
Thanks,
Brian