We are developing components and when using them, we would like to use the same mechanism like for DOM nodes to conditionally define attributes. So for preventing attributes to show up at all, we set the value to null and its not existing in the final HTML output. Great!
<button [attr.disabled]="condition ? true : null"></button>
Now, when using our own components, this does not work. When we set null, we actually get null in the components #Input as the value. Any by default set value will be overwritten.
...
#Component({
selector: 'myElement',
templateUrl: './my-element.component.html'
})
export class MyElementComponent {
#Input() type: string = 'default';
...
<myElment [type]="condition ? 'something' : null"></myElement>
So, whenever we read the type in the component, we get null instead of the 'default' value which was set.
I tried to find a way to get the original default value, but did not find it. It is existing in the ngBaseDef when accessed in constructor time, but this is not working in production. I expected ngOnChanges to give me the real (default) value in the first change that is done and therefore be able to prevent that null is set, but the previousValue is undefined.
We came up with some ways to solve this:
defining a default object and setting for every input the default value when its null
addressing the DOM element in the template again, instead of setting null
<myElement #myelem [type]="condition ? 'something' : myelem.type"></myElement>
defining set / get for every input to prevent null setting
_type: string = 'default';
#Input()
set type(v: string) {if (v !== null) this._type = v;}
get type() { return this._type; }
but are curious, if there are maybe others who have similar issues and how it got fixed. Also I would appreciate any other idea which is maybe more elegant.
Thanks!
There is no standard angular way, because many times you would want null or undefined as value. Your ideas are not bad solutions. I got a couple more
I suppose you can also use the ngOnChanges hook for this:
#Input()
type: string = 'defaultType';
ngOnChanges(changes: SimpleChanges): void {
// == null to also match undefined
if (this.type == null) {
this.type = 'defaultType';
}
}
Or using Observables:
private readonly _type$ = new BehaviorSubject('defaultType');
readonly type$ = this._type$.pipe(
map((type) => type == null ? 'defaultType' : type)
);
#Input()
set type(type: string) {
this._type$.next(type);
}
Or create your own decorator playground
function Default(value: any) {
return function(target: any, key: string | symbol) {
const valueAccessor = '__' + key.toString() + '__';
Object.defineProperty(target, key, {
get: function () {
return this[valueAccessor] != null ? this[valueAccessor] : value
},
set: function (next) {
if (!Object.prototype.hasOwnProperty.call(this, valueAccessor)) {
Object.defineProperty(this, valueAccessor, {
writable: true,
enumerable: false
});
}
this[valueAccessor] = next;
},
enumerable: true
});
};
}
which you can use like this:
#Input()
#Default('defaultType')
type!: string;
Just one more option (perhaps simpler if you don't want to implement your own custom #annotation) based off Poul Krujit solution:
const DEFAULT_VALUE = 'default';
export class MyElementComponent {
typeWrapped = DEFAULT_VALUE;
#Input()
set type(selected: string) {
// this makes sure only truthy values get assigned
// so [type]="null" or [type]="undefined" still go to the default.
if (selected) {
this.typeWrapped = selected;
} else {
this.typeWrapped = DEFAULT_VALUE;
}
}
get type() {
return this.typeWrapped;
}
}
If you need to do this for multiple inputs, you can also use a custom pipe instead of manually defining the getter/setter and default for each input. The pipe can contain the logic and defaultArg to return the defaultArg if the input is null.
i.e.
// pipe
#Pipe({name: 'ifNotNullElse'})
export class IfNotNullElsePipe implements PipeTransform {
transform(value: string, defaultVal?: string): string {
return value !== null ? value : defaultVal;
}
}
<!-- myElem template -->
<p>Type Input: {{ type | ifNotNullElse: 'default' }}</p>
<p>Another Input: {{ anotherType | ifNotNullElse: 'anotherDefault' }}</p>
Related
I am attempting to assign FireStore data(), forwarded by props, to a reactive() proxy object, but am receiving the following error:
Object is possibly 'undefined'.
(property) fireStoreData: Record<string, any> | undefined
I wish to use a forEach loop instead of direct assignments i.e. (pageForm.name = props.fireStoreData?.name) to minimize code.
props: {
fireStoreData: Object,
}
...
setup(props){
...
const pageForm: { [key: string]: any } = reactive({
name: '',
address: '',
...
})
onMounted(() => {
Object.keys(pageForm).forEach(key => {
if(key in props.fireStoreData) // <-- error found here
pageForm[key] = props.fireStoreData[key] // <-- and here
})
})
})
...
}
The issie is that the fireStoreData prop is not required yet in your code you assume it is.
try:
props: {
fireStoreData: {
required: true,
type: Object
}
}
If you dont want the prop to be required, its always possible to check for it being defined and add a watcher to look for changes.
Also, dont forget props can change in value.
// on mobile, will format later on pc
I get input data in this form:
[
{ name: "Producent", checked: true },
{ name: "Handel", checked: true },
{ name: "Simple", checked: true }
];
Only the checked values from true to false and vice versa can change. This is assigned to the checkedTypeOfClient variable. Later, I'd like to filter out all my clients (the currentClientList array) based on the checkedTypeOfClient variable.
Here are the properties of the Client class:
export class Client {
clientId: number;
name: string ;
district: string;
province: string;
zip: string;
city: string;
// tslint:disable-next-line: variable-name
full_Address: string;
latitude: number;
longitude: number;
segment: string;
ph: string;
bh: number;
canal: string;
isSimple: string;
}
The complication of this task is that the filtration goes like this. The values Producent and Handel are values that can be placed in the canal column that are in the Client Class, and the Simple value is a value that is also in the Client class in the isSimple column and can take the value "YES" or "NO"
for now, what I was able to do is extract what values Producent , Handel, Simple are marked and grind the Simple field to "TAK" or NIE "
filterClients() {
console.log(this.checkedTypeOfClient);
const filter={
Producent:this.checkedTypeOfClient.find(x=>x.name=="Producent").checked,
Handel:this.checkedTypeOfClient.find(x=>x.name=="Handel").checked,
Simple:this.checkedTypeOfClient.find(x=>x.name=="Simple").checked
}
let simpleFilter = this.returnFilterSimpleValue(filter.Simple);
this.currentClientList = this.baseClientList;
}
returnFilterSimpleValue(value : boolean) : string {
switch (value) {
case true:
return "TAK";
case false:
return "NIE";
}
If(Producent = True){
this.currentClientList(client.producent)
}
If(Handel= True){
this.currentClientList(client.handel)
}
If(Handel= True || Producent = True){
this.currentClientList(client.handel) && this.currentClientList(client.producent)
}
The question is how to filter it?
I'm not sure that I'm fully understanding your question. However, here's how I'd solve it.
First of all, I'd like to introduce you to union types (a feature of typescript that allows you to combine multiple types, but can also be used for literal values.
In your case that would be useful for:
isSimple: "YES" | "NO" meaning that it can only have either values. Also, why not make it a boolean?
canal: "Handel" | "Producent" meaning that it can only have either the "Handel" or "Producent" string value.
Secondly you can simple use filter array method to filter the objects that have a certain property values, which can also be chained.
Is this what you wanted to do or did I miss something?
I'm not sure either if I get the question right. As i understand it, you have two different types of clients: CanalClient and SimpleClient.
with the superclass Client sharing the common attributes.
export class CanalClient extends Client {
canal: string;
}
export class Simpleclientextends Client {
isSimple: string; // as Ruben asked - why is this not a simple boolean?
}
In the filter Operation, you can then check the class with instanceof CanalClient etc. to have type safety.
Actually, in an if block- this is type guarded:
if(client instanceof CanalClient) {
console.log( client.canal) // Typescript will know this is of type Canalclient!
}
I tried to keep it as simple as I could by reusing your code as per my understanding. Try this:
filterClients() {
console.log(this.checkedTypeOfClient);
const filter={
Producent:this.checkedTypeOfClient.find(x=>x.name=="Producent").checked,
Handel:this.checkedTypeOfClient.find(x=>x.name=="Handel").checked,
Simple:this.checkedTypeOfClient.find(x=>x.name=="Simple").checked
}
let simpleFilter = this.returnFilterSimpleValue(filter.Simple);
this.currentClientList = this.baseClientList.filter(c => {
if (c.canal=='Producent' && filter.Producent) {
return true;
}
if (c.canal=='Handel' && filter.Handel) {
return true;
}
});
this.currentClientList.foEach(c => { c.isSimple = this.returnFilterSimpleValue(c.isSimple) });
}
I need value :
If(Producent = True){
this.currentClientList(client.producent)
}
If(Handel= True){
this.currentClientList(client.handel)
}
If(Handel= True || Producent = True){
this.currentClientList(client.handel) && this.currentClientList(client.producent)
}
I have an object. I use it to store the data I receive from API.
This object looks like this:
class Store {
isLoaded: boolean = false;
data: object || null = null;
}
Now I know that if isLoaded is false data will be null. If isData is true, then data will be object. How should I type this efficiently?
I could make from isLoaded, that would check if data is null. That would be awesome, but sometimes those objects are too dynamic to be easily checked.
I could do this:
const store = new Store() as ({isLoaded: false, data:null} | {isLoaded: true, data: object});
But that is somewhat cumbersome, especially for larger objects.
What I would like is something like this:
class Store {
_isLoaded: boolean = false;
data: object | null = null;
get isLoaded(): typeof this.data is object {
return this_isLoaded();
}
Thanks for any tips.
EDIT:
Just for clarification (I'm sorry if my question is not clear enough.).
I'm trying to solve this problem:
class Store {
isLoaded: boolean = false;
data: {prop:string} || null = null;
load = () => {
this.data = {prop: "value"};
this.isLoaded = true;
}
}
let store = new Store();
store.load();
if (store.isLoaded) {
console.log(store.data.prop); // Now typescript thinks that store.data can be null and throws an error
}
Unfourtunatelly it is not possible to do this on getters, though it is possible in methods.
interface LoadedStore {
data: object
isLoaded(): true
}
class Store {
private _isLoaded: boolean = false
data: object | null = null
isLoaded(): this is LoadedStore {
return this._isLoaded
}
}
I don't know if I understood the question right, but based on the fact that your class is a Store, it may store different kind of objects right?
Thinking in this way, I can only think about a generic abstract class representing the store, and then I would extend this abstract class to each of the products that my store may have.
I created a playground here representing my point of view
abstract class Store<T> {
private _isLoaded: boolean = false;
abstract data: T | null;
get isLoaded(): boolean {
return this._isLoaded;
}
set isLoaded(v: boolean) {
this._isLoaded = v;
}
abstract load(): Promise<void>;
abstract show(): void;
}
Here we have the abstract class implementation, you can see that data is abstract and the type is either T or null.
interface IProduct {
name: string;
value: number;
stock: number;
}
Then this interface represents an item of our store
class ProductA extends Store<IProduct> {
data: IProduct | null = null;
async load(): Promise<void> {
// load data from db if succesfull set isLoaded to true
super.isLoaded = true;
this.data = {
name: 'product',
value: 2,
stock: 10,
}
}
show() {
if (super.isLoaded) {
console.log(this.data);
} else {
console.log('null');
}
}
}
And here we can extend Store and the methods to fetch and show the data
Maybe I misunderstood your question, but that will be the way I will approach a store problem.
I would keep it simple and just do it like that:
type Store = {
isLoaded: true;
data: object; // Should maybe vary to Record<string, unknown>
} | {
isLoaded: false;
data: null;
};
i see no problems in using an or in a type as it always should be clear.
You can use the interface. For example:
interface IStore {
isLoaded: boolean;
data: object | null;
}
class Store implements IStore {
isLoaded: boolean = false;
data: object || null = null;
}
"One of the most common uses of interfaces in languages like C# and Java, that of explicitly enforcing that a class meets a particular contract, is also possible in TypeScript." - more info: https://www.typescriptlang.org/docs/handbook/interfaces.html#class-types
Challenge
Below is a simplified example of how we control and pass data in an app. It is used in many places and works to translate data between UI, APIs, and a database.
API and UI use camelCase.
Database uses snake_case.
Currently, it's an awkward combination of Partial/Pick types to get some typing where...
const item = { fooBar: 'something' }
Item.cast(item).value // returns type Partial<ItemModel>
Item.create(item).value // returns type ItemModel
The goal is to return the real returned object type.
// Examples
const item = { fooBar: 'something' }
Item.cast(item).value // returns { fooBar: 'something' }
Item.cast(item).databaseFormat // returns { foo_bar: 'something' }
Item.create(item).value // returns { id: '{uuid}', fooBar: 'something' }
Item.create(item).databaseFormat // returns { id: '{uuid}', foo_bar: 'something' }
const itemFromDatabase = { id: '{uuid}', foo_bar: 'something', baz: null }
Item.cast(itemFromDatabase).value // returns { id: '{uuid}', fooBar: 'something', baz: null }
Item.cast(itemFromDatabase).databaseFormat // returns { id: '{uuid}', foo_bar: 'something', baz: null }
Any ideas on this? I would image it's something like the Object.entries() return type but I can't figure out that right T keyof combination.
// https://mariusschulz.com/blog/keyof-and-lookup-types-in-typescript
interface ObjectConstructor {
// ...
entries<T extends { [key: string]: any }, K extends keyof T>(o: T): [keyof T, T[K]][];
// ...
}
Code
import camelcaseKeys from 'camelcase-keys'
import snakecaseKeys from 'snakecase-keys'
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>
interface ItemModel {
id: string
fooBar: any
baz?: number
}
interface ItemDatabaseModel {
id: string
foo_bar: any
baz?: number
}
export class Item {
private _data: Partial<ItemModel>
public static cast(item: Partial<ItemModel | ItemDatabaseModel>): Item {
return new this(camelcaseKeys(item))
}
public static create(item: Optional<ItemModel | ItemDatabaseModel, 'id'>): Item {
// Use item.id or add item.id === null to force key
return new this(camelcaseKeys(item))
}
private constructor(input: Partial<ItemModel>) {
// Validate "input" properties have a Item class property setter, else throw
// foreach => this[key] = input[key]
}
get databaseFormat() { return snakecaseKeys(this._data) }
get value() { return this._data }
set id(value: string | null) {
// automatically generate an ID if null, otherwise validate
this._data.id = value
}
set fooBar(value: any) {
// validate
this._data.fooBar = value
}
set baz(value: number | null) {
// validate
this._data.baz = value
}
}
The best way you could achieve this would be to have some tool which goes through your typescript files looking to type/interface definitions and automatically outputs additional types with the converted keys based on what it finds.
At the moment typescript doesn't support this kind of conversion automatically via a type definition and I'd guess that they'd be quite cautious about adding something like that; concatenating string literals in type definitions is something which has yet to make it into the language, for instance, and what you're looking for here is quite a bit more complex than that, unfortunately.
i need to find object in list .
this is my list :
export interface Controllermodel {
controllerDisplayName: string;
controllerFarsiName: string;
roleId: number;
controllerId: string;
actionsVM: Actionmodel[];
}
export interface Actionmodel {
displayName: string;
actionEnglishName: string;
actionId: number;
}
now i need to find object in a list but when i use this code :
export class ValidatePermissionDirective implements OnInit {
show: boolean;
constructor(private templateRef: TemplateRef<any>,
private viewContainerRef: ViewContainerRef
, private dynamic: DynamicPermissionService) { }
// tslint:disable-next-line:no-input-rename
#Input('appValidatePermission') AccessName:string;
ngOnInit() {
this.ValidatePemission();
if (this.show) {
this.viewContainerRef.createEmbeddedView(this.templateRef);
} else {
this.viewContainerRef.clear();
}
}
ValidatePemission()
{
console.log(this.AccessName)
const find = this.dynamic.dynamicModel.find(x =>
x.actionsVM.find(z => z.actionEnglishName === this.AccessName));
console.log(find)
if (find) {
console.log(false);
this.show = false;
} else {
console.log(true);
this.show = true;
}
}
}
but when i use this code it show me this error :
Type 'Actionmodel' is not assignable to type 'boolean'.
whats the problem ?? how can i solve this problem ?
Add !! to ensure that your find result is a boolean:
const find = this.dynamic.dynamicModel.find(x =>
!!x.actionsVM.find(z => z.actionEnglishName === this.AccessName));
find takes a single parameter: a function that accepts an array element and returns a boolean. The returned object will either be the found instance or undefined.
const find = this.dynamic.dynamicModel.find(
x => x.actionsVM.find(
z => z.actionEnglishName === this.AccessName));
In the inner find call, you have this correct: z.actionEnglishName === this.AccessName returns a boolean.
In the outer find call, you are returning the result of the inner find, which will either be an Actionmodel instance or the value undefined. These values can be coerced to true and false, but Typescript wants that to be explicit. By starting with !!, you ensure that "truthy" values like an instance will return the value true, and "falsy" values like undefined will return the value false, which matches Typescript's definition of the find method.