I created a mechanism for generating CSS Grids which I'm trying to implement with ngStyle. Here's what's generated
"GridAreas": {
"LinkContainer": "{'grid-area': 'link-row-start / content-col-start / link-row-end / content-col-end'}",
"BodySection" : "{'grid-area': 'content-row-start / content-col-start / content-row-end / content-col-end', 'justify-content': 'stretched'}",
"FooterSection": "{'grid-area': 'footer-row-start / content-col-start / footer-row-end / footer-col-end'}"
}
I tried using it in the html like this
<link-container [ngStyle]="Output.GridAreas.LinkContainer" [SiteLinks]="Links"></link-container>
and I got this error
Cannot find a differ supporting object '{'grid-area': 'link-row-start / content-col-start / link-row-end / content-col-end'}'
I then tried using functions like this
loadLinkArea(){ return this.Output.GridAreas.LinkContainer; }
calling in the HTML like this
<link-container [ngStyle]="loadLinkArea()"></link-container>
I received the same exact error. Here's how the data gets processed.
Shape of overall data
export class GridType {
GridName : string = '';
Columns : Array<GridLine> = new Array();
Rows : Array<GridLine> = new Array();
Areas : Array<AreaList> = new Array(); /*the part I'm focusing on*/
Type : string = '';
}
export class AreaList {
AreaName : string = '';
Specs : GridArea = new GridArea();
}
export class GridArea {
Area : string = '';
Params? : Array<ParamData> = new Array();
}
export class ParamData {
Param : string = '';
Setting : string = '';
}
Areas Data
Areas : [
{
AreaName : 'LinkContainer',
Specs : { Area: 'link-row-start / content-col-start / link-row-end / content-col-end' }
},
{
AreaName : 'BodySection',
Specs : {
Area : 'content-row-start / content-col-start / content-row-end / content-col-end',
Params : [ { Param: 'justify-content', Setting: 'stretched' } ]
}
},
{
AreaName : 'FooterSection',
Specs : { Area: 'footer-row-start / content-col-start / footer-row-end / footer-col-end' }
}
]
The above body of data gets passed into this function
export function buildGridAreas( data: AreaList[] ){
let areas: StringList = new StringList();
data.forEach( a => {
let b: string = '';
if( a.Specs.Params ){
let c: string[] = [];
a.Specs.Params.forEach( prs => {
const d: string = "'"+prs.Param+"': "+prs.Setting;
c.push(d);
});
b = "{'grid-area': "+a.Specs.Area+", "+c.join(', ')+"}";
}
else{ b = "{'grid-area': "+a.Specs.Area+"}"; }
areas[ a.AreaName ]=b;
});
return areas;
}
the rest of the code that generates for defining the grid looks like this
"GridCode": "[top-bleed-start] 10.245000000001024px [top-bleed-end link-row-start] 40.9800000000041px [link-row-end content-row-start] [content-row-end footer-row-start] 163.9200000000164px [footer-row-end bottom-bleed-start] 10.245000000001024px [bottom-bleed-end]/[left-bleed-start] 10.245000000001024px [left-bleed-end content-col-start] 1331.8500000001332px [content-col-end right-bleed-start] 10.245000000001024px [right-bleed-end]"
Can anybody spot where I'm going wrong?
The syntax for ngStyle is like this:
<div [ngStyle]="{'background-color' : color}"></div>
If you do that, the attribute value is an expression. So in your case, Angular treats the value of Output.GridAreas.LinkContainer as an expression. On the other hand, it seems you expected the value of Output.GridAreas.LinkContainer to be treated as if it has been literally the attribute value in the first place.
As far as I understand your code, the solution should be simple: just remove the doublequotes from the right-hand side of your style declarations, i.e. just use plain objects.
[UPDATE] In response to your comment, just two snippets of code:
a) This component basically does what you had in your initial problem description:
import {Component} from '#angular/core';
#Component({
selector: 'app-root',
template: `<div [ngStyle]="Output.GridAreas.LinkContainer">Works</div>`
})
export class AppComponent {
public Output = {
GridAreas: {
LinkContainer: "{'grid-area': 'link-row-start / content-col-start / link-row-end / content-col-end'}"
}
};
}
This code will result in the “Cannot find a differ …” error you mentioned.
b) Code with the quotes removed, which will work:
import {Component} from '#angular/core';
#Component({
selector: 'app-root',
template: `<div [ngStyle]="Output.GridAreas.LinkContainer">Works</div>`
})
export class AppComponent {
public Output = {
GridAreas: {
LinkContainer: {'grid-area': 'link-row-start / content-col-start / link-row-end / content-col-end'}
}
};
}
I admit I was not committed enough to go through all your code in detail (BTW: IMO, the code would become more readable if it was using ES6 template strings), but it looks like you construct strings instead of objects. In other words, instead of …
b = "{'grid-area': "+a.Specs.Area+"}";
… you should use something like …
b = {'grid-area': a.Specs.Area};
(untested). So it seems to be a problem of how you construct your code, not an Angular problem.
Related
Why does this property in react CSS not work if it is of type CSSProperties? How can I get it to work with Properties<string | number> ?
export const fields: GridFieldsConfiguration[] = [
{
...defaultColDefs,
field: 'amInitials',
displayNameRule: 'Asset Manager',
flex: 1.1,
minWidth: 75,
cellStyle: (params: any): any => {
getCellStyle(params, 'amInactive')
}
}
];
const isDisabledbStyle = {
color: '#FF0000'
};
const getCellStyle = ((params: any, inactiveCol: string): CSSProperties => {
console.log(params);
if (params?.api?.getValue(inactiveCol, params.node) === true) {
return isDisabledbStyle;
} else {
return isDisabledbStyle;
}
}
);
Here are the types. cellStyle comes from CSSProperties which is an extension of CSS.Properties<string | number>.
export interface GridFieldConfiguration extends FieldConfiguration {
cellStyle?: CSSProperties;
}
export interface CSSProperties extends CSS.Properties<string | number> {
/**
* The index signature was removed to enable closed typing for style
* using CSSType. You're able to use type assertion or module augmentation
* to add properties or an index signature of your own.
*
* For examples and more information, visit:
* https://github.com/frenic/csstype#what-should-i-do-when-i-get-type-errors
*/
}
Here is Properties
export interface Properties<TLength = string | 0> extends StandardProperties<TLength>, VendorProperties<TLength>, ObsoleteProperties<TLength>, SvgProperties<TLength> {}
Sounds like you are using AG Grid, and trying to configure a cellStyleFunc for the cellStyle option of column definitions?
As shown in the linked documentation, it is indeed possible to provide a function that takes a params argument.
But it looks like in your case, you have an intermediate GridFieldsConfiguration custom type, that expects cellStyle to be of type Properties<string | number> (which is very probably actually React.CSSProperties), which does not accepts the function form, hence the error message.
If the rest of your code that handles GridFieldsConfiguration really expects CSSProperties and not the function form, then you would have to refactor it first, so that it can handle that form.
If all it does is to pass the cellStyle option to AG Grid, then you just need to improve the definition of GridFieldsConfiguration type. You can re-use the actual types grom AG Grid, e.g.:
import { AgGridColumnProps as ColDef } from "ag-grid-react";
export interface GridFieldsConfiguration {
cellStyle?: ColDef["cellStyle"];
}
But note that CSSProperties is actually not type-compatible with cellStyle. To fix it, simply remove the return type assertion on your getCellStyle function. If you want to ensure that the returned objects still resembles a CSS object, you can use the new satisfies operator:
const isDisabledbStyle = {
color: '#FF0000'
} satisfies CSSProperties;
const getCellStyle = (params: any, inactiveCol: string) => {
return isDisabledbStyle;
};
Playground Link
I have a console on the screen that I log things to. The component looks like this.
export class AppComponent { logs: string[]; ... }
<div *ngFor="let log of logs" class="log-type-a">{{log}}</div>
I'd like to set a class on each of the DIVs dynamically. At the moment, each of those is log-type-a but I'd prefer it to be log-type-b when the string contains a certain value (e.g. it starts with "b", so that the class would be log-type-first_char_of_log.
I'm not sure what to google for to being with. I've tried horsing around with ngClass but failed due to ignorance and uncertainty on how.
NgFor can have an index, like this one: *ngFor="let item of items; index as i;"
You could use that index in order to set different classes for your items, like class="log-type-{{ i }}".
https://angular.io/api/common/NgForOf
You can use expresion in ngClass like this:
export class AppComponent { logs: string[]; ... }
<div *ngFor="let log of logs" [ngClass]="'log-type-' + log">{{log}}</div>
If your logs array has lets say: red and blue
the class output should be:
log-type-red
log-type-blue
OR
you can use functions like this and make decisions depending on the log value:
<div *ngFor="let log of logs" [ngClass]="'class-test-' + ngClassConvert (log)">
My log value: {{ log }}
Converted in cssClass: {{ ngClassConvert (log)}}
</div>
and the component:
export class AppComponent implements OnInit {
name = 'Angular';
logs: string[];
ngClassConvert(value):string{
let returnValue = '';
switch (value){
case 'a': {
returnValue = 'Apple';
break;
}
case 'b': {
returnValue = 'Banana';
break;
}
case 'c': {
returnValue = 'Cherry';
break;
}
case 'd': {
returnValue = 'dddd';
break;
}
default: {
returnValue = 'default';
break;
}
}
return returnValue;
}
ngOnInit(): void {
this.logs = ['a', 'b', 'c', 'd'];
}
}
Also and Demo
Based on the answer of SehaxX: You could parse the list of logs using a setter. This will save you function calls in the template.
private _logs: string[];
public parsedLogs: any[];
set logs(value: string[]) {
this._logs = value;
this.parsedLogs = this.parseLogs(value);
}
private parseLogs(logs: string[]): any[] {
let parsed = [];
for (let i = 0; i < logs.length; i++){
parsed.push({
value: logs[i],
style: this.ngClassConvert(logs[i])
});
}
return parsed;
}
Demo
In an aurelia project I have several components that import additional stylesheets, e.g. from semantic-ui. After leaving the components page, the stylesheet is still active and not removed. Is it possible to 'unload' the stylesheets?
Update (2018-03-27):
I submitted a PR to enable this as an opt-in, you can keep track of it here: https://github.com/aurelia/templating-resources/pull/344
Original answer:
A word of warning, this is untested and aurelia-internals-hacky.
What you could do is override the default CSSViewEngineHooks and CSSResource classes to keep track of the style elements it injects, and then add an beforeUnbind hook to remove the styles again.. before unbind (right after detached)
Unfortunately the CSSResource class is not exported from aurelia-templating-resources so we need to go one layer deeper and overwrite the existing style loader plugins that returns instances of CSSResource.
Here's how:
First we grab the code from aurelia-templating-resources/src/css-resource.js, put it in our own src/css-resource.js/ts and make a few tweaks to it (don't think too much of the size, it's just a copy-paste with a few small tweaks, annotated with comments):
import {ViewResources, resource, ViewCompileInstruction} from 'aurelia-templating';
import {Loader} from 'aurelia-loader';
import {Container} from 'aurelia-dependency-injection';
import {relativeToFile} from 'aurelia-path';
import {DOM, FEATURE} from 'aurelia-pal';
let cssUrlMatcher = /url\((?!['"]data)([^)]+)\)/gi;
function fixupCSSUrls(address, css) {
if (typeof css !== 'string') {
throw new Error(`Failed loading required CSS file: ${address}`);
}
return css.replace(cssUrlMatcher, (match, p1) => {
let quote = p1.charAt(0);
if (quote === '\'' || quote === '"') {
p1 = p1.substr(1, p1.length - 2);
}
return 'url(\'' + relativeToFile(p1, address) + '\')';
});
}
class CSSResource {
constructor(address: string) {
this.address = address;
this._scoped = null;
this._global = false;
this._alreadyGloballyInjected = false;
}
initialize(container: Container, target: Function): void {
this._scoped = new target(this);
}
register(registry: ViewResources, name?: string): void {
if (name === 'scoped') {
registry.registerViewEngineHooks(this._scoped);
} else {
this._global = true;
}
}
load(container: Container): Promise<CSSResource> {
return container.get(Loader)
.loadText(this.address)
.catch(err => null)
.then(text => {
text = fixupCSSUrls(this.address, text);
this._scoped.css = text;
if (this._global) {
this._alreadyGloballyInjected = true;
// DOM.injectStyles(text); <- replace this
// this is one of the two possible moments where the style is injected
// _scoped is the CSSViewEngineHooks instance, and we handle the removal there
this._scoped.styleNode = DOM.injectStyles(text);
}
});
}
}
class CSSViewEngineHooks {
constructor(owner: CSSResource) {
this.owner = owner;
this.css = null;
}
beforeCompile(content: DocumentFragment, resources: ViewResources, instruction: ViewCompileInstruction): void {
if (instruction.targetShadowDOM) {
DOM.injectStyles(this.css, content, true);
} else if (FEATURE.scopedCSS) {
let styleNode = DOM.injectStyles(this.css, content, true);
styleNode.setAttribute('scoped', 'scoped');
} else if (this._global && !this.owner._alreadyGloballyInjected) {
// DOM.injectStyles(this.css); <- replace this
// save a reference to the node so we can easily remove it later
this.styleNode = DOM.injectStyles(this.css);
this.owner._alreadyGloballyInjected = true;
}
}
// this is the hook we add, here we remove the node again
beforeUnbind(): void {
if (this._global && this.owner._alreadyGloballyInjected) {
DOM.removeNode(this.styleNode);
this.owner._alreadyGloballyInjected = false;
}
}
}
export function _createCSSResource(address: string): Function {
#resource(new CSSResource(address))
class ViewCSS extends CSSViewEngineHooks {}
return ViewCSS;
}
Then, in our main.ts/js we do the same thing aurelia-templating-resources.js does, but with our own version.
So we do this after the call to aurelia.use.standardConfiguration() etc, to override the existing one
let viewEngine = config.container.get(ViewEngine);
let styleResourcePlugin = {
fetch(address) {
return { [address]: _createCSSResource(address) };
}
};
['.css', '.less', '.sass', '.scss', '.styl'].forEach(ext => viewEngine.addResourcePlugin(ext, styleResourcePlugin));
And that should pretty much do the trick.. :)
I have found a plugin to resolve the issue:
https://github.com/jbockle/aurelia-useable-style-loader
But for the latest Webpack webpack.config.js should be a little bit different than in a plugin readme.
You should load .css files this way:
use: [
{ loader: 'style-loader', options: { injectType: 'lazyStyleTag' } },
'css-loader'
]
Instead of this:
use: ['style-loader/useable', 'css-loader']
I'm creating a mechanism for defining and calculating my own reusable grids. Here's an example of what's returned
[left-bleed-start] 10.245000000001024px [left-bleed-end content-col-start] 1331.8500000001332px [content-col-end right-bleed-start] 10.245000000001024px [right-bleed-end]/[top-bleed-start] 10.245000000001024px [top-bleed-end link-row-start] 81.9600000000082px [content-row-end footer-row-start] 163.9200000000164px [footer-row-end bottom-bleed-start] 10.245000000001024px [bottom-bleed-end]
When applying it like this
<div class="appCompGrid" [style.grid]="Grid.GridCode"></div>
I get the santization warning. However if I copy and paste the value in like this
<div class="appCompGrid" style="grid: (same code);"></div>
everything works. The css class is where I define the display as grid seeing that it'll be consistent no matter what size the screen is. The only thing I could think to do was go back into the function and add + ';' to the end of where the grid code is put together figuring maybe that was throwing something off but it still gives the same error. I tried applying display: grid; inline to see if maybe there was a problem with it reading both from a css class and inline for some odd reason.
I'm using #HostListener to re-calculate the grid as the size or orientation changes, so far I haven't run into a problem with Angular functioning in this manner so I don't understand where to begin with figuring out why this is happening. Here's how I have my component classes set up.
Base Class
export class GridBuilder {
Settings : GridInit = new GridInit();
GridData : Array<GridType> = new Array();
Grid : GridOutput = new GridOutput();
constructor() { this.GridData = GridDefs; }
public buildGrid() {
const coreSettings : GridInit = this.Settings;
const gridData : GridType[] = this.GridData;
const w: number = multiply( coreSettings.Size.Width, coreSettings.Size.PixelRatio );
const h: number = multiply( coreSettings.Size.Height, coreSettings.Size.PixelRatio );
const o: string = checkOrientation( w, h );
const c: CellSpecs = calcCell( o, w );
const t: GridType = gridData.find( a => a.GridName == coreSettings.GridStyle );
const cols: string = calcArea( t.Columns, c );
const rows: string = calcArea( t.Rows, c );
this.Grid.GridCode = cols + '/' + rows + ';';
this.Grid.GridAreas = t.Areas;
}
}
Secondary class for app component/ any top tier container
export class SiteGrid extends GridBuilder {
constructor(){
super();
this.applySizeSettings();
}
applySizeSettings(){
this.Settings.Size.Width = window.innerWidth;
this.Settings.Size.Height = window.innerHeight;
this.Settings.Size.PixelRatio = window.devicePixelRatio;
}
}
the AppComponent
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent extends SiteGrid {
title = 'app';
#HostListener( 'window: resize', [ '$event' ] )onResize( event ){ this.applySizeSettings(); this.buildGrid(); }
constructor(){
super();
this.Settings.GridStyle = 'SiteGridA';
this.buildGrid();
}
}
I don't know how relevant this may be in helping figure out the solution but thought I'd show how things are flowing just incase. Anyone know why this warning is occurring?
You need to implement a sanitizer to cleanse your css, or bypass it...
constructor(private sanitizer: DomSanitizer) {
this.sanitizedCSS = sanitizer.bypassSecurityTrustStyle(Grid.GridCode) ;
}
As for why, this blog explains it pretty well, as does the DomSanitizer documentation.
DomSanitizer helps preventing Cross Site Scripting Security bugs (XSS) by sanitizing values to be safe to use in the different DOM contexts.
I'm using aurelia-validate and my validation works fine if I use variables, but I need it to validate properties of an object rather than a variable:
Here's what works:
import {Validation} from 'aurelia-validation';
import {ensure} from 'aurelia-validation';
import {ItemService} from './service';
export class EditItem {
static inject() {
return [Validation, ItemService];
}
#ensure(function(it){
it.isNotEmpty()
.hasLengthBetween(3,10);
})
name = '';
#ensure(function(it){
it.isNotEmpty()
.hasMinLength(10)
.matches(/^https?:\/\/.{3,}$/) //looks like a url
.matches(/^\S*$/); //no spaces
})
url = '';
constructor(validation, service) {
this.validation = validation.on(this);
this.service = service;
}
activate(params){
return this.service.getItem(params.id).then(res => {
console.log(res);
this.name = res.content.name; //populate
this.url = res.content.url;
});
}
update() {
this.validation.validate().then(
() => {
var data = {
name: this.name,
url: this.url
};
this.service.updateItem(data).then(res => {
this.message = "Thank you!";
})
}
);
}
}
Here's what I'm trying to do (but doesn't work)...also I'm not sure if it's better to keep the properties on the class or have a property called this.item which contains the properties (this is the typical angular way):
import {Validation} from 'aurelia-validation';
import {ensure} from 'aurelia-validation';
import {ItemService} from './service';
export class EditItem {
static inject() {
return [Validation, ItemService];
}
#ensure(function(it){
it.isNotEmpty()
.hasLengthBetween(3,10);
})
this.item.name; //no assignment here should happen
#ensure(function(it){
it.isNotEmpty()
.hasMinLength(10)
.matches(/^https?:\/\/.{3,}$/) //looks like a url
.matches(/^\S*$/); //no spaces
})
this.item.url; //no assignment?
constructor(validation, service) {
this.validation = validation.on(this);
this.service = service;
this.item = null;
}
activate(params){
return this.service.getItem(params.id).then(res => {
console.log(res);
this.item = res.content; //populate with object from api call
});
}
update() {
this.validation.validate().then(
() => {
var data = {
name: this.item.name,
url: this.item.url
};
this.service.updateItem(data).then(res => {
this.message = "Thank you!";
})
}
);
}
}
Can someone give me some guidance here on how to use a validator against an existing object (for an edit page)?
The validation works in all kinds of situations, but using the #ensure decorator can only be used to declare your rules on simple properties (like you found out).
Hence...
Option a: replace the ensure decorator with the fluent API 'ensure' method, this supports 'nested' or 'complex' binding paths such as:
import {Validation} from 'aurelia-validation';
import {ItemService} from './service';
export class EditItem {
static inject() {
return [Validation, ItemService];
}
constructor(validation, service) {
this.validation = validation.on(this)
.ensure('item.url')
.isNotEmpty()
.hasMinLength(10)
.matches(/^https?:\/\/.{3,}$/) //looks like a url
.matches(/^\S*$/)
.ensure('item.name')
.isNotEmpty()
.hasLengthBetween(3,10);
this.service = service;
this.item = null;
}
activate(params){
return this.service.getItem(params.id).then(res => {
console.log(res);
this.item = res.content; //populate with object from api call
});
}
update() {
this.validation.validate().then(
() => {
var data = {
name: this.item.name,
url: this.item.url
};
this.service.updateItem(data).then(res => {
this.message = "Thank you!";
})
}
);
}
}
Note: you can set up your validation even before item is set. Cool, no?
Option b: Since the validation rules are specific to the item, you could move your validation rules inside your item class using the #ensure decorator inside that class instead.
You can then set up validation in your VM after you've retrieved the item: this.validation = validation.on(this.item); or, your service can set up the validation when it returns your item to your VM and make it an intrinsic part of the model: item.validation = validation.on(item);
Option a is easiest and seems to match your experience. Option b is more maintainable, as the validation rules for your model will live on the model, not on the view-model. However if you go with option b, you might have to adjust your HTML a bit to make sure validation hints appear.
Use the .on method of the validator to apply your rules to object properties.
The example below is called after I retrieve an object named stock, it validates that the quantity is not empty and is numeric only. Hope this helps...
let stock = {
name: 'some name'
minimumQuantity: '1'
};
applyRules() {
ValidationRules
.ensure((m: EditStock) => m.minimumQuantity)
.displayName("Minimum Quantity")
.required()
.withMessage(`\${$displayName} cannot be blank.`)
.matches( /^[0-9]*$/)
.withMessage(`\${$displayName} must be numeric only.`)
.on(this.stock);
}