I want to write a custom element like this.
<dom-module id="custom-element">
<template>
<!-- This part I want to use document.createElement -->
<template is="dom-repeat" items="[[data]]">
<div>
<span>[[item.name]]</span>
<span>[[item.count]]</span>
</div>
</template>
</template>
</dom-module>
The count value may be changed by other element.
Is it possible to bind properties count to a document.createElement element?
class CustomElement extend Polymer.Element {
static get is () { return "custom-element" }
static get properties () {
return {
data: {
type: Array,
value: [{
"name": "item1",
"count": 0
}, {
"name": "item2",
"count": 0
}, {
"name": "item3",
"count": 0
}],
notify: true,
observer: "_dataChanged"
}
}
}
_dataChanged: (data) => {
data.map((item) => {
let div = document.createElemnt("div")
let itemName = document.createElement("span")
itemName.textContent = item.name
let itemCount = document.createElement("span")
// I want to bind value count here
itemCount.textContent = item.count
div.appendChild(itemName)
div.appendChild(itemCount)
this.shadowRoot.appendChild(div)
})
}
}
window.customElements.define(CustomElement.is, CustomElement)
When other element change the count value, the element which create by document.createElement's count will be change.
Is this possible?
So, data binding works, between two custom elements
Whether you use a dom-repeat with the bind, or bind it to an element like an input in the shadow DOM,
OR,
you use document.createElement to append an element to your shadowDOM, with the bound value,
does not matter
you use {{}} for a two way binding, and a [[]] for a one way binding.
So, If other-element changes adata property bound to custom-element, and you want custom-element to accommodate them, use one way
If you want the changes made by custom-elemnt to also change values in other-element, use two way.
If you are using the console to mutate data of one element, I am afraid, those mutations aren't reflected to the custom element.
If however, you are having a binding between custom-element and other-element on the property data, and
other-element mutates the property data as the result of an event, then, these changes are notified to all communicating elements.
Edit: I see that you have a subsequent query in the comments, which asks why is the plunk you made not working
While you are now correctly binding the elements two-way via data ,
there is a bug in polymer, that doesn't notify observers on data mutation if data is an array.
Solution for now: Shallow clone your data using slice after you change it
like so:
add (e) {
this.set(`data.${e.target.index}.count`, (this.data[e.target.index].count + 1))
this.data = this.data.slice();
}
Related
My requirement is as follows
In parent component, i am passing an array of Child Components(array can be 1 or more than 1)
As the image shows, a child component consists of elements like, input[type=range], input[type=number], dropdown menu, etc
Parent component has a button
<button>Search Location</button>
When I click on Search button in Parent, I need the value of every single elements in each Child Component,
for eg. structure can be as follows
let finalObj={
child1: {
dropValue: "Room1",
cond: "AND"
},
child2: {
inputVal: 50,
cond: "OR"
},
child[n]: {
rangeVal: 1,
cond: ""
}
}
Also, we can change the value again(before clicking search), and Search button should always pickup, the current set value of each component.
I am not sure how to go ahead with this. Any pointers will be really helpful. Please help
So you need to change an array of components into an object of... well first of all that's a .reduce use.
const almostFinalObj = components.reduce((retval, each, i) => {
retval['child'+i] = each;
return retval;
}, {});
That will give an object like
almostFinalObj = {
child1: component1,
child2: component2,
childN: componentN,
}
Now we can .forEach through it, transforming each child component into whatever format you're looking for. (I'm unclear on that part but maybe you can figure out the rest.)
Object.keys(almostFinalObj).forEach((each, i, array) => {
let component = array[i];
array[i] = {};
component.querySelectorAll('input').forEach(e => {
array[i][e.name] = e.value;
});
});
This assumes the name attribute exists on each element in each child row-component. (As in, each radio button in your example has <input type='radio' name='cond' .... />.) You could use id or even a data-XXX attribute as well instead of e.name.
I have computed property in my data this.coinPairingOptions that needs to render its radio buttons based on some of the other fields in this schema. I have reduced the amount of code in the file to save space.
data: function () {
return {
schema: {
{model: "symbolPair", type: "radios", label: "Pair with", values:
this.coinPairingOptions, required: true}
},
computed: {
coinPairingOptions() {
console.log("computing coinPairingOptions")
let coin = this.model.symbol.toUpperCase();
let options = [];
if (this.model.exchange === 'Coinbase') {
options = this.getCoinbasePairs
} else if (this.model.exchange === 'Binance') {
options = this.getBinancePairs
} else {
}
console.log(options.get(coin));
return options.get(coin);
},
}
In the dev tools I can see the computed property changing to the correct values however it is not changing in the data. Apparently, this is appropriate behavior, but what is a way around this? I have tried putting {{this.coinPairingOptions}} in the html and it errors because it's a computed property with not value initially.
Any help would be appreciated!
You can't use computed property in data, because data evaluates before the computed properties did.
You can use a watcher to achieve the intended result. Have a look at the documentation, you can add the argument immediate to trigger the callback immediately with the current value of the expression.
Computed properties are already accessible in the template by using {{}}. You don't need to put a this in front of the computed.
I am trying to dynamically create/remove a Vue component. I have figured out how to dynamically add the component, but I am having some troubles with allowing the users to remove the specific component.
Consider below two Vue files:
TableControls.vue
<a v-on:click="addColumn">Add Column</a>
<script>
export default {
methods: {
addColumn: function () {
Event.$emit('column-was-added')
}
}
};
</script>
DocumentViewer.vue:
<div v-for="count in columns">
<VueDragResize :id="count">
<a #click="removeColumn(count)">Remove Column</a>
</VueDragResize>
</div>
<script>
import VueDragResize from 'vue-drag-resize';
export default {
components: {
VueDragResize
},
data() {
return {
columns: [1],
}
},
created() {
Event.$on("column-was-added", () => this.addColumn())
},
methods: {
addColumn: function () {
this.columns.push(this.columns.length + 1)
},
removeColumn: function (id) {
this.columns.splice(id, 1)
}
}
};
</script>
As you can see, whenever a user clicks on <a v-on:click="addColumn">Add Column</a>, it will submit an event, and the DocumentViewer.vue file will pick up it, firing the addColumn method. This will ultimately create a new <VueDragResize></VueDragResize> component.
This works great.
The problem is when I want to remove the component again. My removeColumn method simply removes an id from the columns array:
removeColumn: function (id) {
this.columns.splice(id, 1)
}
This results in that a column is in fact removed. However, consider below example. When user clicks on the remove icon for the first column, it will remove the 2nd column instead. (And when there is only one column present, it cannot be removed).
I believe this is due to the fact that I splice() the array, but I cannot see how else I can remove the component dynamically?
I see, Array on Vue does not re render when you modify them.
You need to use the
Vue.set(items, indexOfItem, newValue)
if you want to modify
and use
Vue.delete(target, indexOfObjectToDelete);
If you want to delete an item from an array
You may read the additional info here
https://v2.vuejs.org/v2/api/#Vue-delete
If you want to delete an item from array. Using this will cause the component to rerender.
In this case it will be intuitive to do this
removeColumn: function (id) {
Vue.delete(this.columns, id)
}
Note that id should be the index. Vue.delete ensures the re-render of the component.
EDIT, you must use the index, instead of the count here.
<div v-for="(count, index) in columns">
<VueDragResize :id="index">
<a #click="removeColumn(index)">Remove Column</a>
</VueDragResize>
</div>
I would recommend reshaping your data, each element should be an object with an id and whatever other properties you want. Not simply an id then you would need something like this:
removeColumn(id) {
const elToRemove = this.columns.findIndex(el => el.id === id)
let newArr = [elToRemove, ...this.columns]
this.columns = newArr
}
Also make another computed property for columns like this to make sure they change dynamically (when you add/remove):
computed: {
dynColumns(){ return this.columns}
}
I have same problem, and I found the solution of this problem. It is need to set #key with v-for. This is Built-in Special Attributes.
By default, if you do not set "#key", array index is set to#key. So if array length is 3, #key is 0,1,2. Vue identify eash v-for elements by key. If you remove second value of array, then array index is 0 and 1, because array length is 2. Then Vue understand that #key==2 element removed, So Vue remove 3rd component. So if you remove second value of array, if no #key, third component will be removed.
To avoid this, need to set #key to identify component like this:
let arr = [
{ id: 'a', ...},
{ id: 'b', ...},
{ id: 'c', ...}
];
<div v-for="obj in arr" :key="obj.id">
<someYourComponent>
...
</someYourComponent>
</div>
Situation 1 - I check my individual checkboxes, my header checkbox gets checked. This is method 1 and works fine.
Code for same
index.vue
<VCheckbox
checked={this.checkSelections[idx]}
nativeOnChange={e => {
this.$set(this.checkSelections, idx, e.target.checked);
let allSelected = true;
for (let i = 0; i < this.checkSelections.length; i++) {
allSelected = this.checkSelections[i];
if (!allSelected) break;
}
this.$root.$emit("checkmarkHead", { allSelected });
}}
/>
Head.vue
mounted() {
this.$nextTick(() => {
this.$root.$on("checkmarkHead", ({ allSelected }) => {
console.log("checkmarkHead", allSelected);
this.allSelected = allSelected;
});
}
},
Situation 2 -
I check my header checkbox and all my checkboxes are checked. Vice-versa is true as well. So method 2 corresponding to this works fine too.
Code for same -
Head.vue
<HeadItem>
<VCheckbox
checked={this.allSelected}
nativeOnChange={e => {
this.allSelected = e.target.checked;
this.$root.$emit("selectAll", {
allSelected: this.allSelected
});
}}
/>
</HeadItem>
index.vue
mounted() {
this.$nextTick(() => {
this.$root.$on("selectAll", ({ allSelected }) => {
this.checkSelections = Array(this.sortedData.length).fill(allSelected);
});
});
}
Problem - When I do situation 2 after Situation 1, the same methods don't work as expected. The view isn't updated. Similarly, executing Situation 1 after Situation 2 won't work either.
Here's the link to
Code Link - https://codesandbox.io/s/vmwy3v4203
I'm clueless now after handling all mutations caveats etc.
I owe you an apology. This is indeed another reactivity issue which could be solved by providing a key attribute ..
The key special attribute is primarily used as a hint for Vue’s
virtual DOM algorithm to identify VNodes when diffing the new list of
nodes against the old list. Without keys, Vue uses an algorithm that
minimizes element movement and tries to patch/reuse elements of the
same type in-place as much as possible. With keys, it will reorder
elements based on the order change of keys, and elements with keys
that are no longer present will always be removed/destroyed.
You can assign a unique key value to an element, which if changed will force Vue to re-render that element. In your case you can assign keys to your VCheckbox elements equal to their checked value, forcing Vue to re-render them when they're checked/unchecked. For example ..
<HeadItem>
<VCheckbox
key={this.allSelected}
checked={this.allSelected}
nativeOnChange={e => {
this.$emit('change', !this.allSelected)
}}
/>
</HeadItem>
I've taken the liberty to re-write your allSelected property as a computed property and removed the event listener you set up on the root instance. I think it's much cleaner this way ..
I basically want the equivalent to binding to 'add' and 'remove' events in Backbone's Collections. I see basically no way of doing this in AngularJS, and the current workaround we've settled for is $watch()ing the array's length and manually diffing/recalculating the whole thing. Is this really what the cool kids do?
Edit: Specifically, watching the array's length means I don't easily know which element has been changed, I need to manually "diff".
I think using $watch is a good solution, but $watchCollection can be better for you. $watchCollection doesn't perform deep comparison and just watchs for array modification like insert, delete or sort (not item update).
For exemple, if you want to keep an attribut order synchronize with the array order :
$scope.sortableItems = [
{order: 1, text: 'foo'},
{order: 2, text: 'bar'},
{order: 3, text: 'baz'}
];
$scope.$watchCollection('sortableItems', function(newCol, oldCol, scope) {
for (var index in newCol) {
var item = newCol[index];
item.order = parseInt(index) + 1;
}
});
But for your problem, I do not know if there is a better solution than manually browse the array to identify the change.
The way to watch an array in Angular is $watch(array, function(){} ,true)
I would create child scopes and watch them individually.
here is an example:
$scope.myCollection = [];
var addChild = function()
{
var Child = $scope.$new();
Child.name = 'Your Name here';
Child.$watch('name', function(newValue) {
// .... do something when the attribute 'name' is changed ...
});
Child.$on('$destroy', function() {
//... do something when this child gets destroyed
});
$scope.myCollection.push(Child); // add the child to collection array
};
// Pass the item to this method as parameter,
// do it within an ngRepeat of the collection in your views
$scope.deleteButtonClicked = function(item)
{
var index = $scope.myCollection.indexOf(item); //gets the item index
delete $scope.myCollection[index]; // removes the item on the array
item.$destroy(); // destroys the original items
}
Please tell more about your usecase. One of the solutions of tracking element persistance is using ngRepeat directive with custom directive that listening element's $destroy event:
<div ng-repeat="item in items" on-delete="doSomething(item)">
angular.module("app").directive("onDelete", function() {
return {
link: function (scope, element, attrs) {
element.on("$destroy", function () {
scope.$eval(attrs.onDelete);
});
}
}
});
Perhaps the solution is to create the collection class ( like backbone does ) and you can hook into events pretty easily as well.
The solution I have done here isnt really comprehensive, but should give you a general guidance on how this could be done perhaps.
http://beta.plnkr.co/edit/dGJFDhf9p5KJqeUfcTys?p=preview