I've created a widget with a CActiveForm in it. Everything works ok, but now i want to enable ajax validation for it.
The problem is that the output of my ajax validation is containing, besides the validation JSON string, all (well a part of it, since Yii::app()->end() stops the rest) of my html as well. Not weird, because i'm using it within a widget and the validation request is done to the controller/action where i've placed this widget on.
Is there some way to prevent outputting all the html, so a valid JSON string is returned?
I've already tried to set the validationUrl in the CActiveForm to another controller/action, but the problem is that i have to send the model with it and this model is determined in my widget and not on the validationUrl.
Widget:
public function run()
{
$model = new User;
$model->scenario = 'create';
$this->performAjaxValidation($model);
if (isset($_POST['User'])) {
$model->attributes = $_POST['User'];
if ($model->save()) {
}
}
$this->render('register-form', array(
'model' => $model
));
}
/**
* Performs the AJAX validation.
* #param User $model the model to be validated
*/
protected function performAjaxValidation($model)
{
if(isset($_POST['ajax']))
{
echo CActiveForm::validate($model);
Yii::app()->end();
}
}
Output of performAjaxValidation() (the ajax call):
.. more html here ..
<section class="box">
<h1>Register form simple</h1>
{"UserPartialSignup_email":["Email is geen geldig emailadres."]}
I solved it this way:
I've created an AJAX controller where the validation is done:
AjaxController:
/**
* Validates a model.
*
* Validates a model, POST containing the data. This method is usually used for widget based forms.
*
* #param $m model name which have to be validated
* #param $s scenario for this model, optional.
* #return string JSON containing the validation data
*/
public function actionValidate($m, $s = null)
{
if ($this->checkValidationData($m, $s) && isset($_POST['ajax']))
{
$model = new $m;
$model->scenario = $s;
echo CActiveForm::validate($model);
Yii::app()->end();
} else {
throw new CHttpException(500, 'No valid validation combination used');
}
}
You can give the model name and a scenario as GET parameters with it, i'm checking if this combination is allowed by the checkValidationData() method.
In the view of my widget where the CActiveForm is placed, i've added the validationUrl, referring to ajax/validate:
widgets/views/registerform.php:
<?php $form = $this->beginWidget('CActiveForm', array(
'id'=>'signup-form-advanced',
'enableAjaxValidation'=>true,
'clientOptions' => array(
'validationUrl' => array('ajax/validate', 'm' => get_class($model), 's' => 'create')
)
//'enableClientValidation'=>true,
)); ?>
Related
I created a yii\base\DynamicModel in controller and I have one form with attributes from this model. I need access these attributes after submitting form in controller.
controller.php
public function actionCreate()
{
$model = new DynamicModel([
'name', 'age', 'city'
]);
if($model->load(Yii::$app->request->post())){
$model->age = $model->age + 5;
/*
* code....
* */
return $this->redirect(['index']);
} else {
return $this->render('create', [
'model' => $model,
]);
}
}
But $model->age, $model->name etc. returns nothing.
I could only access the attribute this way: Yii::$app->request->get('DynamicModel')['age']
What is the correct way to access these attributes?
You need to configure validation rules in order to automatically load attributes by load():
$model = new DynamicModel(['name', 'age', 'city']);
$model->addRule(['name', 'age', 'city'], 'safe');
if ($model->load(Yii::$app->request->post())) {
// ...
Using safe will accept values as is without actual validation, but you may consider adding real validation rules to ensure correct state of your model.
I´ve been working with aspnet for quite a while, and i´d like to implement aspnet viewstate with in php (exactly in codeigniter). Is there a way to implement ASP ViewState with Codeigniter ?
This answer will accomplish some of the goals of Viewstate in that is will preserve control values between form submits. If you navigate away from the page the data will be lost. But it's the same for Viewstate. The example code make heavy use of the CodeIgniter (CI) Framework.
Here is some documentation related to the parts of CI used here.
Form Validation
Form Helper
Session
The CI_Form_validation library suffers from not being able to keep validation results across a redirect. This class extends CI_Form_validation to overcome that limitation.
File: application/libraries/My_Form_validation.php
/**
* Extends CI_Form_validation to facilitate using the Post/Redirect/Get pattern for form handling.
* https://en.wikipedia.org/wiki/Post/Redirect/Get
* http://solidlystated.com/design/best-way-to-process-forms-with-php/
*
* The base class (CI_Form_validation) has the protected property $_field_data
* which holds all the information provided by form_validation->set_rules()
* and all the results gathered by form_validation->run().
* Form Helper and Form_Validation use the $_field_data to re-populate fields and present error messages.
*
* To use this class you must have CodeIgniter Session setup and working.
*
* This class stores $_field_data in session flash data for use in the controller method that is the
* redirect target.
*
* Class is handy for defining custom validation methods which will NOT require the "callback_" prefix.
*
*/
defined('BASEPATH') OR exit('No direct script access allowed');
class MY_Form_validation extends CI_Form_validation
{
public function __construct($rules = array())
{
parent :: __construct($rules);
$this->CI->load->library('session');
}
/**
* Enhanced version of form_validation->run(). Sets a $_SESSION item with the contents of $this->_field_data
*
* #param string $group The name of the validation group to run
* #param bool $persistent If TRUE save the state of $_field_data even if validation passes
* #return boolean TRUE on success and FALSE on failure
*/
public function run($group = '', $persistent = FALSE)
{
if(parent:: run($group))
{
if($persistent)
{
$this->CI->session->set_flashdata('validation_field_data', $this->_field_data);
}
return TRUE;
}
$this->CI->session->set_flashdata('validation_field_data', $this->_field_data);
return FALSE;
}
/**
* This is used in the redirect target defined in the form processing controller/method
*
* #return bool TRUE if $_SESSION['validation_field_data'] is set. It indicates that validation failed.
* Returns FALSE if there is no indication that validation has failed or even been run.
*/
public function is_failed_validation()
{
if(isset($_SESSION['validation_field_data']))
{
// Validation failed or is being persisted.
$this->_field_data = $_SESSION['validation_field_data'];
return TRUE;
}
return FALSE;
}
/**
* A validation function to cleanup strings
*
* #param string $str Value of the field
* #return string|bool The sanitized string or FALSE if filter_var() fails
*/
public function sanitize_str($str)
{
return filter_var($str, FILTER_SANITIZE_STRING);
}
/**
* A do-nothing routine assigned to any field we want included in validation results
* #return boolean
*/
public function alwaysTrue($val)
{
return TRUE;
}
}
Hopefully the comments explain what's going on. One major thing to understand is that form_validation only captures the $_POST data for controls that have validation rules. That's the reason for the "do-nothing" validation routine alwaysTrue(). Use this rule on any control where you want the values to persist.
The following controller shows a usage example.
File: application/controllers/Viewstate.php
class Viewstate extends CI_Controller
{
public function __construct()
{
parent::__construct();
$this->load
->library('form_validation', NULL, 'fv') //renames 'form_validation' to 'fv'
->helper(['form', 'url']);
$this->fv->set_error_delimiters('<span class="error">', '</span>');
}
public function index()
{
$this->fv->is_failed_validation(); //captures validation data if it's there
//Define some data for consumption by CI's Form Helper functions
$data['username_field'] = [
'name' => 'username',
'id' => 'username',
'value' => $this->fv->set_value('username'),
'class' => 'your_css',
];
$data['confirm_username_field'] = [
'name' => 'usernameconf',
'id' => 'usernameconf',
'value' => $this->fv->set_value('usernameconf'),
'class' => 'your_css',
];
$data['comment_field'] = [
'name' => 'comment',
'id' => 'comment',
'value' => $this->fv->set_value('comment'),
'class' => 'comment',
];
$data['project_lead_field'] = [
'name' => 'project_lead',
'value' => 1,
'checked' => $this->fv->set_radio('project_lead', 1, FALSE)
];
$selected_status = ['None' => "None"]; //default dropdown item
$status_items = ["None" => "None", "Good" => "Good", 'Bad' => 'Bad', "Ugly" => "Ugly"];
//recover previously posted select item - if any
if($item = $this->session->validation_field_data['status']['postdata'])
{
$selected_status = [$item => $item];
}
$data['status_field'] = [
'name' => 'status',
'options' => $status_items,
'selected' => $selected_status
];
$this->load->view('testcase_view', $data);
}
/**
* This is the "action" that processes the form's posted data
*/
public function process()
{
//set rules and error messages at same time
$this->fv
->set_rules('username', 'User Name', ['trim', 'required', 'matches[usernameconf]'],
['required' => '<em>{field}</em> required.', 'matches' => "User Names don't match."])
->set_rules('usernameconf', '', ['trim', 'required'], ['required' => 'Retyping the User Name is required.'])
->set_rules('comment', "", ['trim', 'sanitize_str'])
->set_rules('project_lead', "", 'alwaysTrue')
->set_rules('status', "", 'alwaysTrue')
;
//So an uncheck checkbox will be part of the $_POST array
if(!isset($_POST['project_lead']))
{
$_POST['project_lead'] = 0;
}
if(FALSE == $this->fv->run('', TRUE))
{
redirect('viewstate');
}
else
{
//do something with field values e.g.
//$this->model->instert($_POST);
redirect('viewstate'); //to prove the page state is persistent
}
}
}
I included some actual field validation so readers can see how that works and how the validation results persist across a redirect.
Here is the view
File: application/views/textcase_view.php
<!DOCTYPE html>
<html>
<head>
<title>Test Persistent Page</title>
<style type="text/css">
p{
margin: 0;
}
.error {
color: #FF0000;
font-size: small;
}
.fld-label{
color: #555;
font-size: .9em;
}
.comment{
color: Blue;
font-weight: bold;
}
div{
margin-bottom: 1em;
}
div + .fld-label
/*div + .error*/
{
margin-bottom: 0;
}
</style>
</head>
<body>
<?= form_open('viewstate/process'); ?>
<span class="fld-label">User Name</span>
<div><?= form_input($username_field) ?>
<p><?= form_error('username'); ?></p>
</div>
<div class="fld-label">Retype User Name</div>
<div><?= form_input($confirm_username_field) ?>
<p><?= form_error('usernameconf'); ?></p>
</div>
<div class="fld-label">Comment</div>
<div><?= form_input($comment_field); ?></div>
<div class="fld-label">Project Lead?</div>
<div><?= form_checkbox($project_lead_field); ?></div>
<div class="fld-label">Status</div>
<div><?= form_dropdown($status_field); ?></div>
<p><input type="submit" value="Submit"></p>
<?= form_close(); ?>
</body>
</html>
The view makes heavy use of Form Helper functions such as form_open, form_close, form_input, etc.
There is a complex form with a lot of nested fieldsets. Some fields need to be validated depending on field(-s) in another fieldset. So I cannot define all the validation rules directly in the getInputFilterSpecification() of the Fieldset, since there I cannot access other fieldsets (only the sub-fieldsets). The only way to do this is to extend the Form validation. Right? If so, how to do this?
MyForm extends Form
{
public function isValid()
{
$isFormValid = parent::isValid();
$isFieldXyzValid = // my additional validation logic
if(! $isFieldXyzValid) {
$fieldXyz->setMessages(...);
}
return $isFormValid && $isFieldXyzValid;
}
}
Like this? Or is a cleaner way to solve this problem?
I've already developed something similar in my previous project.
For doing this i used a service which take my form and set dynamic fieldset, and obviously custom validation rules.
In your controller, get your form (via the dependancy injection formManager (polyfilled or not).
$form = $this->formManager->get('{your form}');
Call your service and give it your form.
And in your service you can do anything you want like :
get your stuff (from DB or others) to determine wich fields are mandatory
Foreach on your form
Add or remove fieldset
Add or remove validationGroup fields
Add or remove filters
Add or remove validators
I performed those via (sample) in a foreach where $stuff is an element of doctrine collection
$nameFieldset = 'my_fieldset-'.$stuff->getId();
$globalValidator = new GlobalValidator();
$globalValidator->setGlobalValue($gloablValue);
$uoValidator = new UcValidator();
$uoValidator->setOptions(array(
'idToValidate' => $stuff->getId(),
'translator' => $this->translator
));
$globalValidator->setOptions(array(
'idToValidate' => $stuff->getId(),
'translator' => $this->translator
));
$name = 'qty-'.$stuff->getId();
$form = $this->setFilters($form, $name, $nameFieldset);
$globalValidator->setData($data);
$form = $this->setValidators($form, $name, $globalValidator, $uoValidator, $nameFieldset);
Where setFilters and setValidators are custom methods wich add filters and validator to my fields (also custom)
/**
* #param myForm $form
* #param $name
* #param string $nameFieldset
* #return myForm
*/
public function setFilters($form, $name, $nameFieldset)
{
$form->getInputFilter()->get('items')->get($nameFieldset)
->get($name)
->getFilterChain()
->getFilters()
->insert(new StripTags())
->insert(new StringTrim());
return $form;
}
/**
* #param myForm $form
* #param $name
* #param $globalValidator
* #param $uoValidator
* #param $nameFieldset
* #return myForm
*/
public function setValidators($form, $name, $globalValidator, $uoValidator, $nameFieldset)
{
$optionsSpace = [
'translator' => $this->translator,
'type' => NotEmpty::SPACE
];
$optionsString = [
'translator' => $this->translator,
'type' => NotEmpty::STRING
];
$optionsDigits = [
'translator' => $this->translator,
];
$form->getInputFilter()->get('items')
->get($nameFieldset)
->get($name)
->setRequired(true)
->getValidatorChain()
->attach($uoValidator, true, 1)
->attach($globalValidator, true, 1)
// We authorize zéro but not space nor strings
->attach(new NotEmpty($optionsSpace), true, 2)
->attach(new NotEmpty($optionsString), true, 2)
->attach(new Digits($optionsDigits), true, 2);
return $form;
}
I would like a best practice for this kind of problem
I have items, categories and category_item table for a many to many relationship
I have 2 models with these validations rules
class Category extends Basemodel {
public static $rules = array(
'name' => 'required|min:2|max:255'
);
....
class Item extends BaseModel {
public static $rules = array(
'title' => 'required|min:5|max:255',
'content' => 'required'
);
....
class Basemodel extends Eloquent{
public static function validate($data){
return Validator::make($data, static::$rules);
}
}
I don't know how to validate these 2 sets of rules from only one form with category, title and content fields.
For the moment I just have a validation for the item but I don't know what's the best to do:
create a new set of rules in my controller -> but it seems redundant
sequentially validate Item then category -> but I don't know how to handle validations errors, do I have to merges them? and how?
a 3rd solution I'm unaware of
here is my ItemsController#store method
/**
* Store a newly created item in storage.
*
* #return Redirect
*/
public function store()
{
$validation= Item::validate(Input::all());
if($validation->passes()){
$new_recipe = new Item();
$new_recipe->title = Input::get('title');
$new_recipe->content = Input::get('content');
$new_recipe->creator_id = Auth::user()->id;
$new_recipe->save();
return Redirect::route('home')
->with('message','your item has been added');
}
else{
return Redirect::route('items.create')->withErrors($validation)->withInput();
}
}
I am very interested on some clue about this subject
thanks
One way, as you pointed yourself, is to validate it sequentially:
/**
* Store a newly created item in storage.
*
* #return Redirect
*/
public function store()
{
$itemValidation = Item::validate(Input::all());
$categoryValidation = Category::validate(Input::all());
if($itemValidation->passes() and $categoryValidation->passes()){
$new_recipe = new Item();
$new_recipe->title = Input::get('title');
$new_recipe->content = Input::get('content');
$new_recipe->creator_id = Auth::user()->id;
$new_recipe->save();
return Redirect::route('home')
->with('message','your item has been added');
}
else{
return Redirect::route('items.create')
->with('errors', array_merge_recursive(
$itemValidation->messages()->toArray(),
$categoryValidation->messages()->toArray()
)
)
->withInput();
}
}
The other way would be to create something like an Item Repository (domain) to orchestrate your items and categories (models) and use a Validation Service (that you'll need to create too) to validate your forms.
Chris Fidao book, Implementing Laravel, explains that wonderfully.
You can also use this:
$validationMessages =
array_merge_recursive(
$itemValidation->messages()->toArray(),
$categoryValidation->messages()->toArray());
return Redirect::back()->withErrors($validationMessages)->withInput();
and call it in the same way.
$validateUser = Validator::make(Input::all(), User::$rules);
$validateRole = Validator::make(Input::all(), Role::$rules);
if ($validateUser->fails() OR $validateRole->fails()) :
$validationMessages = array_merge_recursive($validateUser->messages()->toArray(), $validateRole->messages()->toArray());
return Redirect::back()->withErrors($validationMessages)->withInput();
endif;
I have this code in my controller (admin):
function save(){
$model = $this->getModel('mymodel');
if ($model->store($post)) {
$msg = JText::_( 'Yes!' );
} else {
$msg = JText::_( 'Error :(' );
}
$link = 'index.php?option=com_mycomponent&view=myview';
$this->setRedirect($link, $msg);
}
In model I have:
function store(){
$row =& $this->getTable();
$data = JRequest::get('post');
if(strlen($data['fl'])!=0){
return false;
}
[...]
And this is working - generate error message, but it return to items list view. I want to stay in edit view with entered data. How to do it?
In your controller you can:
if ($model->store($post)) {
$msg = JText::_( 'Yes!' );
} else {
// stores the data in your session
$app->setUserState('com_mycomponent.edit.mymodel.data', $validData);
// Redirect to the edit view
$msg = JText::_( 'Error :(' );
$this->setError('Save failed', $model->getError()));
$this->setMessage($this->getError(), 'error');
$this->setRedirect(JRoute::_('index.php?option=com_mycomponent&view=myview&id=XX'), false));
}
then, you will need to load the data from session with something like:
JFactory::getApplication()->getUserState('com_mycomponent.edit.mymodel.data', array());
normally this is loaded in the method "loadFormData" in your model. Where to load that data will depend on how are you implementing your component. If you are using the Joomla's form api then you can add the following method to your model.
protected function loadFormData()
{
// Check the session for previously entered form data.
$data = JFactory::getApplication()->getUserState('com_mycomponent.edit.mymodel.data', array());
if (empty($data)) {
$data = $this->getItem();
}
return $data;
}
EDIT:
BUT please note, that Joomla's API already can do all this for you if you controller inherits from "JControllerForm", you don't need to rewrite the save method. The best way to create your component is copying what is in Joomla's core components, com_content for example
It is not recommended to rewrite save or any method.
If you really want to override something and want to update something before or after save, you should use JTable file.
For Example:
/**
* Example table
*/
class HelloworldTableExample extends JTable
{
/**
* Method to store a node in the database table.
*
* #param boolean $updateNulls True to update fields even if they are null.
*
* #return boolean True on success.
*/
public function store($updateNulls = false)
{
// This change is before save
$this->name = str_replace(' ', '_', $this->name);
if (!parent::store($updateNulls))
{
return false;
}
// This function will be called after saving table
AnotherClass::functionIsCallingAfterSaving();
}
}
You can extends any method using JTable class and that's the recommended way to doing it.