aurelia / framework

The Aurelia 1 framework entry point, bringing together all the required sub-modules of Aurelia.
MIT License
11.76k stars 626 forks source link

repeat.for renders same item twice #830

Closed powerbuoy closed 5 years ago

powerbuoy commented 6 years ago

I have a strange issue where when I try to push an empty object to an array that is rendered in my view - the newly pushed element is rendered twice.

The duplicate element is in fact the exact same element as the previous one (if I change the first the second also changes).

I'm unable to reproduce this in a GistRun (perhaps version differences) - but I have managed to reproduce it in a completely new Aurelia CLI project with the following code:

app.js

export class App {
    constructor () {
        this.items = [];
        this.editing = false;
    }

    // Toggle the edit form
    toggleEditing () {
        this.editing = !this.editing;

        // Also add a new item
        if (this.editing) {
            this.items.push({}); // NOTE: This is where things go wrong
        }
    }
}

And this view:

app.html

<template>

    <template if.bind="!editing">
        <h2>${items.length} items!</h2>
        <ul>
            <li repeat.for="item of items">
                ${item.name}
            </li>
        </ul>
        <button click.delegate="toggleEditing()">Edit</button>
    </template>

    <template if.bind="editing">
        <ul>
            <li repeat.for="item of items">
                <input type="text" value.bind="item.name">
            </li>
        </ul>
        <button click.delegate="toggleEditing()">Save</button>
    </template>

</template>

My package.json looks like this (for version info):

package.json

{
  "name": "autest",
  "description": "An Aurelia client application.",
  "version": "0.1.0",
  "repository": {
    "type": "???",
    "url": "???"
  },
  "license": "MIT",
  "dependencies": {
    "aurelia-bootstrapper": "^2.1.1",
    "aurelia-animator-css": "^1.0.2",
    "bluebird": "^3.4.1",
    "requirejs": "^2.3.2",
    "text": "github:requirejs/text#latest"
  },
  "peerDependencies": {},
  "devDependencies": {
    "aurelia-cli": "^0.31.3",
    "aurelia-testing": "^1.0.0-beta.3.0.1",
    "aurelia-tools": "^1.0.0",
    "gulp": "github:gulpjs/gulp#4.0",
    "minimatch": "^3.0.2",
    "through2": "^2.0.1",
    "uglify-js": "^3.0.19",
    "vinyl-fs": "^2.4.3",
    "babel-eslint": "^6.0.4",
    "babel-plugin-syntax-flow": "^6.8.0",
    "babel-plugin-transform-decorators-legacy": "^1.3.4",
    "babel-plugin-transform-es2015-modules-amd": "^6.8.0",
    "babel-plugin-transform-es2015-modules-commonjs": "^6.10.3",
    "babel-plugin-transform-flow-strip-types": "^6.8.0",
    "babel-preset-es2015": "^6.13.2",
    "babel-preset-stage-1": "^6.5.0",
    "babel-polyfill": "^6.9.1",
    "babel-register": "^6.24.0",
    "gulp-babel": "^6.1.2",
    "gulp-eslint": "^2.0.0",
    "gulp-htmlmin": "^3.0.0",
    "html-minifier": "^3.2.3",
    "jasmine-core": "^2.4.1",
    "karma": "^0.13.22",
    "karma-chrome-launcher": "^2.2.0",
    "karma-jasmine": "^1.0.2",
    "karma-sourcemap-loader": "^0.3.7",
    "karma-babel-preprocessor": "^6.0.1",
    "browser-sync": "^2.13.0",
    "connect-history-api-fallback": "^1.2.0",
    "debounce": "^1.0.2",
    "gulp-changed-in-place": "^2.0.3",
    "gulp-plumber": "^1.1.0",
    "gulp-rename": "^1.2.2",
    "gulp-sourcemaps": "^2.0.0-alpha",
    "gulp-notify": "^2.2.0",
    "gulp-watch": "^4.3.11"
  }
}

Maybe it's something obvious I've missed? But it seems like a genuine bug to me.

I'm attaching my example app as well which exhibits the bug and can be run in Firefox directly. Just click "Edit" and two input fields will be shown instead of one - changing one actually changes both.

autest.zip

bigopon commented 6 years ago

I have fixed this issue by instructing Repeat to ignore mutation while processing instance change (main changes can be found at https://github.com/bigopon/templating-resources/blob/d4ff656a44fd2c2fc4d7b5770f6c036723325b60/src/repeat.js#L138-L142). Code:

    this.ignoreMutation = true;
    this.strategy.instanceChanged(this, items);
    this.observerLocator.taskQueue.queueMicroTask(() => {
      this.ignoreMutation = false;
    });

With the above changes, the issue in @powerbuoy 's demo is fixed, demo at https://gist.run/?id=56d85dc0c87214d1f79b5b284c76308a . Though I haven't been able to create a test for it, I built the code and created a branch at https://github.com/bigopon/templating-resources/tree/fix-repeat so folks who need it can use it to workaround if needed.

@EisenbergEffect @jdanyow @fkleuver / all Would love to have some help on the test here (it always got the number of elements right even without the changes)


Edit 1:

Work around the issue temporarily by pasting this in app entry:

import { Repeat } from 'aurelia-templating-resources';

Repeat.prototype.itemsChanged = function() {
  this._unsubscribeCollection();

  if (!this.scope) {
    return;
  }

  var items = this.items;
  this.strategy = this.strategyLocator.getStrategy(items);
  if (!this.strategy) {
    throw new Error('Value for \'' + this.sourceExpression + '\' is non-repeatable');
  }

  if (!this.isOneTime && !this._observeInnerCollection()) {
    this._observeCollection();
  }
  this.ignoreMutation = true;
  this.strategy.instanceChanged(this, items);
  this.observerLocator.taskQueue.queueMicroTask(() => {
    this.ignoreMutation = false;
  })
};

Repeat.prototype.handleCollectionMutated = function(collection, changes) {
  if (!this.collectionObserver) {
    return;
  }
  if (this.ignoreMutation) {
    return;
  }
  this.strategy.instanceMutated(this, collection, changes);
};
EisenbergEffect commented 6 years ago

@bigopon Thanks for looking into this. @jdanyow Can you do a review? This is a critical issue in repeat. If the fix looks good, I'd like to get it out ASAP. Please let me know!

Alexander-Taran commented 6 years ago

@bigopon could it also improve the rendering perf for patial updates?

bigopon commented 6 years ago

@Alexander-Taran That's a nice suggestion. Not sure if ignoring mutation while processing mutation will cause any issue. Will need to investigate a bit

bigopon commented 6 years ago

@Alexander-Taran To answer the main question: I don't think it would have any impact on that.

Alexander-Taran commented 6 years ago

@bigopon this thread: https://github.com/krausest/js-framework-benchmark/pull/376#issuecomment-388396221 last 4-5 comments. Sometimes partial update benchmark produces way more than 1-2 paints.. and the resultant timing is off charts.

fkleuver commented 6 years ago

I just took a look at this, and the problem is not what it seems.

These two things happen within a single CallScope.evaluate call:

The task queue is flushed, then:

Any changes that happen after setting an if.bind to true will be applied twice for this reason - the if renders its children with the latest scope values (that inherently include the changes), then the changes are applied in the next task.

Try switching them around

// two text boxes
this.editing = !this.editing;
if (this.editing) {
    this.items.push({});
}
// one text box
const editing = !this.editing;
if (editing) {
    this.items.push({});
}
this.editing = editing;

The same principle applies (although in different forms) when you're doing different operations on the array within the same evaluation cycle.

The changes are queued onto different tasks when instead they should be either on the same task or consolidated into a single changeset.

I would say this is neither a problem in repeat nor in if, but rather a problem in "abusing" the TaskQueue for preventing infinite loops between change handlers. It solves the problem over here and then causes the same problem later over there. Before long, there would be multiple nested calls to the TaskQueue and the problems get increasingly more difficult to solve.

I think the only proper solution is to find a way to eliminate usages of queueMicroTask in the Aurelia framework, rather than adding more of them. This could be simple, but will be tedious - it means an extra parameter needs to be passed around between every observer and change handler that keeps track of the sources and targets of changes. On the flip side, it would likely be a nice performance improvement..

zefubachs commented 6 years ago

I just started using Aurelia using aspnetcore, aurelia-cli and the todo tutorial: Each time i add a todo to the array it renders another (empty) <li> element after the 'correct' elements of the array.

When i log the amount elements in the array, it shows the correct amount.

Initially i added aurrelia to an existing aspnetcore project but later i reproduced thesame with a simple emtpy aspnetcore project using Visual Studio and then used aurelia-cli using the following properties:

I have no experience with webpack and only little experience with SPA's (used angular1 a while back and tried vuejs) so much of the files created by the cli is like a black box to me. If any info is required, i'll be happy to supply it.

EDIT: I've followed thesame steps on my work workstation and everything works fine, i'm gonna try to run the project of my laptop on my workstation and see if any file in the project is the culprit.

zefubachs commented 6 years ago

Managed to fix it by updating nodejs to the latest version. After that removing the node_modules folder and manually executing npm install.

shunty-gh commented 6 years ago

The same repeat.for issue (duplicating empty elements) is happening in my project even with all the latest aurelia packages (as of 2018-07-10) including the 'ignoreMutation' change. Code had been working fine for many months before updating the binding/templating packages. I'm guessing it is similar to the "Duplicate of aurelia-binding..." issue mentioned above because I'm using aurelia-breeeze and that has references in package.json to aurelia-binding "^1.0.0-rc.1.0.0" (and several other rc1 references). Thus giving me 2 differing copies of aurelia-binding in my webpack bundle. Reassigning the array rather than using .push gets around the issue.

mikeesouth commented 6 years ago

@shunty-gh Have you tried a fresh npm install? Remove node_modules and package-lock.json and then a fresh npm install with the new package versions from your updated package.json. I had the same issue but a clean npm install solved the problem.

shunty-gh commented 6 years ago

...and I can confirm that using a local (non npm) copy of aurelia-breeze and, thus, ignoring the outdated dependencies allows the Array.push approach to work fine. And makes my bundle a little smaller too :-)

shunty-gh commented 6 years ago

@mikeesouth Thanks for that. I had tried deleting node_modules and re-running npm install but I hadn't tried removing package-lock.json too. However, I have now tried and it gives the same erroneous results with repeat.for so I'll stick with the local copy of aurelia-breeze for now.

ljtn commented 6 years ago

This seems to remain unsolved (EDIT: Issue is solved. Had an old version of aurelia-templating-resources.), so here is my workaround in case someone else is stuck here and need a quick fix.

View Model:

<your-element if.bind="!updatingArray" repeat.for="item of items"></your-element>

Data Model:

    this.updatingArray = true;
      setTimeout(() => {
        this.items.push(item);
        this.updatingArray = false;
      },1);

As stated above - it seems like it works as long as the array is re-rendered in its entirety, as it appears to be mutations of the array that triggers the issue.

fkleuver commented 6 years ago

@ljtn could you share your package-lock.json in a gist?

ljtn commented 6 years ago

Sorry, I was using an old version of aurelia-templating-resources (version 1.6.0.) It works now that I updated to 1.7.1.

Mobe91 commented 6 years ago

What is the status on this? I am using aurelia-templating-resources@1.7.1 and the issue still persists for a simple view like:

<ul>
  <li repeat.for="element of elements">
    test
  </li>
</ul>
<button class="btn btn-ordami-green" click.delegate="add()">Add</button>
export class MyView {
  elements = [];
  add() {
    this.elements.push("test");
  }
}

When I click the "Add" button once, 2 list items are produced instead of just one. No if involved here.

EDIT: Turns out that the combination of

is bad. Using aurelia-binding@1.7.3 my issue is resolved.

bigopon commented 6 years ago

@Mobe91 you should also see the warning about duplicated attempt patching Array prototype if it was causing you issue. If you are using webpack, please see https://github.com/aurelia/cli/pull/906

bigopon commented 6 years ago

@EisenbergEffect I think this can be closed

Alexander-Taran commented 5 years ago

Definitely can be closed