THE BLOG

News, tips and tricks from 2Gears

Sencha Touch TimePicker form field and Picker sheet

Posted · 32 Comments
timepicker

Currently we are using Sencha Touch as the basis for our new splog.me app.The Sencha Touch 1.1 framework includes a very large number of readily available form fields including the DatePicker form field and DatePicker popup sheet itself. Strangely enough, a TimePicker is missing so we decided to build one ourselves. The TimePicker below is heavily based on the DatePicker classes and includes a form field and a picker.

 

 

 

Ext.ux.form.TimePicker

To quickly demonstrate, the screenshots below show a TimePicker without any custom configuration and one that only shows a certain timespan and shows every fifth minute.

Usage

The TimePicker can be inserted into a form using it’s xtype ‘timepickerfield’ or by creating an instance yourselve.

var startTime = new Ext.ux.form.TimePicker({
	name : 'starttime',
	label: 'Start time'
});

Or it can be customized with the starting hour, ending hour and minute interval:

var startTime = new Ext.ux.form.TimePicker({
	name : 'starttime',
	label: 'Start time',
	minuteScale: 5,
	hourFrom: 8,
	hourTo: 18,
	value: {
		hour: (new Date().getHours()+1)%24, /* start with the next hour */
		minute: 0
	}
});

Feel free to use the timepicker in your Sencha Touch app and let me know when you do. Always nice to see other great mobile apps. If  you think of any improvements (EG. making it configurable for 24h/ AM PM format), let me know also. The code is displayed below and attached to this post as a tgz archive.

Cheers,
Rob

Code

Download both classes

32 Responses to "Sencha Touch TimePicker form field and Picker sheet"
  1. Dwilde77 says:


    Ext.define('Ext.field.TimePicker', {
    extend: 'Ext.field.Text',
    alternateClassName: 'Ext.form.TimePicker',
    xtype: 'timepickerfield',
    requires: [
    'Ext.picker.Time',
    ],

    /**
    * @event change
    * Fires when a date is selected
    * @param {Ext.field.DatePicker} this
    * @param {Date} newDate The new date
    * @param {Date} oldDate The old date
    */

    config: {
    ui: 'select',

    /**
    * @cfg {Object/Ext.picker.Date} picker
    * An object that is used when creating the internal {@link Ext.picker.Date} component or a direct instance of {@link Ext.picker.Date}.
    * @accessor
    */
    picker: true,

    /**
    * @cfg {Boolean}
    * @hide
    * @accessor
    */
    clearIcon: false,

    /**
    * @cfg {Object} value
    * Default value for the field and the internal {@link Ext.picker.Date} component. Accepts an object of 'year',
    * 'month' and 'day' values, all of which should be numbers, or a {@link Date}.
    *
    * Example: {year: 1989, day: 1, month: 5} = 1st May 1989 or new Date()
    * @accessor
    */

    /**
    * @cfg {Boolean} destroyPickerOnHide
    * Whether or not to destroy the picker widget on hide. This save memory if it's not used frequently,
    * but increase delay time on the next show due to re-instantiation.
    * @accessor
    */
    destroyPickerOnHide: false,

    /**
    * @cfg {Object}
    * @hide
    */
    component: {
    useMask: true
    }
    },

    initialize: function() {
    var me = this,
    component = me.getComponent();

    me.callParent();

    component.on({
    scope: me,
    masktap: 'onMaskTap'
    });

    if (Ext.os.is.Android2) {
    component.input.dom.disabled = true;
    }
    },

    syncEmptyCls: Ext.emptyFn,

    applyValue: function(value) {

    return value;
    },

    updateValue: function(newValue, oldValue) {
    var me = this,
    picker = me._picker;

    if (picker && picker.isPicker) {
    picker.setValue(newValue);
    }

    // Ext.Date.format expects a Date
    if (newValue !== null) {
    me.getComponent().setValue(newValue);
    } else {
    me.getComponent().setValue('');
    }

    if (newValue !== oldValue) {
    me.fireEvent('change', me, newValue, oldValue);
    }
    },

    /**
    * Returns the {@link Date} value of this field.
    * If you wanted a formated date
    * @return {Date} The date selected
    */
    getValue: function() {
    if (this._picker && this._picker instanceof Ext.picker.Time) {
    return this._picker.getValue();
    }

    return this._value;
    },

    /**
    * Returns the value of the field formatted using the specified format. If it is not specified, it will default to
    * {@link #dateFormat} and then {@link Ext.util.Format#defaultDateFormat}.
    * @param {String} format The format to be returned.
    * @return {String} The formatted date.
    */
    getFormattedValue: function(format) {
    var value = this.getValue();
    return value;
    },

    applyPicker: function(picker, pickerInstance) {
    if (pickerInstance && pickerInstance.isPicker) {
    picker = pickerInstance.setConfig(picker);
    }

    return picker;
    },

    getPicker: function() {
    var picker = this._picker,
    value = this.getValue();

    if (picker && !picker.isPicker) {
    picker = Ext.factory(picker, Ext.picker.Time);
    if (value != null) {
    picker.setValue(value);
    }
    }

    picker.on({
    scope: this,
    change: 'onPickerChange',
    hide : 'onPickerHide'
    });

    this._picker = picker;

    return picker;
    },

    /**
    * @private
    * Listener to the tap event of the mask element. Shows the internal DatePicker component when the button has been tapped.
    */
    onMaskTap: function() {
    if (this.getDisabled()) {
    return false;
    }

    this.onFocus();

    return false;
    },

    /**
    * Called when the picker changes its value.
    * @param {Ext.picker.Date} picker The date picker.
    * @param {Object} value The new value from the date picker.
    * @private
    */
    onPickerChange: function(picker, value) {
    var me = this,
    oldValue = me.getValue();

    me.setValue(value);
    me.fireEvent('select', me, value);
    me.onChange(me, value, oldValue);
    },

    /**
    * Override this or change event will be fired twice. change event is fired in updateValue
    * for this field. TOUCH-2861
    */
    onChange: Ext.emptyFn,

    /**
    * Destroys the picker when it is hidden, if
    * {@link Ext.field.DatePicker#destroyPickerOnHide destroyPickerOnHide} is set to `true`.
    * @private
    */
    onPickerHide: function() {
    var me = this,
    picker = me.getPicker();

    if (me.getDestroyPickerOnHide() && picker) {
    picker.destroy();
    me._picker = me.getInitialConfig().picker || true;
    }
    },

    reset: function() {
    this.setValue(this.originalValue);
    },

    onFocus: function(e) {
    var component = this.getComponent();
    this.fireEvent('focus', this, e);

    if (Ext.os.is.Android4) {
    component.input.dom.focus();
    }
    component.input.dom.blur();

    if (this.getReadOnly()) {
    return false;
    }

    this.isFocused = true;

    this.getPicker().show();
    },

    // @private
    destroy: function() {
    var picker = this._picker;

    if (picker && picker.isPicker) {
    picker.destroy();
    }

    this.callParent(arguments);
    }
    //
    }, function() {
    this.override({
    getValue: function(format) {
    if (format) {
    //
    Ext.Logger.deprecate("format argument of the getValue method is deprecated, please use getFormattedValue instead", this);
    //
    return this.getFormattedValue(format);
    }
    return this.callOverridden();
    }
    });

    /**
    * @method getDatePicker
    * @inheritdoc Ext.field.DatePicker#getPicker
    * @deprecated 2.0.0 Please use #getPicker instead
    */
    Ext.deprecateMethod(this, 'getTimePicker', 'getPicker');
    //
    });


    Ext.define('Ext.picker.Time', {
    extend: 'Ext.picker.Picker',
    xtype: 'timepicker',
    alternateClassName: 'Ext.TimePicker',

    /**
    * @event change
    * Fired when the value of this picker has changed and the done button is pressed.
    * @param {Ext.picker.Date} this This Picker
    * @param {Date} value The date value
    */

    config: {
    /**
    * @cfg {Number} minutecale
    * List every how many minute, eg. 5 lists 0, 5, 10, 15, etc. Defaults to 1
    */

    minuteScale: 1,

    /**
    * @cfg {Number} hourFrom
    * The start hour for the time picker. Defaults to 0
    * @accessor
    */
    hourFrom: 0,

    /**
    * @cfg {Number} hourTo
    * The last hour for the time picker. Defaults to 23
    * @accessor
    */
    hourTo: 23,

    /**
    * @cfg {String} hourText
    * The label to show for the hour column. Defaults to 'Hour'.
    * @accessor
    */
    hourText: 'Hour',

    /**
    * @cfg {String} minuteText
    * The label to show for the minute column. Defaults to 'Minute'.
    * @accessor
    */
    minuteText: 'Minute',

    /**
    * @cfg {Array} slotOrder
    * An array of strings that specifies the order of the slots. Defaults to ['hour', 'minute'].
    * @accessor
    */
    slotOrder: ['hour', 'minute'],
    /**
    * @cfg {Object} value
    * @accessor
    */
    /**
    * @cfg {Array} slots
    * @hide
    * @accessor
    */
    },

    initialize: function() {
    this.callParent();

    this.on({
    scope: this,
    delegate: '> slot',
    slotpick: this.onSlotPick
    });

    this.on({
    scope: this,
    show: this.onSlotPick
    });
    },

    setValue: function(value, animated) {
    var arr = value.split(':');
    if (arr.length >= 2) {
    value = {hour: arr[0], minute:arr[1]}
    //code
    }

    this.callParent([value, animated]);
    this.onSlotPick();
    },

    getValue: function(useDom) {
    var values = {},
    items = this.getItems().items,
    ln = items.length,
    hour, minute;

    for (i = 0; i < ln; i++) {
    item = items[i];
    if (item instanceof Ext.picker.Slot) {
    values[item.getName()] = item.getValue(useDom);
    }
    }

    //if all the slots return null, we should not return a date
    if (values.hour === null && values.minute === null ) {
    return null;
    }

    hour = Ext.isNumber(values.hour) ? values.hour : 1;
    minute = Ext.isNumber(values.minute) ? values.minute : 0;
    if (hour < 10) {
    hour = '0'+hour;
    }
    if ( minute < 10) {
    minute = '0'+minute;
    }
    return hour +":"+minute;
    },

    /**
    * Updates the yearFrom configuration
    */
    updateHourFrom: function() {
    if (this.initialized) {
    this.createSlots();
    }
    },

    /**
    * Updates the yearTo configuration
    */
    updateHourTo: function() {
    if (this.initialized) {
    this.createSlots();
    }
    },

    /**
    * Updates the hourText configuration
    */
    updateHourText: function(newHourText, oldHourText) {
    var innerItems = this.getInnerItems,
    ln = innerItems.length,
    item, i;

    //loop through each of the current items and set the title on the correct slice
    if (this.initialized) {
    for (i = 0; i < ln; i++) {
    item = innerItems[i];

    if ((typeof item.title == "string" && item.title == oldHourText) || (item.title.html == oldHourText)) {
    item.setTitle(newHourText);
    }
    }
    }
    },

    /**
    * Updates the {@link #dayText} configuration.
    */
    updateMinuteText: function(newMinuteText, oldMinuteText) {
    var innerItems = this.getInnerItems,
    ln = innerItems.length,
    item, i;

    //loop through each of the current items and set the title on the correct slice
    if (this.initialized) {
    for (i = 0; i hoursTo) {
    tmp = hoursFrom;
    hoursFrom = hoursTo;
    hoursTo = tmp;
    }
    for (i = j = hoursFrom; i 1 ? j : "0"+j;
    hours.push({
    text: j,
    value: i
    });
    }
    for (i = j = 0; i 1 ? j : "0"+j;
    minutes.push({
    text: j,
    value: i
    });
    }
    var slots = [];
    slotOrder.forEach(function(item){
    slots.push(me.createSlot(item, hours, minutes ));
    });
    me.setSlots(slots);
    },
    createSlot: function(name, hours, minutes ){
    switch (name) {
    case 'hour':
    return {
    name: name,
    align: 'right',
    data: hours,
    title: this.getHourText(),
    flex: 5
    };
    case 'minute':
    return {
    name: name,
    align: 'left',
    data: minutes,
    title: this.getMinuteText(),
    flex: 5
    };
    }
    },
    onSlotPick: function() {
    this.callParent();
    var value = this.getValue(true),
    slot = this.getTimeSlot()
    i;

    if (!value || !slot) {
    return;
    }

    this.callParent();

    return;

    // slot._value = value;
    },

    getTimeSlot: function() {
    var innerItems = this.getInnerItems(),
    ln = innerItems.length,
    i, slot;

    if (this.timeSlot) {
    return this.timeSlot;
    }

    for (i = 0; i < ln; i++) {
    slot = innerItems[i];
    if (slot.isSlot && slot.getName() == "hour") {
    this.timeSlot = slot;
    return slot;
    }
    }
    return null;
    },

    onDoneButtonTap: function() {
    var oldValue = this._value,
    newValue = this.getValue(true),
    testValue = newValue;

    if (Ext.isDate(newValue)) {
    testValue = newValue.toDateString();
    }
    if (Ext.isDate(oldValue)) {
    oldValue = oldValue.toDateString();
    }

    if (testValue != oldValue) {
    this.fireEvent('change', this, newValue);
    }

    this.hide();
    }
    });

  2. Monika says:

    Unable to view http://market.sencha.com/addon/datetimepicker.
    Pls tell the solution ASAP.Thanks in advance…

  3. dan says:

    can you share the time picker of sencha 1.1 with the improvement of 24h AM/PM format.

  4. Hey Rob, This is great. Works smooth and very useful. Thank you very much for sharing

  5. Arbi says:

    Guys, this is one of working solution:
    http://market.sencha.com/addon/datetimepicker

  6. Cem says:

    Hi there, any progress with ST 2? Im so desperately looking forward to it. Many thanks.

    • Rob Boerman says:

      Hehe, working hard on it. I created a new time picker component for ST2 which works like a charm. Now I just have to create a form time picker field. I’ll create a new blog post and link to it when I am ready. Have patience my friend :)

  7. Cem says:

    Hi, thanks for sharing this, however I have got a little problem, I put these 2 files into ux folder under /lib/touch folder and reference it in my index.html just under sencha-touch.js.

    When i added it into my view as below;

    xtype: ‘timepickerfield’,
    name: ‘pickuptime’,
    label: ‘Pick up Time’,
    minuteScale: 5,
    hourFrom: 0,
    hourTo: 23,
    value: {
    hour: (new Date().getHours() + 1) % 24, /* start with the next hour */
    minute: 0
    }

    it throws an error saying Uncaught Error: [Ext.createByAlias] Cannot create an instance of unrecognized alias: widget.timepickerfield

    I use sencha touch 2

    Thanks for help.

    • Rob Boerman says:

      Hi Cem,

      A couple of other people have reported this as well, the current implementation only works in ST 1.
      I will try and free some time to create a similar solution for ST 2 this afternoon.

  8. Steve c says:

    OK, so I put TimePicker.js in my project in the same directory, register it in index.html, then go to a panel and add an item with an xtype of ‘timepickerfield’, and it doesn’t recognise it. What else do I have to do?

  9. Divya says:

    Hi,
    Has anyone ported in Sencha touch 2.0. If yes, can you please share the code.

    Thanks,

    • Rob Boerman says:

      No, not yet, Ryan mentioned he was working on it. Otherwise I will port it over in a few days. Have been incredibly busy but should be easy enough to do

  10. Ryan says:

    I have ported this to Sencha 2.0. After I wasn’t able to deploy this to a Sencha 2.0 app, I created a time picker in the same manner by copying and modifying the existing Sencha 2.0 date picker (control and picker).

    I’ll post it up at some stage.

  11. smantscheff says:

    Thanks a lot. Saved me a lot of work. Works out of the box.

  12. Jakob says:

    Hi Robb,

    I have still not figured out how to get the make the picker set its values to my default values. I’ve console.log:ged this.value practically everywhere in the code, and it always seems to be correct, but the picker still has 00:00 set upon instantiation. I really need your help, and would really appreciate if you could answer my questions.

  13. ChrisW says:

    Rob – what is the best way to integrate these classes into Sencha Touch? I tried dropping into src folder, but figured that sencha-touch.js needs to reference this new components somehow. Or should you just directly include – ?

    • Rob Boerman says:

      Hi Chris,

      In my production environment I concatenate all Sencha sources and my own custom sources using Sencha build and some custom perl scripts. For my development environment everything is just included into the index.html file in this order:
      - sencha-touch.js
      - UX classes (including my own, this also includes the time picker)
      - app.js
      - utils
      - models
      - stores
      - views
      - controllers

      I built a perl script that scans my sources and automatically creates the index.html file for me (among other things). If you like I can send you an example of this index.html file. Hope this helps.

  14. Jakob says:

    The picker component does not seem to work, so I had to inherit from Ext.Sheet instead. Now I can’t seem to get a default value inserted into the picker. I can set the default values (hour and minute), but for some reason the picker values are set to 00:00 instead of the default values I set.

    I would be really happy if you could provide me with a solution to this problem, or point me to where (in the code) the default value is set inside the picker.

    • Rob Boerman says:

      Hi Jacob,

      I will be happy to look into it, first off, which version of Sencha Touch are you using? This picker is based on 1.1

      • Jakob says:

        I’m using Sencha Touch 1.1.0, so that shouldn’t be a problem. Also, what I’ve noticed is that the picker seems to get the correct time after I’ve picked a time once, but it does not get the correct time if I’ve set a default time.

        I’m aware that it could be my fault as I’ve done some changes in order to make the picker work at all, but I still need to know where to find the code where the picker gets/sets its default values

  15. Mac says:

    Hey Rob, Thanks a lot for this ! saved me plenty of time and works great!

Leave a Reply

Your email address will not be published. Required fields are marked *

20 − 5 =

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>