For now it's impossible to set attributes for drop-down list options using Phalcon\Tag::select()
I propose to implement it this way
$this->tag->select([
'robotId',
Robots::find(),
'using' => ['id', 'name'],
// parameter 'options' is array of attributes which must be set for OPTIONs
'options' => [
// class "favorite" will be set for OPTIONs with id 1 and 3
'class' => ['favorite', [1, 3]],
// "disabled" attribute will be set for destroyed robots
'disabled' => function($robot){return $robot->is_destroyed ? 'disabled' : false;},
// dir="rtl" will be set for all OPTIONs *
'dir' => 'rtl',
],
]);
_* actually I don't know the reason to set the same attribute for every OPTION_
Output:
<select name="robotId">
<option value="1" dir="rtl" class="favorite">Robocop</option>
<option value="2" dir="rtl" disabled="disabled">Unicron</option>
<option value="3" dir="rtl" class="favorite">Bender</option>
<option value="4" dir="rtl">R2-D2</option>
</select>
Want to back this issue? Post a bounty on it! We accept bounties via Bountysource.
+1
+1
The only think I'm not liking is the "class" option you have listed. The second parameter is kinda arbitrary really. It would be less gimmicky if the options parser were written to, for each key, take either a string, array, or closure($data):
$this->tag->select([
'robotId',
Robots::find(),
'using' => ['id', 'name'],
'options' => [
'class' => function($robot){return ($robot->id != 2) ? 'favorite' : false;},
'disabled' => function($robot){return $robot->is_destroyed ? 'disabled' : false;},
'dir' => 'rtl',
],
]);
It is still a touch confusing to read the code as it's not obvious if 'options' refers to the select tag, or its options' tags. Thoughts?
+1
Hey, i quickly created a custom element to handle this, here is my code:
<?php
use Phalcon\Forms\Element;
class SelectEnhanced extends Element {
protected $optionsElement = array();
public function __construct($elementName, $elementValues = NULL, $parameters = NULL) {
$this->setName($elementName);
if($parameters['useEmpty']) {
if(!isset($parameters['emptyText'])) {
$parameters['emptyText'] = 'Choose...';
}
$this->addOption(array('value' => '', 'content' => $parameters['emptyText']));
}
if(!empty($parameters['options'])){
$haveOptions = true;
}
else {
$haveOptions = false;
}
// If we got data to generate optionElements
if(!empty($elementValues)) {
foreach($elementValues as $elementValue) {
// Initialize optionElement parameters
$optionParameters = array('value' => $elementValue->getId(), 'content' => $elementValue->getName());
// If somes options are defined to set attributes on optionElements
if($haveOptions) {
foreach($parameters['options'] as $attribute => $condition) {
// If condition is string assign it to result
if(is_string($condition)) {
$result = $condition;
}
// Otherwise execute condition to get result
else {
$result = $condition($elementValue);
}
// if condition is passed we had attribute with his value
if($result) {
$optionParameters[$attribute] = $result;
}
}
}
// add optionElement with parameters
$this->addOption($optionParameters);
}
}
}
public function render($attributes = null) {
// Set new attributes passed in parameter
$attributes = $this->prepareAttributes($attributes);
if(!empty($attributes)) {
foreach($attributes as $attrName => $attrValue) {
$this->setAttribute($attrName, $attrValue);
}
}
$html = '<select name="'.$this->getName().'"';
// write every attributes of select element in DOM
$attributes = $this->getAttributes();
if(!empty($attributes)) {
foreach($attributes as $attrName => $attrValue) {
$html .= ' '.$attrName.'="'.$attrValue.'"';
}
}
$html .= '>';
if(!empty($this->optionsElement)) {
foreach($this->optionsElement as $optionElement) {
// extract value an content from optionElement
$value = $optionElement['value'];
unset($optionElement['value']);
$content = $optionElement['content'];
unset($optionElement['content']);
$html .= '<option value="'.$value.'"';
// write every attributes of optionElement in DOM
if(!empty($optionElement)) {
foreach($optionElement as $attrName => $attrValue) {
$html .= ' '.$attrName.'="'.$attrValue.'"';
}
}
$html .= '>';
$html .= $content.'</option>';
}
}
$html .= '</select>';
return $html;
}
public function addOption($parameters) {
array_push($this->optionsElement, $parameters);
}
}
It's not perfect and could be improved with handling 'using' => ['id', 'name']
(actually i forced it to use $myObject->getId() and $myObject->getName() )
Here is an use case example in my Form class :
// Category
$categorys = $this->modelsManager->createBuilder()
->from('Category')
->where('type = :type:', array('type' => \Category::TYPE_SERVICE))
->andWhere('enabled = true')
->orderBy('lft')
->getQuery()
->execute();
$element = new \SelectEnhanced('category', $categorys,
array(
'useEmpty' => true,
'emptyText' => '',
'options' => array(
'disabled' => function($category){return !$category->getParentId() ? 'disabled' : false;},
'class' => 'myCustomClass'
)
)
);
$element->setLabel('Category');
$element->setAttribute('required', '');
$this->add($element);
Let me know if you like it and/or have suggestions to improve it !
+1
+1
+1
+1
+5
+1
+1
+1
And please, selected attr too :)
I've made these from 2.0 branch
See using option? First two elements in array have same meaning as before, but each element after the first two will be used to generate option attributes.
use Jungle\Backend\Forms\Element\Select as SelectEnchanced;
$categories = new SelectEnchanced('categories');
$categories->setLabel($this->translator->_('LABEL_CATEGORIES'));
$categories->setOptions(TourCategoryComplex::find());
$categories->setAttributes(array(
'class' => 'form-control',
'multiple' => true,
'name' => 'categories[]',
'using' => array('id', 'full_name', 'parent_id', 'created_at')
));
$this->add($categories);
This will generate (Category with id 1 is not visible since it's root that holds first level categories. Just so you dont get confused):
<select id="categories" name="categories[]" class="form-control" multiple="1">
<option value="4" parent_id="1" created_at="2015-18-04 01:39:14">Test cat 3</option>
<option value="2" parent_id="1" created_at="2015-18-04 01:36:35">Test cat 1</option>
<option value="3" parent_id="2" created_at="2015-18-04 01:37:24">Test cat 1 > Test cat 2</option>
</select>
NOTE THE NAMESPACES. CHANGE THEM SO THEY FIT YOU
Only difference here is that we're rendering field with our Tag\Select.
<?php
namespace Jungle\Backend\Forms\Element;
use Phalcon\Forms\Element\Select as PhSelect;
/**
* Phalcon\Forms\Element\Select
*
* Component SELECT (choice) for forms
*/
class Select extends PhSelect {
/**
* Renders the element widget returning html
*
* @param array attributes
* @return string
*/
public function render($attributes = null)
{
/**
* Merged passed attributes with previously defined ones
*/
return \Jungle\Tag\Select::selectField($this->prepareAttributes($attributes), $this->_optionsValues);
}
}
This is Phalcon\Tag\Select from branch 2.0 rewritten in PHP with ::_renderAttributes() added to it as originally Phalcon\Tag from 2.0 branch has that method and Phalcon\Tag\Select utilizes it, but it's missing in Phalcon\Tag from 1.3, so instead of rewriting Phalcon\Tag as well, I just added that method to Tag\Select.
NOTE: It's late and I haven't ported _optionsFromArray() from Zephir to PHP, so currently it only works with objects
<?php
namespace Jungle\Tag;
use Phalcon\Tag\Exception;
use Phalcon\Tag as BaseTag;
/**
* Jungle\Tag\Select
*
* Generates a SELECT html tag using a static array of values or a Phalcon\Mvc\Model resultset
*/
abstract class Select
{
/**
* Generates a SELECT tag
*
* @param array parameters
* @param array data
*/
public static function selectField($parameters, $data = null)
{
if (!is_array($parameters)) {
$params = [$parameters, $data];
} else {
$params = $parameters;
}
if (isset($params[0])) {
$id = $params[0];
} else {
$params[0] = $params['id'];
}
/**
* Automatically assign the id if the name is not an array
*/
if (strpos($id, '[') === false) {
if (!isset($params['id'])) {
$params['id'] = $id;
}
}
if (isset($params['name'])) {
$name = $params['name'];
} else {
$params['name'] = $id;
}
if (!isset($params['value'])) {
$value = BaseTag::getValue($id, $params);
} else {
$value = $params['value'];
unset($params['value']);
}
if (isset($params['useEmpty'])) {
$useEmpty = $params['useEmpty'];
if (isset($params['emptyValue'])) {
$emptyValue = $params['emptyValue'];
unset($params['emptyValue']);
} else {
$emptyValue = '';
}
if (isset($params['emptyText'])) {
$emptyText = $params['emptyText'];
unset($params['emptyText']);
} else {
$emptyText = 'Choose...';
}
unset($params['useEmpty']);
}
if (isset($params[1])) {
$options = $params[1];
} else {
$options = $data;
}
if (is_object($options)) {
/**
* The options is a resultset
*/
if (isset($params['using'])) {
$using = $params['using'];
if (!is_array($using) && !is_object($using)) {
throw new Exception("The 'using' parameter should be an array");
}
} else {
throw new Exception("The 'using' parameter is required");
}
}
unset($params['using']);
$code = self::_renderAttributes("<select", $params) . ">" . PHP_EOL;
if ($useEmpty) {
/**
* Create an empty value
*/
$code .= "\t<option value=\"" . $emptyValue . "\">" . $emptyText . "</option>" . PHP_EOL;
}
if (is_object($options)) {
/**
* Create the SELECT's option from a resultset
*/
$code .= self::_optionsFromResultset($options, $using, $value, "</option>" . PHP_EOL);
} else {
if (is_array($options)) {
/**
* Create the SELECT's option from an array
*/
$code .= self::_optionsFromArray($options, $value, "</option>" . PHP_EOL);
} else {
throw new Exception("Invalid data provided to SELECT helper");
}
}
$code .= "</select>";
return $code;
}
/**
* Generate the OPTION tags based on a resulset
*
* @param Phalcon\Mvc\Model\Resultset resultset
* @param array using
* @param mixed value
* @param string closeOption
*/
protected static function _optionsFromResultset($resultset, $using, $value, $closeOption)
{
$code = "";
$params = null;
if (is_array($using)) {
$usingZero = $using[0];
$usingOne = $using[1];
# Unseting 'using' options which are used for value and text
unset($using[0], $using[1]);
}
foreach ($resultset as $option) {
if (is_array($using)) {
if (is_object($option)) {
if (method_exists($option, "readAttribute")) {
$optionValue = $option->readAttribute($usingZero);
$optionText = $option->readAttribute($usingOne);
} else {
$optionValue = $option->$usingZero;
$optionText = $option->$usingOne;
}
# We're getting attributes from the object
$optionAttributes = array();
foreach ($using as $property) {
if (method_exists($option, "readAttribute")) {
$optionAttributes[$property] = $option->readAttribute($property);
} else {
$optionAttributes[$property] = $option->$poperty;
}
}
} else {
if (is_array($option)) {
$optionValue = $option[$usingZero];
$optionText = $option[$usingOne];
# Or from array?
$optionAttributes = array();
foreach ($using as $property) {
$optionAttributes[$property] = $option[$property];
}
} else {
throw new Exception("Resultset returned an invalid value");
}
}
# Now let's render them :)
$attributes = '';
foreach ($optionAttributes as $attribute => $value) {
$attributes = ' ' . $attribute . '="' . $value . '"';
}
/**
* If the value is equal to the option"s value we mark it as selected
*/
if (is_array($value)) {
if (in_array($optionValue, $value)) {
$code .= "\t<option selected=\"selected\" value=\"" . $optionValue . "\"" . $attributes . ">" . $optionText . $closeOption;
} else {
$code .= "\t<option value=\"" . $optionValue . "\"" . $attributes . ">" . $optionText . $closeOption;
}
} else {
if ($optionValue == $value) {
$code .= "\t<option selected=\"selected\" value=\"" . $optionValue . "\"" . $attributes . ">" . $optionText . $closeOption;
} else {
$code .= "\t<option value=\"" . $optionValue . "\"" . $attributes . ">" . $optionText . $closeOption;
}
}
} else {
/**
* Check if using is a closure
*/
if (is_object($using)) {
if (is_null($params)) {
$params = [];
}
$params[0] = $option;
$code .= call_user_func_array($using, $params);
}
}
}
return $code;
}
/**
* Generate the OPTION tags based on an array
*
* @param array data
* @param mixed value
* @param string closeOption
*/
protected static function _optionsFromArray($data, $value, $closeOption)
{
/*var code, optionValue, optionText, escaped;
let code = "";
for optionValue, optionText in data {
let escaped = htmlspecialchars(optionValue);
if typeof optionText == "array" {
let code .= "\t<optgroup label=\"" . escaped . "\">" . PHP_EOL . self::_optionsFromArray(optionText, value, closeOption) . "\t</optgroup>" . PHP_EOL;
} else {
if typeof value == "array" {
if in_array(optionValue, value) {
let code .= "\t<option selected=\"selected\" value=\"" . escaped . "\">" . optionText . closeOption;
} else {
let code .= "\t<option value=\"" . escaped . "\">" . optionText . closeOption;
}
} else {
if optionValue == value {
let code .= "\t<option selected=\"selected\" value=\"" . escaped . "\">" . optionText . closeOption;
} else {
let code .= "\t<option value=\"" . escaped . "\">" . optionText . closeOption;
}
}
}
}
return code;*/
}
/**
* Renders parameters keeping order in their HTML attributes
*/
protected static function _renderAttributes($code, $attributes)
{
//var order, escaper, attrs, attribute, value, escaped, key, newCode;
$order = [
"rel" => null,
"type" => null,
"for" => null,
"src" => null,
"href" => null,
"action" => null,
"id" => null,
"name" => null,
"value" => null,
"class" => null
];
$attrs = [];
foreach ($order as $key => $value) {
if (isset($attributes[$key])) {
$attribute = $attributes[$key];
$attrs[$key] = $attribute;
}
}
foreach ($attributes as $key => $value) {
if (!isset($attrs[$key])) {
$attrs[$key] = $value;
}
}
$escaper = BaseTag::getEscaperService();
unset($attrs["escape"]);
$newCode = $code;
foreach ($attrs as $key => $value) {
if (is_string($key) && !is_null($value)) {
if (is_array($value) || is_resource($value)) {
throw new Exception("Value at index: '" . $key . "' type: '" . gettype($value) . "' cannot be rendered");
}
if ($escaper) {
$escaped = $escaper->escapeHtmlAttr($value);
} else {
$escaped = $value;
}
$newCode .= " " . $key . "=\"" . $escaped . "\"";
}
}
return $newCode;
}
}
Hope someone will find this helpful. Use it on your own risk :stuck_out_tongue_winking_eye:
change this line
$attributes = ' ' . $attribute . '="' . $value . '"';
to this
$attributes .= ' ' . $attribute . '="' . $value . '"';
+1
+1
+1
+1
@sergeyklay is this fixed?
$field = new Select("name", Model::find(), [
"using" => function($every_result_from_model_select /*record*/) {
return "<option value='1'>Option 1</option>"
}
]
keep in mind that only works with model::find, not with an array
@sergeyklay any chances this will be implemented before 4.0? Need any help?
聽@scrnjakovic Most likely no. We will continue bug fixes and release some minor releases for 3.x branch in the future, but we are now going to concentrate fully on version 4.0.
Closing in favor of #13855. Will revisit if the community votes for it, or in later versions.