add dynamic class with css binding - css

I want to add a class behind payment-method by function with the knockout css binding (in Magento 2.1).
So this is the current code:
<div class="payment-method" data-bind="css: {'_active': (getCode() == isChecked())}">
<div class="payment-method-title field choice">
<input type="radio"
name="payment[method]"
class="radio"
data-bind="attr: {'id': getCode()}, value: getCode(), checked: isChecked, click: selectPaymentMethod, visible: isRadioButtonVisible()"/>
The class is returned by getCode() which works above with the id and value.
So I thought I could do just:
<div class="payment-method" data-bind="css: {'_active': (getCode() == isChecked()), getCode()}">
But then it fails with:
knockout.js:2624 Uncaught SyntaxError: Unable to parse bindings.
Bindings value: css: {'_active': (getCode() == isChecked()), getCode() }
Message: Unexpected token }
<div class="payment-method" data-bind="css: getCode()">
This works.
<div class="payment-method" data-bind="css: {getCode()}">
This doesn't.
<div class="payment-method" data-bind="css: {'_active': (getCode() == isChecked())}, attr: {'class': getCode()}">
This works too but will overwrite the payment-method class and the _active class isn't set either initally anymore.
How do I set that dynamic class?

This piece of code is redundant, as the css data-bind is getting overwrite with your attr binding.
<div class="payment-method" data-bind="css: {'_active': (getCode() == isChecked())}, attr: {'class': getCode()}">
This is how you can do your dynamic class (assumption these properties are observable):
<div class="payment-method" data-bind="css: CSS">
self.CSS = ko.computed(function() {
var code = self.getCode();
var isChecked = self.isChecked();
return code + ' ' + (code == isChecked ? '_active' : '');
}, viewModel);

The comment from #tyler_mitchell helped me find the solution myself through this thread: https://stackoverflow.com/a/21528681/928666
I can do:
<div class="payment-method" data-bind="attr: { 'class': 'payment-method ' + getCode() }, css: {'_active': (getCode() == isChecked())}">
Not brilliant but well...

Another example should anyone need it
<div data-bind="attr: { 'class': 'mainClass' + (dynamicClassSetOnce ? ' ' + dynamicClassSetOnce : '' }, css: { 'mainClass--focussed': isFocussed }">
...
</div>

Related

How do I properly deprecate Gutenberg block

I have a custom Gutenberg block (https://pastebin.com/bV2k5Ekc) in which I display text, link and an image. I want to change it so so instead of saving the image URL as a background image of the container, to use an img tag. Unfortunately - I can't manage to create the deprecation correctly - I fail at assigning the attributes parameters in the deprecation:
From this:
const {
attributes: {
mediaURL,
boxTitle,
boxDescription,
linkText,
linkHref,
linkTitle
},
className,
} = props;
let boxClass = 'cta-box';
let contentDescription = '';
if (boxDescription.length) {
boxClass += ' cta-box-description';
contentDescription = (
<p>
{boxDescription}
</p>
)
}
return (
<div className={`cta-block-box ${className}`}>
<a
className="cta-box-link"
href={linkHref}
style={{ backgroundImage: "url(" + mediaURL + ")" }}
rel="noopener noreferrer"
>
<div className={boxClass}>
<h3>
{boxTitle}
</h3>
{contentDescription}
<span className="arrow">{linkText ? linkText : linkTitle}</span>
</div>
</a>
</div>
);
},
To this (I only change what's in the return statement):
return (
<div className={`cta-block-box ${className}`}>
<a
className="cta-box-link"
rel="noopener noreferrer"
>
<img className="cta-box-image" src={linkHref} alt=""/>
<div className={boxClass}>
<h3>
{boxTitle}
</h3>
{contentDescription}
<span className="arrow">{linkText ? linkText : linkTitle}</span>
</div>
</a>
</div>
);
Which of course broke the Gutenberg element. So I added a deprecate to the blog, as much as I could following the official Wordpress documentation:
deprecated: [
{
attributes: {...this.attributes},
save: (props) => {
const {
attributes: {
mediaURL,
boxTitle,
boxDescription,
linkText,
linkHref,
linkTitle
},
className,
} = props;
console.log('dep');
console.log(props);
let boxClass = 'cta-box';
let contentDescription = '';
if (boxDescription.length) {
boxClass += ' cta-box-description';
contentDescription = (
<p>
{boxDescription}
</p>
)
}
return (
<div className={`cta-block-box ${className}`}>
<a
className="cta-box-link"
style={{ backgroundImage: "url(" + mediaURL + ")" }}
rel="noopener noreferrer"
>
<div className={boxClass}>
<h3>
{boxTitle}
</h3>
{contentDescription}
<span className="arrow">{linkText ? linkText : linkTitle}</span>
</div>
</a>
</div>
);
},
}
],
After this the editor page crashes, and I get error message in console, that attributes is not defined (displaying incorrect row in the script file). This is the "after" script contents (https://pastebin.com/dVdLMx7N).
react-dom.min.js?ver=16.13.1:125 ReferenceError: attributes is not defined
at save (cta-box2.js?f66a:242)
at Wt (blocks.min.js?ver=9ed25ffa009c799f99a4340915b6dc6a:3)
at Qt (blocks.min.js?ver=9ed25ffa009c799f99a4340915b6dc6a:3)
at block-editor.min.js?ver=4378547cec8f5157a02ead3dfc5c65b2:12
at hooks.min.js?ver=50e23bed88bcb9e6e14023e9961698c1:2
at $r (blocks.min.js?ver=9ed25ffa009c799f99a4340915b6dc6a:3)
at blocks.min.js?ver=9ed25ffa009c799f99a4340915b6dc6a:3
at Ur (blocks.min.js?ver=9ed25ffa009c799f99a4340915b6dc6a:3)
at blocks.min.js?ver=9ed25ffa009c799f99a4340915b6dc6a:3
at Array.reduce (<anonymous>)
Any help would be greatly appreciated! I suspect I'm missing some small detail, but so far I've failed to locate it. And was not able to find relevant enough information on the web.
Thanks in advance!
There are two issues in your "after" script:
The attributes do not match (and the this is actually the window object): attributes: {...this.attributes} (see line 212).
So what you used with the attributes property on line 24, should also be used with the same property on line 212. (because you only changed the output, so the block attributes remain the same)
The save output/markup also do not match — in the "before" script, you've got href={linkHref}, but in the deprecated property of the "after" script, the save output did not have that href. (see this diff)
So make sure the attributes and save output match the ones in the old/"before" script, and the following is how your code would look like, but note that I only included the main parts that need to be fixed:
// Define the attributes and use it with the root "attributes" property and
// the one in the "deprecated" property.
const blockAttributes = {
mediaID: {
type: 'number'
},
mediaURL: {
type: 'string'
},
boxTitle: {
type: 'string',
default: ''
},
// ... the rest of the attributes here.
};
registerBlockType('hswp/test-box', {
title: __('Test Box', 'modula'),
// ... your code.
attributes: blockAttributes,
// ... your code.
deprecated: [
{
attributes: blockAttributes,
save: (props) => {
// ... your code.
return (
<div className={`cta-block-box ${className}`}>
<a
className="cta-box-link"
href={linkHref}
style={{ backgroundImage: "url(" + mediaURL + ")" }}
rel="noopener noreferrer"
>
... your code.
</a>
</div>
);
},
}
],
});
Additionally, note that the PlainText component doesn't (as of writing) have a property named tagName.

OverFlow on form tags

So i am using bootstrap-vue, and precisely i am using the Form-tag as it's what i exactly needs. The issue is that the dropdown is as the long as the list of options.
Here's what i mean :
Tag button
& the
Dropdown
What i actually want is a similar css to overflow:scroll but i can't seem to make it work.
here's the code :
<template>
<div>
<b-form-group label="Tagged input using dropdown">
<b-form-tags v-model="value" no-outer-focus class="mb-2">
<template v-slot="{ tags, disabled, addTag, removeTag }">
<ul v-if="tags.length > 0" class="list-inline d-inline-block mb-2">
<li v-for="tag in tags" :key="tag" class="list-inline-item">
<b-form-tag
#remove="removeTag(tag)"
:title="tag"
:disabled="disabled"
variant="info"
>{{ tag }}</b-form-tag>
</li>
</ul>
<b-dropdown size="sm" variant="outline-secondary" block menu-class="w-100">
<template v-slot:button-content>
<b-icon icon="tag-fill"></b-icon> Choose tags
</template>
<b-dropdown-form #submit.stop.prevent="() => {}">
<b-form-group
label-for="tag-search-input"
label="Search tags"
label-cols-md="auto"
class="mb-0"
label-size="sm"
:description="searchDesc"
:disabled="disabled"
>
<b-form-input
v-model="search"
id="tag-search-input"
type="search"
size="sm"
autocomplete="off"
></b-form-input>
</b-form-group>
</b-dropdown-form>
<b-dropdown-divider></b-dropdown-divider>
<b-dropdown-item-button
v-for="option in availableOptions"
:key="option"
#click="onOptionClick({ option, addTag })"
>
{{ option }}
</b-dropdown-item-button>
<b-dropdown-text v-if="availableOptions.length === 0">
There are no tags available to select
</b-dropdown-text>
</b-dropdown>
</template>
</b-form-tags>
</b-form-group>
</div>
</template>
<script>
export default {
data() {
return {
options: ['Apple', 'Orange', 'Banana', 'Lime', 'Peach', 'Chocolate', 'Strawberry'],
search: '',
value: []
}
},
computed: {
criteria() {
// Compute the search criteria
return this.search.trim().toLowerCase()
},
availableOptions() {
const criteria = this.criteria
// Filter out already selected options
const options = this.options.filter(opt => this.value.indexOf(opt) === -1)
if (criteria) {
// Show only options that match criteria
return options.filter(opt => opt.toLowerCase().indexOf(criteria) > -1);
}
// Show all options available
return options
},
searchDesc() {
if (this.criteria && this.availableOptions.length === 0) {
return 'There are no tags matching your search criteria'
}
return ''
}
},
methods: {
onOptionClick({ option, addTag }) {
addTag(option)
this.search = ''
}
}
}
</script>
If you could please help me... Thank you
Well, i did it with adding a div with the following css
#test{
max-height:500px;
overflow:auto;
}
here's the code if anyone need it :
<template>
<div>
<b-form-group label="Tagged input using dropdown">
<b-form-tags v-model="value" no-outer-focus class="mb-2">
<template v-slot="{ tags, disabled, addTag, removeTag }">
<ul v-if="tags.length > 0" class="list-inline d-inline-block mb-2">
<li v-for="tag in tags" :key="tag" class="list-inline-item">
<b-form-tag
#remove="removeTag(tag)"
:title="tag"
:disabled="disabled"
variant="info"
>{{ tag }}</b-form-tag>
</li>
</ul>
<b-dropdown size="sm" variant="outline-secondary" block menu-class="w-100">
<template v-slot:button-content>
<b-icon icon="tag-fill"></b-icon> Choose tags
</template>
<div id="test">
<b-dropdown-form #submit.stop.prevent="() => {}">
<b-form-group
label-for="tag-search-input"
label="Search tags"
label-cols-md="auto"
class="mb-0"
label-size="sm"
:description="searchDesc"
:disabled="disabled"
>
<b-form-input
v-model="search"
id="tag-search-input"
type="search"
size="sm"
autocomplete="off"
></b-form-input>
</b-form-group>
</b-dropdown-form>
<b-dropdown-divider></b-dropdown-divider>
<b-dropdown-item-button
v-for="option in availableOptions"
:key="option"
#click="onOptionClick({ option, addTag })"
>
{{ option }}
</b-dropdown-item-button>
<b-dropdown-text v-if="availableOptions.length === 0">
There are no tags available to select
</b-dropdown-text>
</div>
</b-dropdown>
</template>
</b-form-tags>
</b-form-group>
</div>
</template>
<script>
export default {
data() {
return {
options: ['Apple', 'Orange', 'Banana', 'Lime', 'Peach', 'Chocolate', 'Strawberry'],
search: '',
value: []
}
},
computed: {
criteria() {
// Compute the search criteria
return this.search.trim().toLowerCase()
},
availableOptions() {
const criteria = this.criteria
// Filter out already selected options
const options = this.options.filter(opt => this.value.indexOf(opt) === -1)
if (criteria) {
// Show only options that match criteria
return options.filter(opt => opt.toLowerCase().indexOf(criteria) > -1);
}
// Show all options available
return options
},
searchDesc() {
if (this.criteria && this.availableOptions.length === 0) {
return 'There are no tags matching your search criteria'
}
return ''
}
},
methods: {
onOptionClick({ option, addTag }) {
addTag(option)
this.search = ''
}
}
}
</script>

Assign value to input without v-model and without databinding

In my v-for i need to initialize some input field with some text WITHOUT binding it to the object. Currently i'm trying:
<div v-for="item in allItems">
<input type="text" class="header-title" value="item.name"></input>
</div>
but item.name is printed in the input instead of the item name. How to accomplish this?
v-model is just syntactic sugar for :value and an event, usually #input. See docs here.
You can pass a noop function () => {} to cancel the value update or do whatever you want with the new value, maybe assign it to another object.
Note: <input> elements are void, they do not require a closing tag.
new Vue({
el: '#app',
data() {
return {
allItems: [{ name: 'foo' },{ name: 'bar' }]
}
},
})
<script src="https://unpkg.com/vue#2.5.9/dist/vue.js"></script>
<div id="app">
<div v-for="item in allItems">
<input type="text" class="header-title" :value="item.name" #input="() => {}">
{{ item.name }}
</div>
</div>
You can have your input show value of item.value by only using the ref attribute.
Just add ref='' and map it's values to the inputs value, inside your mounted function.
new Vue({
el: '#app',
data () {
return {
allItems: [
{name: 'foo'},
{name: 'bar'}
]
}
},
mounted () {
let self = this;
this.$refs.inp.map( (m, k) => {
m.value = self.allItems[k].name
})
}
})
<script src="https://unpkg.com/vue#2.5.9/dist/vue.js"></script>
<div id="app">
<div v-for="item in allItems">
<input type="text" class="header-title" value="item.name" ref='inp'></input>
</div>
</div>

binding attributes in vuejs component

I am trying to use the last version of vuejs with Laravel 5.3 ! The idea I am trying to fulfill is make a component foreach user. So that I have all users listed and foreach one there is a button "edit" , when I click this button I should see the form to update this user.
So this is how I defined the component :
<script>
new Vue({
el: '.view-wrap',
components: {
user-view: {
template: '#user-view',
props: ['user']
}
},
data: {
users: <?php echo json_encode($users); ?>,
},
methods: {
showForm: function(number){
$('div.update-user-'+number).css({'display':'block'});
},
getClassName: function (index) {
return "update-user-"+index;
},
getUpdateUrl: function(id){
return '/users/update/'+id;
},
}
});
This is the template for the "user-view" which take a class name "updateClass" which contains the id of every user (for show/hide purposes), an "updateUrl" which is the url to update the user to bind it with each form action and finally the object user :
<template id="user-view">
<div>
<div class="updateclass">
<form class="form-horizontal" method="PUT" action="updateUrl">
{{ csrf_field() }}
<ul>
<li>
<label for="name"> Name </label>
<input type="text" name="name" :value="user.name">
</li>
<li>
{!! Form::submit('Save', ['class' => 'button-green smaller right']) !!}
</li>
</ul>
{!! Form::close() !!}
</div>
and This is finally how I call the template :
<user-view v-for="user in users" :updateclass="getClassName(user.id)" :user="user" :updateUrl="getUpdateUrl(user.id)"></user-view>
The issue then : it seems that for example [class="updateclass"] doesn't change the value of updateclass with the result of getClassName(user.id) as defined in template call that is binded to. When I try it with [:class="updateclass"] in the template I get : Property or method "updateclass" is not defined on the instance ...
and the same thing applies to all other binded attributes.
The syntax you are using to assign a class dynamically is wrong. from the getClassName method you have to return a object having className like this : {"className: true} , like following
getClassName: function (index) {
var tmp = {}
var className = 'update-user-'+index
tmp[className] = true
return tmp
}
Than you can assign it like following as is in documentation:
<div :class="updateclass"></div>

knockout set css conditionally

Sorry for maybe uncorrect question, but I feel really confused. I need to set the css class of an item in a foreach loop based on the value of an item's property.
self.CssBind = ko.computed(function (task) {
var CssBind = '';
if (getComplexity(task) === 'Easy') {
CssBind = "green";
} else if (getComplexity(task) === 'Intermediate') {
CssBind = 'yellow';}
else if (getComplexity(task) === 'Difficult') {
CssBind = 'red';
}
return CssBind;
});
I tried to get complexity in such way but have undefined.... (in controller there is method that accepts task and returns complexity)
self.complexity = ko.observable();
function getComplexity (task) {
ajaxHelper(taskItem, 'GET').done(function (data) { self.complexity(data); });
};
In html
<div class="panel panel-default" data-bind="foreach:{data:tasks, as: 'task'}">
<div class="panel-heading">
<a data-toggle="collapse" data-parent="#accordion" data-bind="text: Name, attr: { href : '#collapse' + task.Id}, css: {color: CssBind}">
</a>
</div>
<div data-bind="attr: { id : 'collapse' + task.Id}" class="panel-collapse collapse">
<div class="panel-body">
<span data-bind="text: Name"></span>
</div>
</div>
</div>
What to change to make it work?
Your computed is probably defined on the root view-model and when you're calling it you're already in a foreach: tasks scope. You need to use the $root keyword to point to CssBind.
Also, no need for a computed, regular function will do easier (especially with argument passing):
self.CssBind = function (task) {
var CssBind = '';
if (ko.unwrap(task.getComplexity) === 'Easy') {
CssBind = "green";
} else if (self.getComplexity() === 'Intermediate') {
CssBind = 'yellow';}
else if (self.getComplexity() === 'Difficult') {
CssBind = 'red';
}
return CssBind;
});
And in your HTML:
<a data-toggle="collapse"
data-parent="#accordion"
data-bind="text: Name, attr: { href : '#collapse' + task.Id}, style: {color: $root.CssBind.bind(null, task)}">
Please notice that I change the binding handler from css to style (the former is used to apply CSS classes while the latter applies explicit CSS rules).

Resources