import {Directive, Component, ElementRef, Renderer, forwardRef, Provider,Input, OnInit, Inject, EventEmitter, Output, OnChanges, AfterViewInit, ViewChild} from '@angular/core'; //import { Http} from '@angular/http'; import { DyCommon } from '../Common' import { ControlValidation } from '../Interfaces/ControlValidation' import {ReadOnlyControl} from "../Interfaces/ReadOnlyControl" import { NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms'; import { HttpClient } from '@angular/common/http'; import { Subject } from 'rxjs'; import { ModalComponent } from './Modal'; import { ContactResolver } from '../Services/Contact.Resolver'; import { RecordTypeService } from '../Services/RecordTypeService'; import { DxPopoverComponent } from 'devextreme-angular'; const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AutocompleteComponent), multi: true }; declare var service; export interface AutoCompleteConfig { resultFormat?: string, selectFormat?: string, popoverFormat?: string, popoverTitleFormat?: string, idFormat?: string, object?: string, filter?: string } export interface AdditionalOption { text: string; id: string; isCustomSelection?: boolean; } export interface IFilter { filter: string, filterModelProperties:string[] } @Component({ selector: 'DyAutocomplete', //properties: ["width","customClass:custom-class","searchType:search-type","idField:id-field","addressLookup:address-lookup","filter:filter","placeholder:placeholder"], providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR], //events: ["ValueChange"], //inputs: ["id","text"], template: `
{{text}}
` }) export class AutocompleteComponent implements OnInit, AfterViewInit, ControlValueAccessor, ReadOnlyControl { elementRef: ElementRef; //@Input() id: string; @Input() width; @ViewChild("addNew", { static: true }) modal: ModalComponent; @ViewChild("popover", { static: false }) popover: DxPopoverComponent; @Output() idChange: EventEmitter = new EventEmitter() @Input() additionalResults: AdditionalOption[]; private _readOnly: boolean; @Input() addNewOptions; @Input() set readOnly(value) { this._readOnly = value; } get readOnly() { return this._readOnly; } @Input() mutiple: boolean = false; //@Input() text: string @Input() placeholder: string; public IsValid: boolean; private _isMultiSelect: boolean @Input() set MultiSelect(value) { this._isMultiSelect = true; } get MultiSelect() { return this._isMultiSelect; } @Input("dy-required") dyRequired:boolean; @Output() textChange: EventEmitter = new EventEmitter() //@Input() model: any; @Output() Changed:EventEmitter= new EventEmitter(); @Input() filter: string; @Input("allowAddNew") allowAddNew: boolean; private commonHelper:DyCommon; @Output("onAddNew") OnAddNew = new EventEmitter(); @Input() _searchType: string @Input() Config: AutoCompleteConfig @Input() select; @Input() searchParam1; @Input() lookupId: boolean = false; public http: HttpClient; public customClass = "form-group"; //httpService: Http; @Input() term: string; private _id: string; private _text: string; private _popoverFormat: string; private _popoverTitleFormat: string; private _model: any; private itemsInformation; public recordTypeServce: RecordTypeService; private _onTouchedCallback: (_: any) => void; private _onChangeCallback: (_: any) => void; @Input() control:any @Input() set model(value) { this._model = value; if (value != undefined && this.select != undefined && !(Object.keys(value).length === 0 && value.constructor === Object)) { if (Object.getOwnPropertyNames(value).length == 0) { var sel = $(this.elementRef.nativeElement).find("select"); $(this.elementRef.nativeElement).find("select").empty(); var option = new Option("","", true, true); this.select.append(option).trigger("change"); //this.select.select2("placeholder", "test"); //console.log("placeholder_ "); //console.log(this); $(this.elementRef.nativeElement).popover("destroy"); return; } var option = new Option(this.commonHelper.EvalExpression(this.Config.resultFormat, this.model), this.model.id, true, true); this.select.append(option).trigger("change"); if (this.Config.popoverFormat != undefined) { //this.setPopover($(this.elementRef.nativeElement), value); } } } get model() { return this._model; } public getDisplay() { return } private set id(value) { if (value !== this._id ) { this._id = value; if (this._onChangeCallback != undefined) { this._onChangeCallback(value); } if (this.lookupId == true && this.text == undefined && value != null) { this.http.get(`/api/AutoComplete/${this.Config.object}/${value}`).takeUntil(this.ngUnsubscribe).subscribe((obj) => { this.text= this.commonHelper.EvalExpression(this.Config.selectFormat, obj.items[0]); this.updateSelected(); }) } else { this.updateSelected(); } }; if (this.text == undefined && this.model != undefined) { } } private get id() { return this._id; } @Input() set text(value) { this._text = value; this.updateSelected(); } get text() { if (this.model != undefined) { return this.commonHelper.EvalExpression(this.Config.resultFormat, this.model) } return this._text; } @Input("search-type") set searchType(value) { this._searchType = value; this.updateSelected(); } get searchType() { return this._searchType; } public ValidateControl = () => { if (this.dyRequired && this.id == undefined || this.id == "") { this.IsValid = false; } this.IsValid = true; return this.IsValid; } constructor( @Inject(ElementRef) elementRef: ElementRef,@Inject(HttpClient)_http:HttpClient,@Inject(RecordTypeService) _recordService) { this.elementRef = elementRef; this.recordTypeServce = _recordService; this.http = _http; this.commonHelper = new DyCommon(); } ngOnInit() { } ngAfterViewInit() { this.autoCompleteInit(); console.log(this.popover); } autoCompleteInit = () => { $(this.elementRef.nativeElement).addClass(this.customClass); var natEle = jQuery(this.elementRef.nativeElement); //this.searchType = natEle.attr('search-type'); this.Config = this.getConfig(); if (this.placeholder == undefined) { this.placeholder = " "; } var option: HTMLOptionElement; var item = natEle.find("select"); if (this.mutiple) { item = natEle.find("input"); } if (this.width != undefined) { item.removeClass("standardFieldWidth") item.css("width", this.width); } this.select = item.select2({ ajax: { url: "/api/AutoComplete/" + this.Config.object, dataType: 'json', delay: 450, data: (params: any) => { this.term = params.term; var ret = { term: params.term, // search term page: params.page, filter: this.getDynamicFilter() }; return ret; }, processResults: (data, params) => { // parse the results into the format expected by Select2 // since we are using custom formatting functions we do not need to // alter the remote JSON data, except to indicate that infinite // scrolling can be used try { params.page = params.page || 1; //console.log(params.page); var items = []; if (this.allowAddNew) { items.push({ text: "Add New", value: "ADDNew", id: "$$addnew$$" }); } for (var i = 0; i < data.items.length; i++) { var object = data.items[i]; var term = this.term; if (term == undefined || term == "" || term == " ") { data.items[i].text = this.commonHelper.EvalExpression(this.Config.resultFormat, object) } else { data.items[i].text = this.commonHelper.EvalExpression(this.Config.resultFormat, object, term) } if (this.Config.popoverFormat != undefined) { var test = `
` + data.items[i].text + `
` //this._popoverFormat = DyCommon.makeStringJsonSafe(this.commonHelper.EvalExpression(this.Config.popoverFormat, object)); //this._popoverTitleFormat = DyCommon.makeStringJsonSafe(this.commonHelper.EvalExpression(this.Config.popoverTitleFormat, object)); data.items[i].text = this.commonHelper.EvalExpression(test, object); } items.push(data.items[i]); } if (data.extraResults != undefined && data.extraResults.length > 0) { if (items.length == 0) { items.push({ text: "No Results Found" }); } for (var i = 0; i < data.extraResults.length; i++) { items.push({ text: data.extraResults[i].resultHeading, children: data.extraResults[i].options }) } } if (this.additionalResults != undefined) { for (var i = 0; i < this.additionalResults.length; i++) { this.additionalResults[i].isCustomSelection = true; items.push(this.additionalResults[i]); } } //this.select.select2("minimumInputLength",1) return { results: items, pagination: { more: (params.page * 10) < data.totalItems } }; } catch (e) { console.error(e); } }, cache: true }, theme: "bootstrap", multiple: this._isMultiSelect, tags: this._isMultiSelect, allowClear: true, escapeMarkup: function (markup) { return markup; }, // let our custom formatter work minimumInputLength: 0, placeholder: this.placeholder, templateResult: this.formatRepo, // omitted for brevity, see the source of this page templateSelection: this.formatRepoSelection // omitted for brevity, see the source of this page }).on("select2:select", (e: any) => { if (!this._isMultiSelect) { if (this.select.val() == '$$addnew$$' || (e.params != undefined && e.params.data.id == '$$addnew$$')) { this.OnAddNew.emit({}); return; } this.id = e.params.data.id; this.itemsInformation = e; if (this.onTouched != undefined) { this.onTouched(); } this.idChange.next(e.params.data.id); var newtext = this.commonHelper.EvalExpression(this.Config.selectFormat, e.params.data); this.textChange.next(newtext); if (this.lookupId == true && this.model == undefined) { this.text = newtext; } this.Changed.emit(e.params.data); $(".acoption").popover("destroy"); $(".popover").remove(); // this.setPopover(natEle, e.params.data); //this._popoverFormat = this.commonHelper.EvalExpression(this.Config.popoverFormat, e.params.data); //this._popoverTitleFormat = this.commonHelper.EvalExpression(this.Config.popoverTitleFormat, e.params.data); } else { this._id = this.select.val().join(','); if (this._onChangeCallback != undefined) { this._onChangeCallback(this._id); } if (this.onTouched != undefined) { this.onTouched(); } this.idChange.next(this.select.val().join(',')); //this.textChange.next(this.commonHelper.EvalExpression(this.Config.selectFormat, e.params.data)); this.Changed.emit(this.select.val().join(',')); } }).on("select2:open", (e: any) => { if (this.onTouched != undefined) { this.onTouched(); } }).on("select2:unselect", (e: any) => { if (!this._isMultiSelect) { this.writeValue(null); } else { //if (this.select.val().includes(e.params.data.id)) { //var index = this.select.val().indexOf(e.params.data.id); // if (index !== -1) this.select.val().splice(index, 1); //} this._id = this.select.val().join(','); if (this._onChangeCallback != undefined) { this._onChangeCallback(this._id); } if (this.onTouched != undefined) { this.onTouched(); } this.idChange.next(this.select.val().join(',')); //this.textChange.next(this.commonHelper.EvalExpression(this.Config.selectFormat, e.params.data)); this.Changed.emit(this.select.val().join(',')); } if (this.onTouched != undefined) { this.onTouched(); } }).on("select2:clear", (e: any) => { if (this._isMultiSelect) { this.select.val(null).trigger('change'); if (this.onTouched != undefined) { this.onTouched(); } } }); if (this.model != undefined ) { //console.log("model is undefined"); option = new Option(this.commonHelper.EvalExpression(this.Config.resultFormat, this.model), this.model.id, true, true); this.select.append(option).trigger("change"); if (this.Config.popoverFormat != undefined) { //this.setPopover(natEle, this.model); } } else if (this.text != undefined && this.id != undefined) { let text = this.text; if (text == null) { text = ""; } option = new Option(text, this.id, true, true); this.select.append(option).trigger("change"); } else if (this.id != undefined) { jQuery.get("/api/AutoComplete/" + this.Config.object + "/" + this.id, (res) => { var object = res.items[0]; let text = this.commonHelper.EvalExpression(this.Config.selectFormat, object); if (text == "null" || text == "undefined") { text = ""; } option = new Option(text, object.id, true, true); // console.log(this.commonHelper.EvalExpression(this.Config.selectFormat, object)); this.select.append(option).trigger("change"); } ) } $(this.elementRef.nativeElement).find(".select2-selection__placeholder").text(this.placeholder); //console.log("PLACEHOLDER | " + this.placeholder); } onShown = (e: any) => { if (this.Config.popoverFormat != undefined) { //setTimeout(() => { this._popoverFormat = "Test time" }, 2000) this.http.get(`/api/AutoComplete/${this.Config.object}/${this.id}`).takeUntil(this.ngUnsubscribe).subscribe((obj) => { this._popoverFormat = DyCommon.makeStringJsonSafe(this.commonHelper.EvalExpression(this.Config.popoverFormat, obj.items[0])); this._popoverTitleFormat = DyCommon.makeStringJsonSafe(this.commonHelper.EvalExpression(this.Config.popoverTitleFormat, obj.items[0])); }) //this._popoverFormat = DyCommon.makeStringJsonSafe(this.commonHelper.EvalExpression(this.Config.popoverFormat, this.itemsInformation.params.data)); //this._popoverTitleFormat = DyCommon.makeStringJsonSafe(this.commonHelper.EvalExpression(this.Config.popoverTitleFormat, this.itemsInformation.params.data)); } } setPopover = (element, object) => { return; $(element).popover("destroy"); $(element).popover({ trigger: "hover", title: DyCommon.makeStringJsonSafe(this.commonHelper.EvalExpression(this.Config.popoverTitleFormat, object)), html: true, content: DyCommon.makeStringJsonSafe(this.commonHelper.EvalExpression(this.Config.popoverFormat, object)), container: "body", placement: "top", delay: { "show": 500, "hide": 1000 } }); } updateSelected = () => { if (this.select != undefined) { if (this.MultiSelect) { if (this.id != null && this.id != undefined) { if (this.searchType == "Custom") { this.additionalResults.forEach((addItem) => { this.id.split(',').forEach((id) => { if (id == addItem.id) { option = new Option(addItem.text, addItem.id, true, true); this.select.append(option).trigger("change"); } }); }); } else { this.http.post("bbapi/AutoComplete/Mutiple/" + this.Config.object, this.id.split(',')).takeUntil(this.ngUnsubscribe).subscribe( (res: any) => { res.items.forEach((object) => { let text = this.commonHelper.EvalExpression(this.Config.selectFormat, object); if (text == "null" || text == "undefined") { text = ""; } option = new Option(text, object.id, true, true); // console.log(this.commonHelper.EvalExpression(this.Config.selectFormat, object)); this.select.append(option).trigger("change"); }); }); } } } else { var option = new Option(this.text, this.id, true, true); //console.log(option); //this.select.empty(); this.select.append(option).trigger("change"); } } } formatRepo = (object) => { if (object.loading) return object.text; if (object.text != "") { return object.text; } if (this.Config.popoverFormat != undefined) { return `
` + this.commonHelper.EvalExpression(this.Config.resultFormat, object, this.term).replace(/undefined/g, "") + `
` } return this.commonHelper.EvalExpression(this.Config.resultFormat, object, this.term); } public addNewProductItem = () => { this.modal.Title = "Add New Product"; // this.modal.Component = ProductItemComponent; DyCommon.newGuid().then((id) => { this.modal.componentLoaded.takeUntil(this.ngUnsubscribe).subscribe((comp) => { var prodItem: any; //ProductItemComponent = comp.instance; var custType; prodItem.Model = { id: id }; prodItem.showButtons = false; prodItem.watchRoute = false; this.modal.Buttons = [{ label: "Save", cssClass: "btn btn-success", action: (dialog) => { prodItem.Save().then((data) => { dialog.close(); this.OnAddNew.emit(data); }); } }] }); this.modal.showDialog(); }); } public addNewContact = () => { if (this.searchType == 'Customer') { this.modal.Title = "Add New Customer"; this.modal.Component = ContactComponent; DyCommon.newGuid().then((id) => { this.recordTypeServce.getRecordTypes('Contact').then((contTypes: Array) => { this.modal.componentLoaded.takeUntil(this.ngUnsubscribe).subscribe((comp) => { var cont: ContactComponent = comp.instance; var custType; contTypes.forEach(item => { if (item.name == "Customer") { custType = item; } }); var data = { contact: { id: id, active: true, contactType: "Individual", recordTypeId: custType.id } }; cont.showButtons = false; ContactResolver.setupContact(data); cont.Model = data; cont.watchRoute = false; this.modal.Buttons = [{ label: "Save", cssClass: "btn btn-success", action: (dialog) => { cont.Save().then((data) => { dialog.close(); this.OnAddNew.emit(data); }); } }] }); this.modal.showDialog(); }); }); } else if (this.searchType == 'Contact') { this.modal.Title = "Add New Contact"; this.modal.Component = ContactComponent; DyCommon.newGuid().then((id) => { this.recordTypeServce.getRecordTypes('Contact').then((contTypes: Array) => { this.modal.componentLoaded.takeUntil(this.ngUnsubscribe).subscribe((comp) => { var cont: ContactComponent = comp.instance; var custType; contTypes.forEach(item => { if (item.name == "Uncategorized") { custType = item; } }); console.log(custType); //recordTypeId: find "uncatergorized" var data = { contact: { id: id, active: true, contactType: "Individual", recordTypeId: custType.id } }; cont.showButtons = false; ContactResolver.setupContact(data); cont.Model = data; cont.watchRoute = false; this.modal.Buttons = [{ label: "Save", cssClass: "btn btn-success", action: (dialog) => { cont.Save().then((data) => { dialog.close(); this.OnAddNew.emit(data); }); } }] }); this.modal.showDialog(); }); }); } else { this.OnAddNew.emit({}); } return; } public getDynamicFilter() { if (this.filter == undefined) { return this.Config.filter; } var evalFilter = ""; if (this.Config.filter != undefined) { evalFilter += " " + this.Config.filter; } if (this.filter != undefined) { evalFilter += this.filter; } return evalFilter; } setUpContactConfig= ():AutoCompleteConfig => { return { object: "Contact", resultFormat: `{{fullName}}`, selectFormat: `{{fullName}}`, } } getConfig = ():AutoCompleteConfig => { var defaultReturn: AutoCompleteConfig; switch (this.searchType) { case "Customer": defaultReturn = this.setUpContactConfig(); defaultReturn.filter = "RecordTypeId.Contains(\"GetRecordTypeId(contact,customer)\") and Active=true"; break; case "Contact": defaultReturn = this.setUpContactConfig(); //defaultReturn.filter = "RecordTypeId.Contains(\"GetRecordTypeId(contact,customer)\") and Active=true"; break; case "SalesAssociate": defaultReturn = this.setUpContactConfig(); defaultReturn.filter = "RecordTypeId.Contains(\"GetRecordTypeId(contact,Sales Associate)\") and Active=true"; break; case "Vendor": defaultReturn = this.setUpContactConfig(); defaultReturn.filter = "RecordTypeId.Contains(\"GetRecordTypeId(contact,vendor)\") and Active=true"; break; case "Employee": defaultReturn = this.setUpContactConfig(); defaultReturn.filter = "RecordTypeId.Contains(\"GetRecordTypeId(contact,employee)\") and Active=true"; break; //case "EmployeeContractor": // defaultReturn = this.setUpContactConfig(); // defaultReturn.filter = "RecordTypeId.Contains(\"GetRecordTypeId(contact,Sub Contractor)\") or RecordTypeId.Contains(\"GetRecordTypeId(contact,employee)\")"; // break; case "SubContractor": case "Subcontractor": defaultReturn = this.setUpContactConfig(); defaultReturn.filter = "RecordTypeId.Contains(\"GetRecordTypeId(contact,Sub Contractor)\") and Active=true"; break; case "EmployeeOrSubcontractor": defaultReturn = this.setUpContactConfig(); defaultReturn.filter = "(RecordTypeId.Contains(\"GetRecordTypeId(contact,Sub Contractor)\") or RecordTypeId.Contains(\"GetRecordTypeId(contact,employee)\")) and Active=true"; break; case "Address": return { object: "Address", resultFormat: `{{addressLine1}}
{{city}} , {{state}} {{zip}}`, selectFormat: `{{addressLine1}}`, } case "User": return { object: "User", resultFormat: `{{addressLine1}}
{{city}} , {{state}} {{zip}}`, selectFormat: `{{addressLine1}}`, } case "UserAccount": return { object: "UserAccount", resultFormat: `{{fullName}}`, selectFormat: `{{fullName}}`, } case "CustomerPaymentTerm": return { object: "PaymentTerm", resultFormat: `{{termName}}`, selectFormat: `{{termName}}`, filter: "Active=true and CustomerTerm=true", } case "PaymentTerm": return { object: "PaymentTerm", resultFormat: `{{termName}}`, selectFormat: `{{termName}}`, filter: "Active=true", } case "BankAccount": return { object: "BankAccount", resultFormat: `{{name}}`, filter: "Active=true", selectFormat: `{{name}}`, } case "Location": return { object: "Location", resultFormat: `{{locationName}}`, selectFormat: `{{locationName}}`, filter:"Active=true" } case "SalesTax": return { object: "SalesTaxRate", resultFormat: `{{city}}, {{state}} - {{combinedRate}}%`, selectFormat: `{{city}}, {{state}}`, } case "ProductCategory": return { object: "ProductCategory", resultFormat: `{{name}}-{{allocation}}`, popoverFormat: `Allocation:{{allocation}}
Taxable:{{taxable}}`, popoverTitleFormat: "{{name}}", selectFormat: `{{name}}-{{allocation}}`, } case "ProductItem": return { object: "ProductItem", resultFormat: `{{name}}`, popoverFormat: `SKU:{{skuNumber}}
Description:{{description}}
Vendor:{{vendorName}}`, popoverTitleFormat:"{{name}}", selectFormat: `{{name}}`, } case "ProductAttribute": return { object: "ProductItemAttribute", resultFormat: `{{name}}`, selectFormat: `{{name}}`, } case "CategoryAttributes": return { object: "PriceListAttributeDefinition", resultFormat: '{{name}}', selectFormat: '{{name}}' } case "RecordType": return { object: "RecordType", resultFormat: `{{name}}`, selectFormat: `{{name}}`, } case "ChartOfAccount": return { object: "ChartOfAccount", resultFormat: `{{accountNumber}}-{{accountName}}`, filter: " Active=true", selectFormat: `{{accountNumber}}-{{accountName}}`, } case "CommissionType": return { object: "CommissionType", resultFormat: `{{name}}`, selectFormat: `{{name}}`, } case "PayrollGroupEmployee": return { object: "PayrollGroup", filter: "PayTypeId=\"GetRecordTypeId(PayrollGroupType,Employee)\"", resultFormat: `{{name}}`, selectFormat: `{{name}}`, } case "PayrollGroupSubcontractor": return { object: "PayrollGroup", filter: "PayTypeId=\"GetRecordTypeId(PayrollGroupType,Sub Contractor)\"", resultFormat: `{{name}}`, selectFormat: `{{name}}`, } case "UserRole": return { object: "UserRole", // filter: "PayTypeId=\"GetRecordTypeId(PayrollGroupType,Sub Contractor)\"", resultFormat: `{{name}}`, selectFormat: `{{name}}`, } case "MessageTemplate": return { object: "MessageTemplate", filter: `ObjectName=\"${this.searchParam1}\"`, resultFormat: `{{name}}`, selectFormat: `{{name}}`, } case "Custom": return { object: "Contact", filter: "1=2", resultFormat: `{{text}}`, selectFormat: `{{text}}`, } } return defaultReturn; } private ngUnsubscribe: Subject = new Subject(); ngOnDestroy(): any { this.ngUnsubscribe.next(); this.ngUnsubscribe.complete(); } public lookupTextValue = () => { var p = new Promise((res, rej) => { this.http.get(`bbapi/AutoComplete/${this.Config.object}/${this.id}`).takeUntil(this.ngUnsubscribe).subscribe((val) => { var txt = this.commonHelper.EvalExpression(this.Config.resultFormat, val) this.text = txt; res(txt); }); }); return p; } onTouched() { if (this._onTouchedCallback != null) { this._onTouchedCallback(null); } } //From ControlValueAccessor interface writeValue(value: any) { this.id = value; } //From ControlValueAccessor interface registerOnChange(fn: any) { this._onChangeCallback = fn; } //From ControlValueAccessor interface registerOnTouched(fn: any) { this._onTouchedCallback = fn; } formatRepoSelection = (object) => { var val = this.commonHelper.EvalExpression(this.Config.selectFormat, object) //this is an inital value if (val == "" || Object.getOwnPropertyNames(object).length < 8) { if (this.model != undefined) { return this.commonHelper.EvalExpression(this.Config.selectFormat, this.model); } return object.text; } return val; } private setPropByString(obj: any, propString: string, value: any) { if (!propString) return; var prop, props = propString.split('.'); for (var i = 0, iLen = props.length - 1; i < iLen; i++) { prop = props[i]; var candidate = obj[prop]; if (candidate !== undefined) { obj = candidate; } else { break; } } obj[props[i]] = value; } private getPropByString(obj, propString) { if (!propString) return obj; var prop, props = propString.split('.'); for (var i = 0, iLen = props.length - 1; i < iLen; i++) { prop = props[i]; var candidate = obj[prop]; if (candidate !== undefined) { obj = candidate; } else { break; } } return obj[props[i]]; } }