I'm writing an application using EmberJS 2.4. I have the following service that
generates objects of objects of random data:
export default Ember.Service.extend({
crits: {},
loadCrits() {
var newCrits = {};
var max = Math.random() * 10;
for(var i=0; i<max; i++) {
var crit = {};
var num = Math.random() * 5;
for(var j=0; j<num; j++) {
crit["data"+j] = j;
}
newCrits["crit"+i] = crit;
}
this.set("crits", newCrits);
}
});
I have a Component that displays the data in a table. For each key-value pair of each nested object, I have a button so that the user can change the value:
<table>
{{#each-in critService.crits as |key crit|}}
<tr>
<td>key</td>
<td><ul>
{{#each-in crit as |k v|}}
<li>
{{k}} = {{v}}
<button {{action "modifyCrit" crit k}}>change</button>
</li>
{{/each-in}}
</ul></td>
</tr>
{{/each-in}}
</table>
The component handles the action thusly:
export default Ember.Component.extend({
critService: Ember.inject.service(),
actions: {
modifyCrit(crit, k) {
Ember.set(crit, k, "new value");
}
}
});
The problem is that the view is not updated. If I understand correctly, this is because Ember doesn't know how to link the "crit" nested object with the "critService" from which is came. There is thus no event/observer triggered that would update the view.
How can I modify this so that when the user clicks the button, the view is updated with the new value?
You need to change data structure. I provide you a twiddle. Please check out this twiddle
Related
So I am trying to make a flashcards website, where users can add, edit, and delete flashcards. There are two cards - front and back. The user can already add words, but cannot edit or delete them. For the purposes of this question I will use an example array:
var flashcards = [["Uomo", "Man"],["Donna", "Woman"],["Ragazzo", "Boy"]]
But I would like a more user-friendly way to edit the flashcards, preferably using a table like this:
<table>
<tr>
<th>Front</th>
<th>Back</th>
</tr>
<tr>
<td><input type="text" name="flashcard" value="Uomo"> </td>
<td><input type="text" name="flashcard" value="Man"></td>
</tr>
<tr>
<td><input type="text" name="flashcard" value="Donna"></td>
<td><input type="text" name="flashcard" value="Woman"></td>
</tr>
<tr>
<td><input type="text" name="flashcard" value="Ragazzo"></td>
<td><input type="text" name="flashcard" value="Boy"></td>
</tr>
</table>
<button type="button">Add more</button>
<br>
<button type="button">Save changes</button>
So they can update their flashcards editing the input fields, or clicking "add more" and it creating a new row. Clicking "save changes" updates the array to the content of the table.
I don't mind it not being a HTML table per se, but something that is easy to edit for the user.
I just cannot figure out the best way to approach this. Any advice?
I already recommended VueJS - it really is a pretty good tool for this problem. Regardless, I have typed up a basic solution using vanilla JavaScript. For the editing part it uses the contenteditable HTML attribute which allows the end-user to double click an element and change it's textContent.
The html display is basic so you can change it however to fit your needs
<div id=style="width: 100%;">
<ul id="table" style="list-style-type: none; display: inline-block;">
</ul>
</div>
<script>
var flashcards = [["Uomo", "Man"],["Donna", "Woman"],["Ragazzo", "Boy"]];
var displayedCard = []; //Using a parallel array to keep track of which side is shown
for(var i = 0; i < flashcards.length; i++){
displayedCard.push(0);
}
function renderFlashcardTable(){ //This will do the initial rendering of the table
let ulTable = document.getElementById("table");
for(var i = 0; i < flashcards.length; i++){
let card = flashcards[i];
let indexOfSideShown = displayedCard[i];
let li = document.createElement("li");
let cardValueSpan = document.createElement("span");
cardValueSpan.innerHTML = card[indexOfSideShown]; //Get the value of the side of the card that is shown
cardValueSpan.setAttribute("contenteditable", "true");
cardValueSpan.oninput = function(e){ //This method gets called when the user de-selects the element they have been editing
let li = this.parentElement;
let sideIndex = parseInt(li.getAttribute("side-index"));
card[sideIndex] = this.textContent;
}
li.appendChild(cardValueSpan);
li.appendChild(getFlipSidesButton(li));
li.setAttribute("side-index", indexOfSideShown);
li.setAttribute("card-index", i);
ulTable.appendChild(li);
}
}
function getFlipSidesButton(listItem){//This is generated for each card and when clicked it "flips the switch"
let btn = document.createElement("button");
btn.innerHTML = "Flip card";
btn.onclick = function(e){
let card = flashcards[listItem.getAttribute("card-index")];
let index = parseInt(listItem.getAttribute("side-index"));
let nextSide = (index == 1) ? 0 : 1;
listItem.setAttribute("side-index", nextSide);
listItem.children[0].innerHTML = card[nextSide];
}
return btn;
}
renderFlashcardTable();
</script>
I've put together a working sample using pure native javascript with a data-driven approach. You can have a look and understand the way how data should be manipulated and worked with in large Js application.
The point here is to isolate the data and logic as much as possible.
Hope this help.
Codepen: https://codepen.io/DieByMacro/pen/rgQBPZ
(function() {
/**
* Default value for Front and Back
*/
const DEFAULT = {
front: '',
back: '',
}
/**
* Class Card: using for holding value of front and back.
* As well as having `update` method to handle new value
* from input itself.
*/
class Card {
constructor({front, back, id} = {}) {
this.front = front || DEFAULT.front;
this.back = back || DEFAULT.back;
this.id = id;
}
update = (side, value) => this[side] = value;
}
/**
* Table Class: handle rendering data and update new value
* according to the instance of Card.
*/
class Table {
constructor() {
this.init();
}
/** Render basic table and heading of table */
init = () => {
const table = document.querySelector('#table');
const thead = document.createElement('tr');
const theadContent = this.renderRow('th', thead, { front: 'Front', back: 'Back' })
const tbody = document.createElement('tbody');
table.appendChild(theadContent);
table.appendChild(tbody);
}
/** Handling add event from Clicking on Add button
* Note the `update: updateFnc` line, this means we will refer
* `.update()` method of Card instance with `updateFnc()`, this is
* used for update value Card instance itself.
*/
add = ({front, back, id, update: updateFnc }) => {
const tbody = document.querySelector('#table tbody');
const row = document.createElement('tr');
const rowWithInput = this.renderRow('td', row, {front, back, id, updateFnc});
tbody.appendChild(rowWithInput);
}
renderInput = (side, id, fnc) => {
const input = document.createElement('input');
input.setAttribute('type','text');
input.setAttribute('name',`${side}-value-${id}`)
input.addEventListener('change', e => this.onInputChangeHandler(e, side, fnc));
return input;
}
renderRow = ( tag, parent, { front, back, id, updateFnc }) => {
const frontColumn = document.createElement( tag );
const backColumn = document.createElement( tag );
/** Conditionally rendering based on `tag` type */
if ( tag === 'th') {
frontColumn.innerText = front;
backColumn.innerText = back;
}else {
/** Create two new inputs for each Card instance. Each handle
* each side (front, back)
*/
const inputFront = this.renderInput('front', id, updateFnc);
const inputBack = this.renderInput('back', id, updateFnc);
frontColumn.appendChild(inputFront);
backColumn.appendChild(inputBack);
}
parent.appendChild(frontColumn)
parent.appendChild(backColumn)
return parent;
}
/** Getting new value and run `.update()` method of Card, now referred as `fnc` */
onInputChangeHandler = (event, side, fnc) => {
fnc(side, event.target.value);
}
}
class App {
/**
* Holding cards data
* Notice this is an object, not an array
* Working with react for a while, I see most of the times data as an object works best when it comes to cRUD, this means we don't have to iterate through the array to find the specific element/item to do the work. This saves a lot of time
*/
cards = {};
constructor(){
this.domTable = new Table();
this.domAdd = document.querySelector('#btn-add');
this.domResult = document.querySelector('#btn-result');
this.domAdd.addEventListener('click', this.onClickAddHandler );
this.domResult.addEventListener('click', this.onClickResultHandler );
}
onClickAddHandler = () => {
const id = uuid();
const newCard = new Card({id});
this.cards[id] = newCard;
this.domTable.add(newCard)
}
onClickResultHandler = () => {
/**
* Using `for ... in ` with object. Or you can use 3rd party like lodash for iteration
*/
for (const id in this.cards) {
console.log({
front: this.cards[id].front,
back: this.cards[id].back,
id: this.cards[id].id
});
}
};
}
// Start the application
const app = new App();
})();
<script src="https://cdnjs.cloudflare.com/ajax/libs/node-uuid/1.4.8/uuid.min.js"></script>
<div id="table"></div>
<button id="btn-add">Add</button>
<button id="btn-result">Result</button>
i think you can use In-Place Editing System and there's a good tutorial i found
Create an In-Place Editing System
I am creating a Vue app, where a list of jobs will be displayed and this data is coming from a JSON object. In this app I also am adding filtering functionality as well as pagination. So what I have so far is:
<div id="app" v-cloak>
<h2>Location</h2>
<select v-model="selectedLocation" v-on:change="setPages">
<option value="">Any</option>
<option v-for="location in locations" v-bind:value="location" >{{ location }}</option>
</select>
<div v-for="job in jobs">
<a v-bind:href="'/job-details-page/?entity=' + job.id"><h2>{{ job.title }}</h2></a>
<div v-if="job.customText12"><strong>Location:</strong> {{ job.customText12 }}</div>
</div>
<div class="paginationBtns">
<button type="button" v-if="page != 1" v-on:click="page--">Prev</button>
<button type="button" v-for="pageNumber in pages.slice(page-1, page+5)" v-on:click="page = pageNumber"> {{pageNumber}} </button>
<button type="button" v-if="page < pages.length" v-on:click="page++">Next</button>
</div>
<script>
var json = <?php echo getBhQuery('search','JobOrder','isOpen:true','id,title,categories,dateAdded,externalCategoryID,employmentType,customText12', null, 200, '-dateAdded');?>;
json = JSON.parse(json);
var jsonData = json.data;
var app = new Vue({
el: '#app',
data() {
return {
//assigning the jobs JSON data to this variable
jobs: jsonData,
locations: ['Chicago', 'Philly', 'Baltimore'],
//Used to filter based on selected filter options
selectedLocation: '',
page: 1,
perPage: 10,
pages: [],
}
},
methods: {
setPages () {
this.pages = [];
let numberOfPages = Math.ceil(this.jobs.length / this.perPage);
for (let i = 1; i <= numberOfPages; i++) {
this.pages.push(i);
}
},
paginate (jobs) {
let page = this.page;
let perPage = this.perPage;
let from = (page * perPage) - perPage;
let to = (page * perPage);
return jobs.slice(from, to);
},
}
watch: {
jobs () {
this.setPages();
}
},
})
computed: {
filteredJobs: function(){
var filteredList = this.jobs.filter(el=> {
return el.customText12.toUpperCase().match(this.selectedLocation.toUpperCase())
});
return this.paginate(filteredList);
}
}
</script>
So the issue I am running into is that I want the amount of pages to change when the user filters the list using the select input. The list itself changes, but the amount of pages does not, and there ends up being a ton of empty pages once you get past a certain point.
I believe the reason why this is happening is the amount of pages is being set based on the length of the jobs data object. Since that never changes the amount of pages stays the same as well. What I need to happen is once the setPages method is ran it needs to empty the pages data array, then look at the filteredJobs object and find the length of that instead of the base jobs object.
The filteredJobs filtering is a computed property and I am not sure how to grab the length of the object once it has been filtered.
EDIT: Okay so I added this into the setPages method:
let numberOfPages = Math.ceil(this.filteredJobs.length / this.perPage);
instead of
let numberOfPages = Math.ceil(this.jobs.length / this.perPage);
and I found out it is actually grabbing the length of filteredJobs, but since I am running the paginate method on that computed property, it is saying there is only 10 items in the filteredJobs array currently and will only add one pagination page. So grabbing the length of filteredJobs may not be the best route for this. Possibly setting a data variable to equal the filtered jobs object may be better and grab the length of that.
Here's a simplified version of my code :
<template>
/* ----------------------------------------------------------
* Displays a list of templates, #click, select the template
/* ----------------------------------------------------------
<ul>
<li
v-for="form in forms.forms"
#click="selectTemplate(form)"
:key="form.id"
:class="{selected: templateSelected == form}">
<h4>{{ form.name }}</h4>
<p>{{ form.description }}</p>
</li>
</ul>
/* --------------------------------------------------------
* Displays the "Editable fields" of the selected template
/* --------------------------------------------------------
<div class="form-group" v-for="(editableField, index) in editableFields" :key="editableField.id">
<input
type="text"
class="appfield appfield-block data-to-document"
:id="'item_'+index"
:name="editableField.tag"
v-model="editableField.value">
</div>
</template>
<script>
export default {
data: function () {
return {
editableFields: [],
}
},
methods: {
selectTemplate: function (form) {
/* ------------------
* My problem is here
*/ ------------------
for (let i = 0; i < form.editable_fields.length; i++) {
this.editableFields.push(form.editable_fields[i]);
}
}
}
}
</script>
Basically I want to update the array EditableFields each time the user clicks on a template. My problem is that Vuejs does not update the display because the detection is not triggered. I've read the documentation here which advise to either $set the array or use Array instance methods only such as splice and push.
The code above (with push) works but the array is never emptied and therefore, "editable fields" keep pilling up, which is not a behavior I desire.
In order to empty the array before filling it again with fresh data, I tried several things with no luck :
this.editableFields.splice(0, this.editableFields.length);
for (let i = 0; i < form.editable_fields.length; i++) {
this.editableFields.push(form.editable_fields[i]);
}
==> Does not update the display
for (let i = 0; i < form.editable_fields.length; i++) {
this.$set(this.editableFields, i, form.editable_fields[i]);
}
==> Does not update the display
this.editableFields = form.editable_fields;
==> Does not update the display
Something I haven't tried yet is setting a whole new array with the fresh data but I can't understand how I can put that in place since I want the user to be able to click (and change the template selection) more than once.
I banged my head on that problem for a few hours now, I'd appreciate any help.
Thank you in advance :) !
I've got no problem using splice + push. The reactivity should be triggered normally as described in the link you provided.
See my code sample:
new Vue({
el: '#app',
data: function() {
return {
forms: {
forms: [{
id: 'form1',
editable_fields: [{
id: 'form1_field1',
value: 'form1_field1_value'
},
{
id: 'form1_field2',
value: 'form1_field2_value'
}
]
},
{
id: 'form2',
editable_fields: [{
id: 'form2_field1',
value: 'form2_field1_value'
},
{
id: 'form2_field2',
value: 'form2_field2_value'
}
]
}
]
},
editableFields: []
}
},
methods: {
selectTemplate(form) {
this.editableFields.splice(0, this.editableFields.length);
for (let i = 0; i < form.editable_fields.length; i++) {
this.editableFields.push(form.editable_fields[i]);
}
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<div id="app">
<ul>
<li v-for="form in forms.forms"
#click="selectTemplate(form)"
:key="form.id">
<h4>{{ form.id }}</h4>
</li>
</ul>
<div class="form-group"
v-for="(editableField, index) in editableFields"
:key="editableField.id">
{{ editableField.id }}:
<input type="text" v-model="editableField.value">
</div>
</div>
Problem solved... Another remote part of the code was in fact, causing the problem.
For future reference, this solution is the correct one :
this.editableFields.splice(0, this.editableFields.length);
for (let i = 0; i < form.editable_fields.length; i++) {
this.editableFields.push(form.editable_fields[i]);
}
Using only Array instance methods is the way to go with Vuejs.
I have a condition that needs to be checked in my view: If any user in the user list has the same name as another user, I want to display their age.
Something like
<div ng-repeat="user in userList track by $index">
<span class="fa fa-check" ng-if="user.isSelected"></span>{{user.firstName}} <small ng-if="true">{{'AGE' | translate}} {{user.age}}</small>
</div>
except I'm missing the correct conditional
You should probably run some code in your controller that adds a flag to the user object to indicate whether or not he/she has a name that is shared by another user.
You want to minimize the amount of logic there is inside of an ng-repeat because that logic will run for every item in the ng-repeat each $digest.
I would do something like this:
controller
var currUser, tempUser;
for (var i = 0; i < $scope.userList.length; i++) {
currUser = $scope.userList[i];
for (var j = 0; j < $scope.userList.length; j++) {
if (i === j) continue;
var tempUser = $scope.userList[j];
if (currUser.firstName === tempUser.firstName) {
currUser.showAge = true;
}
}
}
html
ng-if='user.showAge'
Edit: actually, you probably won't want to do this in the controller. If you do, it'll run every time your controller loads. You only need this to happen once. To know where this should happen, I'd have to see more code, but I'd think that it should happen when a user is added.
You can simulate a hashmap key/value, and check if your map already get the property name. Moreover, you can add a show property for each objects in your $scope.userList
Controller
(function(){
function Controller($scope) {
var map = {};
$scope.userList = [{
name:'toto',
age: 20,
show: false
}, {
name:'titi',
age: 22,
show: false
}, {
name: 'toto',
age: 22,
show: false
}];
$scope.userList.forEach(function(elm, index){
//if the key elm.name exist in my map
if (map.hasOwnProperty(elm.name)){
//Push the curent index of the userList array at the key elm.name of my map
map[elm.name].push(index);
//For all index at the key elm.name
map[elm.name].forEach(function(value){
//Access to object into userList array with the index
//And set property show to true
$scope.userList[value].show = true;
});
} else {
//create a key elm.name with an array of index as value
map[elm.name] = [index];
}
});
}
angular
.module('app', [])
.controller('ctrl', Controller);
})();
HTML
<body ng-app="app" ng-controller="ctrl">
<div ng-repeat="user in userList track by $index">
<span class="fa fa-check"></span>{{user.name}} <small ng-if="user.show">{{'AGE'}} {{user.age}}</small>
</div>
</body>
I've changed the meteor example leaderboard into a voting app. I have some documents with an array and in this array there are 6 values. The sum of this 6 values works fine, but not updating and showing the values in my app.
The values are only updating, if I click on them. The problem is, that I get the booknames (it's a voting app for books) from the "selected_books" variable (previously selected_players), but I don't know how I can get the book names.
By the way: _id are the book names.
I will give you some code snippets and hope, somebody have a solution.
This is a document from my database:
{
_id: "A Dance With Dragons: Part 1",
isbn: 9780007466061,
flag: 20130901,
score20130714: [1,2,3,4,5,0],
}
parts of my html file:
<template name="voting">
...
<div class="span5">
{{#each books}}
{{> book}}
{{/each}}
</div>
...
</template>
<template name="book">
<div class="book {{selected}}">
<span class="name">{{_id}}</span>
<span class="totalscore">{{totalscore}}</span>
</div>
</template>
and parts of my Javascript file:
Template.voting.books = function () {
var total = 0;
var book = Session.get("selected_book");
Books.find({_id:book}).map(function(doc) {
for (i=0; i<6; i++) {
total += parseInt(doc.score20130714[i], 10);
}
});
Books.update({_id:book}, {$set: {totalscore: total}});
return Books.find({flag: 20130901}, {sort: {totalscore: -1, _id: 1}});
};
Thanks in advance
Don't update data in the helper where you fetch it! Use a second helper for aggregating information or a transform for modifying data items. Example:
Template.voting.books = function() {
return Books.find({}, {sort: {totalscore: -1, _id: 1}});
};
Template.books.totalscore = function() {
var total = 0;
for(var i=0; i<6; i++) {
total += this.score[i];
}
return total;
};
As a side note, DO NOT USE the construct for (i=0; i<6; i++), it's deadly. Always declare your index variables: for (var i=0; i<6; i++).