Reference element property within another property - javascript

I am trying to access properties (fieldType, value) within an dynamic property (:class)
<div fieldType="favoriteSports"
#click="update_favorite_sports"
value="Soccer"
:class="{selected: sportsForm[fieldType].indexOf(value) != -1 }">
Soccer
</div>
This logs the following error:
Property or method "fieldType" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.
I suppose it is expecting fieldType to be a data property.
How can this be achieved, or is this an anti-pattern?

Using a v-for to fill values exposes the entire object to the template syntax
I'm assuming your object system but it's universal to different structures.
<div
v-for="sport in sports"
:fieldType="sport.type"
#click="update_favorite_sports(sport)"
:value="sport.name"
:class="{selected: sportsForm[sport.type].indexOf(sport.name) != -1 }"
>
Soccer
</div>
Though honestly you no longer need the value and fieldType properties unless you're referencing them somewhere.
That will allow you to use
methods: {
update_favorite_sports (sport) {
// Whatever your code is guessing something like
let index = this.sportsForm[sport.type].indexOf(sport.name)
if(index >= 0) {
this.sportsForm[sport.type].splice(index, 1)
return
}
this.sportsForm[sport.type].push(sport.name)
}
}
So that the div acts like a radio select.

Related

Accessing and modyfing slot's elements in Vue 3

I'm rewriting my MQTT based dashboard from Vue 2 to Vue 3 currently, and can't solve one problem.
The dashboard has many Vue components, which reacts to specific MQTT topics and values, to display current system state. One of them is the mqtt-multi-state component which is declared like below:
// status-page
<mqtt-multi-state subscribe-topic="home/env/sensor/wc/door/status" json-path="state">
<div value="OPEN"><font-awesome-icon icon="door-open"/></div>
<div value="CLOSED"><font-awesome-icon icon="door-closed"/></div>
</mqtt-multi-state>
It contains dwo div elements, with two states that the system sensor (door) can has. These elements are passed to the default slot and are hidden by default via css.
What I want to achieve is to show one of them based on the equality of the value attr in each of them with the current MQTT value. So if the current value is OPEN then the first div show up, when value is CLOSED then the second one appears.
// mqtt-multi-state
<template>
<div class="mqtt-multi-state">
<div>
<slot></slot>
</div>
</div>
</template>
<script>
export default {
methods: {
messageArrived(value){
let states = this.$slots.default() // here are the two divs
for(let i = 0;i < states.length;i++ ){
if(states[i].props.value === value )
{
//states[i].elm.className = "state-active" <- Vue 2 solution using DOM with elm
//states[i].props.class = "state-active"; <- Vue 3, doesn't work, not reactive?
}
else
{
//states[i].props.class = "";
}
}
}
}
}
</script>
I know, this approach is a little bit different but I really like to describe dashboard element in this way in HTML. In the messsageArrive() method I'm iterating over the default slot children elements and if the value match the current value prop I want to show this item, by add a state-active class. But this solution does not work. The VNode is changed (checked in the console).
In Vue 2 I've simply get to the DOM element directly and change it class, but on Vue 3 I can't figure it out, how to get from VNode to the DOM Node. Or there are maybe an other/better way to do that?
Well, many evenings later I came to this solution. Instead of putting all the different states into a default slot, I've created dynamic named slots.
The different state elements (font-awesome icons in this case) go to each slot's template element. Unfortunately I can't pass the mqtt value to the template element itself, because it does not exists inside mqtt-multi-state parent component. Instead, I've passed the mqtt value into the template's first child element. It can be any element, div, p, font-awesome-icon.
// status-page
<mqtt-multi-state :state-count="2" subscribe-topic="home/env/sensor/wc/door/status" json-path="state">
<template #0><font-awesome-icon value="OPEN" icon="door-open"/></template>
<template #1><font-awesome-icon value="CLOSED" icon="door-closed"/></template>
</mqtt-multi-state>
There is also a prop state-count that defines the number of different states. Note the colon before prop name, so the string "2" is a Number type.
The mqtt-multi-state component becomes like this:
// mqtt-multi-state
<template>
<div class="mqtt-multi-state">
<template v-for="(slot, index) in slots">
<slot :name="index" v-if="slot" ></slot>
</template>
</div>
</template>
<script>
export default {
data: function(){
return {
slots: []
}
},
props: {
stateCount: {
default: 2,
type: Number
}
},
methods: {
messageArrived(value){
for(let i = 0; i < this.stateCount; i++ ) {
this.slots[i] = this.$slots[i]?.()[0]?.props?.value === value
}
}
}
}
</script>
In the messageArrived() function the code iterates through all the slots and compares the slot's first child element prop, named value with the current mqtt value received. If the values are equal then the this.slots[i] array value goes true, and the Vue's v-if directive makes corresponding slot visible, otherwise hides the slot.
Named slots names are created by the index value, so to get their content with this.$slot.slotNameRenderFunction() I call the function 0(), 1() etc using the this.$slot[index]() notation (accessing object member by name stored in the variable) with an optional chaining operator ?. - so the whole notation looks a little weird :).

How to render multiple HTML parts with a plain Javascript function

This is a static website with hundreds of pages. I need to render elements like a topnav or a newsletter or a strap of content and changing those contents periodically, from JS.
This is what I tried:
const components = {
compartirEnFlex: `<h4>Newsletter</h4>`,
newsletterEs: `<h4>Compartir</h4>`,
}
const ids = ['newsletterEs', 'compartirEnFlex', 'infoArticulo', 'infoDeLaWebEnFlexIzq']
function renderComponents(objWithComp, idsArr){
return idsArr.map(function(id){
for(let component in objWithComp){
let arrOfIds = Object.keys(objWithComp);
arrOfIds.map(key => key)
if(id === key){
document.getElementById(id).insertAdjacentHTML('afterbegin', objWithComp[id])
}
}
})
}
renderComponents(components, ids);
Each id has its counterpart in the HTML structure. When I do this individually it works. However, I have to handle this in an elegant way (and there is no possibility for a JS framework like React in this project).
Thanks for the help!
When you run your code, you'll see the error Uncaught ReferenceError: key is not defined in the console.
That's because key in if(id === key) is not defined. The line arrOfIds.map(key => key) returns the same exact array as arrOfIds because Array.prototype.map "returns a new array populated with the results of calling a provided function on every element in the calling array."
Here, you don't assign that new array to a variable, so nothing happens. Even if it was, that new array would be a copy of arrOfIds because your mapping function (key) => key returns key for every key -- meaning that the output is the same as the input.
However, that's not an issue here. If I understand your question correctly, then this demo should show an example of what you're trying to accomplish. If that's what you want to achieve, then here's a solution:
First, you don't need to iterate for component in objWithComponent inside idArr -- you're already doing that in the idArr. You don't need the ids array either, because you can get the keys of the components from the components object using Object.keys().
Let's say your HTML looks something like this:
<div>
<div id="newsletterEs"></div>
<div id="compartirEnFlex"></div>
<div id="infoArticulo"></div>
<div id="infoDeLaWebEnFlexIzq"></div>
</div>
Then, using Object.keys(components) to get an array of the ids of the components that you have, you can map those to HTML tags. In fact, map is not necessary here because map returns a new array, and unless you need that array later, there's no reason to use map. Instead, you can use Object.prototype.forEach.
Here's what that would look like:
const components = {
compartirEnFlex: `<h4>Newsletter</h4>`,
newsletterEs: `<h4>Compartir</h4>`,
}
function renderComponents(objWithComp) {
Object
.keys(components)
.forEach((id) => {
const element = document.getElementById(id)
const component = objWithComp[id]
if (component && element) {
element.insertAdjacentHTML('afterbegin', component)
}
})
}
renderComponents(components)
Then, when you call renderComponents, you can pass just the components argument, and only render the components for which divs with corresponding ids exist with an if statement.

IndexOf indicates that an object doesn't exist, even though it should

I'm trying to get the index of an item in a database query, but for some reason I'm getting -1 when trying to use indexOf on the data.
The data is displayed in a md-select that repeats over a collection of objects (roles):
<div layout="row">
<md-input-container>
<label>Job Activity:</label>
<md-select ng-model="activity" required>
<md-option ng-repeat="activity in activities" ng-value="activity">
{{ activity.Description }}
</md-option>
</md-select>
</md-input-container>
</div>
Displaying works absolutely fine, but when I submit the form that the above md-select lives in, I simply cannot find the index of the selected activity in the collection of activities / roles, even though it COMES from that collection and no manipulation occurs.
Here is the submit that occurs when the form has been submitted. Role is the role that has been selected from the md-select:
vm.submitEmployment = function (role) {
console.log(role);
console.log($scope.activities.indexOf($scope.role));
console.log($scope.activities);
$scope.data.role = role.Id;
$scope.data.roleIndex = $scope.activities.indexOf($scope.role);
$window.location.href = '#!/additionalInformation';
}
The above console.logs give me this:
Now I'm not rocket scientist, but those two objects look exactly the same for me. Is there something I'm missing?
Alter your method slightly, using the Array.findIndex method to get what you want. The lodash _.findIndex() achieves the same thing, if you use lodash:
vm.submitEmployment = function (role) {
// the idx variable should hold your index number
var idx = $scope.activities.findIndex((item) => $scope.role.Id === item.Id);
}
try to map your array to get IDs then search the index of your Id :
$scope.activities.map(function(role) {
return role.Id;
}).indexOf(role.Id);
indexOf() compares searchElement to elements of the Array using strict
equality (the same method used by the === or triple-equals operator)
var a = {a: 2}
var b = {a: 2}
a === b // false
The reason for this is that internally JavaScript actually has two different approaches for testing equality. Primitives like strings and numbers are compared by their value, while objects like arrays, dates, and plain objects are compared by their reference. That comparison by reference basically checks to see if the objects given refer to the same location in memory.
So, if you are searching by id try something like:
$scope.activities.map(function(a) { return a.id; }).indexOf($scope.role.id)
If you're fine with using ES6, you can use the findIndex function. So, you can do something like this:
const index = $scope.activities.findIndex(item => item.id === role.id);

how can i update an item in a collection, without breaking angular databinding?

I have a collection of objects, I think this is called an associative array. Regardless, I would like to locate an item in that list and update it. As a side note this collection is the ng-model for a select. I know lodash has some of this type of functionality. I can find the item but I am not sure what is the best way to update the item so the data binding still works on my select.
Here is an example that doesn't work:
for (x = 0; x < $scope.RulesTemplates.length; x++)
{
if($scope.RulesTemplates[x].Name == $scope.TempRules.Name)
{
$scope.RulesTemplates[x] = $scope.TempRules;
}
}
Assuming a sample dataset, if you want to extend an existing object in an array that is bound to ng-options, you first need to make sure the angular has the array item being tracked by a specific unique property. Then you could just update the new item in the array at the respective place, angular will automatically perform the update in the bound select options (without re-rendering other option values). But that approach might not refresh already bound ng-model.
Another simple approach using traditional loop:-
<select ng-options="item.display for item in items" ng-model="selected"></select>
and your update logic can just be:-
for (x = 0, l = $scope.items.length; x < l; x++){
var item = $scope.items[x];
if(item.name == itemsUpdate.name) {
angular.extend(item,itemsUpdate); //Extend it
break;
}
}
Demo1
or since you are using lodash, probably less lines of code:-
var item = _.first($scope.items, {name:itemsUpdate.name}).pop(); //Get the item add necessary null checks
angular.extend(item, itemsUpdate);
Demo2
angular.extend will make sure underlying source object reference remains the same with updating the properties, you could also use lodash merge.
Try something like this:
angular.forEach($scope.RulesTemplates, function(value, key){
if(value.Name == $scope.TempRules.Name){
value = $scope.TempRules;
}
})
angular.forEach

How to adjust a variable based on the id of the object that is clicked?

I am making a shop system for my HTML game, and I want to make it so that when you click on an item, it gets the id, and lowers the variable for that item by 1.
if(e.shiftKey && inShop[this.id] === 0) {
coins = coins+price[this.id]
coinUpdate();
[this.id]--;
}
var fish1 = 1
<html>
<img class="item cookable" id="fish1" src="source">
</html>
For example, when I click on a fish, I want it to lower the variable for how many fishes you have in your inventory. So I need to change the [this.id] in a variable with the same name.
Do not use the window method. It only works with global variables.
Global variables are defined as some_var = 10; as opposed to var some_var = 10; If you're from a desktop programming background, you'll find globals in JS hella awkward.
Instead, use namespaces or an object (this method).
Define your inventory like this:
var inventory = {
goldfish: 10,
seahorse: 10,
jellyfish: 10
}
As for the HTML, the ID method is OK, but why not use the data attribute? It's made to hold metadata so perfect for this scenario.
<img src="//placehold.it/32x32" class="item cookable" data-type="fish">
Access for this is built into jQuery via the .data method so when you need to decrement or increment the quantity based on what is clicked, simply use the following where necessary:
// Fetch the "type" of fish from the data attribute
var type = $(this).data("type");
// And update the inventory
inventory[type]--;
Use the data attribute for other metadata so data-foo="hello" data-bar="world"
And these can be fetched as an object using jQuery .data() and it will return {foo: "hello", bar: "world"} or fetch them individually by passing the data name.data("foo")`
Personally, I'd use this as opposed to ID.
[this.id]-- is not going to work. This makes an array with a single element (the string that is referenced by this.id), and tries to decrement that array. And decrementing an array doesn't make much sense.
You can't access local variable dynamically like this (well you can in some cases, but really you shouldn't). However, you can do it with properties, so you have to rejigger things a bit.
How about storing all the counts of things you have in an object, maybe call it inventory.
var inventory = {
fish1: 10,
fish2: 5,
worms: 3
};
Now you use you decrement method with only a slight tweak:
if(e.shiftKey && inShop[this.id] === 0) {
coins = coins+price[this.id]
coinUpdate();
inventory[this.id]--; // decrement an item count in your inventory
}
All global variables in JS are created on the window object. If you have a variable per id, then you just need to do window[this.id]-- or window[this.id]++. Anyway, in JavaScript the Window object acts as the global namespace and in general it is bad practice to clutter the global namespace with variables. You should create a new object (for example items) containing all the item counters and for each item that is added you can do items[this.id]++ or items[this.id]-- when they are removed

Categories

Resources