I am using VS2012 / C# / MVC 4.
I am trying to Post a "Contact Us" form without refreshing the whole page. All is fine, the validation kicks in the first time if I miss any required fields and OnSuccess is not fired. After I successfully post the form (not missing any required fields) each additional postings will cause OnSuccess to fire even though the validation failed (required fields are missing). The red exclamation point is displayed correctly and the controller "knows" it failed but the ajax OnSuccess doesn't seem to know about the new failure.
ContactUsForm.cshtml (partialView):
#model contactdemo.Models.ContactUsDetails
<div id="formContainer">
#using (Ajax.BeginForm("ContactUsForm", new AjaxOptions { UpdateTargetId = "formContainer", OnSuccess = "displaySuccess" }))
{
<strong>Send To: </strong>
<div>
#Html.DevExpress().ComboBoxFor(model => model.ContactID,
settings =>
{
settings.ShowModelErrors = true;
settings.Width = System.Web.UI.WebControls.Unit.Percentage(100);
settings.Name = "ContactID";
settings.Properties.ValueField = "ContactID";
settings.Properties.ValueType = typeof(int);
settings.Properties.TextField = "ContactName";
settings.ControlStyle.CssClass = "contactUsCombo";
}).BindList(Session["Names"]).GetHtml()
</div>
<div>
#Html.DevExpress().LabelFor(m => m.Subject).GetHtml()
#Html.DevExpress().TextBoxFor(m => m.Subject,
settings =>
{
settings.ShowModelErrors = true;
settings.Width = System.Web.UI.WebControls.Unit.Percentage(100);
settings.ControlStyle.CssClass = "subjectText";
}).GetHtml()
</div>
<div>
#Html.DevExpress().LabelFor(m => m.Message).GetHtml()
#Html.DevExpress().MemoFor(m => m.Message,
settings =>
{
settings.ShowModelErrors = true;
settings.Height = 50;
settings.Width = System.Web.UI.WebControls.Unit.Percentage(100);
settings.ControlStyle.CssClass = "memoText";
}).GetHtml()
</div>
<div>
#Html.DevExpress().Button(
settings =>
{
settings.Name = "btnSubmit";
settings.Text = "Send Message";
settings.UseSubmitBehavior = true;
settings.ControlStyle.CssClass = "contactUsCombo";
}).GetHtml()
</div>
}
HomeController.cs:
namespace contactdemo.Controllers
{
[OutputCacheAttribute(VaryByParam = "*", Duration = 1, NoStore = true)]
public class HomeController : Controller
{
<snip...>
[HttpGet]
[OutputCache(Duration = 60, VaryByParam = "*")]
public ActionResult ContactUsForm()
{
return PartialView(new ContactUsDetails());
}
[HttpPost]
public ActionResult ContactUsForm(ContactUsDetails data)
{
if (ModelState.IsValid)
{
return PartialView(new ContactUsDetails());
}
else
{
return PartialView(data);
}
}
}
Index.cshtml:
<script src="#Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script>
<script type="text/javascript">
function displaySuccess(result) {
alert("Sent Successfully");
}
</script>
Make sure that inside your displaySuccess callback you reattach the unobtrusive validation handler to the newly added contents to the DOM:
function displaySuccess(result) {
$('#formContainer form').removeData('validator');
$('#formContainer form').removeData('unobtrusiveValidation');
$.validator.unobtrusive.parse('#formContainer form');
alert('Sent Successfully');
}
The reason you need to do this is because you are refreshing the contents of your DOM with the new partial after an AJAX callback and the unobtrusive validation framework has no way of knowing those new elements unless you tell it to. You might also take a look at a similar answer.
Related
<p:commandButton id .......
onclick=”disableButton(this);”
onkeypress=”disableButton(this);”
oncomplete="enableButton('${bean.enableButton()}');"
private boolean enableButton(){
return false;
}
<script>
function disableButton(data) {
data.disable = true;
}
function enableButton(data) {
data.disable = data;
}
</script>
Observed both calls working in the debugger, but the button remains disabled
When disableButton is called data = button#MessageView j_idt183:….
When enableButton is called data = {url: “ ……….}
from the debugger
<p:commandButton id .......
onclick=”disableButton(this);”
onkeypress=”disableButton(this);”
oncomplete="enableButton();"
<script>
function disableButton(data) {
data.disable = true;
window.buttonPressed.disabled = data
}
// Save and use the initial "data" object
function enableButton() {
window.buttonPressed.disabled = false;
}
I have a website where I can upload a .xlsx file which contains some rows of information for my database. I read the documentation for laravel-excel but it looks like it only works with progress bar if you use the console method; which I don't.
I currently just use a plain HTML upload form, no ajax yet.
But to create this progress bar for this I need to convert it to ajax, which is no hassle, that I can do.
But how would I create the progress bar when uploading the file and iterating through each row in the Excel file?
This is the controller and method where the upload gets done:
/**
* Import companies
*
* #param Import $request
* #return \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse
*/
public function postImport(Import $request)
{
# Import using Import class
Excel::import(new CompaniesImport, $request->file('file'));
return redirect(route('dashboard.companies.index.get'))->with('success', 'Import successfull!');
}
And this is the import file:
public function model(array $row)
{
# Don't create or validate on empty rows
# Bad workaround
# TODO: better solution
if (!array_filter($row)) {
return null;
}
# Create company
$company = new Company;
$company->crn = $row['crn'];
$company->name = $row['name'];
$company->email = $row['email'];
$company->phone = $row['phone'];
$company->website = (!empty($row['website'])) ? Helper::addScheme($row['website']) : '';
$company->save();
# Everything empty.. delete address
if (!empty($row['country']) || !empty($row['state']) || !empty($row['postal']) || !empty($row['address']) || !empty($row['zip'])) {
# Create address
$address = new CompanyAddress;
$address->company_id = $company->id;
$address->country = $row['country'];
$address->state = $row['state'];
$address->postal = $row['postal'];
$address->address = $row['address'];
$address->zip = $row['zip'];
$address->save();
# Attach
$company->addresses()->save($address);
}
return $company;
}
I know this is not much at this point. I just need some help figuring out how I would create this progress bar, because I'm pretty stuck.
My thought is to create a ajax upload form though, but from there I don't know.
Just an idea, but you could use the Laravel session to store the total_row_count and processed_row_count during the import execution. Then, you could create a separate AJAX call on a setInterval() to poll those session values (e.g., once per second). This would allow you to calculate your progress as processed_row_count / total_row_count, and output to a visual progress bar. – matticustard
Putting #matticustard comment into practice. Below is just sample of how things could be implemented, and maybe there are areas to improve.
1. Routes
import route to initialize Excel import.
import-status route will be used to get latest import status
Route::post('import', [ProductController::class, 'import']);
Route::get('import-status', [ProductController::class, 'status']);
2. Controller
import action will validate uploaded file, and pass $id to ProductsImport class. As it will be queued and run in the background, there is no access to current session. We will use cache in the background. It will be good idea to generate more randomized $id if more concurrent imports will be processed, for now just unix date to keep simple.
You currently cannot queue xls imports. PhpSpreadsheet's Xls reader contains some non-utf8 characters, which makes it impossible to queue.
XLS imports could not be queued
public function import()
{
request()->validate([
'file' => ['required', 'mimes:xlsx'],
]);
$id = now()->unix()
session([ 'import' => $id ]);
Excel::queueImport(new ProductsImport($id), request()->file('file')->store('temp'));
return redirect()->back();
}
Get latest import status from cache, passing $id from session.
public function status()
{
$id = session('import');
return response([
'started' => filled(cache("start_date_$id")),
'finished' => filled(cache("end_date_$id")),
'current_row' => (int) cache("current_row_$id"),
'total_rows' => (int) cache("total_rows_$id"),
]);
}
3. Import class
Using WithEvents BeforeImport we set total rows of our excel file to the cache. Using onRow we set currently processing row to the cache. And AfterReset clear all the data.
<?php
namespace App\Imports;
use App\Models\Product;
use Maatwebsite\Excel\Row;
use Maatwebsite\Excel\Concerns\OnEachRow;
use Maatwebsite\Excel\Events\AfterImport;
use Maatwebsite\Excel\Events\BeforeImport;
use Maatwebsite\Excel\Concerns\WithEvents;
use Illuminate\Contracts\Queue\ShouldQueue;
use Maatwebsite\Excel\Concerns\WithStartRow;
use Maatwebsite\Excel\Concerns\WithChunkReading;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
class ProductsImport implements OnEachRow, WithEvents, WithChunkReading, ShouldQueue
{
public $id;
public function __construct(int $id)
{
$this->id = $id;
}
public function chunkSize(): int
{
return 100;
}
public function registerEvents(): array
{
return [
BeforeImport::class => function (BeforeImport $event) {
$totalRows = $event->getReader()->getTotalRows();
if (filled($totalRows)) {
cache()->forever("total_rows_{$this->id}", array_values($totalRows)[0]);
cache()->forever("start_date_{$this->id}", now()->unix());
}
},
AfterImport::class => function (AfterImport $event) {
cache(["end_date_{$this->id}" => now()], now()->addMinute());
cache()->forget("total_rows_{$this->id}");
cache()->forget("start_date_{$this->id}");
cache()->forget("current_row_{$this->id}");
},
];
}
public function onRow(Row $row)
{
$rowIndex = $row->getIndex();
$row = array_map('trim', $row->toArray());
cache()->forever("current_row_{$this->id}", $rowIndex);
// sleep(0.2);
Product::create([ ... ]);
}
}
4. Front end
On the front-end side this is just sample how things could be handled. Here I used vuejs, ant-design-vue and lodash.
After uploading file handleChange method is called
On successful upload trackProgress method is called for the first time
trackProgress method is recursive function, calling itself on complete
with lodash _.debounce method we can prevent calling it too much
export default {
data() {
this.trackProgress = _.debounce(this.trackProgress, 1000);
return {
visible: true,
current_row: 0,
total_rows: 0,
progress: 0,
};
},
methods: {
handleChange(info) {
const status = info.file.status;
if (status === "done") {
this.trackProgress();
} else if (status === "error") {
this.$message.error(_.get(info, 'file.response.errors.file.0', `${info.file.name} file upload failed.`));
}
},
async trackProgress() {
const { data } = await axios.get('/import-status');
if (data.finished) {
this.current_row = this.total_rows
this.progress = 100
return;
};
this.total_rows = data.total_rows;
this.current_row = data.current_row;
this.progress = Math.ceil(data.current_row / data.total_rows * 100);
this.trackProgress();
},
close() {
if (this.progress > 0 && this.progress < 100) {
if (confirm('Do you want to close')) {
this.$emit('close')
window.location.reload()
}
} else {
this.$emit('close')
window.location.reload()
}
}
},
};
<template>
<a-modal
title="Upload excel"
v-model="visible"
cancel-text="Close"
ok-text="Confirm"
:closable="false"
:maskClosable="false"
destroyOnClose
>
<a-upload-dragger
name="file"
:multiple="false"
:showUploadList="false"
:action="`/import`"
#change="handleChange"
>
<p class="ant-upload-drag-icon">
<a-icon type="inbox" />
</p>
<p class="ant-upload-text">Click to upload</p>
</a-upload-dragger>
<a-progress class="mt-5" :percent="progress" :show-info="false" />
<div class="text-right mt-1">{{ this.current_row }} / {{ this.total_rows }}</div>
<template slot="footer">
<a-button #click="close">Close</a-button>
</template>
</a-modal>
</template>
<script>
export default {
data() {
this.trackProgress = _.debounce(this.trackProgress, 1000);
return {
visible: true,
current_row: 0,
total_rows: 0,
progress: 0,
};
},
methods: {
handleChange(info) {
const status = info.file.status;
if (status === "done") {
this.trackProgress();
} else if (status === "error") {
this.$message.error(_.get(info, 'file.response.errors.file.0', `${info.file.name} file upload failed.`));
}
},
async trackProgress() {
const { data } = await axios.get('/import-status');
if (data.finished) {
this.current_row = this.total_rows
this.progress = 100
return;
};
this.total_rows = data.total_rows;
this.current_row = data.current_row;
this.progress = Math.ceil(data.current_row / data.total_rows * 100);
this.trackProgress();
},
close() {
if (this.progress > 0 && this.progress < 100) {
if (confirm('Do you want to close')) {
this.$emit('close')
window.location.reload()
}
} else {
this.$emit('close')
window.location.reload()
}
}
},
};
</script>
I'm making a Chrome Extension that gets the DOM of a closed tab and updates the popup.html. So far so good, I can do that through the background script using XMLHttpRequest.
However, I would like my popup to be updated if the closed page is updated. I was thinking of running a timer in the background script to check every 10 sends or so, but I was wondering if XMLHttpRequest has a way of knowing when its page updates? Or even if the timer would work, I couldn't get it working
I've added the relevant files below. Any help is appreciated
popup.html
<body>
<h1>Agile Board Viewer</h1>
<div class="wrapper">
<button id="mainButton">Click me</button>
<p id="testingDisplay">test</p>
</div>
</body>
popup.js
document.addEventListener('DOMContentLoaded', function () {
document.getElementById("mainButton").addEventListener('click', function () {
chrome.runtime.sendMessage({
method : 'POST',
action : 'xhttp',
url : '//My url//',
data : 'q=something'
}, function (responseText) {
document.getElementById("testingDisplay").innerHTML = responseText;
});
});
});
background.js
I've deleted some lines that are pointless (I think) to avoid clutter, just error handlers and what not, also got rid of my attempt at a timer. Basically, what it does is takes a string from the DOM and sends it to the popup. I would like that popup to update whenever the string does.
var testingString = "Testing (";
chrome.runtime.onMessage.addListener(function (request, sender, callback) {
if (request.action == "xhttp") {
`var xhttp = new XMLHttpRequest();
xhttp.onload = function () {
var testingValue = xhttp.responseText.substring(xhttp.responseText.indexOf(testingString), xhttp.responseText.indexOf(testingString) + 16);
callback(testingValue);
//callback(xhttp.responseText);
};
}
});
Sorry if the formatting is a mess, I'm not too well versed on this
Just to follow up, I've solved my issue by using a timer that checks the closed tab every couple of seconds. If the label that lists the number of items in my Testing column is different, I get my notification. Still learning XHR so I'm hoping I can improve on this again but for now, I'm happy enough. Only works for 20 seconds in my example, as I don't want an infinite timer. Will put in an off switch later
var testingString = "Testing (";
var testBuffer = "";
var i = 0;
chrome.runtime.onMessage.addListener(function (request, sender, callback) {
if (request.action == "xhttp") {
var xhttp = new XMLHttpRequest();
var method = request.method ? request.method.toUpperCase() : 'GET';
var testingValue = xhttp.responseText.substring(xhttp.responseText.indexOf(testingString), xhttp.responseText.indexOf(testingString) + 16);
function startTimer() {
window.setTimeout(function () {
if (i < 20) {
xhttp.open(method, request.url, true);
xhttp.onload = function () {
testBuffer = testingValue;
testingValue = xhttp.responseText.substring(xhttp.responseText.indexOf(testingString), xhttp.responseText.indexOf(testingString) + 16);
if (testBuffer != testingValue) {
notification = new Notification('New Item in Testing Column', {
body : "You have a new item in Testing",
});
console.log(testingValue);
}
};
xhttp.onreadystatechange = function () {
if (xhttp.readyState == 4 && xhttp.status == 200) {
callback(testingValue);
}
};
xhttp.onerror = function () {
alert("error");
callback();
};
if (method == 'POST') {
xhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
}
xhttp.send(request.data);
console.log("count");
i++;
startTimer();
}
}, 5000);
}
startTimer();
return true;
}
});
I am developing a custom theme based on bootstrap for moodle 2.7. I am adding a simple class to the custom menu function (render_custom_menu) on line 80. I simply addded the navbar-right class that is applied to the standard menu shown in both code blocks below.
Custom menu:
protected function render_custom_menu(custom_menu $menu) {
global $CFG, $USER;
// TODO: eliminate this duplicated logic, it belongs in core, not
// here. See MDL-39565.
$content = '<ul class="nav navbar-nav navbar-right">';
foreach ($menu->get_children() as $item) {
$content .= $this->render_custom_menu_item($item, 1);
}
return $content.'</ul>';
}
Standard Menu:
protected function render_user_menu(custom_menu $menu) {
global $CFG, $USER, $DB;
$addusermenu = true;
$addlangmenu = true;
$langs = get_string_manager()->get_list_of_translations();
if (count($langs) < 2
or empty($CFG->langmenu)
or ($this->page->course != SITEID and !empty($this->page->course->lang))) {
$addlangmenu = false;
}
if ($addlangmenu) {
$language = $menu->add(get_string('language'), new moodle_url('#'), get_string('language'), 10000);
foreach ($langs as $langtype => $langname) {
$language->add($langname, new moodle_url($this->page->url, array('lang' => $langtype)), $langname);
}
}
if ($addusermenu) {
if (isloggedin()) {
$usermenu = $menu->add(fullname($USER), new moodle_url('#'), fullname($USER), 10001);
$usermenu->add(
'<span class="glyphicon glyphicon-off"></span>' . get_string('logout'),
new moodle_url('/login/logout.php', array('sesskey' => sesskey(), 'alt' => 'logout')),
get_string('logout')
);
$usermenu->add(
'<span class="glyphicon glyphicon-user"></span>' . get_string('viewprofile'),
new moodle_url('/user/profile.php', array('id' => $USER->id)),
get_string('viewprofile')
);
$usermenu->add(
'<span class="glyphicon glyphicon-cog"></span>' . get_string('editmyprofile'),
new moodle_url('/user/edit.php', array('id' => $USER->id)),
get_string('editmyprofile')
);
} else {
$usermenu = $menu->add(get_string('login'), new moodle_url('/login/index.php'), get_string('login'), 10001);
}
}
$content = '<ul class="nav navbar-nav navbar-right">';
foreach ($menu->get_children() as $item) {
$content .= $this->render_custom_menu_item($item, 1);
}
return $content.'</ul>';
}
I'm not sure why this change is not taking effect. Anyone have experience with this and how to solve it. I'm sure I'm overlooking something very simple.
FYI: I have purged the moodle cache many times, reset the server and cleared the history in the browser.
Thanks!
I have a menu and some menu items.when I clcik to menu item I create new panle codebehind and add it to main tabpanel.so far so good ,but it seems for every click on the menu,panel created from the begining,plus,change place of the the tabs.how can I solve this.
here is the my Index.cshtml
<body>
#Html.X().ResourceManager()
#(
Html.X().Viewport()
.Layout(LayoutType.Border)
.Items(
Html.X().Panel()
.Region(Region.West)
.Title("main menu")
.Width(200)
.Collapsible(true)
.Split(true)
.MinWidth(175)
.MaxWidth(400)
.MarginSpec("5 0 5 5")
.Layout(LayoutType.Accordion)
.Items(
Html.X().MenuPanel()
.Collapsed(true)
.Icon(Icon.Note)
.AutoScroll(true)
.Title("menu")
.ID("PNL34")
.BodyPadding(0)
.Menu(menu => {
menu.Items.Add(Html.X().MenuItem().ID("1a").Text("test1").Icon(Icon.Anchor)
.DirectEvents(m => { m.Click.Url = "Desktop/AddTab";
m.Click.ExtraParams.Add(new { conid = "TabPanel1" ,pnlid="tabpnl10",viewname="Urunler"});
}));
menu.Items.Add(Html.X().MenuItem().ID("2a").Text("test2").Icon(Icon.Anchor)
.DirectEvents(m =>
{
m.Click.Url = "Desktop/AddTab";
m.Click.ExtraParams.Add(new { conid = "TabPanel1", pnlid = "tabpnl11", viewname = "Siparisler" });
}));
})
)
,
Html.X().TabPanel()
.ID("TabPanel1")
.Region(Region.Center)
.Title("E-TICARET")
.MarginSpec("5 5 5 0")
))
and codebehind controller
public ActionResult AddTab(string conid,string pnlid,string viewname)
{
var cmp = this.GetCmp<Panel>(pnlid);
var cmp2 = this.GetCmp<TabPanel>(conid);
if (cmp.ActiveIndex==-1)
{
var result = new Ext.Net.MVC.PartialViewResult
{
ViewName = viewname,
ContainerId = conid,
RenderMode = RenderMode.AddTo,
WrapByScriptTag = false
};
cmp2.SetActiveTab(pnlid);
return result;
}
else
{
return null;
}
}
This is not going to work.
if (cmp.ActiveIndex == -1)
In WebForms it is retrieved from the Post data. There is no a WebForms-like Post in MVC. You should send all the required information with a request.
Also if you don't need a tab to be rendered if it is already exists, just stop a request. You can determine on client if a tab is already there or not.