OJS 3.4.0 attach template or script file to Contributor > Edit Modal

Hi everyone,

I am currently working on porting the ROR plugin to OJS version 3.4.0. I am stuck and need assistance, hope someone can help me or direct me in the right direction.

This plugin uses JavaScript (jQuery, Ajax) to retrieve information from “https://api.ror.org/organizations”. The information which is retrieved is the name and rorId of organizations (Affiliation in ojs) and is saved to author::affiliation and author::rorId.

In OJS 3.3, this JavaScript logic is in a .tpl file and is attached to the contributor > edit modal which is shown after clicking “Edit” in the contributor list. This all works as expected.

But in OJS 3.4, the way the modal is shown has changed. I couldn’t find a way to attach this Javascript logic to this modal.

Tried:

  • I tried misusing Form > FieldHTML, but doesn’t help as the values are then in “pkp.registry.init”.
  • I could add the JavaScript file to the “Template::Workflow::Publication” hook, but that gives many challenges in JavaScript to manipulate the modals of different contributors.

Screenshot OJS 3.3.0

Can anyone help me with this.

Thanks.

@Dulip_Withanage
@asmecher

@jardakotesovec wrote:

Hi @gaziyucel
The Contributor Modal is now rendered via Vue.js. So it basically requires very different approach to customise the behaviour.

What should work is to use hook to adjust that particular form field (that contributor form is configured in PHP - ContributorForm.php). So you would want to adjust that affilation field to use different vue component.

You can technically either create your own form field from scratch or try to amend some form field we have in ui-library which is very closed in functionality. I think FieldBaseAutosuggest could work quite well. So I would probably try to extend from this component and customize some methods which handles the fetching.

It might be bit fiddly to enforce that only one affiliation is allowed.

Best example how to extend from existing vue component you can find here - GitHub - jardakotesovec/backend-ui-example-plugin

Let me know whether this would help you to make a progress and if you get stuck I will try to help you out.

Thank you for your help @jardakotesovec. I followed your tip and have it working now.

  • Extend FieldText class
use PKP\components\forms\FieldText;

class FieldTextLookup extends FieldText
{
    /** @copydoc Field::$component */
    public $component = 'ror-field-text-lookup';
}
  • Add to the ContributorForm
function register($category, $path, $mainContextId = null): bool
{
    ...
    Hook::add('Form::config::before', [$this, 'addFields']);
    Hook::add('Template::Workflow::Publication', [$this, 'addWorkflow']);
    Hook::add('Template::SubmissionWizard::Section', [$this, 'addWorkflow']);
    ...
}

function addFields(string $hookName, object $form): bool
{
    if (!$form instanceof ContributorForm) return Hook::CONTINUE;
    
    $form->addField(new FieldTextLookup(Constants::$idName . '_Lookup', [
        'label' => __('plugins.generic.ror.input.lookup.label'),
        'tooltip' => __('plugins.generic.ror.input.lookup.tooltip'),
    ]), [FIELD_POSITION_BEFORE, 'affiliation']);
    
    return Hook::CONTINUE;
}

function addWorkflow(string $hookName, array &$args): void
{
    /* @var TemplateManager $templateMgr */
    $templateMgr = &$args[1];
    $request = Application::get()->getRequest();
    
    $templateMgr->display($this->getTemplateResource('contributor.tpl'));
}

And the contributor.tpl file as follows

<script>
let rorPluginTemplateCompiled = pkp.Vue.compile(`
<div class="pkpFormField pkpFormField--text RorPlugin_rorId_Lookup_div" :class="classes">
    <div class="pkpFormField__heading">
        <form-field-label :controlId="controlId"
                          :label="label"
                          :localeLabel="localeLabel"
                          :isRequired="isRequired"
                          :requiredLabel="__('common.required')"
                          :multilingualLabel="multilingualLabel">
        </form-field-label>
        <tooltip v-if="isPrimaryLocale && tooltip"
                 aria-hidden="true"
                 :tooltip="tooltip"
                 label="">
        </tooltip>
    </div>
    <div class="pkpFormField__control" :class="controlClasses">
        <div class="pkpFormField__control_top">
            <label>
                <span class='pkpSearch__icons'>
                    <icon icon='search' class='pkpSearch__icons--search'/>
                </span>
                <input class="pkpFormField__input pkpFormField--text__input"
                       ref="input"
                       v-model="searchPhrase"
                       :type="inputType"
                       :id="controlId"
                       :name="localizedName"
                       :aria-describedby="describedByIds"
                       :aria-invalid="errors && errors.length"
                       :disabled="isDisabled"
                       :required="isRequired"
                       :style="inputStyles"/>
            </label>
            <button class="pkpSearch__clear"
                    v-if="searchPhrase"
                    @click.prevent="clearSearchPhrase">
                <icon icon="times"/>
            </button>
        </div>
    </div>
    <div v-if="searchPhrase" class="searchPhraseOrganizations">
        <ul>
            <li v-for="(organization, index) in organizations">
                <a @click.prevent="selectOrganization(index)">{{ organization.name }} [{{ organization.id }}]</a>
            </li>
        </ul>
    </div>
</div>
`);

pkp.Vue.component("ror-field-text-lookup", {
    name: "FieldTextLookup",
    extends: pkp.Vue.component("field-text"),
    data() {
        return {
            organizations: [], // [ { id: id1, name: name1 }, ... ]
            searchPhrase: "",
            minimumSearchPhraseLength: 3,
        }
    },
    methods: {
        selectOrganization(index) {
            let fields = this.$parent._props.fields;
            fields[this.getIndex('rorId')].value = this.organizations[index].id;
            let values = fields[this.getIndex('affiliation')].value;
            Object.keys(values).forEach(key => {
                values[key] = this.organizations[index].name;
                if (typeof this.organizations[index].labels[key] !== 'undefined') {
                    values[key] = this.organizations[index].labels[key];
                }
            });
        },
        clearSearchPhrase() {
            this.organizations = [];
            this.searchPhrase = "";
        },
        getIndex(fieldName) {
            let fields = this.$parent._props.fields;
            for (let i = 0; i < fields.length; i++) {
                if (fields[i].name === fieldName) {
                    return i;
                }
            }
        },
        apiLookup() {
            fetch('https://api.ror.org/organizations?affiliation=' + this.searchPhrase + '*')
                .then(response => response.json())
                .then(data => {
                    this.organizations = [];
                    let items = data.items;
                    items.forEach((item) => {
                        let labels = { /* */};
                        for (let i = 0; i < item.organization.labels.length; i++) {
                            labels[item.organization.labels[i].iso639]
                                = item.organization.labels[i].label
                        }
                        let row = {
                            id: item.organization.id,
                            name: item.organization.name,
                            labels: labels
                        };

                        this.organizations.push(row);
                    });
                })
                .catch(error => console.log(error));
        }
    },
    watch: {
        searchPhrase() {
            if (this.searchPhrase.length >= this.minimumSearchPhraseLength) {
                this.apiLookup();
            }
        }
    },
    render: function (h) {
        return rorPluginTemplateCompiled.render.call(this, h);
    }
});
</script>
1 Like