2
\$\begingroup\$

I had a project with repetitive HTML, JS code blocks in which should be ten fields (A, B, C...) with certain functionality. Three fields are already implemented, but I don't want to copy and paste code blocks. I was able to solve some of the repetitions, but majority of that code I wasn't able to fix. I am not sure what is even possible to do in that regard according to app logic. Otherwise, the app works well. I would appreciate any help, or anything to point me out to the right direction. Please refer to full working code example: https://codesandbox.io/s/black-sound-rseqb.

Task requirements:
Create a Vue.js application with two pages.

  1. First page should be on url '/'

    • On load this page should show 10 fields marked as A, B, C, D .... with initial value 3.
    • After page load, every 2 seconds all field values should be changed randomly. Change is randomly calculated as a number between 1 and 2 (1.45, 1.05...), with a separate random sign (-, +).
    • When adding the change to the previous value you should show an arrow pointing up or down, depending on the change sign (arrow down is for -, arrow up is for +).
    • Under each field there should be a toggle button to disable/enable the change on that field.
  2. Second page should be on url '/statistics'

    • This page should show change statistics for all 10 fields.
    • Chart should show value changes in time.
  3. When going from '/' to '/statistics' all the changing should be paused, and on returning back it should be resumed.

You can choose any libraries you want

How can DRY principles be applied to this code?

TableFields.vue

<template>
  <div>
    <div class="wrapper" v-for="(field, index) in fields" :key="index">
      <table>
        <tr>
          <th>{{ field }}</th>
          <td class="sign">{{ randomSign[index] }}</td>
          <td>{{ initialValues[index].toFixed(2) }}</td>
          <td v-show="randomSign[index] == '+'">&#x2B06;</td>
          <td v-show="randomSign[index] == '-'">&#x2B07;</td>
        </tr>
      </table>

      <button
        @click="toggleInterval(field)"
        v-if="field === 'A'"
        :class="[startStopA ? 'button-start' : 'button-stop']"
      >
        <span v-show="startStopA">Stop</span>
        <span v-show="!startStopA">Start</span>
      </button>

      <button
        @click="toggleInterval(field)"
        v-if="field === 'B'"
        :class="[startStopB ? 'button-start' : 'button-stop']"
      >
        <span v-show="startStopB">Stop</span>
        <span v-show="!startStopB">Start</span>
      </button>

      <button
        @click="toggleInterval(field)"
        v-if="field === 'C'"
        :class="[startStopC ? 'button-start' : 'button-stop']"
      >
        <span v-show="startStopC">Stop</span>
        <span v-show="!startStopC">Start</span>
      </button>
    </div>
  </div>
</template>

<script>
import store from "../store";
export default {
  name: "TableFields",
  data() {
    return {
      timer: [undefined, undefined, undefined],
      fields: ["A", "B", "C"],
      startStopA: true,
      startStopB: true,
      startStopC: true,
      initialValueA: 3,
      initialValueB: 3,
      initialValueC: 3,
      randomNumbers: [],
      randomSign: ["+", "+", "+"],
      signs: ["+", "-"],
      changes: store.changes
    };
  },
  computed: {
    initialValues() {
      const array = [
        this.initialValueA,
        this.initialValueB,
        this.initialValueC
      ];
      return array;
    }
  },
  methods: {
    firstObjects() {
      // creates first objects A, B, C...
      for (let i = 0; i < this.fields.length; i++) {
        const date = new Date();

        const obj = {};
        obj.field = this.fields[i];
        obj.value = Number((Math.random() * 1 + 1).toFixed(2));
        obj.indicator = this.signs[
          Math.floor(Math.random() * this.signs.length)
        ];
        obj.time = date.toLocaleTimeString();

        this.changes.push({ ...obj });
        this.$emit("update:changes", [...this.changes]);
      }
    },
    replaceNumbersArray() {
      // replace random A, B, C... numbers at time interval
      const A = Number((Math.random() * 1 + 1).toFixed(2)); // first number A
      const B = Number((Math.random() * 1 + 1).toFixed(2)); // first number B
      const C = Number((Math.random() * 1 + 1).toFixed(2)); // first number C
      this.randomNumbers.splice(0, 3, A, B, C);
    },
    toggleInterval(field) {
      // button toggle
      if (field === "A") {
        this.startStopA = !this.startStopA;
        if (this.startStopA) {
          this.timer[0] = setInterval(() => {
            this.calculations("A");
          }, 2000);
        } else {
          clearInterval(this.timer[0]);
        }
      }
      if (field === "B") {
        this.startStopB = !this.startStopB;
        if (this.startStopB) {
          this.timer[1] = setInterval(() => {
            this.calculations("B");
          }, 2000);
        } else {
          clearInterval(this.timer[1]);
        }
      }
      if (field === "C") {
        this.startStopC = !this.startStopC;
        if (this.startStopC) {
          this.timer[2] = setInterval(() => {
            this.calculations("C");
          }, 2000);
        } else {
          clearInterval(this.timer[2]);
        }
      }
    },
    calculations(field) {
      this.fields.forEach((value, index) => {
        if (field === value) {
          this.randomSign[index] = this.signs[
            Math.floor(Math.random() * this.signs.length)
          ];

          const date = new Date();
          const newChange = [{}, {}, {}];

          newChange[index].field = field;
          newChange[index].indicator = this.randomSign[index];
          newChange[index].value = this.randomNumbers[index];
          newChange[index].time = date.toLocaleTimeString();

          this.changes[index].push(newChange[index]);
          this.$emit("update:changes[index]", [...this.changes[index]]);
        }
      });

      if (field === "A") {
        this.randomSign[0] === "+"
          ? (this.initialValueA += this.randomNumbers[0])
          : (this.initialValueA -= this.randomNumbers[0]);
      }

      if (field === "B") {
        this.randomSign[1] === "+"
          ? (this.initialValueB += this.randomNumbers[1])
          : (this.initialValueB -= this.randomNumbers[1]);
      }

      if (field === "C") {
        this.randomSign[2] === "+"
          ? (this.initialValueC += this.randomNumbers[2])
          : (this.initialValueC -= this.randomNumbers[2]);
      }
    }
  },
  beforeUpdate() {
    const array = [this.startStopA, this.startStopB, this.startStopC];
    array.forEach((value, index) => {
      if (!value) {
        clearInterval(this.timer[index]);
      }
    });
  },
  mounted() {
    if (this.changes === []) {
      this.firstObjects();
    }
    setInterval(this.replaceNumbersArray, 2000);

    this.initialValueA = this.$root.initialValueA || 3;
    this.initialValueB = this.$root.initialValueB || 3;
    this.initialValueC = this.$root.initialValueC || 3;

    this.fields.forEach((value, index) => {
      this.timer[index] = setInterval(() => {
        this.calculations(value);
      }, 2000);
    });

    this.startStopA = !this.$root.startStopA || !this.startStopA;
    this.startStopB = !this.$root.startStopB || !this.startStopB;
    this.startStopC = !this.$root.startStopC || !this.startStopC;
  },
  beforeDestroy() {
    this.$root.initialValueA = this.initialValueA;
    this.$root.initialValueB = this.initialValueB;
    this.$root.initialValueC = this.initialValueC;

    this.$root.startStopA = !this.startStopA;
    this.$root.startStopB = !this.startStopB;
    this.$root.startStopC = !this.startStopC;

    this.timer.forEach(value => {
      clearInterval(value);
    });
  }
};
</script>

Statistics.vue

<template>
  <div class="statistics">
    <table v-for="(field, index) in fields" :key="index">
      <tr>
        <th>Field</th>
        <th>Value</th>
        <th>+/-</th>
        <th>Time</th>
      </tr>
      <tr v-for="(change, index) in changes[index]" :key="index">
        <td>{{ change.field }}</td>
        <td>{{ change.value }}</td>
        <td v-show="change.indicator == '+'">&#x2B06;</td>
        <td v-show="change.indicator == '-'">&#x2B07;</td>
        <td>{{ change.time }}</td>
      </tr>
    </table>
  </div>
</template>

<script>
import store from "../store";
export default {
  name: "Statistics",
  data() {
    return {
      fields: ["A", "B", "C"],
      changes: store.changes
    };
  },
  mounted() {
  }
};
</script>

App.vue

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link>&nbsp;|
      <router-link to="/statistics">Statistics</router-link>
    </div>
    <router-view :changes.sync="changes" />
  </div>
</template>

<script>
import store from "./store";
export default {
  name: "App",
  data() {
    return {
      changes: store.changes
    };
  },
  mounted() {
    for (let i = 0; i < 3; i++) {
      this.changes.push([]);
    }
  }
};
</script>

store.js

import Vue from "vue";

export default Vue.observable({
  changes: []
});
\$\endgroup\$
0

1 Answer 1

1
\$\begingroup\$

Functionality

Good job satisfying the requirements! Aside from the obvious repetition you asked about, the code seems like a good start. It uses const and let appropriately.

Repetition

You asked:

How can DRY principles be applied to this code?

You have a good start using components - e.g. TableFields but keep going with that approach: make a component for things like each field in the loop.

After you make a component, e.g. TableField (Hint: which has one table and one button in the template) And import it into TableFields you can replace

<div class="wrapper" v-for="(field, index) in fields" :key="index">

With the component in the v-for:

<table-field v-for="(field, index) in fields" :key="index" :field="field">
</table-field>

Then each instance of that component can have its own properties like the index and field name, as well as data properties like the value, the sign, whether the timer is started/stopped, etc. This would allow for the removal of all the conditional logic to check the name of the field, etc.

You'd have to likely move the signs array elsewhere - e.g. declare it as a constant in the field component, declare it as a constant property on the App component (and then import the App in the component), or something similar.

I question whether randomNumbers really needs to be maintained... Unless I am missing something, a field component could just generate a random number when necessary...

Maybe you recently read it but in case not, familiarize yourself with the VueJS documentation for v-for with a components.


One micro-optimization I see is in the calculations method:

this.randomSign[0] === "+"
      ? (this.initialValueA += this.randomNumbers[0])
      : (this.initialValueA -= this.randomNumbers[0]);

Instead of having the ternary statement wrap the entire line, the assignment operation could be moved out to the start of the statement:

this.initialValueA += (this.randomSign[0] === "+"
      ?  this.randomNumbers[0])
      : -1 * this.randomNumbers[0]);
\$\endgroup\$
5
  • \$\begingroup\$ If I can follow, you suggest that for every one of ten fields I create a separate component, and data like signs I should declare "globally", so that every component has access to it? \$\endgroup\$ May 14, 2020 at 11:06
  • \$\begingroup\$ Well you would make one component - e.g. in a file named TableField.vue and import that into TableFields. See my updated answer above where I expanded that section a bit. Then each instance of TableField would need to access what is currently in signs So that could be declared in the new component... \$\endgroup\$ May 14, 2020 at 12:56
  • \$\begingroup\$ I think I get the point. I will try that approach in the coming days. Thank you very much for the effort and your time. \$\endgroup\$ May 15, 2020 at 7:02
  • \$\begingroup\$ I've created another component and everything works well except dynamic class on button click (when it's stooped(false)) and you go to statistics page and back to home page. I was able to preserve the class state, but for all the buttons at once, not just active one. It seems impossible to me, to change startStopA, startStopB... as well as initialValueA, initialValueB... and other variables (with same values), with only one variable. There must be separate values for all the tables (A, B, C...) in order for everything to work properly. \$\endgroup\$ May 19, 2020 at 15:39
  • \$\begingroup\$ I already tried with loops and objects instead of variables, but that did't work. So, I'm still not sure is it possible to DRY this code one way or another!? Here is the working code with new component and proposed approach: https://codesandbox.io/s/goofy-feynman-ygrgm \$\endgroup\$ May 19, 2020 at 15:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.