Multi-Entry Tag Input

Collect multiple freeform or structured values in a compact tag-style input for forms and workflow records.

Custom CodeUI Design & LayoutsYeeflow
custom codetag inputmulti-entryformstsx

Explore the key screens

Explore the key screens and structure included in this template.

Abstract illustration for Multi-Entry Tag Input, showing multi-value structured input.

What this template helps teams build

Overview

The Multi-Entry Tag Input template is a reusable custom code control concept for Yeeflow builders. It helps teams add a focused interface pattern for multi-value structured input while keeping the asset in review before public launch.

Key capabilities

  • Supports structured input scenarios
  • Designed for use in Approval form, Data list form
  • Includes TSX source, user guidance, and example configuration
  • Prepared as a preview asset for technical review and future public documentation

Recommended use cases

  • multi-value structured input
  • Structured Input template library planning
  • Internal builder education and implementation review

Governance notes

This record is created in review status for editorial and product validation. It should not be positioned as launchable until implementation, security, runtime, and documentation review are complete.

Build patterns behind this template

Use this template as a reference while reviewing the Custom Code developer guide. Learn how the required export structure, input parameters, and rendering patterns fit together.

Code preview

Download source file
import React from 'react';

const TEMPLATE_KIND = 'multi-entry-tag-input';
const TEMPLATE_TITLE = 'Multi-Entry Tag Input';
const TEMPLATE_DESC = 'Multi-Entry Tag Input - reusable Yeeflow chip input for entering multiple emails, IDs, keywords, labels, or SKUs.';
const WRITABLE_TARGETS = ["saveToField"];

export class CodeInApplication implements CodeInComp {
  description() { return TEMPLATE_DESC; }
  requiredFields(params?: CodeInParams) {
    const required: string[] = [];
    const safeParams: any = params || {};
    const map: any = {};
    ["saveToField"].forEach((key) => {
      const candidates = this.collectRequiredFieldCandidates(safeParams[key]).filter((value) => this.isLikelyWritableTargetName(value));
      if (candidates.length > 0) { map[key] = candidates[0]; }
      candidates.forEach((value) => { if (value && required.indexOf(value) === -1) { required.push(value); } });
    });
    (this as any).configuredTargetMap = map;
    return required;
  }
  normalizeExpressionText(value: any) { if(value===undefined||value===null){return '';} if(typeof value==='string'||typeof value==='number'||typeof value==='boolean'){return String(value).trim();} if(Array.isArray(value)){return value.map((item)=>this.normalizeExpressionText(item)).filter(Boolean).join(',');} if(typeof value==='object'){return this.normalizeExpressionText(value.fieldId!==undefined?value.fieldId:value.FieldId!==undefined?value.FieldId:value.variableName!==undefined?value.variableName:value.VariableName!==undefined?value.VariableName:value.tempVariableName!==undefined?value.tempVariableName:value.TempVariableName!==undefined?value.TempVariableName:value.key!==undefined?value.key:value.Key!==undefined?value.Key:value.id!==undefined?value.id:value.Id!==undefined?value.Id:value.name!==undefined?value.name:value.Name!==undefined?value.Name:value.label!==undefined?value.label:value.Label!==undefined?value.Label:value.value!==undefined?value.value:value.Value!==undefined?value.Value:'');} return String(value).trim(); }
  collectRequiredFieldCandidates(value: any) { const out: string[]=[]; const seen:any={}; const add=(v:any)=>{const s=this.normalizeExpressionText(v); if(s&&!seen[s]){seen[s]=true;out.push(s);}}; const visit=(v:any,d:number)=>{if(v===undefined||v===null||d>3){return;} if(typeof v==='string'||typeof v==='number'||typeof v==='boolean'){add(v);return;} if(Array.isArray(v)){v.forEach((x)=>visit(x,d+1));return;} if(typeof v!=='object'){return;} ['fieldId','FieldId','fieldName','FieldName','variableId','VariableId','variableName','VariableName','tempVariableId','TempVariableId','tempVariableName','TempVariableName','id','Id','key','Key','name','Name','code','Code','label','Label','title','Title','path','Path','value','Value'].forEach((k)=>{if(v[k]!==undefined&&v[k]!==null){add(v[k]);}}); ['target','Target','binding','Binding','field','Field','variable','Variable','tempVariable','TempVariable','data','Data','meta','Meta','metadata','Metadata'].forEach((k)=>{if(v[k]!==undefined&&v[k]!==null){visit(v[k],d+1);}});}; visit(value,0); return out; }
  isLikelyWritableTargetName(value: any) { const s=this.normalizeExpressionText(value); if(!s||s==='[]'||s==='{}'){return false;} if(s.charAt(0)==='['||s.charAt(0)==='{'){return false;} if(s.indexOf('":')!==-1||s.indexOf('","')!==-1){return false;} return s.length<=160; }
  inputParameters(): InputParameter[] { return [
      {"id": "saveToField", "name": "Save To Field", "type": "variable", "desc": "Writable field for saved tags.", "example": "TagsJson"},
      {"id": "titleText", "name": "Title Text", "type": "string", "desc": "Panel title.", "example": "Recipients"},
      {"id": "subtitleText", "name": "Subtitle Text", "type": "string", "desc": "Panel subtitle.", "example": "Press Enter to add each value"},
      {"id": "placeholderText", "name": "Placeholder Text", "type": "string", "desc": "Input placeholder.", "example": "Type email and press Enter"},
      {"id": "maxTags", "name": "Max Tags", "type": "string", "desc": "Maximum tags.", "example": "20"},
      {"id": "allowDuplicates", "name": "Allow Duplicates", "type": "variable", "desc": "Allow duplicate values.", "example": "false"},
      {"id": "saveMode", "name": "Save Mode", "type": "string", "desc": "json or delimiter.", "example": "json"},
      {"id": "delimiter", "name": "Delimiter", "type": "string", "desc": "Delimiter for delimiter mode.", "example": ","},
      {"id": "emptyText", "name": "Empty Text", "type": "string", "desc": "Empty text.", "example": ""}
    ]; }
  render(context: CodeInContext, fieldsValues: any, readonly: boolean) { return <ReusableTemplatePanel context={context} fieldsValues={fieldsValues} readonly={readonly} params={(context && context.params) || {}} configuredOutputTargets={(this as any).configuredTargetMap || {}} />; }
}

class ReusableTemplatePanel extends React.Component<any, any> {
  private mountedFlag: boolean;
  constructor(props: any) {
    super(props);
    this.mountedFlag = false;
    this.state = this.createInitialState(props);
  }
  componentDidMount() { this.mountedFlag = true; if (this.needsQuery()) { this.loadData(); } }
  componentDidUpdate(previousProps: any) { if (this.getConfigKey(previousProps) !== this.getConfigKey(this.props)) { this.setState(this.createInitialState(this.props), () => { if (this.needsQuery()) { this.loadData(); } }); } }
  componentWillUnmount() { this.mountedFlag = false; }
  createInitialState(props: any) { const config=this.getConfigFromProps(props); return { loading:false,errorText:'',rows:[],selected:[],tags:this.readInitialArray(config),checklist:this.readInitialChecklist(config),parentValue:this.toString(this.readTarget(config.parentTarget)),childValue:this.toString(this.readTarget(config.childTarget)),textValue:'',validationMessage:'' }; }
  needsQuery() { return ['approval-timeline','related-record-summary-grid','hierarchical-selector','dependent-selector','activity-timeline','exception-alert-panel'].indexOf(TEMPLATE_KIND)!==-1; }
  getConfig() { return this.getConfigFromProps(this.props); }
  getConfigFromProps(props: any) { const params=(props&&props.params)||{}; return {
    titleText:this.toString(params.titleText)||TEMPLATE_TITLE,
    subtitleText:this.toString(params.subtitleText)||'Enter multiple values quickly.',
    emptyText:this.toString(params.emptyText)||'No records to display.',
    dataListId:this.toString(params.dataListId||params.sourceListId||params.targetListId),
    recordIdValue:this.toString(params.recordIdValue),
    recordIdField:this.toString(params.recordIdField),
    relationField:this.toString(params.relationField),
    relationValue:this.toString(params.relationValue),
    displayedFields:this.parseStringArray(params.displayedFieldsJson||params.displayedFields),
    eventDateField:this.toString(params.eventDateField||params.dateField||params.sortField),
    actorField:this.toString(params.actorField),
    titleField:this.toString(params.titleField||params.labelField||params.eventTitleField),
    statusField:this.toString(params.statusField||params.decisionField||params.severityField),
    commentFieldName:this.toString(params.commentField||params.descriptionField),
    idField:this.toString(params.idField)||'ListDataID',
    parentField:this.toString(params.parentField),
    childField:this.toString(params.childField),
    labelField:this.toString(params.labelField||params.titleField)||'Title',
    valueField:this.toString(params.valueField)||'ListDataID',
    filterExpression:this.resolveParameterValue(params.filterExpression),
    maxItems:Math.max(1,this.toNumber(params.maxItems||params.maxRecords||params.maxResults,8)),
    displayType:this.toString(params.displayType)||'card',
    saveMode:this.toString(params.saveMode)||'json',
    delimiter:this.toString(params.delimiter)||',',
    placeholderText:this.toString(params.placeholderText)||'Type and press Enter',
    allowDuplicates:this.toBoolean(params.allowDuplicates,false),
    maxTags:Math.max(1,this.toNumber(params.maxTags||params.maxItems,20)),
    items:this.parseChecklistItems(params.itemsJson),
    requireAllChecked:this.toBoolean(params.requireAllChecked,true),
    showProgress:this.toBoolean(params.showProgress,true),
    multiSelect:this.toBoolean(params.multiSelect,false),
    rules:this.parseRules(params.rulesJson),
    showSeverity:this.toBoolean(params.showSeverity,true),
    saveTarget:this.resolveWriteTarget('saveToField',params.saveToField),
    selectedItemsTarget:this.resolveWriteTarget('selectedItemsField',params.selectedItemsField),
    parentTarget:this.resolveWriteTarget('parentSaveToField',params.parentSaveToField),
    childTarget:this.resolveWriteTarget('childSaveToField',params.childSaveToField),
  }; }
  getConfigKey(props:any) { try { return JSON.stringify((props&&props.params)||{}); } catch(error) { return String(new Date().getTime()); } }
  loadData() { const config=this.getConfig(); if(!config.dataListId){return;} const client=this.props.context&&this.props.context.modules&&this.props.context.modules.yeeSDKClient; if(!client||!client.lists||typeof client.lists.queryItems!=='function'){this.setState({errorText:'Data list service is not available'});return;} const fields=[config.recordIdField,config.relationField,config.eventDateField,config.actorField,config.titleField,config.statusField,config.commentFieldName,config.idField,config.parentField,config.childField,config.labelField,config.valueField].concat(config.displayedFields).filter(Boolean); this.setState({loading:true,errorText:''}); this.queryList(client,config.dataListId,fields,Math.max(50,config.maxItems*10),config.filterExpression).then((rows)=>{if(this.mountedFlag){this.setState({loading:false,rows:this.filterRows(rows,config)});}}).catch(()=>{if(this.mountedFlag){this.setState({loading:false,errorText:'Unable to load records'});}}); }
  filterRows(rows:any[],config:any) { let output=rows||[]; if(config.relationField&&config.relationValue){output=output.filter((r)=>this.toString(this.readField(r,config.relationField))===config.relationValue);} if(config.recordIdField&&config.recordIdValue){output=output.filter((r)=>this.toString(this.readField(r,config.recordIdField))===config.recordIdValue);} if(config.eventDateField){output=output.slice().sort((a,b)=>{const ad=this.parseDate(this.readField(a,config.eventDateField));const bd=this.parseDate(this.readField(b,config.eventDateField));return (bd?bd.getTime():0)-(ad?ad.getTime():0);});} return output.slice(0,config.maxItems); }
  readInitialArray(config:any) { const raw=this.readTarget(config.saveTarget); const parsed=this.safeParseJson(raw); if(Array.isArray(parsed)){return parsed.map((x)=>this.toString(x)).filter(Boolean);} return this.toString(raw).split(config.delimiter||',').map((x)=>x.trim()).filter(Boolean); }
  readInitialChecklist(config:any) { const raw=this.readTarget(config.saveTarget); const parsed=this.safeParseJson(raw); const map:any={}; if(Array.isArray(parsed)){parsed.forEach((x:any)=>{if(x&&typeof x==='object'){map[this.toString(x.id||x.value||x.label)]=x.checked===true;}});} return map; }
  parseStringArray(value:any) { const parsed=this.safeParseJson(value); if(Array.isArray(parsed)){return parsed.map((x)=>this.toString(x)).filter(Boolean);} return this.toString(value).split(',').map((x)=>x.trim()).filter(Boolean); }
  parseChecklistItems(value:any) { const parsed=this.safeParseJson(value); const src=Array.isArray(parsed)?parsed:[]; return src.map((item:any,index:number)=>{ if(typeof item==='string'){return {id:item,label:item,required:true};} return {id:this.toString(item.id||item.value||item.key)||'item-'+index,label:this.toString(item.label||item.title||item.name)||('Item '+(index+1)),required:this.toBoolean(item.required,true),helper:this.toString(item.helper||item.description)}; }); }
  parseRules(value:any) { const parsed=this.safeParseJson(value); return Array.isArray(parsed)?parsed:[]; }
  persistTags(tags:string[],config:any) { const clean=config.allowDuplicates?tags:this.unique(tags); const value=config.saveMode==='delimiter'?clean.join(config.delimiter):JSON.stringify(clean); this.writeTarget(config.saveTarget,value); this.setState({tags:clean,textValue:''}); }
  persistChecklist(map:any,config:any) { const value=config.items.map((item:any)=>({id:item.id,label:item.label,checked:map[item.id]===true,required:item.required===true})); this.writeTarget(config.saveTarget,JSON.stringify(value)); const required=config.items.filter((x:any)=>x.required!==false); const done=required.filter((x:any)=>map[x.id]===true).length; this.setState({checklist:map,validationMessage:config.requireAllChecked&&done<required.length?'Complete all required checklist items before submitting.':''}); }
  unique(values:string[]) { const seen:any={}; const out:string[]=[]; values.forEach((v)=>{const s=this.toString(v); const k=s.toLowerCase(); if(s&&!seen[k]){seen[k]=true;out.push(s);}}); return out; }

  resolveParameterValue(value: any): any {
    if (value === undefined || value === null) { return ''; }
    if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || Array.isArray(value)) { return value; }
    if (typeof value === 'object') {
      const preferred = this.firstDefined([value.value,value.Value,value.data,value.Data,value.result,value.Result,value.key,value.Key,value.id,value.Id,value.fieldId,value.FieldId,value.fieldName,value.FieldName,value.label,value.Label,value.name,value.Name]);
      if (preferred !== undefined) { return this.resolveParameterValue(preferred); }
    }
    return value;
  }
  toString(value: any): string {
    const resolved = this.resolveParameterValue(value);
    if (resolved === undefined || resolved === null) { return ''; }
    if (typeof resolved === 'string' || typeof resolved === 'number' || typeof resolved === 'boolean') { return String(resolved).trim(); }
    if (Array.isArray(resolved)) { return resolved.map((item) => this.toString(item)).filter(Boolean).join(', '); }
    if (typeof resolved === 'object') {
      const preferred = this.firstDefined([resolved.value,resolved.Value,resolved.text,resolved.Text,resolved.display,resolved.Display,resolved.label,resolved.Label,resolved.name,resolved.Name,resolved.title,resolved.Title,resolved.key,resolved.Key]);
      if (preferred !== undefined) { return this.toString(preferred); }
      try { return JSON.stringify(resolved); } catch (error) { return ''; }
    }
    return '';
  }
  toBoolean(value: any, fallback: boolean): boolean {
    const resolved = this.resolveParameterValue(value);
    if (resolved === undefined || resolved === null || resolved === '') { return fallback; }
    if (typeof resolved === 'boolean') { return resolved; }
    if (typeof resolved === 'number') { return resolved !== 0; }
    const text = this.toString(resolved).toLowerCase();
    if (['true','1','yes','y','on'].indexOf(text) !== -1) { return true; }
    if (['false','0','no','n','off'].indexOf(text) !== -1) { return false; }
    return fallback;
  }
  toNumber(value: any, fallback: number): number { const parsed = Number(this.toString(value).replace(/,/g,'')); return isNaN(parsed) ? fallback : parsed; }
  safeParseJson(value: any): any { if (Array.isArray(value) || (value && typeof value === 'object')) { return value; } const text=this.toString(value); if(!text){return null;} try{return JSON.parse(text);}catch(error){return null;} }
  firstDefined(values: any[]) { for (let i=0;i<values.length;i+=1){ if(values[i]!==undefined && values[i]!==null){ return values[i]; } } return undefined; }
  readObjectValue(source: any, key: string): any { if(!source || typeof source !== 'object' || !key){return undefined;} if(source[key]!==undefined){return source[key];} const wanted=key.toLowerCase(); const keys=Object.keys(source); for(let i=0;i<keys.length;i+=1){ if(keys[i].toLowerCase()===wanted){ return source[keys[i]]; } } return undefined; }
  normalizeCell(value: any): any { if(value===undefined||value===null){return value;} if(typeof value==='string'||typeof value==='number'||typeof value==='boolean'){return value;} if(Array.isArray(value)){return value.map((item)=>this.toString(this.normalizeCell(item))).filter(Boolean).join(', ');} if(typeof value==='object'){ return this.firstDefined([value.value,value.Value,value.fieldValue,value.FieldValue,value.dataValue,value.DataValue,value.valueText,value.ValueText,value.text,value.Text,value.display,value.Display,value.label,value.Label,value.name,value.Name,value.title,value.Title]); } return value; }
  readField(row: any, field: string): any { if(!row||!field){return undefined;} const direct=this.readObjectValue(row,field); if(direct!==undefined){return this.normalizeCell(direct);} const containers=[row.values,row.Values,row.fields,row.Fields,row.data,row.Data,row.item,row.Item]; for(let i=0;i<containers.length;i+=1){ const v=this.readObjectValue(containers[i],field); if(v!==undefined){return this.normalizeCell(v);} } const arrays=[row.fieldValues,row.FieldValues,row.fieldsValues,row.FieldsValues]; for(let g=0;g<arrays.length;g+=1){ const arr=arrays[g]; if(Array.isArray(arr)){ for(let i=0;i<arr.length;i+=1){ const e=arr[i]||{}; const k=this.toString(this.firstDefined([e.fieldId,e.FieldId,e.fieldID,e.FieldID,e.fieldCode,e.FieldCode,e.name,e.Name,e.key,e.Key])); if(k && k.toLowerCase()===field.toLowerCase()){ return this.normalizeCell(this.firstDefined([e.value,e.Value,e.fieldValue,e.FieldValue,e.dataValue,e.DataValue,e.text,e.Text])); } } } } return undefined; }
  extractRows(response: any): any[] { if(!response){return [];} if(Array.isArray(response)){return response;} const candidates=[response.items,response.Items,response.data,response.Data,response.rows,response.Rows,response.records,response.Records,response.list,response.List,response.result,response.Result,response.value,response.Value]; for(let i=0;i<candidates.length;i+=1){ if(Array.isArray(candidates[i])){return candidates[i];} if(candidates[i]&&typeof candidates[i]==='object'){ const nested=this.extractRows(candidates[i]); if(nested.length>0){return nested;} } } return []; }
  queryList(client: any, listId: string, fields: string[], pageSize: number, filter: any): Promise<any[]> { const selected=['ListDataID','ListDataId'].concat(fields||[]); const parsed=this.safeParseJson(filter); const filterValue=parsed!==null&&parsed!==undefined?parsed:filter; const payload:any={listId:listId,dataListId:listId,fields:selected,fieldIds:selected,selectedFields:selected,pageIndex:1,pageNo:1,pageSize:pageSize}; if(filterValue!==undefined&&filterValue!==null&&this.toString(filterValue)!==''){payload.filters=filterValue;payload.filter=filterValue;payload.where=filterValue;payload.filterExpression=filterValue;} const payload2:any={}; Object.keys(payload).forEach((k)=>{payload2[k]=payload[k];}); delete payload2.listId; delete payload2.dataListId; const attempts=[()=>client.lists.queryItems(payload),()=>client.lists.queryItems(listId,payload2),()=>client.lists.queryItems(listId,payload),()=>client.lists.queryItems(listId,payload2,{})]; const run=(index:number):Promise<any[]>=>{ if(index>=attempts.length){return Promise.resolve([]);} let result:any; try{result=attempts[index]();}catch(error){return run(index+1);} return Promise.resolve(result).then((response)=>{const rows=this.extractRows(response); if(rows.length===0&&index<attempts.length-1){return run(index+1);} return rows;}).catch(()=>run(index+1)); }; return run(0); }
  parseDate(value: any): Date | null { const cell=this.normalizeCell(value); if(cell===undefined||cell===null||cell===''){return null;} if(cell instanceof Date && !isNaN(cell.getTime())){return cell;} if(typeof cell==='number'){ if(cell>100000000000){const d=new Date(cell);return isNaN(d.getTime())?null:d;} if(cell>1000000000){const d=new Date(cell*1000);return isNaN(d.getTime())?null:d;} if(cell>20000&&cell<80000){const d=new Date(Math.round((cell-25569)*86400*1000));return isNaN(d.getTime())?null:d;} } const text=this.toString(cell); const iso=/^(\d{4})-(\d{1,2})-(\d{1,2})/.exec(text); if(iso){return new Date(Number(iso[1]),Number(iso[2])-1,Number(iso[3]));} const parsed=new Date(text); return isNaN(parsed.getTime())?null:parsed; }
  formatDate(value: any): string { const d=this.parseDate(value); if(!d){return this.toString(value);} return d.getFullYear()+'-'+this.pad(d.getMonth()+1)+'-'+this.pad(d.getDate()); }
  pad(value:number){return value<10?'0'+value:String(value);}


  resolveWriteTarget(paramName: string, rawValue: any): any { const configured=(this.props&&this.props.configuredOutputTargets)||{}; const candidates=this.mergeCandidates(this.collectTargets(configured[paramName]),this.collectTargets(rawValue)).filter((x)=>this.isLikelyTarget(x)); return {paramName:paramName,candidates:candidates,displayValue:candidates[0]||this.toString(rawValue)}; }
  collectTargets(rawValue:any): string[]{ const out:string[]=[]; const seen:any={}; const add=(v:any)=>{const s=this.toString(v); if(s&&!seen[s]){seen[s]=true;out.push(s);}}; const visit=(v:any,d:number)=>{ if(v===undefined||v===null||d>4){return;} if(typeof v==='string'||typeof v==='number'||typeof v==='boolean'){add(v);return;} if(Array.isArray(v)){v.forEach((x)=>visit(x,d+1));return;} if(typeof v!=='object'){return;} ['fieldId','FieldId','fieldName','FieldName','variableId','VariableId','variableName','VariableName','tempVariableId','TempVariableId','tempVariableName','TempVariableName','id','Id','key','Key','name','Name','code','Code','label','Label','title','Title','path','Path','value','Value'].forEach((k)=>{if(v[k]!==undefined&&v[k]!==null){add(v[k]);}}); ['target','Target','binding','Binding','field','Field','variable','Variable','tempVariable','TempVariable','data','Data','meta','Meta','metadata','Metadata'].forEach((k)=>{if(v[k]!==undefined&&v[k]!==null){visit(v[k],d+1);}}); }; visit(rawValue,0); return out; }
  mergeCandidates(a:string[],b:string[]){const out:string[]=[];const seen:any={};(a||[]).concat(b||[]).forEach((x)=>{const s=this.toString(x);if(s&&!seen[s]){seen[s]=true;out.push(s);}});return out;}
  isLikelyTarget(value:any){const s=this.toString(value); if(!s||s==='[]'||s==='{}'){return false;} if(s.charAt(0)==='['||s.charAt(0)==='{'){return false;} if(s.indexOf('":')!==-1||s.indexOf('\",\"')!==-1){return false;} return s.length<=160;}
  writeTarget(target:any,value:any){const candidates=target&&target.candidates?target.candidates:[]; for(let i=0;i<candidates.length;i+=1){ if(this.writeValue(candidates[i],value)){return true;} } return false;}
  writeValue(name:string,value:any){const context=this.props.context||{}; const fieldsValues=this.props.fieldsValues||{}; const hosts=[context,context.formContext,context.pageContext,context.variableContext,context.variables,context.tempVariables,context.runtimeContext,context.dataContext].filter(Boolean); const methods=['setFieldValue','setFormFieldValue','setFieldData','setValue','setVariableValue','setVariable','setTempVariableValue','setTempVariable','updateFieldValue','updateVariableValue']; for(let h=0;h<hosts.length;h+=1){for(let m=0;m<methods.length;m+=1){const fn=hosts[h][methods[m]]; if(typeof fn==='function'){try{const result=fn.call(hosts[h],name,value); if(result!==false){return true;}}catch(error){}}}} const objectMethods=['setFieldsValue','setFieldValues','setValues','setVariables','setTempVariables']; for(let h=0;h<hosts.length;h+=1){for(let m=0;m<objectMethods.length;m+=1){const fn=hosts[h][objectMethods[m]]; if(typeof fn==='function'){try{const payload:any={};payload[name]=value;const result=fn.call(hosts[h],payload); if(result!==false){return true;}}catch(error){}}}} fieldsValues[name]=value; return true;}
  readTarget(target:any){const candidates=target&&target.candidates?target.candidates:[]; const context=this.props.context||{}; const fieldsValues=this.props.fieldsValues||{}; for(let i=0;i<candidates.length;i+=1){const k=candidates[i]; if(fieldsValues&&fieldsValues[k]!==undefined){return fieldsValues[k];} if(context&&typeof context.getFieldValue==='function'){try{const v=context.getFieldValue(k); if(v!==undefined&&v!==null&&this.toString(v)!==''){return v;}}catch(error){}}} return '';}

  render() { const config=this.getConfig(); return <div className="yft-panel"><style>{this.getStyles()}</style><div className="yft-header"><div><div className="yft-title">{config.titleText}</div><div className="yft-subtitle">{config.subtitleText}</div></div>{this.renderBadge(config)}</div>{this.renderBody(config)}</div>; }
  renderBadge(config:any) { if(TEMPLATE_KIND==='multi-entry-tag-input'){return <span className="yft-badge">{this.state.tags.length} / {config.maxTags}</span>;} if(TEMPLATE_KIND==='checklist-compliance-block'){const total=config.items.length; const done=config.items.filter((x:any)=>this.state.checklist[x.id]===true).length; return <span className="yft-badge">{done} / {total}</span>;} if(this.state.rows&&this.state.rows.length){return <span className="yft-badge">{this.state.rows.length} records</span>;} return null; }
  renderBody(config:any) { if(this.state.loading){return <div className="yft-loading">Loading...</div>;} if(this.state.errorText){return <div className="yft-empty yft-error">{this.state.errorText}</div>;} if(TEMPLATE_KIND==='multi-entry-tag-input'){return this.renderTagInput(config);} if(TEMPLATE_KIND==='checklist-compliance-block'){return this.renderChecklist(config);} if(TEMPLATE_KIND==='hierarchical-selector'){return this.renderTree(config);} if(TEMPLATE_KIND==='dependent-selector'){return this.renderDependent(config);} if(TEMPLATE_KIND==='exception-alert-panel'){return this.renderAlerts(config);} if(TEMPLATE_KIND==='related-record-summary-grid'){return this.renderRelated(config);} return this.renderTimeline(config); }
  renderTagInput(config:any) { const readonly=this.props.readonly; return <div><div className="yft-chiprow">{this.state.tags.map((tag:string,index:number)=><span className="yft-chip" key={tag+'-'+index}>{tag}{!readonly?<button type="button" onClick={()=>{const next=this.state.tags.slice();next.splice(index,1);this.persistTags(next,config);}}>x</button>:null}</span>)}</div>{!readonly?<input className="yft-input" value={this.state.textValue} placeholder={config.placeholderText} onChange={(e)=>this.setState({textValue:e.target.value})} onKeyDown={(e)=>{if(e.key==='Enter'){e.preventDefault();const value=this.toString(this.state.textValue);if(value&&this.state.tags.length<config.maxTags){this.persistTags(this.state.tags.concat([value]),config);}}}} />:null}</div>; }
  renderChecklist(config:any) { if(!config.items.length){return <div className="yft-empty">Configure checklist items JSON.</div>;} const total=config.items.length; const done=config.items.filter((x:any)=>this.state.checklist[x.id]===true).length; return <div>{config.items.map((item:any)=><label className="yft-row" key={item.id}><input type="checkbox" checked={this.state.checklist[item.id]===true} disabled={this.props.readonly} onChange={(e)=>{const map:any={};Object.keys(this.state.checklist).forEach((k)=>map[k]=this.state.checklist[k]);map[item.id]=e.target.checked;this.persistChecklist(map,config);}} /> <span className="yft-row-title">{item.label}{item.required!==false?' *':''}</span>{item.helper?<div className="yft-row-meta">{item.helper}</div>:null}</label>)}{config.showProgress?<div className="yft-progress"><span style={{width:(total?Math.round(done/total*100):0)+'%'}} /></div>:null}{this.state.validationMessage?<div className="yft-validation">{this.state.validationMessage}</div>:null}</div>; }
  renderTimeline(config:any) { const rows=this.state.rows||[]; if(!rows.length){return <div className="yft-empty">{config.emptyText}</div>;} return <div className="yft-timeline">{rows.map((row:any,index:number)=><div className="yft-event" key={index}><div className="yft-row-title">{this.toString(this.readField(row,config.titleField))||this.toString(this.readField(row,config.statusField))||'Event'}</div><div className="yft-row-meta">{this.formatDate(this.readField(row,config.eventDateField))} {this.toString(this.readField(row,config.actorField))}</div>{this.toString(this.readField(row,config.commentFieldName))?<div className="yft-row-meta">{this.toString(this.readField(row,config.commentFieldName))}</div>:null}</div>)}</div>; }
  renderRelated(config:any) { const rows=this.state.rows||[]; if(!rows.length){return <div className="yft-empty">{config.emptyText}</div>;} const fields=config.displayedFields.length?config.displayedFields:[config.titleField,config.statusField,config.commentFieldName].filter(Boolean); if(config.displayType==='table'){return <table className="yft-table"><thead><tr>{fields.map((f:string)=><th key={f}>{f}</th>)}</tr></thead><tbody>{rows.map((row:any,i:number)=><tr key={i}>{fields.map((f:string)=><td key={f}>{this.toString(this.readField(row,f))}</td>)}</tr>)}</tbody></table>;} return <div className="yft-grid">{rows.map((row:any,i:number)=><div className="yft-card" key={i}>{fields.map((f:string,index:number)=><div key={f} className={index===0?'yft-row-title':'yft-row-meta'}>{index>0?f+': ':''}{this.toString(this.readField(row,f))}</div>)}</div>)}</div>; }
  renderTree(config:any) { const rows=this.state.rows||[]; if(!rows.length){return <div className="yft-empty">{config.emptyText}</div>;} const byParent:any={}; rows.forEach((r:any)=>{const p=this.toString(this.readField(r,config.parentField)); if(!byParent[p]){byParent[p]=[];} byParent[p].push(r);}); const renderNodes=(parent:string):any=> (byParent[parent]||[]).map((row:any,index:number)=>{const value=this.toString(this.readField(row,config.valueField)||this.readField(row,config.idField)); const label=this.toString(this.readField(row,config.labelField))||value; const selected=this.state.selected.indexOf(value)!==-1; return <div className="yft-tree-node" key={value||index}><button type="button" className={selected?'yft-btn yft-btn-active':'yft-btn'} onClick={()=>{let next=config.multiSelect?this.state.selected.slice():[]; const pos=next.indexOf(value); if(pos===-1){next.push(value);}else{next.splice(pos,1);} this.writeTarget(config.saveTarget,JSON.stringify(next)); this.setState({selected:next});}}>{label}</button><div className="yft-tree-children">{renderNodes(value)}</div></div>;}); return <div>{renderNodes('')}</div>; }
  renderDependent(config:any) { const rows=this.state.rows||[]; const parents=this.unique(rows.map((r:any)=>this.toString(this.readField(r,config.parentField))).filter(Boolean)); const children=this.unique(rows.filter((r:any)=>this.toString(this.readField(r,config.parentField))===this.state.parentValue).map((r:any)=>this.toString(this.readField(r,config.childField))).filter(Boolean)); return <div className="yft-grid"><div><div className="yft-muted">Parent</div><select className="yft-select" value={this.state.parentValue} disabled={this.props.readonly} onChange={(e)=>{this.setState({parentValue:e.target.value,childValue:''});this.writeTarget(config.parentTarget,e.target.value);this.writeTarget(config.childTarget,'');}}><option value="">Select</option>{parents.map((p)=><option key={p} value={p}>{p}</option>)}</select></div><div><div className="yft-muted">Child</div><select className="yft-select" value={this.state.childValue} disabled={this.props.readonly||!this.state.parentValue} onChange={(e)=>{this.setState({childValue:e.target.value});this.writeTarget(config.childTarget,e.target.value);}}><option value="">Select</option>{children.map((c)=><option key={c} value={c}>{c}</option>)}</select></div></div>; }
  renderAlerts(config:any) { const rows=this.state.rows||[]; const rules=config.rules; let alerts:any[]=[]; if(rules.length){ rows.forEach((row:any)=>{ rules.forEach((rule:any)=>{const field=this.toString(rule.field); const op=this.toString(rule.operator||'equals'); const expected=this.toString(rule.value); const actual=this.toString(this.readField(row,field)); const hit=op==='notEmpty'?actual!=='':op==='empty'?actual==='':op==='contains'?actual.indexOf(expected)!==-1:actual===expected; if(hit){alerts.push({title:this.toString(rule.title)||this.toString(this.readField(row,config.titleField))||'Alert',message:this.toString(rule.message)||field+' '+op+' '+expected,severity:this.toString(rule.severity)||'warning'});} }); });} else { alerts=rows.map((row:any)=>({title:this.toString(this.readField(row,config.titleField))||'Alert',message:this.toString(this.readField(row,config.commentFieldName)),severity:this.toString(this.readField(row,config.statusField))||'warning'})); } if(!alerts.length){return <div className="yft-empty">{config.emptyText}</div>;} return <div>{alerts.slice(0,config.maxItems).map((a:any,i:number)=><div className={'yft-alert yft-alert-'+(a.severity||'warning')} key={i}><div className="yft-row-title">{a.title}</div>{a.message?<div className="yft-row-meta">{a.message}</div>:null}</div>)}</div>; }
  getStyles() { return [".yft-panel{box-sizing:border-box;width:100%;min-width:0;background:#fff;border:1px solid #e5eaf3;border-radius:8px;padding:16px;color:#111827;font-family:Inter,-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;box-shadow:0 1px 2px rgba(16,24,40,.04);}", ".yft-panel *{box-sizing:border-box;}", ".yft-header{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:14px;}", ".yft-title{font-size:16px;line-height:22px;font-weight:700;color:#101828;}", ".yft-subtitle{font-size:12px;line-height:18px;color:#667085;margin-top:3px;}", ".yft-badge{border:1px solid #e5eaf3;background:#f8fafc;border-radius:999px;padding:3px 9px;font-size:12px;color:#475467;}", ".yft-empty,.yft-loading{min-height:120px;display:flex;align-items:center;justify-content:center;text-align:center;border:1px dashed #d8deea;border-radius:8px;background:#f8fafc;color:#667085;font-size:13px;line-height:20px;padding:20px;}", ".yft-error{color:#b42318;background:#fff7f6;border-color:#fecdca;}", ".yft-row{border:1px solid #e5eaf3;border-radius:8px;padding:10px 12px;background:#fff;margin-bottom:8px;}", ".yft-row-title{font-size:13px;font-weight:700;line-height:18px;color:#101828;}", ".yft-row-meta{font-size:12px;line-height:18px;color:#667085;margin-top:3px;}", ".yft-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;}", ".yft-card{border:1px solid #e5eaf3;border-radius:8px;background:#fff;padding:12px;}", ".yft-table{width:100%;border-collapse:separate;border-spacing:0;border:1px solid #e5eaf3;border-radius:8px;overflow:hidden;}", ".yft-table th{background:#f8fafc;color:#475467;font-size:12px;text-align:left;padding:9px;border-bottom:1px solid #e5eaf3;}", ".yft-table td{font-size:13px;color:#344054;padding:9px;border-bottom:1px solid #eef2f7;}", ".yft-chiprow{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:10px;}", ".yft-chip{display:inline-flex;align-items:center;gap:6px;border:1px solid #c8dafc;background:#edf4ff;color:#0d4fb3;border-radius:999px;padding:5px 9px;font-size:13px;line-height:18px;}", ".yft-chip button{appearance:none;border:0;background:transparent;color:#0d4fb3;cursor:pointer;font-size:15px;line-height:15px;padding:0;}", ".yft-input,.yft-textarea,.yft-select{width:100%;border:1px solid #d8deea;border-radius:8px;padding:9px 10px;font-size:13px;line-height:20px;color:#101828;background:#fff;outline:none;font-family:inherit;}", ".yft-textarea{min-height:86px;resize:vertical;}", ".yft-input:focus,.yft-textarea:focus,.yft-select:focus{border-color:#146ff6;box-shadow:0 0 0 3px rgba(20,111,246,.12);}", ".yft-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px;}", ".yft-btn{appearance:none;border:1px solid #d8deea;background:#fff;color:#344054;border-radius:8px;padding:8px 11px;font-size:13px;font-weight:650;cursor:pointer;}", ".yft-btn-active{border-color:#146ff6;background:#edf4ff;color:#0d4fb3;}", ".yft-btn-primary{border-color:#146ff6;background:#146ff6;color:#fff;}", ".yft-validation{margin-top:10px;border:1px solid #fecdca;background:#fff7f6;color:#b42318;border-radius:8px;padding:9px 10px;font-size:12px;line-height:18px;}", ".yft-progress{height:8px;background:#eef2f7;border-radius:999px;overflow:hidden;margin-top:10px;}", ".yft-progress span{display:block;height:100%;background:#146ff6;border-radius:999px;}", ".yft-timeline{position:relative;padding-left:18px;}", ".yft-timeline:before{content:\"\";position:absolute;left:5px;top:4px;bottom:4px;width:2px;background:#e5eaf3;}", ".yft-event{position:relative;margin-bottom:12px;border:1px solid #e5eaf3;border-radius:8px;padding:10px 12px;background:#fff;}", ".yft-event:before{content:\"\";position:absolute;left:-17px;top:14px;width:10px;height:10px;border-radius:999px;background:#146ff6;box-shadow:0 0 0 4px #edf4ff;}", ".yft-alert{border:1px solid #fecdca;background:#fff7f6;border-radius:8px;padding:10px 12px;margin-bottom:8px;color:#7a271a;}", ".yft-alert-warning{border-color:#fedf89;background:#fffbeb;color:#7a4b00;}", ".yft-alert-info{border-color:#b2ddff;background:#eff8ff;color:#1849a9;}", ".yft-alert-success{border-color:#abefc6;background:#f0fdf4;color:#067647;}", ".yft-tree-node{border:1px solid #e5eaf3;border-radius:8px;padding:9px 10px;margin-bottom:8px;background:#fff;}", ".yft-tree-children{margin-left:18px;margin-top:8px;}", ".yft-muted{color:#667085;font-size:12px;line-height:18px;}", "@media(max-width:640px){.yft-panel{padding:14px}.yft-header{flex-direction:column}.yft-table{display:block;overflow-x:auto}.yft-actions{flex-direction:column}.yft-btn{width:100%;text-align:center}}"].join(''); }
}

User guide

Download guide
# Multi-Entry Tag Input

## Short Description

Multi-Entry Tag Input - reusable Yeeflow chip input for entering multiple emails, IDs, keywords, labels, or SKUs.

## Purpose / What This Is For

Captures multiple free-text values as clean chips and saves them to a configured field.

## Supported Placement

Approval form; Data list form

## When To Use

Use this control when teams need a reusable Yeeflow control for emails, reference numbers, SKUs, labels, keywords, and IDs.

## Recommended Defaults

Start with the required source/save parameters, keep labels concise, and test with a small data set before publishing.

## Input Parameters Overview

| Parameter | Type | Purpose |
| --- | --- | --- |
| `saveToField` | `variable` | Writable field for saved tags. |
| `titleText` | `string` | Panel title. |
| `subtitleText` | `string` | Panel subtitle. |
| `placeholderText` | `string` | Input placeholder. |
| `maxTags` | `string` | Maximum tags. |
| `allowDuplicates` | `variable` | Allow duplicate values. |
| `saveMode` | `string` | json or delimiter. |
| `delimiter` | `string` | Delimiter for delimiter mode. |
| `emptyText` | `string` | Empty text. |

## Detailed Parameter Explanation

Configure expression-editor parameters with Yeeflow fields, variables, temp variables, or static values as appropriate. Writable target parameters save output values and are resolved defensively at runtime. Plain text parameters control display labels and helper text.

## Step-By-Step Setup

1. Add a Custom Code control to the supported Yeeflow placement.
2. Paste the `multi-entry-tag-input.tsx` code file.
3. Configure required data source, field, and save-target parameters.
4. Configure labels, limits, and display options.
5. Preview with realistic records.
6. Confirm readonly, empty, and validation states.
7. Publish when behavior is correct.

## Result / Expected Output

A tag/chip input that saves either a JSON array or delimited text.

## Real Business Examples

- Procurement approval and vendor review.
- Case, request, or task management.
- Policy, compliance, and operational control checks.
- Department, location, product, or category-driven workflows.

## Notes / Assumptions / Limitations

- Runtime query behavior uses `yeeSDKClient.lists.queryItems(...)` where list data is needed.
- Expression-editor values are normalized from strings, booleans, objects, arrays, and variable selector values.
- Writable targets use defensive setter paths and a local `fieldsValues` fallback.
- Tenant-specific query filter shapes may require adjustment in `filterExpression`.

## Testing Checklist

- Required parameters configured.
- Empty state displays safely.
- Existing saved values initialize correctly when applicable.
- Save targets update as expected when applicable.
- Readonly mode prevents editing.
- Narrow layout remains readable.

## Troubleshooting

If data does not appear, verify list ID, field IDs, filter expression, and permissions. If saving does not work, confirm the target parameter was selected as a writable form field or variable and that the field exists on the form.

Example configuration

Download config
# Multi-Entry Tag Input Example Config

| Parameter | Example Value |
| --- | --- |
| `saveToField` | `TagsJson` |
| `titleText` | `Recipients` |
| `subtitleText` | `Press Enter to add each value` |
| `placeholderText` | `Type email and press Enter` |
| `maxTags` | `20` |
| `allowDuplicates` | `false` |
| `saveMode` | `json` |
| `delimiter` | `,` |
| `emptyText` | `` |

Adjust field IDs and list IDs to match the target Yeeflow application.