--- title: Custom Auto Forms group: Component Gallery --- ## Custom AutoForm UIs [CoffeeShop's Admin UI](https://servicestack.net/posts/building-typechat-coffeeshop-modelling) is a good example of the rapid development model of AutoQuery and Vue's [AutoQueryGrid](/vue/autoquerygrid) and [Auto Form](/vue/autoform) Components was nearly able to develop the entire CRUD management UI using just AutoQuery's Typed DTOs. The one Form that it wasn't able to generate the entire UI for is its **Many-to-Many** `CategoryOption` relationship which requires a custom AutoForm component to be able to specify which Options a category of CoffeeShop Products can have.
### Implementing Many to Many CategoryOption Admin UI The easier way to implement this functionality would be to have the UI call an API each time an `Option` was added or removed to a `Category`. The problem with this approach is that it doesn't match the existing behavior where if a User **cancels** a form they'd expect for none of their changes to be applied. To implement the desired functionality we'll instead create a custom `UpdateCategory` implementation that also handles any changes to `CategoryOption` using new `AddOptionIds` and `RemoveOptionIds` properties that we'll want rendered as **hidden** inputs in our HTML Form with: ```csharp public class UpdateCategory : IPatchDb, IReturn { public int Id { get; set; } public string? Name { get; set; } public string? Description { get; set; } [Input(Type = "tag"), FieldCss(Field = "col-span-12")] public List? Sizes { get; set; } [Input(Type = "tag"), FieldCss(Field = "col-span-12")] public List? Temperatures { get; set; } public string? DefaultSize { get; set; } public string? DefaultTemperature { get; set; } [Input(Type = "file"), UploadTo("products")] public string? ImageUrl { get; set; } [Input(Type = "hidden")] public List? AddOptionIds { get; set; } [Input(Type = "hidden")] public List? RemoveOptionIds { get; set; } } ``` ## Custom AutoQuery Implementation The [Custom AutoQuery Implementation](/autoquery/rdbms#custom-autoquery-implementations) in [CoffeeShopServices.cs](https://github.com/NetCoreApps/TypeChatExamples/blob/main/TypeChatExamples.ServiceInterface/CoffeeShopServices.cs) contains the custom implementation which continues to utilize AutoQuery's **Partial Update** functionality if there's any changes to update, as well as removing or adding any Options the user makes to the `Category`: ```csharp public class CoffeeShopServices(IAutoQueryDb autoQuery) : Service { public async Task Any(UpdateCategory request) { // Perform all RDBMS Updates within the same Transaction using var trans = Db.OpenTransaction(); Category? response = null; var ignore = new[]{nameof(request.Id),nameof(request.AddOptionIds),nameof(request.RemoveOptionIds)}; // Only call AutoQuery Update if there's something to update if (request.ToObjectDictionary().HasNonDefaultValues(ignoreKeys:ignore)) { response = (Category) await autoQuery.PartialUpdateAsync(request, Request, Db); } if (request.RemoveOptionIds?.Count > 0) { await Db.DeleteAsync(x => x.CategoryId == request.Id && request.RemoveOptionIds.Contains(x.OptionId)); } if (request.AddOptionIds?.Count > 0) { await Db.InsertAllAsync(request.AddOptionIds.Map(id => new CategoryOption { CategoryId = request.Id, OptionId = id })); } trans.Commit(); response ??= request.ConvertTo(); return response; } } ``` ## Custom AutoForm Component It now needs to implement a Custom UI that Adds/Removes Options from a Category which is done in a custom `CategoryOptions` Vue Component that displays all the Category Options with a button to remove existing ones and a Select Input to add non existing options. The purpose of the component is to populate the `addOptionIds` field with Option Ids that should be added and `removeOptionIds` with Ids to be removed, which updates the Request DTO of the parent Form Model with the `update:modelValue` event: ```js const CategoryOptions = { template:`
  • {{optionType}} Remove Option
Add Option
`, props:['type','id','modelValue'], emits:['update:modelValue'], setup(props, { emit }) { const client = useClient() const options = ref([]) const model = props.modelValue model.addOptionIds ??= [] model.removeOptionIds ??= [] const origOptionIds = model.categoryOptions?.map(x => x.optionId) || [] const currentOptionIds = computed(() => [...origOptionIds, ...model.addOptionIds] .filter(x => !model.removeOptionIds.includes(x))) const currentOptionTypes = computed(() => currentOptionIds.value.map(id => options.value.find(x => x.id === id)?.type).filter(x => !!x)) function addOption(e) { const optionType = e.target.value if (!optionType) return const option = options.value.find(x => x.type === optionType) if (model.removeOptionIds.includes(option.id)) model.removeOptionIds = model.removeOptionIds.filter(id => id !== option.id) else if (!model.addOptionIds.includes(option.id)) model.addOptionIds.push(option.id) emit('update:modelValue', model) } function removeOption(optionType) { const option = options.value.find(x => x.type === optionType) if (model.addOptionIds.includes(option.id)) model.addOptionIds = model.addOptionIds.filter(id => id !== option.id) else if (!model.removeOptionIds.includes(option.id)) model.removeOptionIds.push(option.id) } onMounted(async () => { const api = await client.api(new QueryOptions({ orderBy:'id' })) options.value = api.response.results || [] emit('update:modelValue', model) }) return { options, addOption, removeOption, currentOptionTypes } } } ``` Which is then attached to the AutoQueryGrid Form Components using its `