Why doesn't CSS transition get applied?

2019-05-07 06:39发布

问题:

I've built a small stacked bar visual just using floated divs that underneath is bound to some data using knockout. What I want to be able to do is to animate changes in the size of these stacks when the data changes.

I've managed to do this in the general case, so of the 4 bars that I've got, 3 of them transition correctly. The problem is my final bar seems to ignore the transition and instantly re-sizes and I can't understand why. Here's a picture of the before/during/after states:

The way that I've defined this transition is simply via css

-webkit-transition: width 1s;
transition: width 1s;

The width of the bars is a computed value, calculating the percentage of items, so each bar should have it's width defined as a percentage. Although the red bar is calculated differently to the other 3 bars, I don't see why that should affect the transition.

What I find quite strange, is that if I modify the width through the developer console for example, then the bar does correctly animate. I'm wondering if anyone can suggest why this might be the case?

var vm = (function generateModel() {
    var data = {
        name: "Sign-off",
        id: "XX",
        values: [{ text: "Signed-off", count: 150, color: "#5fb5cc" }, 
                 { text: "Submitted", count: 90, color: "#75d181" }, 
                 { text: "Not Submitted", count: 75, color: "#f8a25b" }
                ],
        aggregates: {
            count: 650
        }
    };

    // Create a view model directly from the data which we will update
    var vm = ko.mapping.fromJS(data);

    // Add a computed value to calculate percentage
    vm.values().forEach(function (d) {
        d.percentage = ko.computed(function () {
            return d.count() / vm.aggregates.count() * 100;
        });
    });
    
    // Create a 
    vm.allValues = ko.computed(function() {
        var values = [];
        var count = 0;
        var total = vm.aggregates.count();
        
        debugger;
        
        // Add each of these results into those that will be returned
        vm.values().forEach(function(d) { 
           values.push(d); 
            count += d.count();
        });
        
        // Create an other category for everything else
        values.push({
            text: ko.observable("Other"),
            count: ko.observable(total - count),
            percentage: ko.observable((total - count) / total * 100),
            color: ko.observable("#ff0000")
        });
        
        return values;
    });

    return vm;
})();

ko.applyBindings(vm);

setTimeout(function() {
   vm.values()[0].count(90);
    vm.values()[1].count(40);
    vm.values()[2].count(35);
    vm.aggregates.count(3550);
}, 3000);
body {
    background: rgb(40, 40, 40);
}
.spacer {
    height: 230px;
}
.cards {
    float: right;
}
/* Small Card */
 .card {
    margin-bottom: 3px;
    background: white;
    border-radius: 3px;
    width:398px;
    float: right;
    clear: both;
    min-height: 100px;
    padding: 10px 5px 15px 5px;
    font-family:'Open Sans', Arial, sans-serif;
}
.title {
    color: rgb(105, 161, 36);
    font-size: 16px;
}
.states {
    padding-top: 10px;
}
.state {
    font-size: 12px;
    color: rgb(67, 88, 98);
    padding: 0px 5px 2px 5px;
    clear: both;
}
.circle {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    float: left;
    margin: 1px 5px 5px 0px;
}
.value {
    float: right;
}
.graph {
    padding: 10px 5px 0px 5px;
}
.bar {
    float: left;
    height: 10px;
    -webkit-transition: width 10s;
    transition: width 10s;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.js"></script>
<div class="card">
    <div class="content">
        <div class="graph" data-bind="foreach: allValues">
            <div class="bar" data-bind="style: { background: color, width: percentage() + '%' }"/>
        </div>
    </div>
</div>

回答1:

As the first 3 are based on object references that don't change, knockout is preserving the actual <div> that's been rendered.

For the final bar, each time allValues is evaluated, it's pushing a brand new object into the returned array. I would assume that since knockout sees that as a new object, it re-renders the div from scratch, rather than updating existing bindings.

You'll need to rework your model slightly to hold an actual object for that final value so that you can then update the observables on it in the same way.

Here's a fixed version using a static object for the "other" value:

var vm = (function generateModel() {
    var data = {
        name: "Sign-off",
        id: "XX",
        values: [{ text: "Signed-off", count: 150, color: "#5fb5cc" }, 
                 { text: "Submitted", count: 90, color: "#75d181" }, 
                 { text: "Not Submitted", count: 75, color: "#f8a25b" }
                ],
        aggregates: {
            count: 650
        }
    };

    // Create a view model directly from the data which we will update
    var vm = ko.mapping.fromJS(data);

    // Add a computed value to calculate percentage
    vm.values().forEach(function (d) {
        d.percentage = ko.computed(function () {
            return d.count() / vm.aggregates.count() * 100;
        });
    });
    
    //Create a static "others" object
    vm.other = {
        text: ko.observable("Other"),
        count: ko.computed(function() { 
            var total = vm.aggregates.count();
            var count = 0;
            vm.values().forEach(function(d) { count += d.count(); });
            return total - count;
        }),
        percentage: ko.computed(function(d, b) {
            var total = vm.aggregates.count();
            var count = 0;
            vm.values().forEach(function(d) { count += d.count(); });
            return (total - count) / total * 100;
        }),
        color: ko.observable("#ff0000")
    };
  
    // Create a 
    vm.allValues = ko.computed(function() {
        var values = [];
        var count = 0;
        var total = vm.aggregates.count();
        
        debugger;
        
        // Add each of these results into those that will be returned
        vm.values().forEach(function(d) { 
           values.push(d); 
            count += d.count();
        });
              
        // and push static object in instead of creating a new one
        values.push(vm.other);
        
        return values;
    });

    return vm;
})();

ko.applyBindings(vm);

setTimeout(function() {
   vm.values()[0].count(90);
    vm.values()[1].count(40);
    vm.values()[2].count(35);
    vm.aggregates.count(3550);
}, 3000);
body {
    background: rgb(40, 40, 40);
}
.spacer {
    height: 230px;
}
.cards {
    float: right;
}
/* Small Card */
 .card {
    margin-bottom: 3px;
    background: white;
    border-radius: 3px;
    width:398px;
    float: right;
    clear: both;
    min-height: 100px;
    padding: 10px 5px 15px 5px;
    font-family:'Open Sans', Arial, sans-serif;
}
.title {
    color: rgb(105, 161, 36);
    font-size: 16px;
}
.states {
    padding-top: 10px;
}
.state {
    font-size: 12px;
    color: rgb(67, 88, 98);
    padding: 0px 5px 2px 5px;
    clear: both;
}
.circle {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    float: left;
    margin: 1px 5px 5px 0px;
}
.value {
    float: right;
}
.graph {
    padding: 10px 5px 0px 5px;
}
.bar {
    float: left;
    height: 10px;
    -webkit-transition: width 10s;
    transition: width 10s;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.js"></script>
<div class="card">
    <div class="content">
        <div class="graph" data-bind="foreach: allValues">
            <div class="bar" data-bind="style: { background: color, width: percentage() + '%' }"/>
        </div>
    </div>
</div>