Subclassing * * Define a {@link dataValue()} method that returns a value suitable for * inserting into a single database field. For example, you might tidy up the * format of a date or currency field. Define {@link saveInto()} to totally * customise saving. For example, data might be saved to the filesystem instead * of the data record, or saved to a component of the data record instead of * the data record itself. Define {@link validate()} to validate the form field * and ensure that the content provided is valid. * * @package forms * @subpackage core */ class FormField extends RequestHandler { /** * @var Form */ protected $form; protected $name, $title, $value ,$message, $messageType, $extraClass; /** * @var $description string Adds a "title"-attribute to the markup. * @todo Implement in all subclasses */ protected $description; /** * @var $extraClasses array Extra CSS-classes for the formfield-container */ protected $extraClasses; /** * @config * @var array $default_classes The default classes to apply to the FormField */ private static $default_classes = array(); public $dontEscape; /** * @var $rightTitle string Used in SmallFieldHolder to force a right-aligned label, or in FieldHolder * to create contextual label. */ protected $rightTitle; /** * @var $leftTitle string Used in SmallFieldHolder() to force a left-aligned label with correct spacing. * Please use $title for FormFields rendered with FieldHolder(). */ protected $leftTitle; /** * Stores a reference to the FieldList that contains this object. * @var FieldList */ protected $containerFieldList; /** * @var boolean */ protected $readonly = false; /** * @var boolean */ protected $disabled = false; /** * @var string custom validation message for the Field */ protected $customValidationMessage = ""; /** * Name of the template used to render this form field. If not set, then * will look up the class ancestry for the first matching template where * the template name equals the class name. * * To explicitly use a custom template or one named other than the form * field see {@link setTemplate()}, {@link setFieldHolderTemplate()} * * @var string */ protected $template, $fieldHolderTemplate, $smallFieldHolderTemplate; /** * @var array All attributes on the form field (not the field holder). * Partially determined based on other instance properties, please use {@link getAttributes()}. */ protected $attributes = array(); /** * Takes a fieldname and converts camelcase to spaced * words. Also resolves combined fieldnames with dot syntax * to spaced words. * * Examples: * - 'TotalAmount' will return 'Total Amount' * - 'Organisation.ZipCode' will return 'Organisation Zip Code' * * @param string $fieldName * @return string */ public static function name_to_label($fieldName) { if(strpos($fieldName, '.') !== false) { $parts = explode('.', $fieldName); $label = $parts[count($parts)-2] . ' ' . $parts[count($parts)-1]; } else { $label = $fieldName; } $label = preg_replace("/([a-z]+)([A-Z])/","$1 $2", $label); return $label; } /** * Construct and return HTML tag. * * @param string $tag * @param array $attributes * @param mixed $content * * @return string */ public static function create_tag($tag, $attributes, $content = null) { $preparedAttributes = ''; foreach($attributes as $k => $v) { // Note: as indicated by the $k == value item here; the decisions over what to include in the attributes // can sometimes get finicky if(!empty($v) || $v === '0' || ($k == 'value' && $v !== null) ) { $preparedAttributes .= " $k=\"" . Convert::raw2att($v) . "\""; } } if($content || $tag != 'input') { return "<$tag$preparedAttributes>$content"; } else { return "<$tag$preparedAttributes />"; } } /** * Creates a new field. * * @param string $name The internal field name, passed to forms. * @param string $title The human-readable field label. * @param mixed $value The value of the field. */ public function __construct($name, $title = null, $value = null) { $this->name = $name; $this->title = ($title === null) ? self::name_to_label($name) : $title; if($value !== NULL) $this->setValue($value); parent::__construct(); $this->setupDefaultClasses(); } /** * set up the default classes for the form. This is done on construct so that the default classes can be removed * after instantiation */ protected function setupDefaultClasses() { $defaultClasses = self::config()->get('default_classes'); if ($defaultClasses) { foreach ($defaultClasses as $class) { $this->addExtraClass($class); } } } /** * Return a link to this field. * * @param string $action * * @return string */ public function Link($action = null) { return Controller::join_links($this->form->FormAction(), 'field/' . $this->name, $action); } /** * Returns the HTML ID of the field - used in the template by label tags. * * The ID is generated as FormName_FieldName. All Field functions should ensure * that this ID is included in the field. * * @return string */ public function ID() { return $this->getTemplateHelper()->generateFieldID($this); } /** * Returns the HTML ID for the form field holder element. * * @return string */ public function HolderID() { return $this->getTemplateHelper()->generateFieldHolderID($this); } /** * Returns the current {@link FormTemplateHelper} on either the parent * Form or the global helper set through the {@link Injector} layout. * * To customize a single {@link FormField}, use {@link setTemplate} and * provide a custom template name. * * @return FormTemplateHelper */ public function getTemplateHelper() { if($this->form) { return $this->form->getTemplateHelper(); } return Injector::inst()->get('FormTemplateHelper'); } /** * Returns the raw field name. * * @return string */ public function getName() { return $this->name; } /** * Returns the field message, used by form validation. * * Use {@link setError()} to set this property. * * @return string */ public function Message() { return $this->message; } /** * Returns the field message type, used by form validation. * * Arbitrary value which is mostly used for CSS classes in the rendered HTML, * e.g. "required". Use {@link setError()} to set this property. * * @return string */ public function MessageType() { return $this->messageType; } /** * Returns the field value - used by templates. */ public function Value() { return $this->value; } /** * Method to save this form field into the given {@link DataObject}. * * By default, makes use of $this->dataValue() * * @param DataObjectInterface $record DataObject to save data into */ public function saveInto(DataObjectInterface $record) { if($this->name) { $record->setCastedField($this->name, $this->dataValue()); } } /** * Returns the field value suitable for insertion into the * {@link DataObject}. * * @return mixed */ public function dataValue() { return $this->value; } /** * Returns the field label - used by templates. * * @return string */ public function Title() { return $this->title; } /** * @param string $val * * @return FormField */ public function setTitle($val) { $this->title = $val; return $this; } /** * Gets the contextual label than can be used for additional field description. * Can be shown to the right or under the field in question. * * @return string Contextual label text. */ public function RightTitle() { return $this->rightTitle; } /** * Sets the contextual label. * * @param $val string Text to set on the label. */ public function setRightTitle($val) { $this->rightTitle = $val; return $this; } public function LeftTitle() { return $this->leftTitle; } public function setLeftTitle($val) { $this->leftTitle = $val; return $this; } /** * Compiles all CSS-classes. Optionally includes a "nolabel"-class * if no title was set on the formfield. * Uses {@link Message()} and {@link MessageType()} to add validatoin * error classes which can be used to style the contained tags. * * @return string CSS-classnames */ public function extraClass() { $classes = array(); $classes[] = $this->Type(); if($this->extraClasses) $classes = array_merge($classes, array_values($this->extraClasses)); // Allow customization of label and field tag positioning if(!$this->Title()) $classes[] = "nolabel"; // Allow custom styling of any element in the container based // on validation errors, e.g. red borders on input tags. // CSS-Class needs to be different from the one rendered // through {@link FieldHolder()} if($this->Message()) $classes[] .= "holder-" . $this->MessageType(); return implode(' ', $classes); } /** * Add one or more CSS-classes to the formfield-container. Multiple class * names should be space delimited. * * @param string $class */ public function addExtraClass($class) { $classes = preg_split('/\s+/', $class); foreach ($classes as $class) { $this->extraClasses[$class] = $class; } return $this; } /** * Remove one or more CSS-classes from the formfield-container. * * @param string $class */ public function removeExtraClass($class) { $classes = preg_split('/\s+/', $class); foreach ($classes as $class) { unset($this->extraClasses[$class]); } return $this; } /** * Set an HTML attribute on the field element, mostly an tag. * * Some attributes are best set through more specialized methods, to avoid interfering with built-in behaviour: * - 'class': {@link addExtraClass()} * - 'title': {@link setDescription()} * - 'value': {@link setValue} * - 'name': {@link setName} * * CAUTION Doesn't work on most fields which are composed of more than one HTML form field: * AjaxUniqueTextField, CheckboxSetField, CompositeField, ConfirmedPasswordField, * CountryDropdownField, CreditCardField, CurrencyField, DateField, DatetimeField, FieldGroup, GridField, * HtmlEditorField, ImageField, ImageFormAction, InlineFormAction, ListBoxField, etc. * * @param string * @param string */ public function setAttribute($name, $value) { $this->attributes[$name] = $value; return $this; } /** * Get an HTML attribute defined by the field, or added through {@link setAttribute()}. * Caution: Doesn't work on all fields, see {@link setAttribute()}. * * @return string */ public function getAttribute($name) { $attrs = $this->getAttributes(); if(isset($attrs[$name])) return $attrs[$name]; } /** * Allows customization through an 'updateAttributes' hook on the base class. * Existing attributes are passed in as the first argument and can be manipulated, * but any attributes added through a subclass implementation won't be included. * * @return array */ public function getAttributes() { $attrs = array( 'type' => 'text', 'name' => $this->getName(), 'value' => $this->Value(), 'class' => $this->extraClass(), 'id' => $this->ID(), 'disabled' => $this->isDisabled(), 'readonly' => $this->isReadonly() ); if ($this->Required()) { $attrs['required'] = 'required'; $attrs['aria-required'] = 'true'; } $attrs = array_merge($attrs, $this->attributes); $this->extend('updateAttributes', $attrs); return $attrs; } /** * @param Array Custom attributes to process. Falls back to {@link getAttributes()}. * If at least one argument is passed as a string, all arguments act as excludes by name. * * @return string HTML attributes, ready for insertion into an HTML tag */ public function getAttributesHTML($attrs = null) { $exclude = (is_string($attrs)) ? func_get_args() : null; if(!$attrs || is_string($attrs)) { $attrs = $this->getAttributes(); } // Remove empty $attrs = array_filter((array)$attrs, function($v) { return ($v || $v === 0 || $v === '0'); }); // Remove excluded if($exclude) { $attrs = array_diff_key($attrs, array_flip($exclude)); } // Create markup $parts = array(); foreach($attrs as $name => $value) { $parts[] = ($value === true) ? "{$name}=\"{$name}\"" : "{$name}=\"" . Convert::raw2att($value) . "\""; } return implode(' ', $parts); } /** * Returns a version of a title suitable for insertion into an HTML * attribute. * * @return string */ public function attrTitle() { return Convert::raw2att($this->title); } /** * Returns a version of a title suitable for insertion into an HTML * attribute. * * @return string */ public function attrValue() { return Convert::raw2att($this->value); } /** * Set the field value. * * @param mixed $value * @param mixed $data Optional data source passed in by {@see Form::loadDataFrom} * @return FormField Self reference */ public function setValue($value) { $this->value = $value; return $this; } /** * Set the field name * * @param string $name * * @return FormField */ public function setName($name) { $this->name = $name; return $this; } /** * Set the container form. * * This is called whenever you create a new form and put fields inside it, * so that you don't have to worry about linking the two. * * @param Form * * @return FormField */ public function setForm($form) { $this->form = $form; return $this; } /** * Get the currently used form. * * @return Form */ public function getForm() { return $this->form; } /** * Return TRUE if security token protection is enabled on the parent * {@link Form}. * * @return bool */ public function securityTokenEnabled() { $form = $this->getForm(); if(!$form) { return false; } return $form->getSecurityToken()->isEnabled(); } /** * @param string $message Message to show to the user. Allows HTML content, * which means you need to use Convert::raw2xml() for any user supplied data. * @param string $messageType * @return FormField */ public function setError($message, $messageType) { $this->message = $message; $this->messageType = $messageType; return $this; } /** * Set the custom error message to show instead of the default * format of Please Fill In XXX. Different from setError() as * that appends it to the standard error messaging. * * @param string $msg Message for the error * * @return FormField */ public function setCustomValidationMessage($msg) { $this->customValidationMessage = $msg; return $this; } /** * Get the custom error message for this form field. If a custom * message has not been defined then just return blank. The default * error is defined on {@link Validator}. * * @return string */ public function getCustomValidationMessage() { return $this->customValidationMessage; } /** * Set name of template (without path or extension). * * Caution: Not consistently implemented in all subclasses, please check * the {@link Field()} method on the subclass for support. * * @param string $template * * @return FormField */ public function setTemplate($template) { $this->template = $template; return $this; } /** * @return string */ public function getTemplate() { return $this->template; } /** * @return string */ public function getFieldHolderTemplate() { return $this->fieldHolderTemplate; } /** * Set name of template (without path or extension) for the holder, * which in turn is responsible for rendering {@link Field()}. * * Caution: Not consistently implemented in all subclasses, * please check the {@link Field()} method on the subclass for support. * * @param string $template * * @return FormField */ public function setFieldHolderTemplate($template) { $this->fieldHolderTemplate = $template; return $this; } /** * @return string */ public function getSmallFieldHolderTemplate() { return $this->smallFieldHolderTemplate; } /** * Set name of template (without path or extension) for the small holder, * which in turn is responsible for rendering {@link Field()}. * * Caution: Not consistently implemented in all subclasses, * please check the {@link Field()} method on the subclass for support. * * @param string */ public function setSmallFieldHolderTemplate($template) { $this->smallFieldHolderTemplate = $template; return $this; } /** * Returns the form field - used by templates. * Although FieldHolder is generally what is inserted into templates, all of the field holder * templates make use of $Field. It's expected that FieldHolder will give you the "complete" * representation of the field on the form, whereas Field will give you the core editing widget, * such as an input tag. * * @param array $properties key value pairs of template variables * @return string */ public function Field($properties = array()) { $obj = ($properties) ? $this->customise($properties) : $this; $this->extend('onBeforeRender', $this); return $obj->renderWith($this->getTemplates()); } /** * Returns a "field holder" for this field - used by templates. * * Forms are constructed by concatenating a number of these field holders. * The default field holder is a label and a form field inside a div. * @see FieldHolder.ss * * @param array $properties key value pairs of template variables * @return string */ public function FieldHolder($properties = array()) { $obj = ($properties) ? $this->customise($properties) : $this; return $obj->renderWith($this->getFieldHolderTemplates()); } /** * Returns a restricted field holder used within things like FieldGroups. * * @param array $properties * * @return string */ public function SmallFieldHolder($properties = array()) { $obj = ($properties) ? $this->customise($properties) : $this; return $obj->renderWith($this->getSmallFieldHolderTemplates()); } /** * Returns an array of templates to use for rendering {@link FieldH} * * @return array */ public function getTemplates() { return $this->_templates($this->getTemplate()); } /** * Returns an array of templates to use for rendering {@link FieldHolder} * * @return array */ public function getFieldHolderTemplates() { return $this->_templates( $this->getFieldHolderTemplate(), '_holder' ); } /** * Returns an array of templates to use for rendering {@link SmallFieldHolder} * * @return array */ public function getSmallFieldHolderTemplates() { return $this->_templates( $this->getSmallFieldHolderTemplate(), '_holder_small' ); } /** * Generate an array of classname strings to use for rendering this form * field into HTML * * @param string $custom custom template (if set) * @param string $suffix template suffix * * @return array $stack a stack of */ private function _templates($custom = null, $suffix = null) { $matches = array(); foreach(array_reverse(ClassInfo::ancestry($this)) as $className) { $matches[] = $className . $suffix; if($className == "FormField") break; } if($custom) array_unshift($matches, $custom); return $matches; } /** * Returns true if this field is a composite field. * To create composite field types, you should subclass {@link CompositeField}. */ public function isComposite() { return false; } /** * Returns true if this field has its own data. * Some fields, such as titles and composite fields, don't actually have any data. It doesn't * make sense for data-focused methods to look at them. By overloading hasData() to return false, * you can prevent any data-focused methods from looking at it. * * @see FieldList::collateDataFields() */ public function hasData() { return true; } /** * @return boolean */ public function isReadonly() { return $this->readonly; } /** * Sets readonly-flag on form-field. Please use performReadonlyTransformation() * to actually transform this instance. * @param $bool boolean Setting "false" has no effect on the field-state. */ public function setReadonly($bool) { $this->readonly = $bool; return $this; } /** * @return boolean */ public function isDisabled() { return $this->disabled; } /** * Sets disabed-flag on form-field. Please use performDisabledTransformation() * to actually transform this instance. * @param $bool boolean Setting "false" has no effect on the field-state. */ public function setDisabled($bool) { $this->disabled = $bool; return $this; } /** * Returns a readonly version of this field */ public function performReadonlyTransformation() { $readonlyClassName = $this->class . '_Disabled'; if(ClassInfo::exists($readonlyClassName)) { $clone = $this->castedCopy($readonlyClassName); } else { $clone = $this->castedCopy('ReadonlyField'); $clone->setReadonly(true); } return $clone; } /** * Return a disabled version of this field. * Tries to find a class of the class name of this field suffixed with "_Disabled", * failing that, finds a method {@link setDisabled()}. * * @return FormField */ public function performDisabledTransformation() { $disabledClassName = $this->class . '_Disabled'; if(ClassInfo::exists($disabledClassName)) { $clone = $this->castedCopy($disabledClassName); } else { $clone = clone $this; $clone->setDisabled(true); } return $clone; } public function transform(FormTransformation $trans) { return $trans->transform($this); } public function hasClass($class){ $patten = '/'.strtolower($class).'/i'; $subject = strtolower($this->class." ".$this->extraClass()); return preg_match($patten, $subject); } /** * Returns the field type - used by templates. * The field type is the class name with the word Field dropped off the end, all lowercase. * It's handy for assigning HTML classes. Doesn't signify the attribute, * see {link getAttributes()}. * * @return string */ public function Type() { return strtolower(preg_replace('/Field$/', '', $this->class)); } /** * @deprecated 3.2 Use FormField::create_tag() */ public function createTag($tag, $attributes, $content = null) { Deprecation::notice('3.2', 'Use FormField::create_tag()'); return self::create_tag($tag, $attributes, $content); } /** * Validation method each {@link FormField} subclass should implement, * determining whether the field is valid or not based on the value. * * @param Validator $validator * @return boolean */ public function validate($validator) { return true; } /** * Describe this field, provide help text for it. * By default, renders as a * underneath the form field. * * @return string Description */ public function setDescription($description) { $this->description = $description; return $this; } /** * @return string */ public function getDescription() { return $this->description; } public function debug() { return "$this->class ($this->name: $this->title : $this->message)" . " = $this->value"; } /** * This function is used by the template processor. If you refer to a field as a $ variable, it * will return the $Field value. */ public function forTemplate() { return $this->Field(); } /** * @uses Validator->fieldIsRequired() * @return boolean */ public function Required() { if($this->form && ($validator = $this->form->Validator)) { return $validator->fieldIsRequired($this->name); } } /** * Set the FieldList that contains this field. * * @param FieldList $list * @return FieldList */ public function setContainerFieldList($list) { $this->containerFieldList = $list; return $this; } /** * Get the FieldList that contains this field. * * @return FieldList */ public function getContainerFieldList() { return $this->containerFieldList; } public function rootFieldList() { if(is_object($this->containerFieldList)) return $this->containerFieldList->rootFieldList(); else user_error("rootFieldList() called on $this->class object without a containerFieldList", E_USER_ERROR); } /** * Returns another instance of this field, but "cast" to a different class. * The logic tries to retain all of the instance properties, * and may be overloaded by subclasses to set additional ones. * * Assumes the standard FormField parameter signature with * its name as the only mandatory argument. Mainly geared towards * creating *_Readonly or *_Disabled subclasses of the same type, * or casting to a {@link ReadonlyField}. * * Does not copy custom field templates, since they probably won't apply to * the new instance. * * @param String $classOrCopy Class name for copy, or existing copy instance to update * @return FormField */ public function castedCopy($classOrCopy) { $field = (is_object($classOrCopy)) ? $classOrCopy : new $classOrCopy($this->name); $field ->setValue($this->value) // get value directly from property, avoid any conversions ->setForm($this->form) ->setTitle($this->Title()) ->setLeftTitle($this->LeftTitle()) ->setRightTitle($this->RightTitle()) ->addExtraClass($this->extraClass()) ->setDescription($this->getDescription()); // Only include built-in attributes, ignore anything // set through getAttributes(), since those might change important characteristics // of the field, e.g. its "type" attribute. foreach($this->attributes as $k => $v) { $field->setAttribute($k, $v); } $field->dontEscape = $this->dontEscape; return $field; } }