Jump to content
Gadgetto

Custom multi-column Fieldtype + Inputfield - extending for multi language usage?

Recommended Posts

Hi,

for my GroupMailer module I've created a custom Fieldtype + Inputfield module which provides multi-column field values. The first field column is a visible text field and there are some other columns which are not presented to user (they are rendered as hidden form fields).

262017940_Bildschirmfoto2019-02-20um19_42_04.png.0d61e10a7761a26218c16bc01c60aea4.png

This is the database schema:

$schema['data'] = 'text NOT NULL'; // we're using 'data' to represent our 'subject' field
$schema['sendstatus'] = 'tinyint NOT NULL DEFAULT 0'; // message send status
$schema['recipients'] = "int(10) unsigned NOT NULL DEFAULT 0";  // recipients counter
$schema['sent'] = "int(10) unsigned NOT NULL DEFAULT 0";  // sent counter
$schema['started'] = "int(10) unsigned NOT NULL DEFAULT 0";  // message sending start
$schema['finished'] = "int(10) unsigned NOT NULL DEFAULT 0";  // message sending finished

This are the ___wakeupValue and ___sleepValue methods:

Spoiler

    /**
     * Convert from DB storage to API value.
     *
     * @param Page $page
     * @param Field $field
     * @param string|int|array $value
     * @return string|int|array|object $messagemeta
     *
     */
    public function ___wakeupValue(Page $page, Field $field, $value) {

        // if for some reason we already get a valid value, then just return it
        if ($value instanceof MessageMeta) return $value; 

        // start a blank value to be populated
        $messagemeta = $this->getBlankValue($page, $field); 

        // if we were given a blank value, then we've got nothing to do: just return a blank MessageMeta
        if (empty($value) || !is_array($value)) return $messagemeta; 

        // create new MessageMeta object
        $messagemeta = new MessageMeta();
        $messagemeta->subject = $value['data']; // @note: we're converting 'data' to 'subject'
        $messagemeta->sendstatus = $value['sendstatus'];
        $messagemeta->recipients = $value['recipients'];
        $messagemeta->sent = $value['sent'];
        $messagemeta->started = $value['started'];
        $messagemeta->finished = $value['finished']; 

        return $messagemeta;  
    }

    /**
     * Convert from API to DB storage value. 
     *              
     * @param Page $page
     * @param Field $field
     * @param string|int|array|object $value
     * @return array
     *
     */
    public function ___sleepValue(Page $page, Field $field, $value) {
        $messagemeta = $value;
        $sleepValue = array();
        // if we are given something other than an MessageMeta, 
        // then just return a blank array
        if (!$messagemeta instanceof MessageMeta) return $sleepValue; 

        // set MessageMeta to sleepValue
        $sleepValue = array(
            'data' => $messagemeta->subject,  // @note: subject is becoming data
            'sendstatus' => (int) $messagemeta->sendstatus,
            'recipients' => (int) $messagemeta->recipients, 
            'sent' => (int) $messagemeta->sent, 
            'started' => (int) $messagemeta->started, 
            'finished' => (int) $messagemeta->finished, 
        );

        return $sleepValue;
    }

 

Now I try to extend this Fieldtype/Inputfield to provide multi language features.

Only the first value ("data" which represents the "subject" field) should be/needs to be multi language!

I had a look at the built in Fieldtypes (e.g FieldtypeText & FieldtypeTextLanguage) which provides multi language support but I couldn't find a similar case (multi-value field with language support). All built in Fieldtypes are single-value fields.

I know this is a very "general" question but maybe somebody could push me in the right direction?

Share this post


Link to post
Share on other sites

I've never created any custom field types myself, but maybe this recent thread helps:

 

Share this post


Link to post
Share on other sites
On 2/20/2019 at 7:58 PM, dragan said:

I've never created any custom field types myself, but maybe this recent thread helps:

Thanks, but this doesn't help. It addresses a different problem.

Share this post


Link to post
Share on other sites

I' really stuck here guys... 😞 Documentation is no help. Comparing with other multi-language fields doesn't help because they are all single-value fields.

Here are the full sources of my multi-value input field:

FieldtypeMessageMeta.module.php

Spoiler

<?php namespace ProcessWire;

/**
 * Fieldtype that stores MessageMeta for GroupMailer Messages
 *
 */

class FieldtypeMessageMeta extends Fieldtype {

    public static function getModuleInfo() {
        return array(
            'title' => 'GroupMailer MessageMeta',
            'version' => 1,
            'summary' => 'Fieldtype that stores meta data for GroupMailer Messages.',
            'installs' => 'InputfieldMessageMeta',
            //'requires' => 'ProcessGroupMailer', // needed to be uninstalled with ProcessGroupMailer!
        );
    }

    /**
     * Initialize this Fieldtype
     *
     */
    public function init() {
        parent::init();
        require_once dirname(__FILE__) . DIRECTORY_SEPARATOR . 'MessageMeta.php'; 
    }

    /**
     * Return the required Inputfield used to populate a field of this type
     * 
     * @param Page $page
     * @param Field $field
     * @return null|_Module|\Inputfield|\Module
     * @throws WirePermissionException
     * 
     */
    public function getInputfield(Page $page, Field $field) {
        /** @var InputfieldMessageMeta $inputfield */
        $inputfield = $this->modules->get('InputfieldMessageMeta'); 

        // our inputfield requires a Page and Field
        $inputfield->setPage($page);
        $inputfield->setField($field); 

        return $inputfield; 
    }

    /**
     * Return all Fieldtypes derived from FieldtypeMessageMeta, which we will consider compatible
     *
     */
     public function ___getCompatibleFieldtypes(Field $field) {
        $fieldtypes = $this->wire(new Fieldtypes());
        foreach ($this->wire('fieldtypes') as $fieldtype) {
            if ($fieldtype instanceof FieldtypeMessageMeta) {
                $fieldtypes->add($fieldtype);
            }
        }
        return $fieldtypes; 
    }

    /**
     * Return a blank ready-to-populate version of a field of this type
     *
     */
    public function getBlankValue(Page $page, Field $field) {
        $messagemeta = new MessageMeta($page);
        $messagemeta->setTrackChanges(true); 
        return $messagemeta; 
    }


    /**
     * Convert from DB storage to API value.
     *
     * @param Page $page
     * @param Field $field
     * @param string|int|array $value
     * @return string|int|array|object $messagemeta
     *
     */
    public function ___wakeupValue(Page $page, Field $field, $value) {

        // if for some reason we already get a valid value, then just return it
        if ($value instanceof MessageMeta) return $value; 

        // generate a blank value to be populated
        $messagemeta = $this->getBlankValue($page, $field); 

        // if we were given a blank value, then we've got nothing to do: just return a blank MessageMeta
        if (empty($value) || !is_array($value)) return $messagemeta; 

        // create new MessageMeta object
        $messagemeta = new MessageMeta();
        $messagemeta->subject = $value['data']; // @note: we're converting 'data' to 'subject'
        $messagemeta->sendstatus = $value['sendstatus'];
        $messagemeta->recipients = $value['recipients'];
        $messagemeta->sent = $value['sent'];
        $messagemeta->started = $value['started'];
        $messagemeta->finished = $value['finished']; 

        return $messagemeta;  
    }

    /**
     * Convert from API to DB storage value. 
     *              
     * @param Page $page
     * @param Field $field
     * @param string|int|array|object $value
     * @return array
     *
     */
    public function ___sleepValue(Page $page, Field $field, $value) {
        $messagemeta = $value;
        $sleepValue = array();
        // if we are given something other than an MessageMeta, 
        // then just return a blank array
        if (!$messagemeta instanceof MessageMeta) return $sleepValue; 

        // set MessageMeta to sleepValue
        $sleepValue = array(
            'data' => $messagemeta->subject,  // @note: subject is becoming data
            'sendstatus' => (int) $messagemeta->sendstatus,
            'recipients' => (int) $messagemeta->recipients, 
            'sent' => (int) $messagemeta->sent, 
            'started' => (int) $messagemeta->started, 
            'finished' => (int) $messagemeta->finished, 
        );

        return $sleepValue;
    }

    /**
     * Given a value, make it clean for storage within a Page
     *
     * @param Page $page
     * @param Field $field
     * @param int|object|WireArray|string $value
     * @return int|null|object|MessageMeta|WireArray|string
     * @throws WireException
     * 
     */
    public function sanitizeValue(Page $page, Field $field, $value) {

        // if given a blank value, return a valid blank value
        if (empty($value)) return $this->getBlankValue($page, $field, $value); 

        // if given something other than an MessageMeta, throw an error
        if (!$value instanceof MessageMeta) {
            throw new WireException("Value set to field '$field->name' must be MessageMeta data"); 
        }

        // note that sanitization of individual fields within a given MessageMeta is already 
        // performed by the MessageMeta::set() method, so we don't need to do anything else here.

        return $value; 	
    }

    /**
     * Format a value for output, called when a Page's outputFormatting is on
     *
     */
    public function formatValue(Page $page, Field $field, $value) {
        // we actually don't need to do anything in here since each MessageMeta object
        // is doing this work in the MessageMeta::get() method.
        return $value; 
    }

    /**
     * Return the database schema that defines a MessageMeta object
     *
     */
    public function getDatabaseSchema(Field $field) {
        $schema = parent::getDatabaseSchema($field); 

        $schema['data'] = 'text NOT NULL'; // we're using 'data' to represent our 'subject' field
        $schema['sendstatus'] = 'tinyint NOT NULL DEFAULT 0'; // message send status
        $schema['recipients'] = "int(10) unsigned NOT NULL DEFAULT 0";  // recipients counter
        $schema['sent'] = "int(10) unsigned NOT NULL DEFAULT 0";  // sent counter
        $schema['started'] = "int(10) unsigned NOT NULL DEFAULT 0";  // message sending start
        $schema['finished'] = "int(10) unsigned NOT NULL DEFAULT 0";  // message sending finished

        // indexes, for any fields that need to be searchable from selectors
        $schema['keys']['data'] = 'FULLTEXT KEY `data` (`data`)';
        $schema['keys']['sendstatus'] = 'KEY sendstatus (sendstatus)';
        $schema['keys']['recipients'] = "KEY recipients (recipients)";
        $schema['keys']['sent'] = "KEY sent (sent)";
        $schema['keys']['started'] = "KEY started (started)";
        $schema['keys']['finished'] = "KEY finished (finished)";

        return $schema; 
    }

    /**
     * Method called when the field is database-queried from a selector 
     *
     */
    public function getMatchQuery($query, $table, $subfield, $operator, $value) {

        // If searching 'subject' then assume our default (data) field 
        if ($subfield == 'subject') $subfield = 'data';

        // if value is a formatted date, convert it to unix timestamp
        if (!ctype_digit("$value")) $value = strtotime($value);

        return parent::getMatchQuery($query, $table, $subfield, $operator, $value); 
    }

}

 

FieldtypeMessageMetaLanguage.module.php

Spoiler

<?php namespace ProcessWire;

require_once wire('config')->paths->modules . 'LanguageSupport/FieldtypeLanguageInterface.php';

/**
 * Multi-language capable MessageMeta field
 *
 */

class FieldtypeMessageMetaLanguage extends FieldtypeMessageMeta implements FieldtypeLanguageInterface {

    public static function getModuleInfo() {
        return array(
            'title' => 'GroupMailer MessageMeta (Multi-language)',
            'version' => 1,
            'summary' => 'Fieldtype that stores meta data for GroupMailer Messages in multiple languages.',
            'requires' => 'LanguageSupportFields',
            //'requires' => 'ProcessGroupMailer', // needed to be uninstalled with ProcessGroupMailer!
        );
    }

    /**
     * Sanitize value for storage
     * 
     * @param Page $page
     * @param Field $field
     * @param LanguagesValueInterface|string $value
     * @return LanguagesPageFieldValue
     *
     */
    public function sanitizeValue(Page $page, Field $field, $value) {
        if (is_object($value) && $value instanceof LanguagesPageFieldValue) {
            // great, already what we wanted
        } else {
            // convert it to a LanguagesPageFieldValue
            $pageValue = $page->data($field->name); // raw unformatted value, with no load possible
            if (!$pageValue instanceof LanguagesPageFieldValue) {
                $pageValue = new LanguagesPageFieldValue($page, $field, $pageValue); // #98
            }
            if (is_array($value)) {
                $pageValue->importArray($value); 
            } else {
                $user = $this->wire('user');
                $language = $user ? $user->language : null;
                if ($language) $pageValue->setLanguageValue($language->id, (string) $value); 
            }
            $value = $pageValue; 
        }
        return $value; 
    }

    /**
     * Return the database schema in specified format
     * 
     * @param Field $field
     * @return array
     *
     */
    public function getDatabaseSchema(Field $field) {
        $schema = parent::getDatabaseSchema($field);
        
        $languageSupport = $this->wire('modules')->get('LanguageSupport'); 
        $maxIndex = (int) $this->wire('database')->getMaxIndexLength();
    
        // note that we use otherLanguagePageIDs rather than wire('languages') because
        // it's possible that this method may be called before the languages are known 
        foreach ($languageSupport->otherLanguagePageIDs as $languageID) {
            // $schema['data' . $languageID] = $schema['data'];
            $schema['data' . $languageID] = 'text';
            $schema['keys']["data_exact{$languageID}"] = "KEY `data_exact{$languageID}` (`data{$languageID}`($maxIndex))";
            $schema['keys']["data{$languageID}"] = "FULLTEXT KEY `data{$languageID}` (`data{$languageID}`)";
        }
    
        return $schema;
    }

    /**
     * Format value for output, basically typecasting to a string and sending to textformatters from FieldtypeText
     * 
     * @param Page $page
     * @param Field $field
     * @param LanguagesValueInterface|string $value
     * @return string
     *
     */
    public function formatValue(Page $page, Field $field, $value) {
        return parent::formatValue($page, $field, (string) $value); 
    }

    /**
     * Given a value, return an portable version of it as array
     *
     * @param Page $page
     * @param Field $field
     * @param string|int|float|array|object|null $value
     * @param array $options Optional settings to shape the exported value, if needed.
     * @return string|float|int|array
     *
     */
    public function ___exportValue(Page $page, Field $field, $value, array $options = array()) {
        if (isset($options['sleepValue'])) {
            // allow a sleepValue option, for use by other language Fieldtypes that delegate
            // their exportValue to this one, like FieldtypeTextareaLanguage
            $sleepValue = $options['sleepValue'];
        } else {
            $sleepValue = $this->sleepValue($page, $field, $value);
        }
        $exportValue = array();
        foreach ($sleepValue as $k => $v) {
            if ($k === 'data') {
                $exportValue['default'] = $v;
            } elseif (strpos($k, 'data') === 0) {
                $languageID = substr($k, 4);
                $language = $this->wire('languages')->get((int) $languageID);
                $exportValue[$language->name] = $v;
            } else {
                $exportValue[$k] = $v;
            }
        }
        return $exportValue;
    }
    
    public function ___importValue(Page $page, Field $field, $value, array $options = array()) {
        if (is_null($value)) $value = '';
        if (is_string($value)) {
            $v = $value;
            $value = $field->type->exportValue($page, $field, $page->getUnformatted($field->name), $options);
            $value['default'] = $v; 
        }
        if (!is_array($value)) {
            throw new WireException('Array value expected for multi-language importValue');
        }
        /** @var Languages $languages */
        $languages = $this->wire('languages');
        /** @var LanguagesPageFieldValue $importValue */
        $importValue = $page->get($field->name); 
        foreach ($value as $languageName => $languageValue) {
            $language = $languages->get($languageName); 
            if (!$language->id) continue; 
            $importValue->setLanguageValue($language->id, $languageValue); 
        }
        return $importValue;
    }

}

 

MessageMeta.php

Spoiler

<?php namespace ProcessWire;

/**
 * An individual MessageMeta object for a Page
 *
 */
class MessageMeta extends WireData {

	/** @var string $dateFormat Holds the date format from wire config */
	protected $dateFormat;

	/**
	 * We keep a copy of the $page that owns this MessageMeta so that we can follow
	 * its outputFormatting state and change our output per that state
	 *
	 */
	protected $page; 

	/**
	 * Construct a new MessageMeta object
	 *
	 */
	public function __construct() {
		$this->dateFormat = $this->wire('config')->dateFormat;

		// define the fields that represent our MessageMeta object (and their default/blank values)
        $this->set('subject', ''); 
        $this->set('sendstatus', 0); 
        $this->set('recipients', 0);
        $this->set('sent', 0);
        $this->set('started', 0); 
		$this->set('finished', 0);
	}

	/**
	 * Set a value to the MessageMeta object
	 *
	 */
	public function set($key, $value) {
		if ($key == 'page') {
			$this->page = $value;
			return $this;

		} elseif ($key == 'subject') {
			// regular text sanitizer
			$value = $this->sanitizer->text($value);

		} elseif ($key == 'started' || $key == 'finished') {
			// convert date string to unix timestamp
			if ($value && !ctype_digit("$value")) $value = strtotime($value); 	

			// sanitized date value is always an integer
			$value = (int) $value;
		}

		return parent::set($key, $value); 
	}

	/**
	 * Retrieve a value from the MessageMeta object
	 *
	 */
	public function get($key) {
		$value = parent::get($key); 

		// if the page's output formatting is on, then we'll return formatted values
		if ($this->page && $this->page->of()) {

			if ($key == 'subject') {
				// return entity encoded versions of string
				$value = $this->sanitizer->entities($value); 
			} elseif ($key == 'started' || $key == 'finished') {
				// format a unix timestamp to a date string
				$value = date($this->dateFormat, $value); 				
			}

		}
		
		return $value; 
	}

	/**
	 * Provide a default rendering for a MessageMeta object
	 *
	 */
	public function renderMessageMeta() {
		// remember page's output formatting state
		//$of = $this->page->of();

		// turn on output formatting for our rendering (if it's not already on)
		//if (!$of) $this->page->of(true);

		$out = "
		<p>
		$this->subject<br>
		$this->sendstatus<br>
		$this->recipients<br>
		$this->sent<br>
		$this->started<br>
		$this->finished
		</p>
		";

		//if (!$of) $this->page->of(false); 
		return $out; 
	}

	/**
	 * Return a string representing this MessageMeta object
	 *
	 */
	public function __toString() {
		return $this->renderMessageMeta();
	}

}

 

InputfieldMessageMeta.module.php

Spoiler

<?php namespace ProcessWire;

/**
 * Inputfield that provides form fields for GroupMailer FieldtypeMessageMeta
 * (some of the form fields are type="hidden")
 *
 */

class InputfieldMessageMeta extends Inputfield {

    public static function getModuleInfo() {
        return array(
            'title' => 'GroupMailer MessageMeta',
            'version' => 1,
            'summary' => 'Provides form input for the GroupMailer MessageMeta Fieldtype (some fields not visible!).',
            'requires' => 'FieldtypeMessageMeta',  // needed to be uninstalled with FieldtypeMessageMeta!
        );
    }

    const subjectDefaultMaxlength = 2048;

    protected $page;    
    protected $field;

    /**
     * Construct
     * 
     * @throws WireException
     * 
     */
    public function __construct() {
        parent::__construct();
        $this->setAttribute('maxlength', $this->getSubjectDefaultMaxlength()); 
        $this->setAttribute('placeholder', '');
        $this->set('requiredAttr', 0);
        $this->set('stripTags', false); // strip tags from input?

        // if multi-language, support placeholders for each language
        $languages = $this->wire('languages');
        if ($languages) foreach ($languages as $language) {
            // set to blank value so that Field::getInputfield() will recogize this setting is for InputfieldText 
            if (!$language->isDefault()) $this->set("placeholder$language", '');
        }
    }

    /**
     * Initialize this Inputfield
     *
     */
    public function init() {
        parent::init();
        require_once dirname(__FILE__) . DIRECTORY_SEPARATOR . 'MessageMeta.php'; 
    }

    public function setPage(Page $page) {
        $this->page = $page; 
    }

    public function setField(Field $field) {
        $this->field = $field;
    }

    /**
     * Get the default maxlength attribute value for subject
     * 
     * @return mixed
     * 
     */
    public function getSubjectDefaultMaxlength() {
        return self::subjectDefaultMaxlength;
    }

    /**
     * @param array|string $key
     * @param array|int|string $value
     * @return $this
     * @throws WireException
     */
    public function setAttribute($key, $value) {
        if ($key == 'value' && !$value instanceof MessageMeta && !is_null($value)) {
            throw new WireException('Value should be an instance of MessageMeta');
        }

        return parent::setAttribute($key, $value);
    }

    /**
     * Render the entire input area for MessageMeta
     * 
     * @return string
     *
     */
    public function ___render() {

        $id = $this->attr('id'); 
        $name = $this->attr('name'); 
        $messagemeta = $this->attr('value');

        $subject = $this->sanitizer->entities($messagemeta->subject);
        //$started = $messagemeta->started > 0 ? date(MessageMeta::dateFormat, $messagemeta->started) : '';
        //$finished = $messagemeta->finished > 0 ? date(MessageMeta::dateFormat, $messagemeta->finished) : '';

        $out = "
        <input type='text' id='_{$id}_subject' name='_{$name}_subject' value='{$subject}' class='uk-input'>
        <input type='hidden' id='_{$id}_sendstatus' name='_{$name}_sendstatus' value='{$messagemeta->sendstatus}'>
        <input type='hidden' id='_{$id}_recipients' name='_{$name}_recipients' value='{$messagemeta->recipients}'>
        <input type='hidden' id='_{$id}_sent' name='_{$name}_sent' value='{$messagemeta->sent}'>
        <input type='hidden' id='_{$id}_started' name='_{$name}_started' value='{$messagemeta->started}'>
        <input type='hidden' id='_{$id}_finished' name='_{$name}_finished' value='{$messagemeta->finished}'>
        ";

        return $out;
    }

    /**
     * Process the input after a form submission
     *
     * @param WireInputData $input
     * @return $this
     * @throws WireException
     * 
     */
    public function ___processInput(WireInputData $input) {

        if (!$this->page || !$this->field) {
            throw new WireException("This inputfield requires that you set valid 'page' and 'field' properties to it."); 
        }

        $name = $this->attr('name'); 
        $messagemeta = $this->field->type->getBlankValue($this->page, $this->field); 

        //if (!isset($input->$name)) return $this; // TODO: ???

        $messagemeta = new MessageMeta();
        $messagemeta->subject = $input["_{$name}_subject"];
        $messagemeta->sendstatus = $input["_{$name}_sendstatus"];
        $messagemeta->recipients = $input["_{$name}_recipients"];
        $messagemeta->sent = $input["_{$name}_sent"];
        $messagemeta->started = $input["_{$name}_started"];
        $messagemeta->finished = $input["_{$name}_finished"];
        
        // if the processed MessageMeta subject is different from the previous,
        // then flag this Inputfield as changed so that it will be automatically saved with the page
        // TODO: ???
        if ($messagemeta->subject != $this->value->subject) {
            $this->attr('value', $messagemeta); 
            $this->trackChange('value'); 
        }

        return $this;
    }

    /**
     * Output API Notes
     *
     * @return $inputfields
     */
    public function ___getConfigInputfields() {
        $inputfields = parent::___getConfigInputfields();

        /** @var InputfieldMarkup $field */
        $field = $this->modules->get('InputfieldMarkup'); 
        $field->label = $this->_('API Notes'); 
        $field->description = $this->_('Individual values from this field can be get using the following from your template files:');
        $field->value = 
            "<pre>" .
            "\$page->{$this->name}->subject" . PHP_EOL .
            "\$page->{$this->name}->sendstatus" . PHP_EOL .
            "\$page->{$this->name}->recipients" . PHP_EOL .
            "\$page->{$this->name}->sent" . PHP_EOL .
            "\$page->{$this->name}->started" . PHP_EOL .
            "\$page->{$this->name}->finished" . PHP_EOL .
            "</pre>";
    
        $inputfields->add($field); 

        return $inputfields; 	
    }

}

 

 

 

 

Share this post


Link to post
Share on other sites

I'm not really getting what the question is, although I haven't looked at your code in full.

4 hours ago, Gadgetto said:

Comparing with other multi-language fields doesn't help because they are all single-value fields.

Are you sure about this? What do you mean by multi-language fields? Maybe install the multi-lingual site profile and see how things work? For instance, the field images works fine as a multi-language field and it is a multi-value field. I think I'm just not getting the question.

4 hours ago, Gadgetto said:

Here are the full sources of my multi-value input field:

if you Fieldtype is meant to store multiple values for the same page (meaning multiple rows in its database table for the same field on the same page) , then this code is not correct:

4 hours ago, Gadgetto said:

class FieldtypeMessageMeta extends Fieldtype {

In that case, you'll need to extend FieldtypeMulti. However, I may have misunderstood what you mean by multiple values. For instance, if you meant multiple database columns on a single db row, that's something different.

  • Like 1

Share this post


Link to post
Share on other sites
2 hours ago, Gadgetto said:

Here are the full sources of my multi-value input field:

Hi @Gadgetto I am sorry for not being able to help but could you please make your thread more readable by "hiding" long code in "Spoiler" blocks (which can be added by clicking on the "eye icon" in the toolbar of this RTE)? 

Share this post


Link to post
Share on other sites
10 hours ago, szabesz said:

Hi @Gadgetto I am sorry for not being able to help but could you please make your thread more readable by "hiding" long code in "Spoiler" blocks (which can be added by clicking on the "eye icon" in the toolbar of this RTE)? 

Sorry for that, I was indeed searching for the "Reveal hidden contents" feature as I saw this in other post. Just couldn't find it. Thanks!

  • Thanks 1

Share this post


Link to post
Share on other sites
12 hours ago, kongondo said:

Are you sure about this? What do you mean by multi-language fields? Maybe install the multi-lingual site profile and see how things work? For instance, the field images works fine as a multi-language field and it is a multi-value field. I think I'm just not getting the question.

I don't know how to describe it better. I thought multi-language field is a common term for fields which provide input for different languages (like the InputfieldText/InpufieldTextLanguage).

12 hours ago, kongondo said:

if you Fieldtype is meant to store multiple values for the same page (meaning multiple rows in its database table for the same field on the same page) , then this code is not correct:

My Fieldtype is a multi-columns field not multi-rows, therefore FieldtypeMulti won't work (as it is only for multi-row fields).

As I explained in my initial post, I created a Fieldtype which has these columns:

$schema['data'] = 'text NOT NULL'; // we're using 'data' to represent our 'subject' field
$schema['sendstatus'] = 'tinyint NOT NULL DEFAULT 0'; // message send status
$schema['recipients'] = "int(10) unsigned NOT NULL DEFAULT 0";  // recipients counter
$schema['sent'] = "int(10) unsigned NOT NULL DEFAULT 0";  // sent counter
$schema['started'] = "int(10) unsigned NOT NULL DEFAULT 0";  // message sending start
$schema['finished'] = "int(10) unsigned NOT NULL DEFAULT 0";  // message sending finished

The "data" col is a text field and I'd like to provide language support for this col only! (the other columns send status, recipients, ... arte integer fields and don't need language support).

I simply can't find an example Fieldtype which provides similar functionality.

Share this post


Link to post
Share on other sites
3 hours ago, Gadgetto said:

I simply can't find an example Fieldtype which provides similar functionality.

There is one actually, sort of. Look at the 'images' field in a multilingual setup. 

As you are aware, for truly (I use this word reservedly) multilingual fields, each language has to have its own column. That makes it easy to search the columns in the language you want. However, in your case, since you want only one column to have multilingual features, you have two choices ( + a 3rd not very good one):

  1. Go the route of images fields. In a multilingual setup, the description column of an image field holds each languages' value, saved as JSON. E.g. {"0":"We're gonna need a bigger terminal.","1012":"Wir brauchen einen größeren Terminal.","1013":"Me tarvitsemme isomman päätteen."}. The index of the values are the language ID. In this case, 0= English, 1012=German and 1013=Finnish.The trade off here is searching in one language is limited.
  2. Change your database design pattern. No need to cram things in if they don't fit 😎. Let your subject be its own multilingual field and let your other single value data live in their own non-multilingual field. Nothing wrong with that. 
  3. I mention this 3rd option hesitantly. Stick with one field as your are doing but for your data (subject) column create a lookup table for other languages. I am no DB guru but the little I know tells me this 3rd option is not a good design at all.
Edited by kongondo
  • Like 3

Share this post


Link to post
Share on other sites

@kongondo Thank you for your hints. In general, I'd like to follow the standard behavior of ProcessWire as closely as possible. So having own "data" columns for each  language would be the best.  I have a Fieldtype "FieldtypeMessageMeta" and try to extend it with "FieldtypeMessageMetaLanguage" - just like other language capable field types do. So an admin can switch between normal Fieldtype and language Fieldtype if necessary.

the longer I think about it, the more uncertain I am if the message subject has to be multilingual at all.

Is it common to automatically send multilingual newsletters? Or is it better to send a separate newsletter for each language?

 

Share this post


Link to post
Share on other sites
4 hours ago, Gadgetto said:

Is it common to automatically send multilingual newsletters? Or is it better to send a separate newsletter for each language?

I might have seen a multilingual newsletter once in my life but I might be wrong, so in my experience multilingual newsletters are very rare.

  • Like 2

Share this post


Link to post
Share on other sites
1 minute ago, szabesz said:

I might have seen a multilingual newsletter once in my life but I might be wrong, so in my experience multilingual newsletters are very rare.

I also think so. So I'll let the field be single-language for now and if there are requests to add multilingual feature it can be added later.

  • Like 2

Share this post


Link to post
Share on other sites
On 2/27/2019 at 5:34 PM, Gadgetto said:

So I'll let the field be single-language for now and if there are requests to add multilingual feature it can be added later.

Just had a thought, so revisiting this. Going by your screenshot, it seems to me that each message is a unique page, no? If that is the case then you may have your cake and eat it too if instead of storing a message's subject in the 'data' column of your Fieldtype, let it be the page's title? This means for multilingual sites, they can decide to have multilingual messages by using language field title and language textarea (assuming that is where the message body is). You would then need to use 'data' to store something else.

 

Just my 2p.

  • Like 1

Share this post


Link to post
Share on other sites
12 hours ago, kongondo said:

Just had a thought, so revisiting this. Going by your screenshot, it seems to me that each message is a unique page, no? If that is the case then you may have your cake and eat it too if instead of storing a message's subject in the 'data' column of your Fieldtype, let it be the page's title? This means for multilingual sites, they can decide to have multilingual messages by using language field title and language textarea (assuming that is where the message body is). You would then need to use 'data' to store something else.

You are right! Each message is a unique page. To be flexible, I decided not to use the title field as email subject. GroupMailer will be designed so that each page can be a message. It will only be necessary to add a custom field (FieldtypeMessageMeta) to the corresponding template.
This field type will include the subject field. So it is possible that pages have separate titles and email subject.

Share this post


Link to post
Share on other sites
25 minutes ago, Gadgetto said:

GroupMailer will be designed so that each page can be a message. It will only be necessary to add a custom field (FieldtypeMessageMeta) to the corresponding template.
This field type will include the subject field. So it is possible that pages have separate titles and email subject.

Gotcha.

Share this post


Link to post
Share on other sites

Based on your description I feel like you're tackling the problem from the wrong end. You're actually trying to do what was requested from MarkupSEO ages ago: Just adding a single field to a template, but getting a whole bunch of fields in the process. In my opinion either let people add all fields to their templates  or even better have an own template and just store the metadata there. Sadly it took quite a few month until the latter option was actually added properly, but now we have it: 

https://processwire.com/blog/posts/processwire-3.0.73-and-new-fieldset-types/#fieldset-page-fieldtypefieldsetpage
https://processwire.com/blog/posts/processwire-3.0.74-adds-new-fieldsetpage-field-type/

Edit:

And some more context from the time I initially proposed the idea (I've no idea if Ryan actually knew this, but it's basically what I proposed): 

 

  • Like 2

Share this post


Link to post
Share on other sites

@LostKobrakai FieldtypeFieldsetPage looks great - didn't even know this exists! Thanks for this hint!

Using a template for configuring/defining a GroupMailer message page was my initial intention. But wouldn't that be too inflexible? Wouldn't it be better if the dev could use any template he likes just by adding a single field? It would also allow to "convert" any existing page to a GroupMailer Message on the fly.

I'd like to hold the Module as flexible as possible but also easy to setup. The longer I work on the project, the more uncertain I will be about the implementation...

Share this post


Link to post
Share on other sites

That's what FieldtypeFieldsetPage would allow you to do. Create the field on installation of your module (and update/delete it from your module; It's not to be edited by the user) and the user would just need to add that field to whatever template needed.

Share this post


Link to post
Share on other sites
Just now, LostKobrakai said:

That's what FieldtypeFieldsetPage would allow you to do. Create the field on installation of your module (and update/delete it from your module; It's not to be edited by the user) and the user would just need to add that field to whatever template needed.

I need to have a look at this Fieldtype and how this is implemented. I currently don't fully understand how this is meant to be used.

Share this post


Link to post
Share on other sites

It's basically like a repeater, but limited to a single page, which you don't need to explicitly create.

Share this post


Link to post
Share on other sites

GroupMailer needs a lot of meta data like some send counters, status-flags, send/finished dates and so on - all meta fields are hidden fields only and not editable by the user. The custom Fieldtype I created has all these columns packed in one field. Using  FieldtypeFieldsetPage would mean that I create separate fields for each of these meta columns (if I understand right). Wouldn't this have a performance impact. Instead of using a single (multi-column) field I'd need to use many separate fields.

Share this post


Link to post
Share on other sites

Sure has, but it's what you do with most everything else in processwire as well. Are the read/write patterns really that demanding that it would matter? 

But if only one field of yours is multi-language I feel like it should be quite possible to adapt existing patterns from other fieldtypes. 

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Recently Browsing   0 members

    No registered users viewing this page.

  • Similar Content

    • By Juergen
      Hello @ all,
      I am creating a new inputfield/fieldtype to store opening hours, but I am struggeling to save values from multiple dynamic created inputfields in 1 column of the database.
      Scenario:
      The user can enter one or more opening times per day in a UI.
      Fe:
      Monday open from 08:00 to 12:00 and from 14:00 to 17:00 Tuesday open from 08:00 to 12:00 and from 14:00 to 19:00 and so on
      Via a little JavaScript you can add as much opening times as you need per day - the additional inputfield will be created dynamically.
      After form submission all the values are in the POST array -> this works (see example below):
      ProcessWire\WireInputData Object ( [openinghours_mo-0-start] => 09:00 [openinghours_mo-0-finish] => 13:00 [openinghours_mo-1-start] => 14:00 [openinghours_mo-1-finish] => 18:00 [openinghours_mo-2-start] => 21:00 [openinghours_mo-2-finish] => 23:00 [openinghours_tu-0-start] => 09:00 [openinghours_tu-0-finish] => 13:00 [openinghours_tu-1-start] => 14:00 [openinghours_tu-1-finish] => 18:00 [openinghours_we-0-start] => 09:00 [openinghours_we-0-finish] => 13:00 [openinghours_we-1-start] => 14:00 [openinghours_we-1-finish] => 18:00 [openinghours_th-0-start] => 09:00 [openinghours_th-0-finish] => 13:00 [openinghours_th-1-start] => 14:00 [openinghours_th-1-finish] => 18:00 [openinghours_fr-0-start] => 09:00 [openinghours_fr-0-finish] => 13:00 [openinghours_fr-1-start] => 14:00 [openinghours_fr-1-finish] => 18:00 [openinghours_sa-0-start] => [openinghours_sa-0-finish] => [openinghours_so-0-start] => [openinghours_so-0-finish] => ) The property name is always the name attribute of the field 😉 . If the property is empty means closed on that day.
      Now I need to combine all those values into 1 array (or json array) and store it in the database in 1 column called 'hours' in my case (see screenshot below):

      In my ___processInput(WireInputData $input) method I have tried to make it work like this:
      public function ___processInput(WireInputData $input): self { $name = $this->attr('name'); $value = $this->attr('value'); //input object includes always every input on the page, so lets filter out only inputs from this field //we need to do this, because the number of values is variable - so extract only values that starts with $name.'_' $nameAttributes = []; foreach($input as $key=>$value){ if(substr($key, 0, strlen($name.'_')) === $name.'_'){ $nameAttributes[$key] = $value; } } // loop through all inputfields of this fieldtype $time_values = []; foreach($nameAttributes as $nameAttr => $value) { $time_values[$nameAttr] = $value; } } //save it in the database $input->set('hours', serialize($time_values)); return $this; } The only important part of this code is the last part with the serialize function.
      After saving it will create a record in the database, but the value is always NULL (default value) (see below).

      Checking $time_values returns all the values, but printing out "$this" shows me that the property "hours" inside the Openinghours object is empty (see below) - so the mistake must be there, but I dont know where?!?!?!?
      [title] => Home [openinghours] => ProcessWire\OpeningHours Object ( [data] => Array ( [hours] => ) ) If I check the sleepValue() method or the sanitizeValue() - they are also empty. So it seems that the values will not reach these methods. I havent found a clear documentation of whats going on behind the saving process of an inputfield.
      As far as I know the saving process starts with the form submission. The values are in the POST array and will be processed by the processInput() method. Before they will be saved in the database they will be sanitized by the sanitizeValue() mehtod and afterwards they will be prepared for storage in the sleepValue() method.  The last step is the storage itself.
      Has someone an idea what is missing by storing values from multiple fields into 1 database column or has someone a working example of such a scenario on github to help me out.
      A clear explanation of the storage process will be also helpful.
      Thanks and best regards
    • By Juergen
      Hello @ all!
      I want to share a simple fieldtype and inputfield to store address data with you.
      I have created this inputfield for learning purposes and it has no fancy functionality. It is simply for storing address data such as street, number, postalcode and so on in one table. As an addition you can store latitude and longitude too, so you can use them in maps.
      Here is a screenshot of what it looks like:

      You can select which fields are mandatory and you can choose if the inputs for longitude and latitude should be displayed. These settings can be configured in the field configuration.
      If you find this inputfield useful you can download it at https://github.com/juergenweb/FieldtypeSimpleAddress
      There you will find a detailed explanation. If you have an idea of an usefull feature that can be added or you have detected a bug, please report it in my github account.
       
    • By MarkE
      Please note that the updated version of this script and any other associated info is now at https://github.com/MetaTunes/Form-update
      I had a need to interactively update the page choices in a multi-choice page select field. I chose to do this with a general-purpose piece of jQuery. By combining it with a InputfieldPage::getSelectablePages hook, you can get the trigger field to alter the selectable pages interactively.  I have also found this to be useful in a number of other situations - e.g. updating a RuntimeMarkup field for changes on a page. There may be more elegant ways of achieving this (I'm open to suggestions), but in case it is useful to others, I'll post it here. Hopefully the comments in the script are self-explanatory and describe how to use it. Note that there are several console.log statements to help with debugging, which you can remove once happy with its operation.
      Happy to answer any questions (if I can 😉 ). Also, if anyone can explain how to get it working fully with checbox/toggle ad radio buttons, I would be grateful.
      /* Script to refresh a form content when an element gets changed To work across all admin pages, this script needs to be loaded in admin.php – add the line $config->scripts->add($config->urls->templates . "scripts/form-update.js"); before the final require in templates/admin.php. Typical use is to modify other elements based on a select drop-down change The trigger element can have the following data attributes assigned to it (typically set these with $myInputfield->attr() in a module or hook): * data-action="form-update" : Required to run the script. * data-update-target="#myid1" : Required - the element to change. Note that this should not be the whole form, otherwise .find(target) will not find it. * data-confirm="Some confirmation text": Optional - if you want to show a confirmation before the update, this holds the text to display. If absent, there will be no confirmation dialogue. If the user chooses ‘cancel’, the script will revert the change and terminate. * data-alert="Some alert text": Optional – if you want to warn the user that the update cannot happen for some reason (the script will then revert the change and terminate). * data-cache="#myid2" : Optional - if you want to cache the (changed) value, this element stores it. * data-cache-prefix="Some prefix string" : Optional (requires data-cache) - a prefix to prepend the value stored in the cache This currently works with the following trigger elements: * select options * select page (single and multiple) * page list select (single and multiple) * asm select * page autocomplete (but note that data attributes must be set in the wrapper element - e.g. $myInputfield->wrapAttr() ) * checkboxes (set attributes in wrapper as above) but not with: * toggle * checkbox * radio buttons (These partly work - the attributes need to be in the wrapper -, but doesn't work completely as wrapper 'value' attribute is not updated by PW (always 0) ) NOTE: If you are using this with other js scripts (e.g. in a module) that listen for events in the target, you must use event delegation (e.g. $(document).on("change","#myid", function(){}); NOT $("#myid").onchange(function(){}); ) because #myid is dynamic if it is inside the target) */ $(document).on('focusin', '[data-action="form-update"]', function(){ // get the value before the element is changed console.log("Saving value " + $(this).val()); $(this).data('val', $(this).val()); }).on('change','[data-action="form-update"]', function(event){ var prev = $(this).data('val'); var current = $(this).val(); console.log("Prev value " + prev); console.log("New value " + current); // if trigger element has data-confirm attribute, confirm or revert and exit var confirmText = $(this).data('confirm'); if (confirmText) { if (!confirm(confirmText)) { $(this).val(prev); return; } } // if trigger element has data-alert attribute, show alert and exit var alertText = $(this).data('alert'); if (alertText) { alert(alertText); $(this).val(prev); return; } // cache the value before proceeding (if data-cache set) var cache = $(this).data('cache'); var cachePrefix = ($(this).data('cache-prefix')) ? $(this).data('cache-prefix') : ''; $(cache).val(cachePrefix + current); var $form = $(this).closest('form'); var target = $(this).data('update-target'); console.log("Target is " + target); var method = $form.attr('method'); var action = $form.attr('action'); var data = $form.serialize(); var encodedName; // .serialize() will omit select elements that do not have a 'null' option (e.g. asm select, page list select) // or checkboxes with nothing selected // so find them and add empty parameters to the data string, otherwise the page field will not be updated $($form.find('select, input').each(function(index){ console.log('Select element no. ' + index + ' with name ' + $(this).attr("name") + ' has serialize = ' + $(this).serialize()); encodedName = encodeURI($(this).attr("name")) if (data.search(encodedName) === -1) { data += ('&' + encodeURI($(this).attr("name")) + '='); } })); console.log("Submitted data: " + data); if (!method) method = 'get'; if (!action) action = window.location.href; // If you want to fade the affected inputfields then assign the loading class to their wrappers with method wrapClass(loading) $(target).find('.loading').css({ display: 'block', opacity: 0.2 }).animate({ opacity: 1 }, 5000); // then send your request $.ajax(action, { type: method, // type used, not method, for older versions of jquery data: data, // you can also add an error handler here if required, in case the server returns an error on the request success: function (data) { // Initial ajax just returns an array with message. Need to GET the form data. $.ajax(window.location.href, { type: 'GET', cache: false, success: function (data) { // then just take the target, and replace it with the target div from the returned data console.log("Returned data: " + data); console.log("Updating html with: " + $(data).find(target).html()); $(target).html($(data).find(target).html()); } }); } }); });  
×
×
  • Create New...