DevExpress Web Dashboard in PiranhaCMS - devexpress

I am currently working on a .NET Core application based on a CMS Framework named PiranhaCMS. This framework allows the definition of configurable "Blocks", basically widgets, that can be added by the users on their pages. The configuration page of the blocks is realized as a Vue.js component, and code is then compiled via gulp in a standard JS format (from the .vue file to the Vue.component(...) syntax) for the Piranha framework to read and render. The author of Piranha confirmed that this is the only way to define new blocks.
In one of our custom blocks, we are trying to implement a DevExpress Web Dashboard. I have tried following the steps outlined at https://docs.devexpress.com/Dashboard/401150/web-dashboard/dashboard-component-for-vue, but to no avail, since the compiler throws an Exception stating that the top-level declaration should be an export default { ... }, and not an import statement.
I came up with a workaround in which I dynamically load the required scripts and styles on the created() method of the component, and then define the dashboard in the same way I would in a classic javascript case (https://docs.devexpress.com/Dashboard/119158/web-dashboard/dashboard-control-for-javascript-applications-jquery-knockout-etc/add-web-dashboard-to-a-javascript-application);;) however, I am sure there is a more elegant solution to this problem.
Below is the code relevant to the problem. Here is the custom block itools-dashboard.vue:
<template>
<div class="form-group block-body">
<div :id="'dashboard-designer-' + uid" class="dashboard-designer">
<div :id="'dashboard_' + uid" style="height: 100%;">
</div>
</div>
<div class="row">
<div class="col-sm-6" style="padding:10px; margin-top: 0px;vertical-align: top;">
<fieldset>
<legend>Dashboard</legend>
<div class="form-group">
<label>Dashboard name</label>
<select class="form-control small" :id="'dashboard-names-' + uid" v-model="model.dashboardName.value">
<option v-for="dash in dashboardNames">{{ dash }}</option>
</select>
</div>
<div class="form-group">
<label>Update time</label>
<input class="form-control small" type="number" v-model="model.updateTime.value">
</div>
<div class="form-group">
<label>Width</label>
<input class="form-control small" type="text" v-model="model.width.value">
</div>
<div class="form-group">
<label>Height</label>
<input class="form-control small" type="text" v-model="model.height.value">
</div>
</fieldset>
</div>
<div class="col-sm-6" style="padding:10px; margin-top: 0px; background-color: #fcfcfc; border:1px dotted lightgray; vertical-align: top;">
<itools-base :model="model"></itools-base>
</div>
</div>
</div>
</template>
<script>
export default {
props: ["uid", "toolbar", "model"],
data: function () {
return {
dashboardNames: [],
dahsboardConfig: null,
updateModes: ["period", "realtime"],
basePath: "../../../../assets/",
// define all the css and js files paths
cssResources: [
"devextreme/dist/css/light.css",
"#devexpress/analytics-core/dist/css/dx-analytics.common.css",
"#devexpress/analytics-core/dist/css/dx-analytics.light.css",
"#devexpress/analytics-core/dist/css/dx-querybuilder.css",
"devexpress-dashboard/dist/css/dx-dashboard.light.min.css"
],
jsResources: [
"js/jquery/jquery-3.3.1.min.js",
"jquery-ui-dist/jquery-ui.js",
"knockout/build/output/knockout-latest.js",
"ace-builds/src-min-noconflict/ace.js",
"ace-builds/src-min-noconflict/ext-language_tools.js",
"ace-builds/src-min-noconflict/theme-dreamweaver.js",
"ace-builds/src-min-noconflict/theme-ambiance.js",
"devextreme/dist/js/dx.all.js",
"devextreme/dist/js/dx.aspnet.mvc.js",
"devextreme-aspnet-data/js/dx.aspnet.data.js",
"#devexpress/analytics-core/dist/js/dx-analytics-core.min.js",
"#devexpress/analytics-core/dist/js/dx-querybuilder.min.js",
"devexpress-dashboard/dist/js/dx-dashboard.min.js"
]
}
},
created: function () {
// dynamically add the required css
this.cssResources.forEach(x => {
let link = document.createElement("link");
link.setAttribute("href", this.basePath + x);
link.setAttribute("rel", "stylesheet");
document.head.appendChild(link);
});
// dynamically add the js files.
// It needs to be a synchronous ajax call so that the exports are visible in the code
// (eg the new DevExpress call)
this.jsResources.forEach(x => {
$.ajax({
async: false,
url: this.basePath + x,
dataType: "script"
})
});
this.model.width.value = this.model.width.value || "100%";
this.model.height.value = this.model.height.value || "300";
this.model.updateTime.value = this.model.updateTime.value || 5000;
},
mounted: function () {
var h = document.getElementById("dashboard-designer-" + this.uid).clientHeight;
DevExpress.Dashboard.ResourceManager.embedBundledResources();
var dashboardControl = new DevExpress.Dashboard.DashboardControl(document.getElementById("dashboard_" + this.uid), {
endpoint: "/api/dashboard",
workingMode: "Designer",
width: "100%",
height: "100%",
initialDashboardId: this.model.dashboardName.value,
});
dashboardControl.render();
},
beforeCreate: function () {
fetch("/api/Dashboards/GetDashboardNames")
.then(response => response.json())
.then(data => {
this.dashboardNames = data;
});
},
}
</script>
which is then compiled via gulp task to
Vue.component("itools-dashboard", {
props: ["uid", "toolbar", "model"],
data: function () {
return {
dashboardNames: [],
dahsboardConfig: null,
updateModes: ["period", "realtime"],
basePath: "../../../../assets/",
cssResources: ["devextreme/dist/css/light.css", "#devexpress/analytics-core/dist/css/dx-analytics.common.css", "#devexpress/analytics-core/dist/css/dx-analytics.light.css", "#devexpress/analytics-core/dist/css/dx-querybuilder.css", "devexpress-dashboard/dist/css/dx-dashboard.light.min.css"],
jsResources: ["js/jquery/jquery-3.3.1.min.js", "jquery-ui-dist/jquery-ui.js", "knockout/build/output/knockout-latest.js", "ace-builds/src-min-noconflict/ace.js", "ace-builds/src-min-noconflict/ext-language_tools.js", "ace-builds/src-min-noconflict/theme-dreamweaver.js", "ace-builds/src-min-noconflict/theme-ambiance.js", "devextreme/dist/js/dx.all.js", "devextreme/dist/js/dx.aspnet.mvc.js", "devextreme-aspnet-data/js/dx.aspnet.data.js", "#devexpress/analytics-core/dist/js/dx-analytics-core.min.js", "#devexpress/analytics-core/dist/js/dx-querybuilder.min.js", "devexpress-dashboard/dist/js/dx-dashboard.min.js"]
};
},
created: function () {
this.cssResources.forEach(x => {
let link = document.createElement("link");
link.setAttribute("href", this.basePath + x);
link.setAttribute("rel", "stylesheet");
document.head.appendChild(link);
});
this.jsResources.forEach(x => {
$.ajax({
async: false,
url: this.basePath + x,
dataType: "script"
});
});
this.model.width.value = this.model.width.value || "100%";
this.model.height.value = this.model.height.value || "300";
this.model.updateTime.value = this.model.updateTime.value || 5000;
},
mounted: function () {
DevExpress.Dashboard.ResourceManager.embedBundledResources();
var dashboardControl = new DevExpress.Dashboard.DashboardControl(document.getElementById("dashboard_" + this.uid), {
endpoint: "/api/dashboard",
workingMode: "Designer",
width: "100%",
height: "100%",
initialDashboardId: this.model.dashboardName.value
});
dashboardControl.render();
},
beforeCreate: function () {
fetch("/api/Dashboards/GetDashboardNames").then(response => response.json()).then(data => {
this.dashboardNames = data;
});
},
template: "\n<div class=\"form-group block-body\">\n <div :id=\"'dashboard-designer-' + uid\" class=\"dashboard-designer\">\n <div :id=\"'dashboard_' + uid\" style=\"height: 100%;\">\n </div>\n </div>\n <div class=\"row\">\n <div class=\"col-sm-6\" style=\"padding:10px; margin-top: 0px;vertical-align: top;\">\n <fieldset>\n <legend>Dashboard</legend>\n <div class=\"form-group\">\n <label>Dashboard name</label>\n <select class=\"form-control small\" :id=\"'dashboard-names-' + uid\" v-model=\"model.dashboardName.value\">\n <option v-for=\"dash in dashboardNames\">{{ dash }}</option>\n </select>\n </div>\n <div class=\"form-group\">\n <label>Update time</label>\n <input class=\"form-control small\" type=\"number\" v-model=\"model.updateTime.value\">\n </div>\n <div class=\"form-group\">\n <label>Width</label>\n <input class=\"form-control small\" type=\"text\" v-model=\"model.width.value\">\n </div>\n <div class=\"form-group\">\n <label>Height</label>\n <input class=\"form-control small\" type=\"text\" v-model=\"model.height.value\">\n </div>\n </fieldset>\n </div>\n <div class=\"col-sm-6\" style=\"padding:10px; margin-top: 0px; background-color: #fcfcfc; border:1px dotted lightgray; vertical-align: top;\">\n <itools-base :model=\"model\"></itools-base>\n </div>\n </div>\n</div>\n"
});
The gulp task responsible for the compilation, defined by Piranha, is:
var gulp = require('gulp'),
sass = require('gulp-sass'),
cssmin = require("gulp-cssmin"),
uglifyes = require('uglify-es'),
composer = require('gulp-uglify/composer'),
uglify = composer(uglifyes, console),
rename = require("gulp-rename"),
concat = require("gulp-concat");
var path = require('path'),
vueCompiler = require('vue-template-compiler'),
babel = require("#babel/core"),
babelTemplate = require("#babel/template").default,
codeFrameColumns = require('#babel/code-frame').codeFrameColumns,
babelTypes = require("#babel/types"),
through2 = require('through2');
function vueCompile() {
return through2.obj(function (file, _, callback) {
var relativeFile = path.relative(file.cwd, file.path);
console.log(relativeFile);
var ext = path.extname(file.path);
if (ext === '.vue') {
var getComponent;
getComponent = function (ast, sourceCode) {
const ta = ast.program.body[0]
if (!babelTypes.isExportDefaultDeclaration(ta)) {
var msg = 'Top level declaration in file ' + relativeFile + ' must be "export default {" \n' + codeFrameColumns(sourceCode, { start: ta.loc.start }, { highlightCode: true });
throw msg;
}
return ta.declaration;
}
var compile;
compile = function (componentName, content) {
var component = vueCompiler.parseComponent(content, []);
if (component.styles.length > 0) {
component.styles.forEach(s => {
const linesToStyle = content.substr(0, s.start).split(/\r?\n/).length;
var msg = 'WARNING: <style> tag in ' + relativeFile + ' is ignored\n' + codeFrameColumns(content, { start: { line: linesToStyle } }, { highlightCode: true });
console.warn(msg);
});
}
var ast = babel.parse(component.script.content, {
parserOpts: {
sourceFilename: file.path
}
});
var vueComponent = getComponent(ast, component.script.content);
vueComponent.properties.push(babelTypes.objectProperty(babelTypes.identifier('template'), babelTypes.stringLiteral(component.template.content)))
var wrapInComponent = babelTemplate("Vue.component(NAME, COMPONENT);");
var componentAst = wrapInComponent({
NAME: babelTypes.stringLiteral(componentName),
COMPONENT: vueComponent
})
ast.program.body = [componentAst]
babel.transformFromAst(ast, null, null, function (err, result) {
if (err) {
callback(err, null)
}
else {
file.contents = Buffer.from(result.code);
callback(null, file)
}
});
}
var componentName = path.basename(file.path, ext);
if (file.isBuffer()) {
compile(componentName, file.contents.toString());
}
else if (file.isStream()) {
var chunks = [];
file.contents.on('data', function (chunk) {
chunks.push(chunk);
});
file.contents.on('end', function () {
compile(componentName, Buffer.concat(chunks).toString());
});
}
} else {
callback(null, file);
}
});
}
var js = {
name: "itools-blocks.js",
path: "wwwroot/assets/js/blocks/*.vue"
}
//
// Compile & minimize js files
//
gulp.task("min:js", function (done) {
gulp.src(js.path, { base: "." })
.pipe(vueCompile())
.pipe(concat("wwwroot/assets/js/blocks/" + js.name))
.pipe(gulp.dest("."))
.pipe(uglify().on('error', function (e) {
console.log(e);
}))
.pipe(rename({
suffix: ".min"
}))
.pipe(gulp.dest("."));
done();
});
any kind of help is well appreciated

The gulpfile with the method “vueCompile” that you’re referring to was specifically written to suit the needs of the internal components we provide in the framework, it’s by no means a silver bullet for all Vue component compilation. However I understand your problem, before writing this code we desperately searched for existing npm-packages that would give us the functionality we needed, but this wasn’t that easy to find as we only use a subset of the features available in Vue.js
We’d be more than happy to get feedback or more information on how this could be done, so we’ll be watching this thread 👍🏼

Related

reCAPTCHA not loading properly in XHTML document

I am writing an SHTML CSS & JS document into an XML document which uses an XHTML parser.
When I try to load the captcha into it, it briefly shows up as a white box then disappears, never to be seen again inside the renderer.
I understand that this might be caused by the scripts being loaded in the wrong order but I don't have the luxury of the async defer keywords that are recommended by google simply because the parser won't accept them.
<script src="https://www.google.com/recaptcha/api.js" type="text/javascript"></script>
<script type="text/javascript"><![CDATA[
class Token {
constructor(tokenInstance) {
this.props = tokenInstance;
this.getStatus();
this.setOnConfirm();
this.setAdditionalProps();
}
setAdditionalProps() {
this.props.claimed = false;
this.props.claimTokensMessage = "Claim free DAI, DeFi & ETH tokens";
this.props.claimedMessage = "You have already claimed DAI, DeFi & ETH tokens, stay posted for new freebies!";
this.props.serverError = "Server Error, please try again later";
this.props.success = "ETH, DeFi & DAI tokens will be sent to your address shortly!";
this.props.fillCaptchaMessage = "Please click the captcha below to receive the tokens";
}
getBaseURL() {
return "http://192.168.26.189:8080/api/"
}
getStatus() {
let suffix = "claimed?userAddress=" + this.props.ownerAddress;
fetch(this.getBaseURL() + suffix).then((res) => {
if(parseInt(res.status) === 200) {
document.getElementById("message").innerHTML = this.props.claimTokensMessage;
} else if(parseInt(res.status) === 412) {
this.props.claimed = true;
document.getElementById("message").innerHTML = this.props.claimedMessage;
} else {
document.getElementById("message").innerHTML = this.props.serverError;
}
}).catch((err) => {
document.getElementById("message").innerHTML = err;
window.onConfirm = function() { window.close() };
});
}
setOnConfirm() {
window.onConfirm = () => {
if(this.props.claimed) {
window.close();
} else {
web3.personal.sign({ data: "To receive free tokens, you must reveal your public address to a smart contract. Is that ok?" }, (err, val) => {
if(err) { throw err; }
//user completes the request by filling the captcha
document.getElementById("message").innerHTML = this.props.success;
document.getElementById("captcha").submit();
window.onConfirm = function () { window.close(); };
});
}
};
}
render() {
let suffix = "discover?userAddress=" + this.props.ownerAddress;
return`
<div class="ui container">
<div class="ui segment">
<img src="data:image/png;base64,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">
<h3 id="message"></h3>
<form action=${this.getBaseURL() + suffix} id="captcha" method="post">
<button class="g-recaptcha" data-sitekey="6LeK070UAAAAAHuzSEGgoqbuLGuJq-GRDd-LA4kH" data-callback="onSubmit" hidden>fill captcha</button>
</form>
</div>
</div>
`;
}
}
web3.tokens.dataChanged = (oldTokens, updatedTokens) => {
const currentTokenInstance = web3.tokens.data.currentInstance;
document.getElementById('root').innerHTML = new Token(currentTokenInstance).render();
};
]]></script>
<div id="root"></div>

How do I add input controls dynamically using Meteor?

I have a form in weather that would have had the condition User add as many lines he needs. He clicks a button and an input is added below the other.
I can do this using jQuery, but I would prefer to use the resources of Meteor. Is it possible to do?
Yes it is, here is an example from one of my apps using the underscore package
In the main template:
<template name="ask">
{{#each answerArray}}
{{>answer}}
{{/each}}
<button id="addItem">Add item</button>
</template>
<template name="answer">
<div class="input-group pt10">
<input class="form-control answer" maxlength="30" placeholder="Answer (max 30 chars)" name="answer" />
<span class="input-group-btn">
<button class="btn btn-danger delButton" id="{{id}}" data-id="{{id}}" type="button">Delete</button>
</span>
</div>
</template>
In the js file:
Template.ask.created = function () {
Session.set('action', 'ask');
answerArray = [ //adding at least two items but it could also be empty
{
id: Random.id(), //using this to give an unique id to the control
value: ''
},
{
id: Random.id(),
value: ''
}
];
Session.set('answerArr', answerArray);
}
And the click event:
Template.ask.events = {
'click #addItem': function () {
var answerArray = Session.get('answerArr');
answerArray.push({
id: Random.id() //just a placeholder, you could put any here
});
Session.set('answerArr', answerArray);
}
}
And finally the helper:
Template.ask.helpers({
answerArray: function () {
var answerArray = Session.get("answerArr")
while (answerArray.length < 2) { //i chose to have it between 2 and 6, you can remove these
answerArray.push({
id: Random.id()
})
}
while (answerArray.length > 6) { // maximum
answerArray.pop();
}
Session.set('answerArr', answerArray);
return answerArray;
}
}
This will reactively increase the number of inputs. After that, if you want to process the inputs you could do the following, on a submit form event or button click:
'click #saveQ': function (e) {
e.preventDefault();
var arr = [];
_.each($('.answer'), function (item) {
if ($(item).val() != '')
arr.push({
answer: $(item).val(), //this you customize for your own purposes
number: 0
})
});
And also if you want to delete an input from the page you can use:
Template.answer.events = {
'click .delButton': function (e) {
var thisId = $(e.target).attr("id");
var answerArray = Session.get('answerArr');
var filteredArray = _.filter(answerArray, function (item) {
return item.id != thisId;
});
Session.set('answerArr', filteredArray);
}
}

Why is there no space between form-inline 's elements?

Demo: http://jsfiddle.net/sunnycpp/MPACc/4/
Same code copy-pasted here:
HTML
<div class="container">
<debug-bar ng-controller="parentController">
<debug-bar ng-controller="childController">
<debug-bar ng-controller="oneMoreChildController"></debug-bar>
</debug-bar>
<debug-bar ng-controller="oneMoreChildController"></debug-bar>
</debug-bar>
</div>
Javascript
var angModule = angular.module('components', []);
angModule.directive('debugBar', function () {
return {
restrict: 'E',
template:
'<div>'+
'<form class="form-inline">' +
'<input type="text" class="input-small" ng-model="myText"/>' +
'<button class="btn btn-primary">Broadcast</button>' +
'<button class="btn btn-primary">Emit</button>' +
'</form>' +
'<div ng-transclude></div>'+
'</div>',
transclude: true,
replace: true
};
});
function createController(myText) {
return function ($scope) {
$scope.myText = myText;
$scope.$on("event", function (senderText) {
console.log("Event received in:" + $scope.myText + " from Sender:" + senderText);
});
$scope.$broadCastEvent = function () {
$scope.$broadcast("event", $scope.myText);
console.log("Sent event from:" + $scope.myText);
};
};
}
angModule.controller("parentController", createController("In parent"));
angModule.controller("childController", createController("in FirstChild"));
angModule.controller("oneMoreChildController", createController("in oneMoreChildController"));
angModule.controller("oneMoreChildController", createController("in secondLevelChild"));
Because you didn't define any. :p jsfiddle and bootstrap reset the margins and you don't define any...
Defining some margins fixes the problems immediately
input,button{
margin:0 5px;
}
Example

Publish not updating properly?

I'm having trouble getting Meteor.publish to update in response to a changing form field. The first call to publish seems to stick, so the query operates in that subset until the page is reloaded.
I followed the approach in this post, but am having no luck whatsoever.
Any help greatly appreciated.
In lib:
SearchResults = new Meteor.Collection("Animals");
function getSearchResults(query) {
re = new RegExp(query, "i");
return SearchResults.find({$and: [ {is_active: true}, {id_species: {$regex: re}} ] }, {limit: 10});
}
In client:
Session.set('query', null);
Template.searchQuery.events({
'keyup .query' : function (event, template) {
query = template.find('.query').value
Session.set("query", query);
}
});
Meteor.autosubscribe(function() {
if (Session.get("query")) {
Meteor.subscribe("search_results", Session.get("query"));
}
});
Template.searchResults.results = function () {
return getSearchResults(Session.get("query"));
}
On server:
Meteor.publish("search_results", getSearchResults);
Template:
Search for Animals
<body>
{{> searchQuery}}
{{> searchResults}}
</body>
<template name="searchQuery">
<form>
<label>Search</label>
<input type="text" class="query" />
</form>
</template>
<template name="searchResults">
{{#each results}}
<div>
{{_id}}
</div>
{{/each}}
</template>
Update [WRONG]
Apparently, the issue is that the collection I was working with was (correctly) generated outside of Meteor, but Meteor doesn't properly support Mongo's ObjectIds. Context here and related Stackoverflow question.
Conversion code shown there, courtesy antoviaque:
db.nodes.find({}).forEach(function(el){
db.nodes.remove({_id:el._id});
el._id = el._id.toString();
db.nodes.insert(el);
});
Update [RIGHT]
So as it turns out, it was an issue with RegExp / $regex. This thread explains. Instead of:
function getSearchResults(query) {
re = new RegExp(query, "i");
return SearchResults.find({$and: [ {is_active: true}, {id_species: {$regex: re}} ] }, {limit: 10});
}
At the moment, one needs to do this instead:
function getSearchResults(query) {
// Assumes query is regex without delimiters e.g., 'rot'
// will match 2nd & 4th rows in Tim's sample data below
return SearchResults.find({$and: [ {is_active: true}, {id_species: {$regex: query, $options: 'i'}} ] }, {limit: 10});
}
That was fun.
PS -- The ddp-pre1 branch has some ObjectId functionality (SearchResults = new Meteor.Collection("Animals", {idGeneration: "MONGO"});)
Here's my working example:
UPDATE the original javascript given was correct. The problem, as noted in the comments, turned out to be that meteor doesn't yet support ObjectIds.
HTML:
<body>
{{> searchQuery }}
{{> searchResults}}
</body>
<template name="searchQuery">
<form>
<label>Search</label>
<input type="text" class="query" />
</form>
</template>
<template name="searchResults">
{{#each results}}
<div>
{{id_species}} | {{name}} - {{_id}}
</div>
{{/each}}
</template>
Javascript:
Animals = new Meteor.Collection("Animals");
function _get(query) {
re = new RegExp(query, "i");
console.log("rerunning query: " + query);
return Animals.find({$and: [ {is_active: true}, {id_species: {$regex: re}} ] }, {limit: 10});
};
if (Meteor.isClient) {
Session.set("query", "");
Meteor.autosubscribe(function() {
Meteor.subscribe("animals", Session.get("query"));
});
Template.searchQuery.events({
'keyup .query' : function (event, template) {
query = template.find('.query').value
Session.set("query", query);
}
});
Template.searchResults.results = function () {
return _get(Session.get("query"));
}
}
if (Meteor.isServer) {
Meteor.startup(function() {
if (Animals.find().count() === 0) {
Animals.insert({name: "panda", is_active: true, id_species: 'bear'});
Animals.insert({name: "panda1", is_active: true, id_species: 'bearOther'});
Animals.insert({name: "panda2", is_active: true, id_species: 'bear'});
Animals.insert({name: "panda3", is_active: true, id_species: 'bearOther'});
}
});
Meteor.publish("animals", _get);
}

MVC Twitter Bootstrap unobtrusive error handling

I've been trying to get MVC Jquery unobtrusive error handling working with twitter bootstrap for some time now. Its got to the point were i'm either going to edit jquery.validate or do some hack and slash on document.ready.
In order to get unobtrusive error handling to work with Bootstrap and MVC I need to make it so the 'error' class it appended to the 'control-group' class. As well as that, the 'error' class is appended to the input.
I was wondering if anyone in the community has already found a solution.
For example
Typical bootstrap markup would be like so...
<div class="control-group">
<label for="Username">Username</label>
<div class="controls">
<input data-val="true" data-val-required="Username is required" id="Username" name="Username" type="text" value="" />
<span class="field-validation-valid" data-valmsg-for="Username" data-valmsg-replace="true"></span>
</div>
</div>
What should happen, on blur when jquery.validate unobtrusive fires... it would change to the following
<div class="control-group error">
<label for="Username">Username</label>
<div class="controls">
<input data-val="true" data-val-required="Username is required" id="Username" name="Username" type="text" value="" />
<span class="field-validation-valid help-inline" data-valmsg-for="Username" data-valmsg-replace="true"></span>
</div>
</div>
To get this to work on postback/submit you can do the following...
//twitter bootstrap on post
$(function () {
$('span.field-validation-valid, span.field-validation-error').each(function () {
$(this).addClass('help-inline');
});
$('form').submit(function () {
if ($(this).valid()) {
$(this).find('div.control-group').each(function () {
if ($(this).find('span.field-validation-error').length == 0) {
$(this).removeClass('error');
}
});
}
else {
$(this).find('div.control-group').each(function () {
if ($(this).find('span.field-validation-error').length > 0) {
$(this).addClass('error');
}
});
}
});
$('form').each(function () {
$(this).find('div.control-group').each(function () {
if ($(this).find('span.field-validation-error').length > 0) {
$(this).addClass('error');
}
});
});
});
However, on blur it won't work as you'd expect. I don't want to edit the bootstrap CSS, or Jquery.validate files as they will likely roll out an update at some-point.
Would I create a delegate, or a bind to the jquery functions and work from there. This is deep JS code which I'm not familiar with but could with time firefight my way through it.
Does any one know where I'd start with this problem, or know where it is implemented/been discussed?
var page = function () {
//Update that validator
$.validator.setDefaults({
highlight: function (element) {
$(element).closest(".control-group").addClass("error");
},
unhighlight: function (element) {
$(element).closest(".control-group").removeClass("error");
}
});
} ();
Finally, this fixed it for me. I hope this helps other people too...
My final JS ended like so.
$(function () {
$('span.field-validation-valid, span.field-validation-error').each(function () {
$(this).addClass('help-inline');
});
$('form').submit(function () {
if ($(this).valid()) {
$(this).find('div.control-group').each(function () {
if ($(this).find('span.field-validation-error').length == 0) {
$(this).removeClass('error');
}
});
}
else {
$(this).find('div.control-group').each(function () {
if ($(this).find('span.field-validation-error').length > 0) {
$(this).addClass('error');
}
});
}
});
$('form').each(function () {
$(this).find('div.control-group').each(function () {
if ($(this).find('span.field-validation-error').length > 0) {
$(this).addClass('error');
}
});
});
});
var page = function () {
//Update that validator
$.validator.setDefaults({
highlight: function (element) {
$(element).closest(".control-group").addClass("error");
},
unhighlight: function (element) {
$(element).closest(".control-group").removeClass("error");
}
});
} ();
Here's a nice solution...
Add this to your _Layout.cshtml file outside jQuery(document).ready():
<script type="text/javascript">
jQuery.validator.setDefaults({
highlight: function (element, errorClass, validClass) {
if (element.type === 'radio') {
this.findByName(element.name).addClass(errorClass).removeClass(validClass);
} else {
$(element).addClass(errorClass).removeClass(validClass);
$(element).closest('.control-group').removeClass('success').addClass('error');
}
},
unhighlight: function (element, errorClass, validClass) {
if (element.type === 'radio') {
this.findByName(element.name).removeClass(errorClass).addClass(validClass);
} else {
$(element).removeClass(errorClass).addClass(validClass);
$(element).closest('.control-group').removeClass('error').addClass('success');
}
}
});
</script>
Add this inside $(document).ready():
$("span.field-validation-valid, span.field-validation-error").addClass('help-inline');
$("div.control-group").has("span.field-validation-error").addClass('error');
$("div.validation-summary-errors").has("li:visible").addClass("alert alert-block alert-error");
You're good to go.
Code pieces taken from:
Twitter Bootstrap validation styles with ASP.NET MVC
Integrating Bootstrap Error styling with MVC’s Unobtrusive Error Validation
#daveb's answer
In addition to the answer provided by #leniel-macaferi I use the following as my $(document).ready() function:
$(function () {
$("span.field-validation-valid, span.field-validation-error").addClass('help-inline');
$("div.control-group").has("span.field-validation-error").addClass('error');
$("div.validation-summary-errors").has("li:visible").addClass("alert alert-block alert-error");
});
This also sets the "error" class on the control group if server side validation has failed on a form post and formats any validation summary nicely as a bootstrap error alert.
I know this is an oldy, but I thought I'd share my answer to update for Bootstrap 3. I scratched my head for quite sometime, before building on top of the solution given by Leniel Macaferi.
On top of changing the clases to reflect Bootstrap 3, I thought it would be a nice touch to present the user with a glyphicon to represent the state of the field.
(function ($) {
var defaultOptions = {
errorClass: 'has-error has-feedback',
validClass: 'has-success has-feedback',
highlight: function (element, errorClass, validClass) {
var _formGroup = $(element).closest(".form-group");
_formGroup
.addClass('has-error')
.removeClass('has-success');
if (!_formGroup.hasClass("has-feedback")) {
_formGroup.addClass("has-feedback");
}
var _feedbackIcon = $(element).closest(".form-group").find(".glyphicon");
if (_feedbackIcon.length) {
$(_feedbackIcon)
.removeClass("glyphicon-ok")
.removeClass("glyphicon-remove")
.addClass("glyphicon-remove");
}
else {
$("<span class='glyphicon glyphicon-remove form-control-feedback' aria-hidden='true'></span>")
.insertAfter(element);
}
},
unhighlight: function (element, errorClass, validClass) {
var _formGroup = $(element).closest(".form-group");
_formGroup
.removeClass('has-error')
.addClass('has-success');
if (!_formGroup.hasClass("has-feedback")) {
_formGroup.addClass("has-feedback");
}
var _feedbackIcon = $(element).closest(".form-group").find(".glyphicon");
if (_feedbackIcon.length) {
$(_feedbackIcon)
.removeClass("glyphicon-ok")
.removeClass("glyphicon-remove")
.addClass("glyphicon-ok");
}
else {
$("<span class='glyphicon glyphicon-ok form-control-feedback' aria-hidden='true'></span>")
.insertAfter(element);
}
}
};
$.validator.setDefaults(defaultOptions);
$.validator.unobtrusive.options = {
errorClass: defaultOptions.errorClass,
validClass: defaultOptions.validClass,
};
})(jQuery);
Try use this plugin I've made https://github.com/sandrocaseiro/jquery.validate.unobtrusive.bootstrap
What I did differently from the others answers was to override the errorPlacement and success methods from validate.unobtrusive with my own implementations, but without removing the original implementation so nothing will break.
My implementation look like this:
erroPlacement:
function onError(formElement, error, inputElement) {
var container = $(formElement).find("[data-valmsg-for='" + escapeAttributeValue(inputElement[0].name) + "']"),
replaceAttrValue = container.attr("data-valmsg-replace"),
replace = replaceAttrValue ? $.parseJSON(replaceAttrValue) !== false : null;
//calling original validate.unobtrusive method
errorPlacementBase(error, inputElement);
if (replace) {
var group = inputElement.parent();
if (group.hasClass('form-group')) {
group.addClass('has-error').removeClass('has-success');
}
group = group.parent();
if (group.hasClass('form-group')) {
group.addClass('has-error').removeClass('has-success');
}
}
}
success:
function onSuccess(error) {
var container = error.data("unobtrusiveContainer");
//calling original validate.unobtrusive method
successBase(error);
if (container) {
var group = container.parent();
if (group.hasClass('form-group')) {
group.addClass('has-success').removeClass('has-error');
}
group = group.parent();
if (group.hasClass('form-group')) {
group.addClass('has-success').removeClass('has-error');
}
}
}
Out of the box I wanted on blur to raise my error validation. I found this wasn't the case with Jquery Unobtrusive. It seemed to work if you had a select input but not on a text type input. To get around this for me, perhaps its clumsy but I used the following.
$(function () {
$("input[type='text']").blur(function () {
$('form').validate().element(this);
});
});
You can change it is just enabled on certain inputs that have a specific css class.
$(function () {
$(".EnableOnblurUnobtrusiveValidation").blur(function () {
$('form').validate().element(this);
});
});
EnableOnblurUnobtrusiveValidation... is a bit of a long name but you get the jist.
Use TwitterBootstrapMvc.
It takes care of unobtrusive validation attributes automatically and all you have to write to get a full control group with label, input and validation is:
#Html.Bootstrap().ControlGroup().TextBoxFor(x => x.Field)
Good luck!

Resources