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 jonatan
      collapsedNoLocked field not showing
      Hi all, ☺️
      I'm a 21 years old 👨‍🦱, danish 🇩🇰, hobby-designer-and-web-stuff-maker and very excited and eager processwire beginner, or "noob" 🐣 if you wish.
      So far I've been fascinated by the very satisfyingly simple and yet powerfull magic ✨ of PW (once you get the hang of it) and the awesome feeling of the strong PW community 🦄🌈☀️❤️! The huge work done by @ryan and all of the other amazing PW people is just so inspiring! I actually really have a hard time understanding why Processwire isn't the most used CMS in the world... or at least just a way more commonly known one!
       
      But now I've encountered a small bump on the road and I'd lovingly appreciate if one of you lovely PW forum members could maybe help me out!
      ❓Problem:
      So I have a problem with the collapsed-constant: https://processwire.com/api/ref/inputfield/#pwapi-methods-collapsed-constants 
      - It's not showing my field when I apply it.
      (I've funnily enough found this old git pull from 2014 which documents the "Locked" state being added as a field -> input -> visibility option: https://github.com/ryancramerdesign/ProcessWire/pull/457 – it also shows the relevant code implementations to the core) 
       
      📝 What I wish to do:
       is to 👀 display some data from a 📦 module (InstagramBasicDisplayApi) in the ⌨️ page editor when editing a page ("About me") using the template (About.php) - So the only possible way to do that as far as for what I've been able to come up with with my restricted PW (end eh.. php) –knowledge was to add a custom field type to the (About.php) template...
      I've set up a custom field using https://modules.processwire.com/modules/mystique/ :
      site/modules/configs/Mystique.php:
      <?php namespace ProcessWire; $modules = wire("modules"); $instagram = $modules->get("InstagramBasicDisplayApi"); $data = array('username'=>''); $account = $instagram->getUserAccount($data["username"]); $username = isset($account["username"]) ? $account["username"] : ""; $at_instausername = "@" . $username; /** * Resource : Instagram account */ return [ 'title' => __('Instagram account'), 'fields' => [ 'window_title' => [ 'label' => __(' '), 'type' => Mystique::TEXT, // or InputfieldText 'useLanguages' => true, 'collapsed' => '0', 'placeholder' => __($at_instausername), ] ] ]; - Basically:
      I'm getting the instagram-username, pulling it from the module "InstagramBasicDisplayApi",  And then I'm using it as the 'placeholder' value for the field, resulting in this:  - Which is actually what I want... almost...
      – The thing is, I would like it to be locked, so that it's not possible to overwrite the 'placeholder' value, but so that the inputfield actually does not take any input but just informatively displays the data...
      so what I do is that I just change 'collapsed' to '7' = 'collapsedNoLocked':
      'collapsed' => '7', , right, and that would be it?
      But unfortunately no...
      When I do I get this:
       
       - I can't figure out why? I'd supposed that the 'placeholder' value would just show, but non-editable?
      A "workaround" is to just set it to back to 0, then manually type in "@sasha_lindegaard" and then press save, and then set it to 7, and I have what I want:

      - But that's not really what I wish, as it displays the data (the instagram username) statically (from what I've typed into the field and have saved) and not dynamically (from the instagram module database) as wished for...
      – also, it's still collapsable? Why so?... 🤔
       
      Any ideas for how I might get my "INSTAGRAM" field to display the username from the instagram module's database dynamically, not as editable placeholder text in a editable/open input field but just as non-editable/locked text?
      I hope that I've made my problem clear enough but if I've failed to provide enough info please don't hesitate to request for more! 🙂 
      Thanks a thousand times in advance!
      All the best,
      Jonatan R.
    • By MarkE
      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()); } }); } }); });  
    • By Marco Ro
      Hi,
      I need to change the position of the label tag. In all render the input field look like this:
      <div class="Inputfield" id=""> <label class="InputfieldHeader" for="...">Email</label> <div class="InputfieldContent"> <input id="..." name="..." class="..." type="" maxlength="512" autocomplete="off"> </div> </div> I would like move the label tag inside the InputfieldContent after the input tag.
      I try to work around the render hook, but didn't found a solution, and also I'm not sure is this the way. 
      How can I do it?
      Thank you.
    • By gebeer
      Hello all,
      sharing my new module FieldtypeImageReference. It provides a configurable input field for choosing any type of image from selectable sources. Sources can be: 
      a predefined folder in site/templates/ and/or a  page (and optionally its children) and/or the page being edited and/or any page on the site CAUTION: this module is under development and not quite yet in a production-ready state. So please test it carefully.
      UPDATE: the new version v2.0.0 introduces a breaking change due to renaming the module. If you have an older version already installed, you need to uninstall it and install the latest master version.
      Module and full description can be found on github https://github.com/gebeer/FieldtypeImageReference
      Install from URL: https://github.com/gebeer/FieldtypeImageReference/archive/master.zip
      Read on for features and use cases.
      Features
      Images can be loaded from a folder inside site/templates/ or site/assets Images in that folder can be uploaded and deleted from within the inputfield Images can be loaded from other pages defined in the field settings Images can be organized into categories. Child pages of the main 'image source page' serve as categories mages can be loaded from any page on the site From the API side, images can be manipulated like native ProcessWire images (resizing, cropping etc.), even the images from a folder Image thumbnails are loaded into inputfield by ajax on demand Source images on other pages can be edited from within this field. Markup of SVG images can be rendered inline with `echo $image->svgcontent` Image names are fully searchable through the API $pages->find('fieldname.filename=xyz.png'); $pages->find('fieldname.filename%=xy.png'); Accidental image deletion is prevented. When you want to delete an image from one of the pages that hold your site-wide images, the module searches all pages that use that image. If any page contains a reference to the image you are trying to delete, deletion will be prevented. You will get an error message with links to help you edit those pages and remove references there before you can finally delete the image. This field type can be used with marcrura's Settings Factory module to store images on settings pages, which was not possible with other image field types When to use ?
      If you want to let editors choose an image from a set of images that is being used site-wide. Ideal for images that are being re-used across the site (e.g. icons, but not limited to that).
      Other than the native ProcessWire images field, the images here are not stored per page. Only references to images that live on other pages or inside a folder are stored. This has several advantages:
      one central place to organize images when images change, you only have to update them in one place. All references will be updated, too. (Provided the name of the image that has changed stays the same) Installation and setup instructions can be found on github.
      Here's how the input field looks like in the page editor:

      If you like to give it a try, I'm happy to hear your comments or suggestions for improvement. Install from URL: https://github.com/gebeer/FieldtypeImageReference/archive/master.zip
      Eventually this will go in the module directory, too. But it needs some more testing before I submit it. So I'd really appreciate your assistance.
      Thanks to all who contributed their feedback and suggestions which made this module what it is now.
       
×
×
  • Create New...