Custom DocField Type

Hello all

There are a few references to creating a custom docfield type on this forum. However, is there any documentation on this, or is it an undocumented capability?

I want to create a custom field type for capturing a HH:MM time value. The field should have 2 dropdowns one for the HH (00 to 23) and another for the MM (00, 15, 30, 45) separated by a ‘:’. This new type can then replace my usage of 2 individual Select docfields.

Frappe already has the Duration and Time data types, which are useful for your requirement. You can refer to the link mentioned below

Custom Field.

Have you considered creating thee fields?

HH, select, with 24 choices (00-23)
MM, select, (00,15,30,45)
Duration, read only, fed from values in previous fields

Thanks @Sanmugam and @volkswagner

Yes, I’m very familiar with the built-in date and time related types. However I cannot get the Time type to not display seconds and for the minutes to step in 15 min intervals. That is why my current solution involves 2 Select types which I then combine into a single value for a Time type which is Hidden.

The current workaround is functional, but is far from being an elegant solution. Especially the HH and MM being separate fields.

The actual question on this topic relates to how do you create a custom type and where is the documentation for that?

Definitely not possible “out of the box”. Your current solution (two SELECT fields) sounds like the best approach. I’m doing this myself in a few places.

To accomplish what you’re asking (2 drop-downs separated by a :), using a single DocField? The solution is probably:

  • Just create an ordinary “Data” DocField.
  • Write some very fancy JS code that overrides the normal behavior, and achieves the UI expereince you want. I have no clue how to write anything nearly that complex. But certainly some JS/CSS expert could.
  • Finally, worth writing some checks and balances in your validate() method server-side. To ensure that the string matches your expected format. This way if you import data from a CSV or something, you won’t end up with wrongly-formatted strings.

I agree with everything you’re saying here, except I don’t think it actually needs to be all that fancy. I’m using a number of custom control widgets similar to what EugeneP is looking for, and all I’m doing is extending the relevant frappe.ui.form.Control* object. I think most of the custom date picker fields with custom calendars work this way.

Everyone is different. For you, this might be a typical afternoon of working with control widgets.

But in my universe, this is fancy Frontend technomagic. I lack the relevant experience in this area. And the framework documentation is woefully inadequate. I would need to find a related “Build With Hussain” video, and hope it’s close enough for me to alter. Without that, I’d probably need days or weeks to decipher what you just described in a few sentences.

It’s true. If you aren’t comfortable with JS/HTML/CSS, customizing the frontend is going to be a frustrating experience.

If you are familiar with that stack, though, I’ve been surprised by what’s possible in 20 lines of code.

Thanks so much @brian_pond and @peterg

There are a number of forum topics describing / providing snippets of the code to create a custom docfield type, but unfortunately nothing comprehensive to follow along and create your own type.

I’ll keep at it and see whether I can make any progress.

You’ll likely need to maintain a fork of Frappe going forward. I’ve never heard of anyone doing this successfully any other way, and I can’t see anything in the code to suggest there are hooks you can plug into.

Edited to add: This PR shows exactly what’s needed to add a new docfield:

1 Like

I’ve stumbled upon this function, but do not know how to use it.
Maybe a more proficient developer can help :crossed_fingers:

…/apps/frappe/frappe/custom/doctype/custom_field/custom_field.py

especially the create_custom_field function

That method doesn’t do what you’re hoping it will do. It just renders custom fields; it doesn’t allow you define custom field types.

Check out line 38 in that file if you want more evidence that field types are not extensible in userland.

1 Like

Just to add: I’m still not entirely clear on why you want a custom docfield type.

Docfield types are complicated. Making a new one would require you to define behavior not just for the UI but also for the database, the object relational model, the api, the form customization app, etc.

It sounds like the data structure you need is already described by the Time docfield, and it’s just the desk interface you don’t like. If that’s the case, you’ll have a much easier time just building your own widget than you will building a whole new docfield type.

A very quick proof of concept:

Here, I’m using the regular Time docfield type, but I’m rendering a custom control widget with two dropdowns if options is set to “Custom”. This is roughly what @brian_pond described above. You just have to run this code somewhere in your app (or, for testing, in the console).

frappe.ui.form.ControlTime = class ControlTimeCustom extends frappe.ui.form.ControlTime {
	make_input() {
		if (this.df.options === 'Custom') {
			console.log(this.value)
			const hours = ['00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', 
							'12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23']
			const minutes = ['00', '15', '30', '45']
			this.$input = $(`
				<div>
					<select name="hours">
						${hours.map(h => '<option>' + h + '</option>').join('')}
					</select>
					<span>:</span>
					<select name="minutes">
						${minutes.map(m => '<option>' + m + '</option>').join('')}
					</select>
				</div>
			`).prependTo(this.input_area);

			this.set_input_attributes();
			this.input = this.$input.get(0);
			this.has_input = true;
			this.bind_change_event();
		} else {
			super.make_input()
		}
	}
	bind_change_event() {
		if (this.df.options === 'Custom') {
			const change_handler = (e) => {
				let value = this.get_input_value()
				this.parse_validate_and_set_in_model(value, e);
			};
			this.$input.find('select').on("change", change_handler);
		} else {
			super.bind_change_event()
		}
	}
	get_input_value() {
		if (this.df.options === 'Custom') {
			return (
				(this.$input.find('select[name="hours"]').val() ?? '00') +
				':' +
				(this.$input.find('select[name="minutes"]').val() ?? '00')
			)
		} else {
			return super.get_input_value();
		}
	}
	set_input(value) {
		this.last_value = this.value;
		this.value = value;
		this.set_formatted_input(value);
		this.set_disp_area(value);
		this.set_mandatory && this.set_mandatory(value);

		if (this.df.options === 'Custom') {
			const currentValue = this.value?.split(':')?.map(n => n.padStart(2, 0))
			this.$input.find('select[name="hours"]').val(currentValue?.[0] ?? '00')
			this.$input.find('select[name="minutes"]').val(currentValue?.[1] ?? '00')
		}
	}
}
2 Likes

Thanks so much @peterg

Can I simply add this coding to my DocType .js file?

Is it a separate section either before or after : frappe.ui.form.on(“LPS Trip”, {…})

Once again thanks so much.

If you’ve got an app, I think the best place to put it would be as a standalone file loaded via the app_include_js hook.

In the doctype js file might work, but I’ve never fully understood when/how often doctype js code gets run. If it runs late or multiple times, you could get some unexpected behavior.

1 Like