With Handlebars, what's the simplest way to add one class or another to each element being rendered by an {{#each ...}} helper? I have to integrate with an existing CSS setup for a site, which requires adding one class or another to alternating elements in a list.
Example helper:
{{#each items}}
<div class="{{what here?}}">...</div>
{{/each}
...where we want even or odd as the class names. (Yes, again, I know this can be done with CSS; I'm integrating with an existing site's CSS, which uses alternating classes instead.)
As a Handlebars newbie, I'm not seeing anything built-in, but the API makes it pretty easy to add a helper that lets you select from an arbitrary-length list of items, like this:
Handlebars.registerHelper('cycle', function(index) {
index = index % (arguments.length - 2); // -2 to leave out `index` and the final argument HB adds
return arguments[index + 1];
});
Using that it would be:
{{#each items}}
<div class="{{cycle #index 'even' 'odd'}}">...</div>
{{/each}
Handlebars.registerHelper('cycle', function(index) {
index = index % (arguments.length - 2); // -2 to leave out `index` and the final argument HB adds
return arguments[index + 1];
});
var items = [
"one", "two", "three", "four", "five"
];
var template = Handlebars.compile(
document.getElementById("template").innerHTML
);
var html = template({items: items});
document.body.insertAdjacentHTML(
"beforeend",
html
);
.even {
color: blue;
}
.odd {
color: green;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/handlebars.js/2.0.0/handlebars.min.js"></script>
<script id="template" type="text/x-handlebars-template">
{{#each items}}
<div class="{{cycle #index 'even' 'odd'}}">{{this}}</div>
{{/each}}
</script>
Or if I wanted three classes in rotation:
{{#each items}}
<div class="{{cycle #index 'one' 'two' 'three'}}">...</div>
{{/each}
Handlebars.registerHelper('cycle', function(index) {
index = index % (arguments.length - 2); // -2 to leave out `index` and the final argument HB adds
return arguments[index + 1];
});
var items = [
"one", "two", "three", "four", "five"
];
var template = Handlebars.compile(
document.getElementById("template").innerHTML
);
var html = template({items: items});
document.body.insertAdjacentHTML(
"beforeend",
html
);
.one {
color: blue;
}
.two {
color: green;
}
.three {
color: red;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/handlebars.js/2.0.0/handlebars.min.js"></script>
<script id="template" type="text/x-handlebars-template">
{{#each items}}
<div class="{{cycle #index 'one' 'two' 'three'}}">{{this}}</div>
{{/each}}
</script>
Related
I am currently building a form builder with vue3 composition API. The user can add in different types of inputs like text, radio buttons etc into the form before saving the form. The saved form will then render with the appropriate HTML inputs. The user can edit the name of the question, eg Company Name <HTML textInput.
Currently, when the user adds an input type eg,text, the type is saved into an ordered array. I run a v-for through the ordered array and creating a custom component formComponent, passing in the type.
My formComponent renders out a basic text input for the user to edit the name of the question, and a place holder string for where the text input will be displayed. My issue is in trying to save the question text from the parent.
<div v-if="type=='text'">
<input type="text" placeholder="Key in title"/>
<span>Input field here</span>
</div>
I have an exportForm button in the parent file that when pressed should ideally return an ordered array of toString representations of all child components. I have tried playing with $emit but I have issue triggering the $emit on all child components from the parent; if I understand, $emit was designed for a parent component to listen to child events.
I have also tried using $refs in the forLoop. However, when I log the $refs they give me the div elements.
<div v-for="item in formItems" ref="formComponents">
<FormComponent :type="item" />
</div>
The ideal solution would be to define a method toString() inside each of the child components and have a forLoop running through the array of components to call toString() and append it to a string but I am unable to do that.
Any suggestions will be greatly appreciated!
At first:
You don't really need to access the child components, to get their values. You can bind them dynamically on your data. I would prefer this way, since it is more Vue conform way to work with reactive data.
But I have also implemented the other way you wanted to achieve, with accessing the child component's methods getValue().
I would not suggest to use toString() since it can be confused with internal JS toString() function.
In short:
the wrapping <div> is not necessary
the refs should be applied to the <FormComponents> (see Refs inside v-for)
this.$refs.formComponents returns the Array of your components
FormComponent is used here as <form-components> (see DOM Template Parsing Caveats)
The values are two-way bound with Component v-model
Here is the working playground with the both ways of achieving your goal.
Pay attention how the values are automatically changing in the FormItems data array.
const { createApp } = Vue;
const FormComponent = {
props: ['type', 'modelValue'],
emits: ['update:modelValue'],
template: '#form-component',
data() {
return { value: this.modelValue }
},
methods: {
getValue() {
return this.value;
}
}
}
const App = {
components: { FormComponent },
data() {
return {
formItems: [
{ type: 'text', value: null },
{ type: 'checkbox', value: false }
]
}
},
methods: {
getAllValues() {
let components = this.$refs.formComponents;
let values = [];
for(var i = 0; i < components.length; i++) {
values.push(components[i].getValue())
}
console.log(`values: ${values}`);
}
}
}
const app = createApp(App)
app.mount('#app')
#app { line-height: 2; }
[v-cloak] { display: none; }
label { font-weight: bold; }
th, td { padding: 0px 8px 0px 8px; }
<div id="app">
<label>FormItems:</label><br/>
<table border=1>
<thead><tr><th>#</th><th>Item Type:</th><th>Item Value</th></tr></thead>
<tbody><tr v-for="(item, index) in formItems" :key="index">
<td>{{index}}</td><td>{{item.type}}</td><td>{{item.value}}</td>
</tr></tbody>
</table>
<hr/>
<label>FormComponents:</label>
<form-component
v-for="(item, index) in formItems"
:type="item.type" v-model="item.value" :key="index" ref="formComponents">
</form-component>
<button type="button" #click="getAllValues">Get all values</button>
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<script type="text/x-template" id="form-component">
<div>
<label>type:</label> {{type}},
<label>value:</label> <input :type='type' v-model="value" #input="$emit('update:modelValue', this.type=='checkbox' ? $event.target.checked : $event.target.value)" />
</div>
</script>
I want to create some tests checking the styles of elements. We use these custom CSS vars. Is there any way to get these in cypress instead of checking for e.g. RGB(0,0,0)?
Thx in advance!
If you use cy.should() alongside have.css, you can specify which CSS property to check, and the value.
Using a simple example from your image, it would look something like this:
cy.get('foo')
.should('have.css', 'min-width', '211px');
If there are more complex checks going on, you can always run the .should() as a callback.
cy.get('foo').should(($el) => {
const minHeight = +($el.css('min-height').split('px')[0]);
expect(minHeight).to.eql(40);
});
I found myself checking a lot of CSS values on elements, and opted to have a custom command that allowed me to pass in an expected object and check for all of those values.
Cypress.Commands.add('validateCss', { prevSubject: 'element' }, (subject, expected: { [key: string]: any }) => {
Object.entries(expected).forEach(([key, value]) => {
cy.wrap(subject).should('have.css', key, value);
});
});
const expected = { 'min-width': '211px', 'min-height': '40px' };
cy.get('foo').validateCss(expected);
Interacting with browser element or Dynamic CSS can be achieved in may ways ,
most use-full is cy.get() with the help of .should()
you can find here ( However i know you already checked this :) )
https://docs.cypress.io/api/commands/get#Get-vs-Find
for Example
cy.get('#comparison')
.get('div')
// finds the div.test-title outside the #comparison
// and the div.feature inside
.should('have.class', 'test-title')
.and('have.class', 'feature')
It is possible to evaluate a css variable fairly simply using getComputedStyle()
Cypress.Commands.add('cssVar', (cssVarName) => {
return cy.document().then(doc => {
return window.getComputedStyle(doc.body).getPropertyValue(cssVarName).trim()
})
})
cy.cssVar('--mycolor')
.should('eq', 'yellow')
where, for example
<html>
<head>
<style>
body {
--mycolor: yellow;
}
p {
background-color: var(--mycolor);
}
</style>
</head>
But asserting that <p> has --mycolor requires a dummy element to evaluate yellow to rgb(255, 255, 0).
Cypress.Commands.add('hasCssVar', {prevSubject:true}, (subject, styleName, cssVarName) => {
cy.document().then(doc => {
const dummy = doc.createElement('span')
dummy.style.setProperty(styleName, `var(${cssVarName})`)
doc.body.appendChild(dummy)
const evaluatedStyle = window.getComputedStyle(dummy).getPropertyValue(styleName).trim()
dummy.remove()
cy.wrap(subject)
.then($el => window.getComputedStyle($el[0]).getPropertyValue(styleName).trim())
.should('eq', evaluatedStyle)
})
})
it('compares element property to CSS variable', () => {
cy.cssVar('--mycolor').should('eq', 'yellow')
cy.get('p').hasCssVar('background-color', '--mycolor') // passes
cy.get('button').click() // change the css var color
cy.cssVar('--mycolor').should('eq', 'red')
cy.get('p').hasCssVar('background-color', '--mycolor') // passes
})
The complication is not really because of the CSS var, but because we are dealing with color names that are automatically translated by the browser CSS engine.
Full test page
<html>
<head>
<style>
body {
--mycolor: yellow;
}
p {
background-color: var(--mycolor);
}
</style>
</head>
<body>
<p>Some text, change the background from yellow to red.</p>
<button onclick="changeColor()">Change color</button>
<script>
function changeColor() {
document.body.style.setProperty('--mycolor', 'red')
};
</script>
</body>
</html>
Test log
I have a scrollspy directive that adds an ".active" class to a nav item. When the first nav item has the ".active" class I want my header bar to contain a certain class too. Attached is a simplified example, but how can I add ".active" to item 1 by only looking at the classes in item 2. jsfiddle
<div ng-app>
<div ng-controller='ctrl'>
<div id="item1" ng-class="if item2 has class=active then add active class here">Item 1</div>
<div id="item2" ng-class="myVar">Item 2</div>
</div>
//I can't use a scope object I can only look at item 2's classes
<button type="button" ng-click="myVar='active'">Add Class</button>
<button type="button" ng-click="myVar=''">Remove Class</button>
Click here for live demo.
You'll need a directive to interact with the element. I would have the directive watch the element's classes and have it call a function from your controller when the classes change. Then, your controller function can apply the logic specific to your need, which is to set a flag letting another element know how to respond.
angular.module('myApp', [])
.controller('MyCtrl', function($scope) {
$scope.foo = function(classes) {
if (~classes.indexOf('active')) {
$scope.otherItemIsActive = true;
}
};
})
.directive('onClassChange', function() {
return {
scope: {
onClassChange: '='
},
link: function($scope, $element) {
$scope.$watch(function() {
return $element[0].className;
}, function(className) {
$scope.onClassChange(className.split(' '));
});
}
};
})
;
I want to detect odd or oven in ng-repeat . now i have a fiddle that display some dots show/hide randomly , now i want to change background to not only red , for example odd = green and even = red .
function MyCtrl($scope, $timeout) {
$scope.lights = [0,1,2,3,4,5,6,8,9,10,11,12,13,14,15,16,17,18,19,20];
$scope.currentLight = 0;
function light(index){
if($scope.lights.length < index) {
light(0);
} else {
$scope.currentLight = index;
index = Math.floor((Math.random() * 20) + 1);
$timeout(function(){
light(index);
}, 1000);
}
}
light(0);
}
Any advice ?
you can use ng-class-odd and ng-class-even
from https://docs.angularjs.org/api/ng/directive/ngClassOdd
<ol ng-init="names=['John', 'Mary', 'Cate', 'Suz']">
<li ng-repeat="name in names">
<span ng-class-odd="'odd'" ng-class-even="'even'">
{{name}}
</span>
</li>
</ol>
I have the following list (the numbers are just for reference)
<div class="A">alpha1</div>
<div class="B">alpha2</div>
<div class="A">alpha3</div>
<div class="A">alpha4</div>
<div class="A">alpha5</div>
<div class="B">alpha6</div>
<div class="A">alpha7</div>
I want to apply one style to DIVS 1, 3 and 7, because they are the first of their class (A) in a row of elements of the same class. Is there a pseudo element / magic I can use for that? Something like (inventing)
not(.A) & .A {color:red} -> if class is A and it is not preceded by an A
Thanks!
You use the :not() pseudo-class with an adjacent sibling combinator + to match an .A that is not immediately preceded by an .A:
:not(.A) + .A
You'll also need to use :first-child to select the very first .A element since it's not preceded by anything:
.A:first-child
Combine them, and you have:
:not(.A) + .A, .A:first-child { color: red; }
jsFiddle demo
Here is a cross browser solution using JavaScript:
http://jsfiddle.net/YhvGw/
function applyStyleToFirstDiv(className, styleAttr, val,baseSelector) {
//Allow to specify a base element to search in
if(baseSelector == null){
baseSelector = document
}
var divElements = baseSelector.getElementsByTagName("div"),
len = divElements.length;
var prevWas = false,currentIs;
// Go through all the divs
for (var i = 0; i < len; i++) {
var cur = divElements[i];
var classes = cur.className.split(" ");
currentIs = false;
for (var j = 0; j < classes.length; j++) {
//If you find a matching class
if (classes[j] === className) {
currentIs = true;
break;
}
}
//If the current one matches, and the last one didn't, apply the style, otherwise don't
if(currentIs && !prevWas){
cur.style[styleAttr] = val;
}
prevWas = currentIs;
}
}
//usage sample
applyStyleToFirstDiv("A","color","yellow");
Here is an example:
div:not(.A) + .A, .A:first-of-type{
color:red;
}