How to add a custom schema to Angular? - html

Problem
It's pretty common, that an Angular project wants to use some web-components, (aka. custom html tags). Of course, the compiler won't recognize these custom html tags and will complain:
1. If 'si-radio' is an Angular component, then verify that it is part of this module. 2. If 'si-radio' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '#NgModule.schemas' of this component to suppress this message.
If you put 'CUSTOM_ELEMENTS_SCHEMA' into play, your templates won't be validated anymore in that module and your code become extremely error-prone.
Solution (?)
It would be much reasonable, if you could define the tags and properties, which you really want to use, and they could pass the validation process.
As far as I know, Angular is using the ElementSchemaRegistry. If you use a web-component-library, where all the custom tags are prefixed like <custom-component-.. you could write a CustomSchemaRegistry like this:
export class CustomSchemaRegistry extends DomElementSchemaRegistry {
constructor() {
super();
}
hasElement(tagName: string, schemaMetas: SchemaMetadata[]): boolean {
const elementExists = tagName.includes('custom-component-') || super.hasElement(tagName, schemaMetas);
console.log(tagName, elementExists);
return elementExists;
}
hasProperty(tagName: string, propName: string, schemaMetas: SchemaMetadata[]): boolean {
return tagName.includes('custom-component-') || super.hasProperty(tagName, propName, schemaMetas);
}
}
And at bootstrapping you could provide it as a compilerOption:
platformBrowserDynamic()
.bootstrapModule(AppModule, {
providers: [{ provide: ElementSchemaRegistry, useValue: new CustomElementSchemaRegistry() }],
})
The problem is, it just doesn't work. And I don't even see any recommendation in the official Angular documentation for such cases. Looks like such a use-case wasn't foreseen, although it's extremely common.
#NgModule.schemas is an array of SchemaMetadata, which seems to be a pretty useless interface with a single name: string property...
https://angular.io/api/core/SchemaMetadata
Summing it up, is it actually possible to add a custom schema to Angular, allowing the usage of custom tags, while still preserving the template validation?

Related

Typescript return HTML Template Element from Constructor typing not working

It is 'illegal' to use new when creating an instance of Template, where Template extends an HTMLTemplateElement.
To overcome this limitation, I get and return an HTMLTemplateElement using document.getElementById(id) from the Template constructor as below:
export class Template {
private htmlTemplateElement: HTMLTemplateElement;
constructor(id: string) {
this.htmlTemplateElement = document.getElementById(id) as HTMLTemplateElement;
return Object.assign(this.htmlTemplateElement, this)
}
public test = () => this.htmlTemplateElement.innerHTML
}
Providing an HTML Template Element exist in the DOM,
I can create a new instance of Template and use the extension method test() as illustrated below:
const template = new Template(id)
console.log(template.test())
console.log(template.innerHTML)
Both console.log() works just fine and prints the correct text to the console.
HOWEVER, the typescript compiler complains about template.innerHTML.
The error I get, saying innerHTML does not exist on type Template
Question: How can I add type information so I do not get a compiler error?
I have tried to use export class Template extends HTMLTemplateElement.
That does not work since it is illegal to create an instance using new.
I love typescript, but sometimes the type checking gets in my way.
Help me out here, please.
Although not ideal, I was able to keep the typescript compiler happy by implementing the following interface:
export interface ITemplate {
[key:string]: any;
test(): string;
}
and then using the interface:
export Template implements ITemplate {
...
}
Note:
Why did I not use customElements.define(<tag-name>,Template)?
I do not intend to create a new custom instance of HTMLTemplateElement, I just want to return an existing HTMLTemplateElement with additional utility extension methods.
Also, It might very well be possible that my approach is completely wrong.
However, that is a different topic than the question asked here.

Html attribute camel-case with vue directive

I am working on a MVC project with Vue.js(Vue3) front-end.
For some reason when I am trying to use any directive that contains camel-case arguments, like in the example below, they are automatically turned to all lower-case, rendering my directive completely ineffective.
<label for="Name" class="required" v-init:camelCaseAttribute="'variableValue"> labelValue </label>
This also happens when I define said atributes with the use of a dictionary:
#Html.TextBoxFor(m => m.Registration.Section03.Address.ZipCode, htmlAttributes: new Dictionary<string, object> { { "v-init:camelCaseAttribute", "'variableValue'" } })
I know this is a HTML thing, but are there any known work-arounds for this?
Should have provided more context to the question:
const myComponent = { // my component
data: function () {
return {
camelCaseProperty: '' // property I am trying to initialise with some value
}
},
directives: {
init: { // custom init directive
mounted(el, binding, vnode) {
console.log(binding.arg)
binding.instance[binding.arg] = binding.value;
}
}
}
};
"init" is indeed not a built in Vue directive, so as I am trying to migrate the project from angularjs, which has an init directive, I tried to come up with something that offers a similar functionality to some extent, as seen in the snippet.
Writing the directive argument in kebab-case (v-init:camel-case-property="something") has no effect, as logging the binding.arg that actually reaches the directive is still in kebab-case.
This can happen; it is therefore recommended in to use kebab-case for all html attributes in Vue, including props.
HTML attribute names are case-insensitive, so browsers will interpret any uppercase characters as lowercase
source

TypeScript: how to JSON stringify a class definition?

Say we have:
class MyClass {
myProperty: string
}
Is there any built in function or easy way to get JSON like this?:
{
"myProperty": "string"
}
EDIT: My end goal is I want to dynamically print typed class definitions to a web view, in some kind of structured object syntax like JSON. I'm trying to make a server API that will return the schema for various custom classes - for example http://myserver.com/MyClass should return MyClass's properties and their types as a JSON string or other structured representation.
Evert is correct, however a workaround can look like this
class MyClass {
myProperty: string = 'string'
}
JSON.stringify(new MyClass) // shows what you want
In other words, setting a default property value lets TS compile properties to JS
If the above solution is not acceptable, then I would suggest you parsing TS files with your classes with https://dsherret.github.io/ts-simple-ast/.
Typescript class properties exist at build-time only. They are removed from your source after compiling to .js. As such, there is no run-time way to get to the class properties.
Your code snippet compiles to:
var MyClass = /** #class */ (function () {
function MyClass() {
}
return MyClass;
}());
As you can see, the property disappeared.
Based on your update, I had this exact problem. This is how I solved it.
My JSON-based API uses json-schema across the board for type validation, and also exposes these schemas for clients to re-use.
I used an npm package to automatically convert json-schema to Typescript.
This works brilliantly.

How to pass an object to child component while creating it in HTML template markup?

I'm doing the following (it's working as expected) in my parent component.
<app-textbox [info]="{caption:'Boink',value:'Oink'}"
... ></app-textbox>
In the receiving child component I have the following declaration.
#Input() info: any;
Now I want to improve the code and make it hard-typed, so I introduced and imported the following class.
export class TextBoxInfo { constructor(public caption: string, public value: string) { } }
Then, I updated the child component's input as follows.
#Input() info: TextBoxInfo;
Everything still works, as expected but I also wanted to improve the markup in HTML by switching to the following syntax.
<app-textbox [info]="new TextBoxInfo('Boink','Oink')"
... ></app-textbox>
That doesn't work and I'm getting the error message .
Uncaught Error: Template parse errors:
Parser Error: Unexpected token 'TextBoxInfo' at column 5 in [new TextBoxInfo('Boink', 'Oink')]
in ng:///AppModule/ParentComponent.html#45:24 ("
/div>
app-textbox [ERROR ->][info]="new TextBoxInfo('Boink', 'Oink')" ...
I've try to google to confirm or contradict that I can use the syntax like new Something(...) in the template's markup. Nothing conclusive this far. I also tried to google for the error but it's simply telling me that the syntax isn't recognized. I haven't found any viable examples of how to create an object and pass it in the template and googlearching it is complicated by the lack of good key words.
Am I approaching the object creation incorrectly?
Using type literals in templates is not supported. The scope of a template is the component instance, and therefore only properties of the component instance can be accessed.
If you need to reference identifiers outside of that scope, you need to move the code/expression to the components class and expose it to the template from there.
class MyComponent {
createTextBoxInfo(p1, p2):TextBoxInfo { return new TextBoxInfo(p1, p2); }
}
[info]="createTextBoxInfo('Boink','Oink')"
while this concrete case is a bad example in practice.
It would create a new TextBoxInfo every time change detection is run which is probably not what you want and will bring the performance of your app to its knees.
It's better to assign the value to a property and bind to that instead:
class MyComponent {
textBoxInfo = new TextBoxInfo('Boink','Oink'); }
}
[info]="textBoxInfo"

Is there a common (not any) type in typescript for [] and LinkedList<>/Dictionary<>

We have to copy objects across between a web worker and the main application. We have set up a system where, for each class that is copied across, we define an interface and then a class that implements the interface.
This works great because the JSON that is copied across can be cast to the interface and then the properties can be assigned. It's not critical to have the class implement the interface, but it's clean and probably will reduce bugs.
But we have a problem. We use the typescript-collections class. So my class will have:
export class DocHeader {
public format:DocumentFormat;
public fonts : collections.LinkedList<fonts.FontAttributes>;
public styles : collections.Dictionary<string, styles.Style>;
But the JSON is an array and therefore will be:
export interface IDocHeader {
format:DocumentFormat;
fonts : fonts.FontAttributes[];
styles : styles.Style[];
}
I know the answer is probably "no, not possible." But I figure it never hurts to ask. Is there some type (and not any) like IEnumerable at least that I can use in the interface that matches both uses?
The JSON type you're looking for is an associative array:
export interface IDocHeader {
format: DocumentFormat;
fonts: fonts.FontAttributes[];
styles: { [styleId: string]: styles.Style; };
}