Angular: Animations for an immutable model inside ngFor - css

I've been unable to get CSS transitions working when using an array of immutable model objects.
Does angular require an in-place update to the property for these to work, or am I doing something wrong?
As an example: (https://plnkr.co/edit/PBSPtk9vMig7cxnHoJqA)
export class Box {
constructor(public selected:boolean){
}
}
function toggle(box:Box):Box{
return new Box(!box.selected)
}
#Component({
selector: 'my-app',
template: `
<ul>
<li *ngFor="let box of boxes">
<div
style="display:inline-block; width: 20px; height:20px; transition: 1s; margin:5px; cursor:pointer;"
[ngStyle]="{'background': box.selected?'green':'red'}"
> </div>
<button (click)="toggle_in_place(box)">toggle (in-place)</button>
<button (click)="toggle_replace_box(box)">toggle (replace box)</button>
<button (click)="toggle_replace_array(box)">toggle (replace array)</button>
</li>
</ul>
`,
})
export class App {
boxes: Box[];
constructor() {
this.boxes = [new Box(true), new Box(false), new Box(true)];
}
toggle_in_place(box:Box){
box.selected=!box.selected;
}
toggle_replace_box(box:Box){
const index = this.boxes.indexOf(box);
this.boxes[index]=toggle(box);
}
toggle_replace_array(box:Box){
this.boxes = this.boxes.map(v=>v===box?toggle(v):v);
}
}
Here I'm trying to animate the color of the div through
[ngStyle]="{'background': box.selected?'green':'red'}"
box.selected can be updated by:
changing the property in-place (toggle_in_place),
replacing the 'box' model (toggle_replace_box)
replacing the entire array (toggle_replace_array).
but toggle_in_place is the only one that results in a visible transition.
Is this just something I have to live with?

What's happening here: When the array changes the ngFor compares the object references in the new array to the old and re-renders any elements that have changed, which breaks the css transitions.
To solve this, ngFor can be given a tracking function which will then be used instead of the reference comparison.
To fix, specify a tracking function to use an id rather than object reference:
trackingFunction(index:number, box:Box){
return box.id;
}
Have ngFor use it:
<li *ngFor="let box of boxes;trackBy:trackingFunction">
Example:
https://plnkr.co/edit/4OXemdSf1Hsm1NU6ZhzT

I think it is something you have to live with. In the latter two cases, since you're creating a new instance of the box each time, it has nothing to transition from.
You could probably make a component which is basically a wrapper around a box and can pass a "transition" boolean to its constructor. Workarounds are pretty dependent on the use case though.

Related

How to dynamically generate CSS class and/or set its property

The title is not really a question it is more like an idea, I don't know what approach is best for my situation.
So, the problem. I have some 3rd party component that have some complex structure and styling. Some part of it has some predefined CSS class that I can override with CSS in my surrounding component. Something like this:
my component:
<div class="my-cmp-container">
<some-3rd-party-cmp></some-3rd-party-cmp>
</div>
3rd party component:
<div class="3rd-party-css-class">
...
</div>
For example, 3rd-party-css-class has style background-color: #f00, I can override it with .my-cmp-container .3rd-party-css-class { background-color: #fff; } etc. But. What if I need to set color dynamically, it's stored in a DB for example and I can't predefine each case in my class' CSS. I just have the color in hex.
In theory I can generate unique string to set as CSS class for every instance of some-3rd-party-cmp and somehow generate CSS in my component? I'm lost a little, what is the best approach for this?
Edit: Code sample to illustrate the situation https://stackblitz.com/edit/angular-kxdatq
What you are trying to do is the subject of this open issue about stylesheet binding in Angular. Until that feature is available, you can get what you want with a custom directive. Here is a directive that retrieves the checkbox element generated by ng-zorro-antd and applies two color attributes to it. The two colors are #Input properties and the directive implements OnChanges which allows to react to property binding changes.
#Directive({
selector: "[nz-checkbox][nz-chk-style]"
})
export class CheckBoxStyleDirective implements OnInit, OnChanges {
#Input("nz-chk-bkgnd") chkBkgndColor: string;
#Input("nz-chk-border") chkBorderColor: string;
private checkbox: HTMLElement;
constructor(private renderer: Renderer2, private el: ElementRef) { }
ngOnInit() {
this.checkbox = this.el.nativeElement.querySelector(".ant-checkbox-inner");
this.updateBackgroundColor();
this.updateBorderColor();
}
ngOnChanges(changes: SimpleChanges) {
if (changes.chkBkgndColor) {
this.updateBackgroundColor();
}
if (changes.chkBorderColor) {
this.updateBorderColor();
}
}
updateBackgroundColor() {
if (this.checkbox) {
this.renderer.setStyle(this.checkbox, "background-color", this.chkBkgndColor);
}
}
updateBorderColor() {
if (this.checkbox) {
this.renderer.setStyle(this.checkbox, "border-color", this.chkBorderColor);
}
}
}
Once the directive attribute selector nz-chk-style is applied to the 3rd party element, you can set the checkbox background and border colors with property binding as follows:
<span nz-checkbox nz-chk-style [nz-chk-bkgnd]="bkgndColor" [nz-chk-border]="borderColor" >
See this interactive stackblitz for a demo.
Not sure if you are using Angular but you tagged it, so I guess you are.
If you want to change only the color and nothing more, instead of having a .3rd-party-css-class class, you could just have your with an ng-style like so:
<some-3rd-party-cmp ng-style="{ color: your_color_hex_variable }"></some-3rd-party-cmp>
You can also define a whole object if styles and pass it.
You can also use ng-class and pass one or an array of class names what you want to put additionally on your component:
<some-3rd-party-cmp ng-class="[cls1, cls2, cls3]"></some-3rd-party-cmp>
<some-3rd-party-cmp ng-class="[3rd-party-css-class, someCondition ? 'another-class-name' : '']"></some-3rd-party-cmp>
In the classes you can define the css rules you want to apply and thats it.
With this solutions you can avoid having extra wrapper elements for styling purposes which is a nice thing.

Vuejs apply loop in css to put hover

In VueJS, I have elements that have hover property in my object.
So, I want to put a foreach in style, but it is not possible.
I want to do that kind of thing :
<style>
#foreach (element in elements) {
if (element.has_backgroundhover) {
'#'+element.id:hover {
background : element.background_hover;
}
}
}
</style>
Notice that each element has a background color different (it is stored in his oibject property)
Thank you
The #mouseenter and #mouseleave event listeners would allow for css classes to be applied to each element.
For example, toggle a .hovered class that has the background color defined.
Something like this?
The HTML:
<div id="app">
<div
v-for="element of elements"
#mouseenter="element.hover=true"
#mouseleave="element.hover=false"
:style="{
background: element.hover? element.background_hover : element.background
}"
>{{element.name}}</div>
</div>
And the JS:
new Vue({
el: "#app",
data: {
elements:[
{
name:"element1",
background:"#f8f",
background_hover:"#a4a",
hover:false
},
{
name:"element2",
background:"#ff8",
background_hover:"#aa4",
hover:false
},
]
},
})
This is not using the CSS, rather using events as suggested by #DigitalDrifter. I think the point is that reactive css is not a good idea, and not supported in vue. Instead you need to have the HTML element properties dependent on your vue data object. A fiddle for this is: https://jsfiddle.net/edzaokum/

Angular mix different [ngClass] expressions

I would like to mix the [ngClass] Expressions in Angular, first I want to add an array of classes to my component and then I would also like to add a class based on a Boolean expression.
I can add an array with <li [ngClass]="item.status"> and I can add a class based on a Boolean expression with <li [ngClass]="{active: item.active}">
Note: item.status is an array like item.status=['done', 'progressed']
But how can I mix both styles?
The following doesn't work because only the attribute is duplicate.
<li [ngClass]="item.status" [ngClass]="{active: item.active}">
Thanks in advance
Because you could not have [ngClass] multiple time in same element you could use the combination of [ngClass] and class. When use class in this way with interpolation then you could add several classes as well. Try to use following way.
E.g. HTML
<li class="t-size {{item.status.join(' ')}}" [ngClass]="{'active': item.active }">
E.g. Component
item = {
status : ['done', 'progressed'],
active: true
}
E.g. CSS
.done{
font-weight: bold;
}
.progressed{
color:blue;
}
.t-size{
font-size: 2rem;
}
.active{
background-color: yellowgreen;
}
WORKING DEMO
Hope this will help to you! :)
You can use both as below:
<li [class]="item.status" [ngClass]="{active: item.active}">
You cannot use ngClass more than one time in the single element.
You can do [class.myClass]="booleanExpression", but I don't recommend adding it together with [ngClass] because then you could have a class multiple times.
Instead I would recommend either making [class.myClass]="booleanExpression" for each and every class you use in that HTML-tag (keep in mind the "booleanExpression" can set/unset more than one class so it would not increase the lines of code in your .ts file, only in the template). Or I would make a getter method for [ngClass]:
[ngClass]="liClasses"
get liClasses(): string {
const result = "";
if (conditionOne) { result += " myClassOne"}
if (conditionTwo) { result += " myClassTwo"}
return result;
}
note that solution 1) is more perfomant as getter-methods get triggered on every change-detection and not only when conditionOne or conditionTwo changes.
you can try this code here your app component is
member = true;
paidAmount = true;
setMyClass() {
let classes = {
member: this.someProperty,
paid: this.paidAmount,
};
return classes;
}
where 'member' and 'paid' is class name
and your template use this similar code
<div class="myclass" [ngClass]="setMyClass()">Call to Action</div>

Change CSS class's property on click

I've read around a little bit and have a good start to what I ultimately want. This was helpful, along with another article which I forgot the link to. However, everything I've read ADDS a CSS class or property to an element. I want to CHANGE a property of an existing CSS class, but I don't know how to target it.
I think I want to use ng-class in one of these use cases taken from the Angular documentation:
If the expression evaluates to a string, the string should be one or more space-delimited class names.
If the expression evaluates to an object, then for each key-value pair of the object with a truthy value the corresponding key is used as a class name.
My existing code uses ng-class along with some controller logic.
HTML
<div ng-controller="ngToggle">
<div ng-class="{'inset-gray-border' : style}">
<div class="subcontainer" ng-click="toggleStyle()">{{item.name}}</div>
</div>
</div>
This currently adds the inset-gray-border class to the nested div, but I just want to change the border property in the subcontainer class.
Controller
angular.module('app').controller('ngToggle', ['$scope', function($scope) {
$scope.style = false;
$scope.toggleStyle = function() {
$scope.style = $scope.style === false ? true: false;
};
}]);
I considered using a directive, but I believe that would be overkill. I think this can be achieved in a controller.
EDIT: After further research I think jQLite can do the trick, but that would probably require a directive.
CHANGE a property of an existing CSS class
Add a css rule that does that using the new class you added using ng-class. The specificity will over ride the original rule
.subcontainer{
color : blue
}
.inset-gray-border .subcontainer{
color:red
}
Instead of a big toggleStyle function, you can write that stuff in UI side only.
Here is fiddle. As you want to change border property of .subcontainer, Overwrite that property by adding .insert-gray-border
<div ng-controller="ngToggle">
<div >
<div ng-class="{'subcontainer':true,'inset-gray-border' : style}" ng-click="style=!style">{{item.name}}</div>
</div>
</div>
The benifit of this is , it uses local scope instead of controller scope.
The best bet would be to have two CSS classes defined, one for the base (untoggled) case, another with all the properties that you want for when the property is toggled on.
In this case you may want something like:
.container .subcontainer {}
.container .subcontainer-bordered { border: solid 1px #123456}
Then your HTML code be updated to reflect this structure
<div ng-controller="ngToggle">
<div class="container">
<div class="subcontainer" ng-class="{'subcontainer-bordered': style}" ng-click="style = !style">{{item.name}}</div>
</div>
</div>

How do I conditionally apply CSS styles in AngularJS?

Q1. Suppose I want to alter the look of each "item" that a user marks for deletion before the main "delete" button is pressed. (This immediate visual feedback should eliminate the need for the proverbial "are you sure?" dialog box.) The user will check checkboxes to indicate which items should be deleted. If a checkbox is unchecked, that item should revert back to its normal look.
What's the best way to apply or remove the CSS styling?
Q2. Suppose I want to allow each user to personalize how my site is presented. E.g., select from a fixed set of font sizes, allow user-definable foreground and background colors, etc.
What's the best way to apply the CSS styling the user selects/inputs?
Angular provides a number of built-in directives for manipulating CSS styling conditionally/dynamically:
ng-class - use when the set of CSS styles is static/known ahead of time
ng-style - use when you can't define a CSS class because the style values may change dynamically. Think programmable control of the style values.
ng-show and ng-hide - use if you only need to show or hide something (modifies CSS)
ng-if - new in version 1.1.5, use instead of the more verbose ng-switch if you only need to check for a single condition (modifies DOM)
ng-switch - use instead of using several mutually exclusive ng-shows (modifies DOM)
ng-disabled and ng-readonly - use to restrict form element behavior
ng-animate - new in version 1.1.4, use to add CSS3 transitions/animations
The normal "Angular way" involves tying a model/scope property to a UI element that will accept user input/manipulation (i.e., use ng-model), and then associating that model property to one of the built-in directives mentioned above.
When the user changes the UI, Angular will automatically update the associated elements on the page.
Q1 sounds like a good case for ng-class -- the CSS styling can be captured in a class.
ng-class accepts an "expression" that must evaluate to one of the following:
a string of space-delimited class names
an array of class names
a map/object of class names to boolean values
Assuming your items are displayed using ng-repeat over some array model, and that when the checkbox for an item is checked you want to apply the pending-delete class:
<div ng-repeat="item in items" ng-class="{'pending-delete': item.checked}">
... HTML to display the item ...
<input type="checkbox" ng-model="item.checked">
</div>
Above, we used ng-class expression type #3 - a map/object of class names to boolean values.
Q2 sounds like a good case for ng-style -- the CSS styling is dynamic, so we can't define a class for this.
ng-style accepts an "expression" that must evaluate to:
an map/object of CSS style names to CSS values
For a contrived example, suppose the user can type in a color name into a texbox for the background color (a jQuery color picker would be much nicer):
<div class="main-body" ng-style="{color: myColor}">
...
<input type="text" ng-model="myColor" placeholder="enter a color name">
Fiddle for both of the above.
The fiddle also contains an example of ng-show and ng-hide. If a checkbox is checked, in addition to the background-color turning pink, some text is shown. If 'red' is entered in the textbox, a div becomes hidden.
I have found problems when applying classes inside table elements when I had one class already applied to the whole table (for example, a color applied to the odd rows <myClass tbody tr:nth-child(even) td>). It seems that when you inspect the element with Developer Tools, the element.style has no style assigned. So instead of using ng-class, I have tried using ng-style, and in this case, the new CSS attribute does appear inside element.style. This code works great for me:
<tr ng-repeat="element in collection">
[...amazing code...]
<td ng-style="myvar === 0 && {'background-color': 'red'} ||
myvar === 1 && {'background-color': 'green'} ||
myvar === 2 && {'background-color': 'yellow'}">{{ myvar }}</td>
[...more amazing code...]
</tr>
Myvar is what I am evaluating, and in each case I apply a style to each <td> depending on myvar value, that overwrites the current style applied by the CSS class for the whole table.
UPDATE
If you want to apply a class to the table for example, when visiting a page or in other cases, you can use this structure:
<li ng-class="{ active: isActive('/route_a') || isActive('/route_b')}">
Basically, what we need to activate a ng-class is the class to apply and a true or false statement. True applies the class and false doesn't. So here we have two checks of the route of the page and an OR between them, so if we are in /route_a OR we are in route_b, the active class will be applied.
This works just having a logic function on the right that returns true or false.
So in the first example, ng-style is conditioned by three statements. If all of them are false, no style is applied, but following our logic, at least one is going to be applied, so, the logic expression will check which variable comparison is true and because a non empty array is always true, that will left an array as return and with only one true, considering we are using OR for the whole response, the style remaining will be applied.
By the way, I forgot to give you the function isActive():
$rootScope.isActive = function(viewLocation) {
return viewLocation === $location.path();
};
NEW UPDATE
Here you have something I find really useful. When you need to apply a class depending on the value of a variable, for example, an icon depending on the contents of the div, you can use the following code (very useful in ng-repeat):
<i class="fa" ng-class="{ 'fa-github' : type === 0,
'fa-linkedin' : type === 1,
'fa-skype' : type === 2,
'fa-google' : type === 3 }"></i>
Icons from Font Awesome
This works well when ng-class can't be used (for example when styling SVG):
ng-attr-class="{{someBoolean && 'class-when-true' || 'class-when-false' }}"
(I think you need to be on latest unstable Angular to use ng-attr-, I'm currently on 1.1.4)
I have published an article on working with AngularJS+SVG. It talks about this issue and numerous others. http://www.codeproject.com/Articles/709340/Implementing-a-Flowchart-with-SVG-and-AngularJS
span class="circle circle-{{selectcss(document.Extension)}}">
and code
$scope.selectcss = function (data) {
if (data == '.pdf')
return 'circle circle-pdf';
else
return 'circle circle-small';
};
css
.circle-pdf {
width: 24px;
height: 24px;
font-size: 16px;
font-weight: 700;
padding-top: 3px;
-webkit-border-radius: 12px;
-moz-border-radius: 12px;
border-radius: 12px;
background-image: url(images/pdf_icon32.png);
}
This solution did the trick for me
<a ng-style="{true: {paddingLeft: '25px'}, false: {}}[deleteTriggered]">...</a>
You can use ternary expression. There are two ways to do this:
<div ng-style="myVariable > 100 ? {'color': 'red'} : {'color': 'blue'}"></div>
or...
<div ng-style="{'color': (myVariable > 100) ? 'red' : 'blue' }"></div>
Another option when you need a simple css style of one or two properties:
View:
<tr ng-repeat="element in collection">
[...amazing code...]
<td ng-style="{'background-color': getTrColor(element.myvar)}">
{{ element.myvar }}
</td>
[...more amazing code...]
</tr>
Controller:
$scope.getTrColor = function (colorIndex) {
switch(colorIndex){
case 0: return 'red';
case 1: return 'green';
default: return 'yellow';
}
};
See the following example
<!DOCTYPE html>
<html ng-app>
<head>
<title>Demo Changing CSS Classes Conditionally with Angular</title>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js"></script>
<script src="res/js/controllers.js"></script>
<style>
.checkboxList {
border:1px solid #000;
background-color:#fff;
color:#000;
width:300px;
height: 100px;
overflow-y: scroll;
}
.uncheckedClass {
background-color:#eeeeee;
color:black;
}
.checkedClass {
background-color:#3ab44a;
color:white;
}
</style>
</head>
<body ng-controller="TeamListCtrl">
<b>Teams</b>
<div id="teamCheckboxList" class="checkboxList">
<div class="uncheckedClass" ng-repeat="team in teams" ng-class="{'checkedClass': team.isChecked, 'uncheckedClass': !team.isChecked}">
<label>
<input type="checkbox" ng-model="team.isChecked" />
<span>{{team.name}}</span>
</label>
</div>
</div>
</body>
</html>
As of AngularJS v1.2.0rc, ng-class and even ng-attr-class fail with SVG elements (They did work earlier, even with normal binding inside the class attribute)
Specifically, none of these work now:
ng-class="current==this_element?'active':' ' "
ng-attr-class="{{current==this_element?'active':' '}}"
class="class1 class2 .... {{current==this_element?'active':''}}"
As a workaround, I've to use
ng-attr-otherAttr="{{current==this_element?'active':''}}"
and then style using
[otherAttr='active'] {
... styles ...
}
One more (in the future) way to conditionally apply style is by conditionally creating scoped style
<style scoped type="text/css" ng-if="...">
</style>
But nowadays only FireFox supports scoped styles.
There is one more option that I recently discovered that some people may find useful because it allows you to change a CSS rule within a style element - thus avoiding the need for repeated use of an angular directive such as ng-style, ng-class, ng-show, ng-hide, ng-animate, and others.
This option makes use of a service with service variables which are set by a controller and watched by an attribute-directive I call "custom-style". This strategy could be used in many different ways, and I attempted to provide some general guidance with this fiddle.
var app = angular.module('myApp', ['ui.bootstrap']);
app.service('MainService', function(){
var vm = this;
});
app.controller('MainCtrl', function(MainService){
var vm = this;
vm.ms = MainService;
});
app.directive('customStyle', function(MainService){
return {
restrict : 'A',
link : function(scope, element, attr){
var style = angular.element('<style></style>');
element.append(style);
scope.$watch(function(){ return MainService.theme; },
function(){
var css = '';
angular.forEach(MainService.theme, function(selector, key){
angular.forEach(MainService.theme[key], function(val, k){
css += key + ' { '+k+' : '+val+'} ';
});
});
style.html(css);
}, true);
}
};
});
well i would suggest you to check condition in your controller with a function returning true or false .
<div class="week-wrap" ng-class="{today: getTodayForHighLight(todayDate, day.date)}">{{day.date}}</div>
and in your controller check the condition
$scope.getTodayForHighLight = function(today, date){
return (today == date);
}
One thing to watch is - if the CSS style has dashes - you must remove them. So if you want to set background-color, the correct way is:
ng-style="{backgroundColor:myColor}"
Here's how i conditionally applied gray text style on a disabled button
import { Component } from '#angular/core';
#Component({
selector: 'my-app',
styleUrls: [ './app.component.css' ],
template: `
<button
(click)='buttonClick1()'
[disabled] = "btnDisabled"
[ngStyle]="{'color': (btnDisabled)? 'gray': 'black'}">
{{btnText}}
</button>`
})
export class AppComponent {
name = 'Angular';
btnText = 'Click me';
btnDisabled = false;
buttonClick1() {
this.btnDisabled = true;
this.btnText = 'you clicked me';
setTimeout(() => {
this.btnText = 'click me again';
this.btnDisabled = false
}, 5000);
}
}
Here's a working example:
https://stackblitz.com/edit/example-conditional-disable-button?file=src%2Fapp%2Fapp.component.html

Resources