web-dev-qa-db-fra.com

Comment créer un composant de champ de formulaire ExtJS personnalisé?

Je veux créer des composants de champ de formulaire ExtJS personnalisés en utilisant d'autres composants ExtJS (par exemple TreePanel). Comment puis-je le faire plus facilement?

J'ai lu des documents de Ext.form.field.Base mais je ne veux pas définir le corps du champ par fieldSubTpl. Je veux juste écrire du code qui crée des composants ExtJS et peut-être un autre code qui obtient et définit des valeurs.

Mise à jour: Les objectifs résumés sont les suivants:

  • Ce nouveau composant doit tenir dans l'interface graphique du formulaire en tant que champ. Il doit avoir une étiquette et le même alignement (étiquette, ancre) des autres champs sans besoin de piratage supplémentaire.

  • Peut-être que je dois écrire une logique getValue, setValue. Je préfère l'intégrer dans ce composant plutôt que de créer du code séparé qui copie les choses dans d'autres champs de formulaire cachés que je dois également gérer.

31
pcjuzer

Pour étendre la réponse de @RobAgar, en suivant un champ date-heure très simple que j'ai écrit pour ExtJS 3 et c'est un quickport que j'ai fait pour ExtJS 4. L'important est l'utilisation de Ext.form.field.Field mixin. Ce mixin fournit une interface commune pour le comportement logique et l'état des champs de formulaire, notamment:

Méthodes d'obtention et de définition des valeurs de champ Événements et méthodes de suivi des modifications de valeur et de validité Méthodes de déclenchement de la validation

Cela peut être utilisé pour combiner plusieurs champs et les laisser agir comme un seul. Pour un type de champ personnalisé total, je recommande d'étendre Ext.form.field.Base

Voici l'exemple que j'ai mentionné ci-dessus. Cela devrait indiquer à quel point cela peut être facile, même pour quelque chose comme un objet de date où nous devons formater les données dans le getter et le setter.

Ext.define('QWA.form.field.DateTime', {
    extend: 'Ext.form.FieldContainer',
    mixins: {
        field: 'Ext.form.field.Field'
    },
    alias: 'widget.datetimefield',
    layout: 'hbox',
    width: 200,
    height: 22,
    combineErrors: true,
    msgTarget: 'side',
    submitFormat: 'c',

    dateCfg: null,
    timeCfg: null,

    initComponent: function () {
        var me = this;
        if (!me.dateCfg) me.dateCfg = {};
        if (!me.timeCfg) me.timeCfg = {};
        me.buildField();
        me.callParent();
        me.dateField = me.down('datefield')
        me.timeField = me.down('timefield')

        me.initField();
    },

    //@private
    buildField: function () {
        var me = this;
        me.items = [
        Ext.apply({
            xtype: 'datefield',
            submitValue: false,
            format: 'd.m.Y',
            width: 100,
            flex: 2
        }, me.dateCfg),
        Ext.apply({
            xtype: 'timefield',
            submitValue: false,
            format: 'H:i',
            width: 80,
            flex: 1
        }, me.timeCfg)]
    },

    getValue: function () {
        var me = this,
            value,
            date = me.dateField.getSubmitValue(),
            dateFormat = me.dateField.format,
            time = me.timeField.getSubmitValue(),
            timeFormat = me.timeField.format;
        if (date) {
            if (time) {
                value = Ext.Date.parse(date + ' ' + time, me.getFormat());
            } else {
                value = me.dateField.getValue();
            }
        }
        return value;
    },

    setValue: function (value) {
        var me = this;
        me.dateField.setValue(value);
        me.timeField.setValue(value);
    },

    getSubmitData: function () {
        var me = this,
            data = null;
        if (!me.disabled && me.submitValue && !me.isFileUpload()) {
            data = {},
            value = me.getValue(),
            data[me.getName()] = '' + value ? Ext.Date.format(value, me.submitFormat) : null;
        }
        return data;
    },

    getFormat: function () {
        var me = this;
        return (me.dateField.submitFormat || me.dateField.format) + " " + (me.timeField.submitFormat || me.timeField.format)
    }
});
27
sra

Maintenant c'est cool. L'autre jour, j'ai créé un violon pour répondre à une autre question avant de réaliser que j'étais hors sujet. Et vous voilà enfin, portant à mon attention la question de ma réponse. Merci!

Voici donc les étapes requises pour implémenter un champ personnalisé à partir d'un autre composant:

  1. Création du composant enfant
  2. Rendre le composant enfant
  3. S'assurer que le composant enfant est dimensionné et redimensionné correctement
  4. Obtention et définition de la valeur
  5. Événements de relais

Création du composant enfant

La première partie, la création du composant, est simple. Il n'y a rien de particulier par rapport à la création d'un composant pour toute autre utilisation.

Cependant, vous devez créer l'enfant dans la méthode initComponent du champ parent (et non au moment du rendu). En effet, le code externe peut légitimement s'attendre à ce que tous les objets dépendants d'un composant soient instanciés après initComponent (par exemple pour leur ajouter des écouteurs).

De plus, vous pouvez être gentil avec vous-même et créer l'enfant avant d'appeler la super méthode. Si vous créez l'enfant après la super méthode, vous pouvez obtenir un appel à la méthode setValue de votre champ (voir ci-dessous) à un moment où l'enfant n'est pas encore instancié.

initComponent: function() {
    this.childComponent = Ext.create(...);
    this.callParent(arguments);
}

Comme vous le voyez, je crée un seul composant, ce que vous voudrez dans la plupart des cas. Mais vous pouvez également avoir envie de créer plusieurs composants enfants. Dans ce cas, je pense qu'il serait intelligent de revenir le plus rapidement possible sur des territoires bien connus: c'est-à-dire de créer un conteneur en tant que composant enfant et d'y composer.

Le rendu

Vient ensuite la question du rendu. Au début, j'ai envisagé d'utiliser fieldSubTpl pour rendre un div de conteneur et faire en sorte que le composant enfant s'y rende. Cependant, nous n'avons pas besoin des fonctionnalités du modèle dans ce cas, nous pouvons donc également le contourner complètement en utilisant la méthode getSubTplMarkup .

J'ai exploré d'autres composants dans Ext pour voir comment ils gèrent le rendu des composants enfants. J'ai trouvé un bon exemple dans BoundList et sa barre d'outils de pagination (voir code ). Ainsi, afin d'obtenir le balisage du composant enfant, nous pouvons utiliser Ext.DomHelper.generateMarkup en combinaison avec la méthode getRenderTree de l'enfant.

Voici donc l'implémentation de getSubTplMarkup pour notre domaine:

getSubTplMarkup: function() {
    // generateMarkup will append to the passed empty array and return it
    var buffer = Ext.DomHelper.generateMarkup(this.childComponent.getRenderTree(), []);
    // but we want to return a single string
    return buffer.join('');
}

Maintenant, ce n'est pas suffisant. Le code de BoundList nous apprend qu'il y a une autre partie importante dans le rendu des composants: appeler la méthode finishRender() du composant enfant. Heureusement, notre champ personnalisé aura sa propre méthode finishRenderChildren appelée juste au moment où cela doit être fait.

finishRenderChildren: function() {
    this.callParent(arguments);
    this.childComponent.finishRender();
}

Redimensionnement

Maintenant, notre enfant sera rendu au bon endroit, mais il ne respectera pas la taille de son champ parent. C'est particulièrement ennuyeux dans le cas d'un champ de formulaire, car cela signifie qu'il n'honorera pas la disposition de l'ancre.

C'est très simple à corriger, nous avons juste besoin de redimensionner l'enfant lorsque le champ parent est redimensionné. D'après mon expérience, c'est quelque chose qui a été considérablement amélioré depuis Ext3. Ici, il suffit de ne pas oublier l'espace supplémentaire pour l'étiquette:

onResize: function(w, h) {
    this.callParent(arguments);
    this.childComponent.setSize(w - this.getLabelWidth(), h);
}

Valeur de manipulation

Cette partie dépendra, bien sûr, de vos composants enfants et du champ que vous créez. De plus, à partir de maintenant, il s'agit juste d'utiliser vos composants enfants de manière régulière, donc je ne détaillerai pas trop cette partie.

A minima, vous devez également implémenter les méthodes getValue et setValue de votre champ. Cela fera fonctionner la méthode getFieldValues du formulaire, et cela suffira pour charger/mettre à jour les enregistrements du formulaire.

Pour gérer la validation, vous devez implémenter getErrors . Pour peaufiner cet aspect, vous souhaiterez peut-être ajouter une poignée de règles CSS pour représenter visuellement l'état non valide de votre champ.

Ensuite, si vous souhaitez que votre champ soit utilisable dans un formulaire qui sera soumis comme un formulaire réel (par opposition à une demande AJAX), vous aurez besoin de getSubmitValue pour renvoyer une valeur qui peut être convertie en chaîne sans dommage.

En dehors de cela, pour autant que je sache, vous n'avez pas à vous soucier du concept ou valeur brute introduit par Ext.form.field.Base Car cela n'est utilisé que pour gérer la représentation de la valeur dans un élément d'entrée réel. Avec notre composant Ext en entrée, nous sommes loin de cette voie!

Événements

Votre dernier travail consistera à implémenter les événements pour vos domaines. Vous voudrez probablement déclencher les trois événements de Ext.form.field.Field, C'est-à-dire change , dirtychange et validitychange.

Encore une fois, l'implémentation sera très spécifique au composant enfant que vous utilisez et, pour être honnête, je n'ai pas trop exploré cet aspect. Je vais donc vous laisser câbler pour vous-même.

Ma conclusion préliminaire est cependant que Ext.form.field.Field Propose de faire tout le travail lourd pour vous, à condition que (1) vous appeliez checkChange en cas de besoin, et (2) isEqual l'implémentation fonctionne avec le format de valeur de votre champ.

Exemple: champ de liste TODO

Enfin, voici un exemple de code complet, utilisant une grille pour représenter un champ de liste TODO.

Vous pouvez le voir en direct sur jsFiddle , où j'essaie de montrer que le champ se comporte de manière ordonnée.

Ext.define('My.form.field.TodoList', {
    // Extend from Ext.form.field.Base for all the label related business
    extend: 'Ext.form.field.Base'

    ,alias: 'widget.todolist'

    // --- Child component creation ---

    ,initComponent: function() {

        // Create the component

        // This is better to do it here in initComponent, because it is a legitimate 
        // expectationfor external code that all dependant objects are created after 
        // initComponent (to add listeners, etc.)

        // I will use this.grid for semantical access (value), and this.childComponent
        // for generic issues (rendering)
        this.grid = this.childComponent = Ext.create('Ext.grid.Panel', {
            hideHeaders: true
            ,columns: [{dataIndex: 'value', flex: 1}]
            ,store: {
                fields: ['value']
                ,data: []
            }
            ,height: this.height || 150
            ,width: this.width || 150

            ,tbar: [{
                text: 'Add'
                ,scope: this
                ,handler: function() {
                    var value = Prompt("Value?");
                    if (value !== null) {
                        this.grid.getStore().add({value: value});
                    }
                }
            },{
                text: "Remove"
                ,itemId: 'removeButton'
                ,disabled: true // initial state
                ,scope: this
                ,handler: function() {
                    var grid = this.grid,
                        selModel = grid.getSelectionModel(),
                        store = grid.getStore();
                    store.remove(selModel.getSelection());
                }
            }]

            ,listeners: {
                scope: this
                ,selectionchange: function(selModel, selection) {
                    var removeButton = this.grid.down('#removeButton');
                    removeButton.setDisabled(Ext.isEmpty(selection));
                }
            }
        });

        // field events
        this.grid.store.on({
            scope: this
            ,datachanged: this.checkChange
        });

        this.callParent(arguments);
    }

    // --- Rendering ---

    // Generates the child component markup and let Ext.form.field.Base handle the rest
    ,getSubTplMarkup: function() {
        // generateMarkup will append to the passed empty array and return it
        var buffer = Ext.DomHelper.generateMarkup(this.childComponent.getRenderTree(), []);
        // but we want to return a single string
        return buffer.join('');
    }

    // Regular containers implements this method to call finishRender for each of their
    // child, and we need to do the same for the component to display smoothly
    ,finishRenderChildren: function() {
        this.callParent(arguments);
        this.childComponent.finishRender();
    }

    // --- Resizing ---

    // This is important for layout notably
    ,onResize: function(w, h) {
        this.callParent(arguments);
        this.childComponent.setSize(w - this.getLabelWidth(), h);
    }

    // --- Value handling ---

    // This part will be specific to your component of course

    ,setValue: function(values) {
        var data = [];
        if (values) {
            Ext.each(values, function(value) {
                data.Push({value: value});
            });
        }
        this.grid.getStore().loadData(data);
    }

    ,getValue: function() {
        var data = [];
        this.grid.getStore().each(function(record) {
            data.Push(record.get('value'));
        });
        return data;        
    }

    ,getSubmitValue: function() {
        return this.getValue().join(',');
    }
});
24
rixo

Il h. Après avoir publié la prime, j'ai découvert que Ext.form.FieldContainer n'est pas seulement un champ conteneur, mais un conteneur à part entière component, il existe donc une solution simple.

Tout ce que vous devez faire est d'étendre FieldContainer, en remplaçant initComponent pour ajouter les composants enfants, et implémenter setValue, getValue et les méthodes de validation appropriées pour votre type de données de valeur.

Voici un exemple avec une grille dont la valeur est une liste d'objets paire nom/valeur:

Ext.define('MyApp.widget.MyGridField', {
  extend: 'Ext.form.FieldContainer',
  alias: 'widget.mygridfield',

  layout: 'fit',

  initComponent: function()
  {
    this.callParent(arguments);

    this.valueGrid = Ext.widget({
      xtype: 'grid',
      store: Ext.create('Ext.data.JsonStore', {
        fields: ['name', 'value'],
        data: this.value
      }),
      columns: [
        {
          text: 'Name',
          dataIndex: 'name',
          flex: 3
        },
        {
          text: 'Value',
          dataIndex: 'value',
          flex: 1
        }
      ]
    });

    this.add(this.valueGrid);
  },

  setValue: function(value)
  {
    this.valueGrid.getStore().loadData(value);
  },

  getValue: function()
  {
    // left as an exercise for the reader :P
  }
});
4
Rob Agar

Étant donné que la question a été posée plutôt vague - je ne peux que fournir le modèle de base pour ExtJS v4.

Même si ce n'est pas trop spécifique, il a l'avantage d'être assez universel comme ça:

Ext.define('app.view.form.field.CustomField', {
    extend: 'Ext.form.field.Base',
    requires: [
        /* require further components */
    ],

    /* custom configs & callbacks */

    getValue: function(v){
        /* override function getValue() */
    },

    setValue: function(v){
        /* override function setValue() */
    },

    getSubTplData: [
       /* most likely needs to be overridden */
    ],

    initComponent: function(){

        /* further code on event initComponent */

        this.callParent(arguments);
    }
});

Le fichier /ext/src/form/field/Base.js fournit les noms de toutes les configurations et fonctions qui peuvent être remplacées.

3
Martin Zeitler

Je l'ai fait plusieurs fois. Voici le processus général/pseudo-code que j'utilise:

  • Créez une extension de champ qui fournit la réutilisation la plus utile (généralement Ext.form.TextField si vous voulez simplement obtenir/définir une valeur de chaîne)
  • Dans le afterrender du champ, masquez le champ de texte et créez un élément d'habillage autour de this.el avec this.wrap = this.resizeEl = this.positionEl = this.el.wrap()
  • Rendre tous les composants à this.wrap (par exemple en utilisant renderTo: this.wrap dans la config)
  • Remplacez getValue et setValue pour parler aux composants que vous avez rendus manuellement
  • Vous devrez peut-être effectuer un dimensionnement manuel dans un écouteur resize si la mise en page de votre formulaire change
  • N'oubliez pas de nettoyer tous les composants que vous créez dans la méthode beforeDestroy!

J'ai hâte de passer notre base de code à ExtJS 4, où ce genre de choses est facile.

Bonne chance!

3
Sean Adkinson

Suivre la documentation sur http://docs.sencha.com/ext-js/4-0/#/api/Ext.form.field.Base

Ce code créera un champ de style TypeAhead/Autocomplete réutilisable pour sélectionner une langue.

var langs = Ext.create( 'Ext.data.store', {
    fields: [ 'label', 'code' ],
    data: [
        { code: 'eng', label: 'English' },
        { code: 'ger', label: 'German' },
        { code: 'chi', label: 'Chinese' },
        { code: 'ukr', label: 'Ukranian' },
        { code: 'rus', label: 'Russian' }
    ]
} );

Ext.define( 'Ext.form.LangSelector', {
    extend: 'Ext.form.field.ComboBox',
    alias: 'widget.LangSelector',
    allowBlank: false,
    hideTrigger: true,
    width: 225,
    displayField: 'label',
    valueField: 'code',
    forceSelection: true,
    minChars: 1,
    store: langs
} );

Vous pouvez utiliser le champ dans un formulaire simplement en définissant le xtype sur le nom du widget:

{
    xtype: 'LangSelector'
    fieldLabel: 'Language',
    name: 'lang'
}
2
James Linden

La plupart des réponses utilisent soit le Mixin Ext.form.field.Field ou s'étendent simplement sur une classe déjà faite qui convient à leurs besoins - ce qui est bien.

Mais je ne recommande pas d'écraser complètement la méthode setValue, c'est une forme vraiment mauvaise de l'OMI!

Il se passe beaucoup plus que simplement définir et obtenir la valeur, et si vous la remplacez complètement - eh bien vous gâcherez par exemple l'état sale, le traitement de rawValue etc.

Je suppose que deux options ici, l'une consiste à appelerParent (arguments) à l'intérieur de la méthode que vous déclarez pour garder les choses rationalisées, ou à la fin, lorsque vous avez terminé, appliquez la méthode héritée d'où vous l'avez obtenue (mixin ou extend).

Mais ne vous contentez pas de l'écraser sans égard pour ce que fait cette méthode déjà faite dans les coulisses.

Rappelez-vous également que si vous utilisez d'autres types de champs dans votre nouvelle classe - définissez la propriété isFormField sur false - sinon votre méthode getValues ​​sur le formulaire prendra ces valeurs et s'exécutera avec em!

1
Splynx