private static void WriteApiEndpointFunction(TypeScriptCodeBuilder b, ClassViewModel model, MethodViewModel method) { var returnIsListResult = method.ReturnsListResult; string signature = string.Concat(method.ClientParameters.Select(f => $"{f.Name}: {new VueType(f.Type).TsType("$models")} | null, ")) + "$config?: AxiosRequestConfig"; if (method.IsModelInstanceMethod) { signature = $"id: {new VueType(model.PrimaryKey.Type).TsType(null)}, " + signature; } using (b.Block($"public {method.JsVariable}({signature})")) { string resultType = method.TransportTypeGenericParameter.IsVoid ? $"{method.TransportType}<void>" : $"{method.TransportType}<{new VueType(method.TransportTypeGenericParameter).TsType("$models")}>"; b.Line($"const $method = this.$metadata.methods.{method.JsVariable}"); using (b.Block($"const $params = this.$mapParams($method,", ')')) { if (method.IsModelInstanceMethod) { b.Line($"id,"); } foreach (var param in method.ClientParameters) { b.Line($"{param.JsVariable},"); } } b.Line("return AxiosClient"); using (b.Indented()) { b.Line($".{method.ApiActionHttpMethodName.ToLower()}("); b.Indented($"`/${{this.$metadata.controllerRoute}}/{method.Name}`,"); switch (method.ApiActionHttpMethod) { case DataAnnotations.HttpMethod.Get: case DataAnnotations.HttpMethod.Delete: b.Indented($"this.$options(undefined, $config, $params)"); break; default: b.Indented($"qs.stringify($params),"); b.Indented($"this.$options(undefined, $config)"); break; } b.Line(")"); b.Line($".then<AxiosResponse<{resultType}>>(r => this.$hydrate{method.TransportType}(r, $method.return))"); } } // Line between methods b.Line(); }
private static void WriteListViewModel(TypeScriptCodeBuilder b, ClassViewModel model) { string name = model.ViewModelClassName; string viewModelName = $"{model.ListViewModelClassName}ViewModel"; string metadataName = $"$metadata.{name}"; using (b.Block($"export class {viewModelName} extends ListViewModel<$models.{name}, $apiClients.{name}ApiClient>")) { foreach (var method in model.ClientMethods.Where(m => m.IsStatic)) { string signature = string.Concat(method.ClientParameters.Select(f => $", {f.Name}: {new VueType(f.Type).TsType("$models")} | null")); // "item" or "list" var transportTypeSlug = method.TransportType.ToString().Replace("Result", "").ToLower(); b.DocComment(method.Comment, true); b.Line($"public {method.JsVariable} = this.$apiClient.$makeCaller(\"{transportTypeSlug}\", "); b.Indented($"(c{signature}) => c.{method.JsVariable}({string.Join(", ", method.ClientParameters.Select(p => p.Name))}))"); } b.Line(); using (b.Block($"constructor()")) { b.Line($"super({metadataName}, new $apiClients.{name}ApiClient())"); } } b.Line(); }
private void WriteListViewModelClass(TypeScriptCodeBuilder b) { using (b.Block($"export class {Model.ListViewModelClassName} extends Coalesce.BaseListViewModel<{ViewModelFullName}>")) { b.Line($"public readonly modelName: string = \"{Model.ClientTypeName}\";"); b.Line($"public readonly apiController: string = \"/{Model.ApiRouteControllerPart}\";"); b.Line($"public modelKeyName: string = \"{Model.PrimaryKey.JsVariable}\";"); b.Line($"public itemClass: new () => {ViewModelFullName} = {ViewModelFullName};"); b.Line(); b.Line("public filter: {"); foreach (var prop in Model.BaseViewModel.ClientProperties.Where(f => f.IsUrlFilterParameter)) { b.Indented($"{prop.JsonName}?: string;"); } b.Line("} | null = null;"); b.DocComment("The namespace containing all possible values of this.dataSource."); b.Line($"public dataSources: typeof {Model.ClientTypeName}DataSources = {Model.ClientTypeName}DataSources;"); b.DocComment("The data source on the server to use when retrieving objects. Valid values are in this.dataSources."); b.Line($"public dataSource: Coalesce.DataSource<{ViewModelFullName}> = new this.dataSources.{DataSourceFactory.DefaultSourceName}();"); b.DocComment($"Configuration for all instances of {Model.ListViewModelClassName}. Can be overidden on each instance via instance.coalesceConfig."); b.Line($"public static coalesceConfig = new Coalesce.ListViewModelConfiguration<{Model.ListViewModelClassName}, {ViewModelFullName}>(Coalesce.GlobalConfiguration.listViewModel);"); b.DocComment($"Configuration for this {Model.ListViewModelClassName} instance."); b.Line($"public coalesceConfig: Coalesce.ListViewModelConfiguration<{Model.ListViewModelClassName}, {ViewModelFullName}>"); b.Indented($"= new Coalesce.ListViewModelConfiguration<{Model.ListViewModelClassName}, {ViewModelFullName}>({Model.ListViewModelClassName}.coalesceConfig);"); // Write client methods b.Line(); foreach (var method in Model.ClientMethods.Where(m => m.IsStatic)) { b.Line(); WriteClientMethodDeclaration(b, method, Model.ListViewModelClassName); } b.Line(); b.Line($"protected createItem = (newItem?: any, parent?: any) => new {ViewModelFullName}(newItem, parent);"); b.Line(); b.Line("constructor() {"); b.Indented("super();"); b.Line("}"); } }
private void WriteEnumMetadata(TypeScriptCodeBuilder b, TypeViewModel model) { using (b.Block($"export const {model.Name} = domain.enums.{model.Name} =")) { b.StringProp("name", model.Name.ToCamelCase()); b.StringProp("displayName", model.DisplayName); b.StringProp("type", "enum"); string enumShape = string.Join("|", model.EnumValues.Select(ev => $"\"{ev.Value}\"")); b.Line($"...getEnumMeta<{enumShape}>(["); foreach (var value in model.EnumValues) { // TODO: allow for localization of displayName b.Indented($"{{ value: {value.Key}, strValue: '{value.Value}', displayName: '{value.Value.ToProperCase()}' }},"); } b.Line("]),"); } }
private static void WriteViewModel(TypeScriptCodeBuilder b, ClassViewModel model) { string name = model.ViewModelClassName; string viewModelName = $"{name}ViewModel"; string metadataName = $"$metadata.{name}"; b.Line($"export interface {viewModelName} extends $models.{name} {{}}"); using (b.Block($"export class {viewModelName} extends ViewModel<$models.{name}, $apiClients.{name}ApiClient>")) { // This is an alternative to calling defineProps() on each class that causes larger and more cluttered files: //foreach (var prop in model.ClientProperties) //{ // b.Line($"get {prop.JsVariable}() {{ return this.$data.{prop.JsVariable} }}"); // b.Line($"set {prop.JsVariable}(val) {{ this.$data.{prop.JsVariable} = val }}"); //} foreach (var method in model.ClientMethods.Where(m => !m.IsStatic)) { string signature = string.Concat(method.ClientParameters.Select(f => $", {f.Name}: {new VueType(f.Type).TsType("$models")} | null")); // "item" or "list" var transportTypeSlug = method.TransportType.ToString().Replace("Result", "").ToLower(); b.DocComment(method.Comment, true); b.Line($"public {method.JsVariable} = this.$apiClient.$makeCaller(\"{transportTypeSlug}\", "); b.Indented($"(c{signature}) => c.{method.JsVariable}(this.$primaryKey{string.Concat(method.ClientParameters.Select(p => ", " + p.Name))}))"); } b.Line(); using (b.Block($"constructor(initialData?: $models.{name})")) { b.Line($"super({metadataName}, new $apiClients.{name}ApiClient(), initialData)"); } } b.Line($"defineProps({viewModelName}, $metadata.{name})"); b.Line(); }
public override void BuildOutput(TypeScriptCodeBuilder b) { using (b.Block($"module {ModuleName(ModuleKind.Service)}")) { using (b.Block($"export class {Model.ServiceClientClassName}")) { b.Line(); b.Line($"public readonly apiController: string = \"/{Model.ApiRouteControllerPart}\";"); b.DocComment($"Configuration for all instances of {Model.ServiceClientClassName}. Can be overidden on each instance via instance.coalesceConfig."); b.Line($"public static coalesceConfig = new Coalesce.ServiceClientConfiguration<{Model.ServiceClientClassName}>(Coalesce.GlobalConfiguration.serviceClient);"); b.DocComment($"Configuration for this {Model.ServiceClientClassName} instance."); b.Line($"public coalesceConfig: Coalesce.ServiceClientConfiguration<{Model.ServiceClientClassName}>"); b.Indented($"= new Coalesce.ServiceClientConfiguration<{Model.ServiceClientClassName}>({Model.ServiceClientClassName}.coalesceConfig);"); b.Line(); foreach (var method in Model.ClientMethods) { WriteClientMethodDeclaration(b, method, Model.ServiceClientClassName); } } } }
private void WriteMethod_LoadFromDto(TypeScriptCodeBuilder b) { b.DocComment(new[] { "Load the ViewModel object from the DTO.", "@param data: The incoming data object to load.", "@param force: Will override the check against isLoading that is done to prevent recursion. False is default.", "@param allowCollectionDeletes: Set true when entire collections are loaded. True is the default. ", "In some cases only a partial collection is returned, set to false to only add/update collections.", }); using (b.Block("public loadFromDto = (data: any, force: boolean = false, allowCollectionDeletes: boolean = true): void =>", ';')) { b.Line("if (!data || (!force && this.isLoading())) return;"); b.Line("this.isLoading(true);"); b.Line("// Set the ID "); b.Line($"this.myId = data.{Model.PrimaryKey.JsonName};"); b.Line($"this.{Model.PrimaryKey.JsVariable}(data.{Model.PrimaryKey.JsonName});"); b.Line("// Load the lists of other objects"); foreach (PropertyViewModel prop in Model.ClientProperties.Where(p => p.Type.IsCollection)) { using (b.Block($"if (data.{prop.JsonName} != null)")) { if (prop.Object.PrimaryKey != null) { b.Line("// Merge the incoming array"); b.Line($"Coalesce.KnockoutUtilities.RebuildArray(this.{prop.JsVariable}, data.{prop.JsonName}, '{prop.Object.PrimaryKey.JsonName}', {prop.Object.ViewModelClassName}, this, allowCollectionDeletes);"); if (prop.IsManytoManyCollection) { b.Line("// Add many-to-many collection"); b.Line("let objs: any[] = [];"); using (b.Block($"$.each(data.{prop.JsonName}, (index, item) =>", ");")) { b.Line($"if (item.{prop.ManyToManyCollectionProperty.JsonName}) {{"); b.Indented($"objs.push(item.{prop.ManyToManyCollectionProperty.JsonName});"); b.Line($"}}"); } b.Line($"Coalesce.KnockoutUtilities.RebuildArray(this.{prop.ManyToManyCollectionName.ToCamelCase()}, objs, '{prop.ManyToManyCollectionProperty.ForeignKeyProperty.JsVariable}', {prop.ManyToManyCollectionProperty.Object.ViewModelClassName}, this, allowCollectionDeletes);"); } } else if (prop.PureType.IsPrimitive) { b.Line($"this.{prop.JsVariable}(data.{prop.JsVariable});"); } else { b.Line($"Coalesce.KnockoutUtilities.RebuildArray(this.{prop.JsVariable}, data.{prop.JsonName}, null, {prop.Object.ViewModelClassName}, this, allowCollectionDeletes);"); } } } // Objects are loaded first so that they are available when the IDs get loaded. // This handles the issue with populating select lists with correct data because we now have the object. foreach (PropertyViewModel prop in Model.ClientProperties.Where(p => p.IsPOCO)) { b.Line($"if (!data.{prop.JsonName}) {{ "); using (b.Indented()) { if (prop.ForeignKeyProperty != null) { // Prop is a reference navigation prop. The incoming foreign key doesn't match our existing foreign key, so clear out the property. // If the incoming key and existing key DOES match, we assume that the data just wasn't loaded, but is still valid, so we do nothing. b.Line($"if (data.{prop.ForeignKeyProperty.JsonName} != this.{prop.ForeignKeyProperty.JsVariable}()) {{"); b.Indented($"this.{prop.JsVariable}(null);"); b.Line("}"); } else { b.Line($"this.{prop.JsVariable}(null);"); } } b.Line("} else {"); using (b.Indented()) { b.Line($"if (!this.{prop.JsVariable}()){{"); b.Indented($"this.{prop.JsVariable}(new {prop.Object.ViewModelClassName}(data.{prop.JsonName}, this));"); b.Line("} else {"); b.Indented($"this.{prop.JsVariable}()!.loadFromDto(data.{prop.JsonName});"); b.Line("}"); if (prop.Object.IsDbMappedType) { b.Line($"if (this.parent instanceof {prop.Object.ViewModelClassName} && this.parent !== this.{prop.JsVariable}() && this.parent.{prop.Object.PrimaryKey.JsVariable}() == this.{prop.JsVariable}()!.{prop.Object.PrimaryKey.JsVariable}())"); b.Line("{"); b.Indented($"this.parent.loadFromDto(data.{prop.JsonName}, undefined, false);"); b.Line("}"); } } b.Line("}"); } b.Line(); b.Line("// The rest of the objects are loaded now."); foreach (PropertyViewModel prop in Model.ClientProperties.Where(p => p.Object == null && !p.IsPrimaryKey)) { if (prop.Type.IsDate) { // Using valueOf/getTime here is a 20x performance increase over moment.isSame(). moment(new Date(...)) is also a 10x perf increase. b.Line($"if (data.{prop.JsonName} == null) this.{prop.JsVariable}(null);"); b.Line($"else if (this.{prop.JsVariable}() == null || this.{prop.JsVariable}()!.valueOf() != new Date(data.{prop.JsonName}).getTime()){{"); b.Indented($"this.{prop.JsVariable}(moment(new Date(data.{prop.JsonName})));"); b.Line("}"); } else { b.Line($"this.{prop.JsVariable}(data.{prop.JsonName});"); } } b.Line("if (this.coalesceConfig.onLoadFromDto()){"); b.Indented("this.coalesceConfig.onLoadFromDto()(this as any);"); b.Line("}"); b.Line("this.isLoading(false);"); b.Line("this.isDirty(false);"); b.Line("if (this.coalesceConfig.validateOnLoadFromDto()) this.validate();"); } }
private void WriteViewModelClass(TypeScriptCodeBuilder b) { using (b.Block($"export class {Model.ViewModelGeneratedClassName} extends Coalesce.BaseViewModel")) { b.Line($"public readonly modelName = \"{Model.ClientTypeName}\";"); b.Line($"public readonly primaryKeyName = \"{Model.PrimaryKey.JsVariable}\";"); b.Line($"public readonly modelDisplayName = \"{Model.DisplayName}\";"); b.Line($"public readonly apiController = \"/{Model.ApiRouteControllerPart}\";"); b.Line($"public readonly viewController = \"/{Model.ControllerName}\";"); b.DocComment($"Configuration for all instances of {Model.ViewModelClassName}. Can be overidden on each instance via instance.coalesceConfig."); b.Line($"public static coalesceConfig: Coalesce.ViewModelConfiguration<{Model.ViewModelClassName}>"); b.Indented($"= new Coalesce.ViewModelConfiguration<{Model.ViewModelClassName}>(Coalesce.GlobalConfiguration.viewModel);"); b.DocComment($"Configuration for the current {Model.ViewModelClassName} instance."); b.Line("public coalesceConfig: Coalesce.ViewModelConfiguration<this>"); b.Indented($"= new Coalesce.ViewModelConfiguration<{Model.ViewModelGeneratedClassName}>({Model.ViewModelClassName}.coalesceConfig);"); b.DocComment("The namespace containing all possible values of this.dataSource."); b.Line($"public dataSources: typeof ListViewModels.{Model.ClientTypeName}DataSources = ListViewModels.{Model.ClientTypeName}DataSources;"); b.Line(); b.Line(); foreach (PropertyViewModel prop in Model.ClientProperties) { b.DocComment(prop.Comment); b.Line($"public {prop.JsVariable}: {prop.Type.TsKnockoutType(true)} = {prop.Type.ObservableConstructorCall()};"); if (prop.Type.IsEnum) { b.DocComment($"Text value for enumeration {prop.Name}"); b.Line($"public {prop.JsTextPropertyName}: KnockoutComputed<string | null> = ko.pureComputed(() => {{"); b.Line($" for(var i = 0; i < this.{prop.JsVariable}Values.length; i++){{"); b.Line($" if (this.{prop.JsVariable}Values[i].id == this.{prop.JsVariable}()){{"); b.Line($" return this.{prop.JsVariable}Values[i].value;"); b.Line(" }"); b.Line(" }"); b.Line(" return null;"); b.Line("});"); } if (prop.IsManytoManyCollection) { if (prop.Comment.Length > 0) { b.DocComment($"Collection of related objects for many-to-many relationship {prop.ManyToManyCollectionName} via {prop.Name}"); } b.Line($"public {prop.ManyToManyCollectionName.ToCamelCase()}: KnockoutObservableArray<ViewModels.{prop.ManyToManyCollectionProperty.Object.ViewModelClassName}> = ko.observableArray([]);"); } } b.Line(); foreach (PropertyViewModel prop in Model.ClientProperties.Where(f => f.IsPOCO)) { b.DocComment($"Display text for {prop.Name}"); b.Line($"public {prop.JsTextPropertyName}: KnockoutComputed<string>;"); } b.Line(); foreach (PropertyViewModel prop in Model.ClientProperties.Where(f => f.Role == PropertyRole.CollectionNavigation && !f.IsManytoManyCollection)) { b.DocComment($"Add object to {prop.JsVariable}"); using (b.Block($"public addTo{prop.Name} = (autoSave?: boolean | null): {prop.Object.ViewModelClassName} =>", ';')) { b.Line($"var newItem = new {prop.Object.ViewModelClassName}();"); b.Line($"if (typeof(autoSave) == 'boolean'){{"); b.Line($" newItem.coalesceConfig.autoSaveEnabled(autoSave);"); b.Line($"}}"); b.Line($"newItem.parent = this;"); b.Line($"newItem.parentCollection = this.{prop.JsVariable};"); b.Line($"newItem.isExpanded(true);"); if (prop.InverseIdProperty != null) { b.Line($"newItem.{prop.InverseIdProperty.JsVariable}(this.{Model.PrimaryKey.JsVariable}());"); } else if (prop.Object.PropertyByName(Model.PrimaryKey.JsVariable) != null) { // TODO: why do we do this? Causes weird behavior simply because key names line up. // If all primary keys are just named "Id", this will copy the PK of the parent to the PK of the child, // which doesn't make sense. b.Line($"newItem.{Model.PrimaryKey.JsVariable}(this.{Model.PrimaryKey.JsVariable}());"); } b.Line($"this.{prop.JsVariable}.push(newItem);"); b.Line($"return newItem;"); } b.DocComment($"ListViewModel for {prop.Name}. Allows for loading subsets of data."); b.Line($"public {prop.JsVariable}List: (loadImmediate?: boolean) => {ListViewModelModuleName}.{prop.Object.ListViewModelClassName};"); } b.Line(); foreach (PropertyViewModel prop in Model.ClientProperties.Where(f => f.Role == PropertyRole.CollectionNavigation)) { b.DocComment($"Url for a table view of all members of collection {prop.Name} for the current object."); b.Line($"public {prop.ListEditorUrlName()}: KnockoutComputed<string> = ko.computed("); if (prop.ListEditorUrl() == null) { b.Indented($"() => \"Inverse property not set on {Model.ClientTypeName} for property {prop.Name}\","); } else { b.Indented($"() => this.coalesceConfig.baseViewUrl() + '/{prop.ListEditorUrl()}' + this.{Model.PrimaryKey.JsVariable}(),"); } b.Indented("null, { deferEvaluation: true }"); b.Line(");"); } b.Line(); foreach (PropertyViewModel prop in Model.ClientProperties.Where(f => f.Role == PropertyRole.ReferenceNavigation)) { b.DocComment($"Pops up a stock editor for object {prop.JsVariable}"); b.Line($"public show{prop.Name}Editor: (callback?: any) => void;"); } b.Line(); foreach (PropertyViewModel prop in Model.ClientProperties.Where(f => f.Type.IsEnum)) { b.DocComment($"Array of all possible names & values of enum {prop.JsVariable}"); b.Line($"public {prop.JsVariable}Values: Coalesce.EnumValue[] = [ "); foreach (var kvp in prop.Type.EnumValues) { b.Indented($"{{ id: {kvp.Key}, value: '{kvp.Value.ToProperCase()}' }},"); } b.Line("];"); } b.Line(); foreach (var method in Model.ClientMethods.Where(m => !m.IsStatic || m.ResultType.EqualsType(Model.Type))) { WriteClientMethodDeclaration(b, method, Model.ViewModelGeneratedClassName, true, true); } WriteMethod_LoadFromDto(b); WriteMethod_SaveToDto(b); b.DocComment(new[] { "Loads any child objects that have an ID set, but not the full object.", "This is useful when creating an object that has a parent object and the ID is set on the new child." }); using (b.Block("public loadChildren = (callback?: () => void): void =>", ';')) { b.Line("var loadingCount = 0;"); // AES 4/14/18 - unsure of the reason for the !IsReadOnly check here. Perhaps just redundant? foreach (PropertyViewModel prop in Model.ClientProperties.Where(f => f.Role == PropertyRole.ReferenceNavigation && !f.IsReadOnly)) { b.Line($"// See if this.{prop.JsVariable} needs to be loaded."); using (b.Block($"if (this.{prop.JsVariable}() == null && this.{prop.ForeignKeyProperty.JsVariable}() != null)")) { b.Line("loadingCount++;"); b.Line($"var {prop.JsVariable}Obj = new {prop.Object.ViewModelClassName}();"); b.Line($"{prop.JsVariable}Obj.load(this.{prop.ForeignKeyProperty.JsVariable}(), () => {{"); b.Indented("loadingCount--;"); b.Indented($"this.{prop.JsVariable}({prop.JsVariable}Obj);"); b.Indented("if (loadingCount == 0 && typeof(callback) == \"function\") { callback(); }"); b.Line("});"); } } b.Line("if (loadingCount == 0 && typeof(callback) == \"function\") { callback(); }"); } b.Line(); using (b.Block("public setupValidation(): void")) { b.Line("if (this.errors !== null) return;"); var validatableProps = Model.ClientProperties.Where(p => !string.IsNullOrWhiteSpace(p.ClientValidationKnockoutJs())); b.Line("this.errors = ko.validation.group(["); foreach (PropertyViewModel prop in validatableProps.Where(p => !p.ClientValidationAllowSave)) { b.Indented($"this.{prop.JsVariable}.extend({{ {prop.ClientValidationKnockoutJs()} }}),"); } b.Line("]);"); b.Line("this.warnings = ko.validation.group(["); foreach (PropertyViewModel prop in validatableProps.Where(p => p.ClientValidationAllowSave)) { b.Indented($"this.{prop.JsVariable}.extend({{ {prop.ClientValidationKnockoutJs()} }}),"); } b.Line("]);"); } b.Line(); using (b.Block($"constructor(newItem?: object, parent?: Coalesce.BaseViewModel | {ListViewModelModuleName}.{Model.ListViewModelClassName})")) { b.Line("super(parent);"); b.Line("this.baseInitialize();"); b.Line("const self = this;"); // Create computeds for display for objects b.Line(); foreach (PropertyViewModel prop in Model.ClientProperties.Where(f => f.IsPOCO)) { // If the object exists, use the text value. Otherwise show 'None' using (b.Block($"this.{prop.JsTextPropertyName} = ko.pureComputed(function()", ");")) { b.Line($"if (self.{prop.JsVariable}() && self.{prop.JsVariable}()!.{prop.Object.ListTextProperty.JsVariable}()) {{"); b.Indented($"return self.{prop.JsVariable}()!.{prop.Object.ListTextProperty.JsVariable}()!.toString();"); b.Line("} else {"); b.Indented("return \"None\";"); b.Line("}"); } } b.Line(); b.Line(); foreach (PropertyViewModel prop in Model.ClientProperties.Where(f => f.Role == PropertyRole.CollectionNavigation && !f.IsManytoManyCollection)) { b.Line($"// List Object model for {prop.Name}. Allows for loading subsets of data."); var childListVar = $"_{prop.JsVariable}List"; b.Line($"var {childListVar}: {ListViewModelModuleName}.{prop.Object.ListViewModelClassName};"); using (b.Block($"this.{prop.JsVariable}List = function(loadImmediate = true)")) { using (b.Block($"if (!{childListVar})")) { b.Line($"{childListVar} = new {ListViewModelModuleName}.{prop.Object.ListViewModelClassName}();"); b.Line($"if (loadImmediate) load{prop.Name}List();"); b.Line($"self.{prop.Parent.PrimaryKey.JsVariable}.subscribe(load{prop.Name}List)"); } b.Line($"return {childListVar};"); } using (b.Block($"function load{prop.Name}List()")) { using (b.Block($"if (self.{prop.Parent.PrimaryKey.JsVariable}())")) { if (prop.InverseIdProperty != null) { b.Line($"{childListVar}.queryString = \"filter.{prop.InverseIdProperty.Name}=\" + self.{prop.Parent.PrimaryKey.JsVariable}();"); } else { b.Line($"{childListVar}.queryString = \"filter.{Model.PrimaryKey.Name}=\" + self.{prop.Parent.PrimaryKey.JsVariable}();"); } b.Line($"{childListVar}.load();"); } } } b.Line(); b.Line(); foreach (PropertyViewModel prop in Model.ClientProperties.Where(f => f.Role == PropertyRole.ReferenceNavigation)) { using (b.Block($"this.show{prop.Name}Editor = function(callback: any)", ';')) { b.Line($"if (!self.{prop.JsVariable}()) {{"); b.Indented($"self.{prop.JsVariable}(new {prop.Object.ViewModelClassName}());"); b.Line("}"); b.Line($"self.{prop.JsVariable}()!.showEditor(callback)"); } } // Register autosave subscriptions on all autosavable properties. // Must be done after everything else in the ctor. b.Line(); foreach (PropertyViewModel prop in Model.ClientProperties.Where(p => p.IsClientWritable && !p.Type.IsCollection)) { b.Line($"self.{prop.JsVariable}.subscribe(self.autoSave);"); } foreach (PropertyViewModel prop in Model.ClientProperties.Where(p => p.IsClientWritable && p.IsManytoManyCollection)) { b.Line(); b.Line($"self.{prop.ManyToManyCollectionName.ToCamelCase()}.subscribe<KnockoutArrayChange<{prop.ManyToManyCollectionProperty.Object.ViewModelClassName}>[]>(changes => {{"); using (b.Indented()) { using (b.Block("for (var i in changes)")) { b.Line("var change = changes[i];"); b.Line("self.autoSaveCollection("); b.Indented("change.status, "); b.Indented($"this.{prop.JsVariable}, "); b.Indented($"{prop.Object.ViewModelClassName}, "); b.Indented($"'{prop.Object.ClientProperties.First(f => f.Type.EqualsType(Model.Type)).ForeignKeyProperty.JsVariable}',"); b.Indented($"'{prop.ManyToManyCollectionProperty.ForeignKeyProperty.JsVariable}',"); b.Indented($"change.value.{prop.ManyToManyCollectionProperty.Object.PrimaryKey.JsVariable}()"); b.Line(");"); } } b.Line("}, null, \"arrayChange\");"); } b.Line(); b.Line("if (newItem) {"); b.Indented("self.loadFromDto(newItem, true);"); b.Line("}"); } } }
/// <summary> /// Writes the class for invoking the given client method, /// as well as the property for the default instance of this class. /// </summary> /// <param name="b"></param> /// <param name="method"></param> /// <param name="parentClassName"></param> /// <param name="methodLoadsParent"> /// If true, code emitted for methods that load their parents' type should /// load the method's parent with the results of the method when called. /// </param> /// <param name="parentLoadHasIdParameter"> /// If true, calls to this.parent.load() will pass null as the first arg and a callback as the second. /// </param> public void WriteClientMethodDeclaration( TypeScriptCodeBuilder b, MethodViewModel method, string parentClassName, bool methodLoadsParent = false, bool parentLoadHasIdParameter = false ) { var model = this.Model; var isService = method.Parent.IsService; var returnIsListResult = method.ReturnsListResult; string reloadArg = !isService ? ", reload" : ""; string reloadParam = !isService ? ", reload: boolean = true" : ""; string callbackAndReloadParam = $"callback?: (result: {method.ResultType.TsType}) => void{reloadParam}"; // Default instance of the method class. b.Line("/**"); b.Indented($"Methods and properties for invoking server method {method.Name}."); if (method.Comment.Length > 0) { b.Indented(method.Comment); } b.Line($"*/"); b.Line($"public readonly {method.JsVariable} = new {parentClassName}.{method.Name}(this);"); string methodBaseClass = returnIsListResult ? "ClientListMethod" : "ClientMethod"; // Not wrapping this in a using since it is used by nearly this entire method. Will manually dispose. var classBlock = b.Block( $"public static {method.Name} = class {method.Name} extends Coalesce.{methodBaseClass}<{parentClassName}, {method.ResultType.TsType}>", ';'); b.Line($"public readonly name = '{method.Name}';"); b.Line($"public readonly verb = '{method.ApiActionHttpMethodName}';"); if (method.ResultType.IsCollection || method.ResultType.IsArray) { b.Line($"public result: {method.ResultType.TsKnockoutType()} = {method.ResultType.ObservableConstructorCall()};"); } // ---------------------- // Standard invoke method - all CS method parameters as TS method parameters. // ---------------------- b.Line(); b.Line($"/** Calls server method ({method.Name}) with the given arguments */"); string parameters = ""; parameters = string.Join(", ", method.ClientParameters.Select(f => f.Type.TsDeclarationPlain(f.Name) + " | null")); if (!string.IsNullOrWhiteSpace(parameters)) { parameters = parameters + ", "; } parameters = parameters + callbackAndReloadParam; using (b.Block($"public invoke = ({parameters}): JQueryPromise<any> =>", ';')) { string jsPostObject = "{ "; if (method.IsModelInstanceMethod) { jsPostObject = jsPostObject + "id: this.parent[this.parent.primaryKeyName]()"; if (method.Parameters.Any()) { jsPostObject = jsPostObject + ", "; } } string TsConversion(ParameterViewModel param) { string argument = param.JsVariable; if (param.Type.HasClassViewModel) { return($"{argument} ? {argument}.saveToDto() : null"); } if (param.Type.IsDate) { return($"{argument} ? {argument}.format() : null"); } return(argument); } jsPostObject += string.Join(", ", method.ClientParameters.Select(f => $"{f.JsVariable}: {TsConversion(f)}")); jsPostObject += " }"; b.Line($"return this.invokeWithData({jsPostObject}, callback{reloadArg});"); } // ---------------------- // Members for methods with parameters only. if (method.ClientParameters.Any()) { b.Line(); // ---------------------- // Args class, and default instance b.Line($"/** Object that can be easily bound to fields to allow data entry for the method's parameters */"); b.Line($"public args = new {method.Name}.Args(); "); using (b.Block("public static Args = class Args", ';')) { foreach (var arg in method.ClientParameters) { b.Line($@"public {arg.JsVariable}: {arg.Type.TsKnockoutType(true)} = {arg.Type.ObservableConstructorCall()};"); } } // Gets the js arguments to pass to a call to this.invoke(...) string JsArguments(string obj = "") { string result; if (obj != "") { result = string.Join(", ", method.ClientParameters.Select(f => $"{obj}.{f.JsVariable}()")); } else { result = string.Join(", ", method.ClientParameters.Select(f => obj + f.JsVariable)); } if (!string.IsNullOrEmpty(result)) { result = result + ", "; } result = result + "callback"; return(result); } // ---------------------- // invokeWithArgs method // ---------------------- b.Line(); b.Line($"/** Calls server method ({method.Name}) with an instance of {method.Name}.Args, or the value of this.args if not specified. */"); // We can't explicitly declare the type of the args parameter here - TypeScript doesn't allow it. // Thankfully, we can implicitly type using the default. using (b.Block($"public invokeWithArgs = (args = this.args, {callbackAndReloadParam}): JQueryPromise<any> =>")) { b.Line($"return this.invoke({JsArguments("args")}{reloadArg});"); } // ---------------------- // invokeWithPrompts method // ---------------------- b.Line(); b.Line("/** Invokes the method after displaying a browser-native prompt for each argument. */"); using (b.Block($"public invokeWithPrompts = ({callbackAndReloadParam}): JQueryPromise<any> | undefined =>", ';')) { b.Line($"var $promptVal: string | null = null;"); foreach (var param in method.ClientParameters.Where(f => f.ConvertsFromJsString)) { b.Line($"$promptVal = {$"prompt('{param.Name.ToProperCase()}')"};"); // AES 1/23/2018 - why do we do this? what about optional params where no value is desired? b.Line($"if ($promptVal === null) return;"); b.Line($"var {param.Name}: {param.Type.TsType} = {param.Type.TsConvertFromString("$promptVal")};"); } // For all parameters that can't convert from a string (is this even possible with what we support for method parameters now?), // pass null as the value. I guess we just let happen what's going to happen? Again, not sure when this case would ever be hit. foreach (var param in method.ClientParameters.Where(f => !f.ConvertsFromJsString)) { b.Line($"var {param.Name}: null = null;"); } b.Line($"return this.invoke({JsArguments("")}{reloadArg});"); } } // ---------------------- // Method response handler - highly dependent on what the response type actually is. // ---------------------- b.Line(); using (b.Block($"protected loadResponse = (data: Coalesce.{(returnIsListResult ? "List" : "Item")}Result, {callbackAndReloadParam}) =>", ';')) { var incomingMainData = returnIsListResult ? "data.list || []" : "data.object"; if (method.ResultType.IsCollection && method.ResultType.PureType.HasClassViewModel) { // Collection of objects that have TypeScript ViewModels. This could be an entity or an external type. // If the models have a key, rebuild our collection using that key so that we can reuse objects when the data matches. var keyNameArg = method.ResultType.PureType.ClassViewModel.PrimaryKey != null ? $"'{method.ResultType.PureType.ClassViewModel.PrimaryKey.JsVariable}'" : "null"; b.Line($"Coalesce.KnockoutUtilities.RebuildArray(this.result, {incomingMainData}, {keyNameArg}, ViewModels.{method.ResultType.PureType.ClassViewModel.ClientTypeName}, this, true);"); } else if (method.ResultType.HasClassViewModel) { // Single view model return type. b.Line("if (!this.result()) {"); b.Indented($"this.result(new ViewModels.{method.ResultType.PureType.ClassViewModel.ClientTypeName}({incomingMainData}));"); b.Line("} else {"); b.Indented($"this.result().loadFromDto({incomingMainData});"); b.Line("}"); } else { // Uninteresting return type. Either void, a primitive, or a collection of primitives. // In any case, regardless of the type of the 'result' observable, this is how we set the results. b.Line($"this.result({incomingMainData});"); } if (isService) { b.Line("if (typeof(callback) == 'function')"); b.Indented("callback(this.result());"); } else if (method.ResultType.EqualsType(method.Parent.Type) && methodLoadsParent) { // The return type is the type of the method's parent. Load the parent with the results of the method. // Parameter 'reload' has no meaning here, since we're reloading the object with the result of the method. b.Line($"this.parent.loadFromDto({incomingMainData}, true)"); using (b.Block("if (typeof(callback) == 'function')")) { b.Line($"callback(this.result());"); } } else { // We're not loading the results into the method's parent. This is by far the more common case. b.Line("if (reload) {"); // If reload is true, invoke the load function on the method's parent to reload its latest state from the server. // Only after that's done do we call the method-completion callback. // Store the result locally in case it somehow gets changed by us reloading the parent. b.Indented("var result = this.result();"); b.Indented($"this.parent.load({(parentLoadHasIdParameter ? "null, " : "")}typeof(callback) == 'function' ? () => callback(result) : undefined);"); b.Line("} else if (typeof(callback) == 'function') {"); // If we're not reloading, just call the callback now. b.Indented("callback(this.result());"); b.Line("}"); } } // End of the method class declaration. classBlock.Dispose(); }
private void WriteMethod_LoadFromDto(TypeScriptCodeBuilder b) { b.DocComment(new[] { "Load the object from the DTO.", "@param data: The incoming data object to load.", }); using (b.Block("public loadFromDto = (data: any) =>", ';')) { b.Line("if (!data) return;"); if (Model.PrimaryKey != null) { b.Line("// Set the ID"); b.Line($"this.myId = data.{Model.PrimaryKey.JsonName};"); } b.Line(); b.Line("// Load the properties."); foreach (PropertyViewModel prop in Model.ClientProperties) { // DB-mapped viewmodels can't have an external type as a parent. // There's no strong reason for this restriction other than that // the code just doesn't support it at the moment. var parentVar = prop.PureTypeOnContext ? "undefined" : "this"; if (prop.Type.IsCollection && prop.Object != null) { b.Line($"if (data.{prop.JsonName} != null) {{"); b.Line("// Merge the incoming array"); if (prop.Object.PrimaryKey != null) { b.Indented($"Coalesce.KnockoutUtilities.RebuildArray(this.{prop.JsVariable}, data.{prop.JsonName}, \'{prop.Object.PrimaryKey.JsVariable}\', ViewModels.{prop.Object.ViewModelClassName}, {parentVar}, true);"); } else { b.Indented($"Coalesce.KnockoutUtilities.RebuildArray(this.{prop.JsVariable}, data.{prop.JsonName}, null, ViewModels.{prop.Object.ViewModelClassName}, {parentVar}, true);"); } b.Line("}"); } else if (prop.Type.IsDate) { b.Line($"if (data.{prop.JsVariable} == null) this.{prop.JsVariable}(null);"); b.Line($"else if (this.{prop.JsVariable}() == null || !this.{prop.JsVariable}()!.isSame(moment(data.{prop.JsVariable}))) {{"); b.Indented($"this.{prop.JsVariable}(moment(data.{prop.JsVariable}));"); b.Line("}"); } else if (prop.IsPOCO) { b.Line($"if (!this.{prop.JsVariable}()){{"); b.Indented($"this.{prop.JsVariable}(new {prop.Object.ViewModelClassName}(data.{prop.JsonName}, {parentVar}));"); b.Line("} else {"); b.Indented($"this.{prop.JsVariable}()!.loadFromDto(data.{prop.JsonName});"); b.Line("}"); } else { b.Line($"this.{prop.JsVariable}(data.{prop.JsVariable});"); } } b.Line(); } }
public override Task <string> BuildOutputAsync() { var b = new TypeScriptCodeBuilder(indentSize: 2); b.Line("import * as metadata from './metadata.g'"); b.Line("import { Model, DataSource, convertToModel, mapToModel } from 'coalesce-vue/lib/model'"); // b.Line("import { Domain, getEnumMeta, ModelType, ExternalType } from './coalesce/core/metadata' "); b.Line(); foreach (var model in Model.ClientEnums.OrderBy(e => e.Name)) { using (b.Block($"export enum {model.Name}")) { foreach (var value in model.EnumValues) { b.Line($"{value.Value} = {value.Key},"); } } b.Line(); b.Line(); } foreach (var model in Model.ClientClasses .OrderByDescending(c => c.IsDbMappedType) // Entity types first .ThenBy(e => e.ClientTypeName)) { var name = model.ViewModelClassName; using (b.Block($"export interface {name} extends Model<typeof metadata.{name}>")) { foreach (var prop in model.ClientProperties) { b.DocComment(prop.Comment); var typeString = new VueType(prop.Type).TsType(); b.Line($"{prop.JsVariable}: {typeString} | null"); } } using (b.Block($"export class {name}")) { b.DocComment($"Mutates the input object and its descendents into a valid {name} implementation."); using (b.Block($"static convert(data?: Partial<{name}>): {name}")) { b.Line($"return convertToModel(data || {{}}, metadata.{name}) "); } b.DocComment($"Maps the input object and its descendents to a new, valid {name} implementation."); using (b.Block($"static map(data?: Partial<{name}>): {name}")) { b.Line($"return mapToModel(data || {{}}, metadata.{name}) "); } b.DocComment($"Instantiate a new {name}, optionally basing it on the given data."); using (b.Block($"constructor(data?: Partial<{name}> | {{[k: string]: any}})")) { b.Indented($"Object.assign(this, {name}.map(data || {{}}));"); } } var dataSources = model.ClientDataSources(Model); if (model.IsDbMappedType && dataSources.Any()) { using (b.Block($"export namespace {name}")) { using (b.Block("export namespace DataSources")) { foreach (var source in dataSources) { b.DocComment(source.Comment, true); var sourceMeta = $"metadata.{name}.dataSources.{source.ClientTypeName.ToCamelCase()}"; using (b.Block($"export class {source.ClientTypeName} implements DataSource<typeof {sourceMeta}>")) { b.Line($"readonly $metadata = {sourceMeta}"); foreach (var param in source.DataSourceParameters) { b.DocComment(param.Comment); var typeString = new VueType(param.Type).TsType(); b.Line($"{param.JsVariable}: {typeString} | null = null"); } } } } } } b.Line(); b.Line(); } return(Task.FromResult(b.ToString())); }