Add element references to repeated items in Aurelia - javascript

So I have a list of objects in my viewModel that need access to each of their own individual elements. I tried using element.ref, but there was no element in the object.
persons.js
export class PersonsViewModel {
persons = [{
id: 0,
name: 'Matt'
}]
// this function fails, person.el is undefined
increase(person) {
person.el.style.height = person.el.clientHeight + 10;
person.el.style.width = person.el.clientWidth + 10;
}
}
persons.html
<template>
<div repeat.for="person of persons" element.ref="el">
<div>${person.name}</div>
<button click.delegate="increase(person)">+</button>
</div>
</template>

The element.ref binding respects binding contexts just like all other bindings, and so the element.ref must reference the person object when attaching the element property.
<div repeat.for="person of persons" element.ref="person.el">

Related

Render a native HTML template with JavaScript

With HTML Templates it makes it easy to stamp out snippets of html.
What is a sane way of populating the stamped out templates? In the MDN link above they render the template as follows:
td = clone2.querySelectorAll("td");
td[0].textContent = "0384928528";
td[1].textContent = "Acme Kidney Beans 2";
This obviously only works if all the elements are the same tag, and ordered in the same way, which is very brittle.
What if I had a template like this:
<template>
<div>
<h2>__heading__</h2>
<label>__label1__</label><input type="text" value="__value1__">
<label>__label2__</label><input type="text" value="__value2__">
<div>__instruction__</div>
<label>__label3__</label><input type="text" value="__value3__">
</div>
</template>
And say one had this data to render it with:
{
__heading__: 'Lots of things',
__label1__: 'label 1',
__value1__: 'value 1',
__label2__: 'label 2',
__value2__: 'value 2',
__instruction__: 'Do the thing',
__label3__: 'label 3',
__value3__: 'value 3',
}
Then the rendered result would be:
<div>
<h2>Lots of things</h2>
<label>label 1</label><input type="text" value="value 1">
<label>label 2</label><input type="text" value="value 2">
<div>Do the thing</div>
<label>label 3</label><input type="text" value="value 3">
</div>
How would one render the template? PS if this is a XY question, you can use some other means to instead of the dunder fields.
I can only think of adding classes or attributes to each element which has a field to populate, and then perform lots of clonedNode.querySelector() ...seems very unelegant.
Note 9/9/2022: There is a proposal for DOM Parts on the table: https://github.com/WICG/webcomponents/blob/gh-pages/proposals/DOM-Parts.md
You could replace content with Template Literal String notation
string:
<h1>Run ${name}! Run!</h1>
Ain't <b>${tooling}</b> great!!
property Object:
{
name: this.getAttribute("name"),
tooling: "Web Components",
}
The parse(str,v) function
creates a new Function
with a String literal (note the back-ticks)
then executes that Function
passing all v Object values
and Returns the parsed String literal
<template id="MY-ELEMENT">
<h1 style="background:${color}"> Run ${name}! Run!</h1>
Ain't <b>${tooling}</b> great!!
</template>
<my-element name="Forrest"></my-element>
<script>
function parse(str, v = {}) {
try {
return new Function("v",
"return((" + Object.keys(v).join(",") + ")=>`" +
str +
"`)(...Object.values(v))")(v) || "";
} catch (e) {
console.error(e);
}
};
customElements.define("my-element", class extends HTMLElement {
color = "gold";
constructor() {
super().attachShadow({mode: 'open'}).innerHTML = parse(
document.getElementById(this.nodeName).innerHTML, // String
{ // properties
name: this.getAttribute("name"),
tooling: "Web Components",
...this, // to show you can add anything you want to the property bag
});
}
})
</script>
One of the possible solution is manipulation of the HTML string itself. There is not much stuff you can do with content inside DOM tree without some special markup (like you mentioned in your question).
So, you clone the template and modify inner HTML of every child inside the template:
// Gather the template from DOM and clone it
const template = document.querySelector('#target_template');
const clonedDocumentFragment = template.content.cloneNode(true);
// Looks like there is no better way to modify HTML of the whole
// DocumentFragment, so we modify HTML of each child node:
Array
.from(clonedDocumentFragment.children)
.forEach(childElement => renderInnerVariables(
childElement,
{
// Pass your values here
}
));
// And here example of the method replacing those values
function renderInnerVariables(targetElement, variables = {}) {
// Reminder: it can be really unsafe to put user data here
targetElement.innerHTML = targetElement.innerHTML.replace(
// Instead of looping through variables, we can use regexp
// to get all the variables in content
/__([\w_]+)__/,
(original, variableName) => {
// Check if variables passed and target variable exists
return variables && variables.hasOwnProperty(variableName)
// Pass the variable value
? variables[variableName]
// Or pass the original string
: original;
}
);
}
But since it's just a string content modification, you can pretty much just use string templates instead.
If I understood correctly you want to create a template based on object.. if yes you can use this as a start..
What I did was to create a function getTagName(key) which will return a specific string based on the object key (it can be made with a switch most probably but eh.. :( switch on regex ) For example __value2__ will return input
Then I created a function createHtmlElementwith with some conditions in order to set the text/value to my element then I iterate through all the properties and append the newly created element
const data = {
__heading__: 'Lots of things',
__label1__: 'label 1',
__value1__: 'value 1',
__label2__: 'label 2',
__value2__: 'value 2',
__instruction__: 'Do the thing',
__label3__: 'label 3',
__value3__: 'value 3',
}
let container = document.getElementById('container');
// this can be done with switch i think, but for demo i did if statements :(
function getTagName(key) {
key = key.replace('__', ''); //remove __ from front
if (/^heading/.test(key)) return "h2";
if (/^label/.test(key)) return "label";
if (/^value/.test(key)) return "input";
if (/^instruction/.test(key)) return "div";
}
function createHtmlElement(name, key, value) {
let element = document.createElement(name);
if (name === 'label' || name === "h2" || name === "div") {
element.innerHTML = value;
}
if (name === "input") {
element.type = "text";
//element.value = key; // this is not working for i don't know what reason :(
element.setAttribute('value', key);
}
// console.log(element) // decomment if you want to see the element as plain html
return element;
}
for (const [key, value] of Object.entries(data)) {
let tagName = getTagName(key);
let element = createHtmlElement(tagName, key, value);
let brTag = document.createElement("br");
container.appendChild(element);
container.appendChild(brTag);
}
<div id="container">
</div>
<!--
<h2>__heading__</h2>
<label>__label1__</label>
<input type="text" value="__value1__">
<label>__label2__</label>
<input type="text" value="__value2__">
<div>__instruction__</div>
<label>__label3__</label>
<input type="text" value="__value3__">
-->

Using ngModel to bind dynamically generated inputs

I'm fairly new to Angular so bear with me if I have some rookie mistakes here :)
I am dynamically adding inputs based on some properties that need to be filled out (this.properties) and I want to bind each of those dynamic inputs to some object (this.selectedProperties = {}) so that I end up with something like this.
this.properties = [new MyData { name = "theName", examples = "theExamples" },
new MyData { name = "anotherName", examples = "moreExamples" }];
this.selectedProperties = { "theName": "valueFromBinding", "anotherName": "anotherValueFromBinding" }
Here's a slimmed down version of what I have so far. Everything is working other than the ngModel to set the selected values.
<div *ngFor="let property of this.properties">
<label>{{property.name}}</label>
<input type="text" placeholder="{{property.examples}}"
[ngModel]="this.selectedProperties[property.name]"
name="{{property.name}}Input">
</div>
Thanks!
Try like this :
component.html
<div *ngFor="let property of properties">
<label>{{property.name}}</label>
<input type="text" placeholder="{{property.examples}}" [ngModel]="selectedProperties[property.name]" name="{{property.name}}Input">
</div>
component.ts
export class AppComponent {
properties = [
new MyData { name: "theName", examples: "theExamples" },
new MyData { name: "anotherName", examples: "moreExamples" }
];
selectedProperties = {
"theName": "valueFromBinding",
"anotherName": "anotherValueFromBinding"
};
}
Turns out I needed two way binding so that selectedProperties[property.name] would update when the user changed it in the UI

Vue.js 2 - Array change detection

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.

WinJS Databind to property get/set

In WinJS can I bind a property getter in a listView? Say I have an object defined like this:
var MyLib = MyLib || {};
MyLib.ToDoItem = function() {
this.name = '';
this.description = '';
Object.defineProperty(this, "completed", {
get : function() {
return false;
}
});
}
MyLib.ToDoList = [];
//MyLib.ToDoList.push....add todo items
I am declaring a WinJS.Binding.Template where all of the properties are binding except the one that is defined with a property getter:
<div id="myItemTemplate" data-win-control="WinJS.Binding.Template">
<div class="titleTile">
<h4 class="item-title" data-win-bind="textContent: name"></h4>
<p data-win-bind="textContent: description"></p>
<div data-win-bind="textContent: completed"></div> <-- Renders as undefined
</div>
</div>
The "completed" property renders as undefined. If I put a breakpoint in the javascript console where I am loading the data, I can get to the completed property, but the databinding doesn't seem to like it...any ideas?
You missed one line after your getter.
get : function() {
return false;
}
, enumerable: true
By setting enumerable to true, you can make data binding works on this property.

Filter users by one keyword in a nested observableArray

I am trying to filter my users observableArray which has a nested keywords observableArray
based on a keywords observableArray on my viewModel.
When I try to use ko.utils.arrayForEach I get a stack overflow exception. See the code below, also posted in this jsfiddle
function User(id, name, keywords){
return {
id: ko.observable(id),
name: ko.observable(name),
keywords: ko.observableArray(keywords),
isVisible: ko.dependentObservable(function(){
var visible = false;
if (viewModel.selectedKeyword() || viewModel.keywordIsDirty()) {
ko.utils.arrayForEach(keywords, function(keyword) {
if (keyword === viewModel.selectedKeyword()){
visible = true;
}
});
if (!visible) {
viewModel.users.remove(this);
}
}
return visible;
})
}
};
function Keyword(count, word){
return{
count: ko.observable(count),
word: ko.observable(word)
}
};
var viewModel = {
users: ko.observableArray([]),
keywords: ko.observableArray([]),
selectedKeyword: ko.observable(),
keywordIsDirty: ko.observable(false)
}
viewModel.selectedKeyword.subscribe(function () {
if (!viewModel.keywordIsDirty()) {
viewModel.keywordIsDirty(true);
}
});
ko.applyBindings(viewModel);
for (var i = 0; i < 500; i++) {
viewModel.users.push(
new User(i, "Man " + i, ["Beer", "Women", "Food"])
)
}
viewModel.keywords.push(new Keyword(1, "Beer"));
viewModel.keywords.push(new Keyword(2, "Women"));
viewModel.keywords.push(new Keyword(3, "Food"));
viewModel.keywords.push(new Keyword(4, "Cooking"));
And the View code:
<ul data-bind="template: { name: 'keyword-template', foreach: keywords }"></ul><br />
<ul data-bind="template: { name: 'user-template', foreach: users }"></ul>
<script id="keyword-template" type="text/html">
<li>
<label><input type="radio" value="${word}" name="keywordgroup" data-bind="checked: viewModel.selectedKeyword" /> ${ word }<label>
</li>
</script>
<script id="user-template" type="text/html">
<li>
<span data-bind="visible: isVisible">${ $data.name }</span>
</li>
</script>
Your isVisible dependentObservable has created a dependency on itself and is recursively trying to evaluate itself based on this line:
if (!visible) {
viewModel.users.remove(this);
}
So, this creates a dependency on viewModel.users, because remove has to access the observableArray's underlying array to remove the user. At the point that the array is modified, subscribers are notified and one of the subscribers will be itself.
It is generally best to not change the state of any observables in a dependentObservable. you can manually subscribe to changes to a dependentObservable and makes your changes there (provided the dependentObservable does not depend on what you are changing).
However, in this case, I would probably instead create a dependentObservable at the viewModel level called something like filteredUsers. Then, return a version of the users array that is filtered.
It might look like this:
viewModel.filteredUsers = ko.dependentObservable(function() {
var selected = viewModel.selectedKeyword();
//if nothing is selected, then return an empty array
return !selected ? [] : ko.utils.arrayFilter(this.users(), function(user) {
//otherwise, filter on keywords. Stop on first match.
return ko.utils.arrayFirst(user.keywords(), function(keyword) {
return keyword === selected;
}) != null; //doesn't have to be a boolean, but just trying to be clear in sample
});
}, viewModel);
You also should not need the dirty flag, as dependentObservables will be re-triggered when any observables that they access have changed. So, since it accesses selectedKeyword, it will get re-evaluated whenever selectedKeyword changes.
http://jsfiddle.net/rniemeyer/mD8SK/
I hope that I properly understood your scenario.

Categories