Stencil.js - Listen to class attribute changes on host element - web-component

How do I listen to changes to the class attribute on the host element?

There's an easy way to do it, but that might not be appropriate in all situations.
Simply add 'class' as a #Prop and a #Watch:
#Prop() class: string;
#Watch('class') handleClassChange(class: string) {
console.log(class);
}

Tracking DOM attribute changes requires the use of a MutationObserver.
https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
componentDidLoad() {
// Target element that will be observed
const target = this.el;
const config = {
attributes: true,
attributeOldValue: true,
attributeFilter: ['class']
};
function subscriberCallback(mutations) {
mutations.forEach((mutation) => {
console.log(mutation);
});
}
const observer = new MutationObserver(subscriberCallback);
observer.observe(target, config);
}

Related

Detect change to modelValue in Vue 3

Is there a way to detect change to modelValue in a custom component? I want to push the change to a wysiwyg editor.
I tried watching modelValue but emitting update for modelValue triggered that watch, which created circular data flow.
Code:
export default {
props: ['modelValue'],
watch: {
modelValue (val) {
this.editor.editor.loadHTML(val)
}
},
mounted () {
this.editor.editor.loadHTML(val)
this.editor.addEventListener('trix-change',
(event) => this.$emit('update:modelValue', event.target.value))
}
}
<TextEditor v-model="someHtml"></TextEditor>
In VueJS v3, the event name for custom v-model handling changed to 'update:modelValue'.
You can listen to these events like this: v-on:update:modelValue="handler"
For a more complete example, lets assume you have a Toggle component with these properties/methods:
...
props: {
modelValue: Boolean,
},
data() {
return {
toggleState: false,
};
},
methods: {
toggle() {
this.toggleState = !this.toggleState;
this.$emit('update:modelValue', this.toggleState);
}
}
...
You can use that Toggle component:
<Toggle v-model="someProperty" v-on:update:modelValue="myMethodForTheEvent"/>
As a side note, you could also v-model on a computed property with a setter; allowing you to internalise your state changes without using the update:modelValue event. In this example, it assumes you v-model="customProperty" on your custom Toggle component.
computed: {
customProperty: {
get() {
return this.internalProperty;
},
set(v) {
this.internalProperty = v;
console.log("This runs when the custom component 'updates' the v-model value.");
},
}
},
I had the same problem and solved it using a slight tweak to the way you call the watch function:
setup(props) {
watch(() => props.modelValue, (newValue) => {
// do something
})
}
Hence, the important thing is to add () => props.modelValue instead of just putting props.modelValue as the first argument of the watch function.
try that:
watch: {
...
modelValue: function(val) {
console.log('!!! model value changed ', val);
},
...

How to update widget when new style is applied

I'm making an extension with a list containing a checkbox with a text item (St.label) that change style when toggled.
I'm listening to the toggle event, and as the item is toggled, I set a new style for my text using set_style_class_name() on a my Stlabel. But the style of the object don't change. The only solution that I have found is to destroy and remake all the item of the list and set a different class in the init of the object.
How could I just update the item that have been checked ?
Here the item that I'm using, I put a listener on the checkbox that trigger the toggle() function, in this function I'm updating the class, which should remove the 'text-checked' class and so the text should't have 'text-decoration:line-through' property.
const PopupMenu = imports.ui.popupMenu;
const Lang = imports.lang;
const { Atk, Clutter, St, GObject } = imports.gi;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const CheckboxLib = Me.imports.src.checkbox;
var PopupCheckBoxMenuItem = GObject.registerClass({
Signals: {
'toggled': { param_types: [GObject.TYPE_BOOLEAN] },
'deleted': { param_types: [GObject.TYPE_BOOLEAN] }
},
}, class PopupCheckBoxMenuItem extends PopupMenu.PopupBaseMenuItem {
_init(text, active, params) {
super._init(params);
this.label = new St.Label({
text: text,
y_align:Clutter.ActorAlign.CENTER,
x_expand: true,
style_class: active ? "text-checked" : ""
});
this.tags = new St.Label({
text: "API",
y_align:Clutter.ActorAlign.CENTER,
style_class: "tag-item"
});
this.icon = new St.Button({
style_class: 'remove-task',
can_focus: true,
});
this.icon.connect('clicked', Lang.bind(this,function(){
this.emit('deleted', this._checkbox.state);
}));
this.icon.add_actor(new St.Icon({
icon_name: 'window-close-symbolic',
style_class: 'icon-remove-task'
}));
this._checkbox = new CheckboxLib.CheckBox(active);
this._checkbox.connect('clicked', Lang.bind(this,function(){
this.toggle();
}));
this.accessible_role = Atk.Role.CHECK_MENU_ITEM;
this.checkAccessibleState();
this._statusBin = new St.Bin({
x_align: Clutter.ActorAlign.START,
x_expand: false,
});
this.add_child(this._statusBin);
this.label_actor = this.label;
this.add_child(this.tags);
this.add_child(this.label);
this.add_child(this.icon);
this._statusLabel = new St.Label({
text: '',
style_class: 'popup-status-menu-item',
});
this._statusBin.child = this._checkbox;
}
setStatus(text) {
if (text != null) {
this._statusLabel.text = text;
this._statusBin.child = this._statusLabel;
this.reactive = false;
this.accessible_role = Atk.Role.MENU_ITEM;
} else {
this._statusBin.child = this._checkbox;
this.reactive = true;
this.accessible_role = Atk.Role.CHECK_MENU_ITEM;
}
this.checkAccessibleState();
}
activate(event) {
super.activate(event);
}
toggle() {
this._checkbox.toggle();
this.emit('toggled', this._checkbox.state);
//Updating class
this.label.set_style_class_name("new_class");
this.label.real_style_changed();
this.checkAccessibleState();
}
get state() {
return this._checkbox.state;
}
get delete_icon() {
return this.icon;
}
setToggleState(state) {
this._checkbox.state = state;
this.checkAccessibleState();
}
checkAccessibleState() {
switch (this.accessible_role) {
case Atk.Role.CHECK_MENU_ITEM:
if (this._checkbox.state)
this.add_accessible_state(Atk.StateType.CHECKED);
else
this.remove_accessible_state(Atk.StateType.CHECKED);
break;
default:
this.remove_accessible_state(Atk.StateType.CHECKED);
}
}
});
The problem was the property I changed in the css class.
For an unknown reason, style change doesn't seem to redraw when I set a class with only text-decoration property but if I add a change in the color, even if it is the same color it does work even without St.Widget.style_changed().
So if I do this.label.set_style_class_name("text-checked"); to change my class, the change doesn't work if my css class is as follow :
.text-checked
{
text-decoration: line-through !important;
}
But this work :
.text-checked
{
text-decoration: line-through !important;
color: black;
}
Must be an issue with how the style change event work for Gjs component.
Issue open here : https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/2811
The most direct seems to be St.Widget.style_changed(). This seems to forcibly mark the style state as dirty and trigger a redraw (St.Label is a subclass, so just call myLabel.style_changed()).
The proper route is probably St.Widget.ensure_style(), though.
I didn't look too deep, but the issue may be that widgets aren't being marked as having their style changed or maybe the change isn't being propagated to children or something.

How to update text element after property change in Polymer 3?

So I'm using a data table which has an active element. When that active elment changes I store the name of the active element in a property of my polymer element. Then I display this String property in a div.
Now I know for certain that the property change works, because I console.log it after a change, the div displaying the property doesn't update and continually displays the default value I have set.
export class ProjectsOverview extends PolymerElement {
static get template() {
return html`
...
<div>{{currentProject}}</div>
...
`
}
static get properties() {
return {
currentProject: {
type: String,
value: "placeholder",
notify: true,
reflectToAttribute: true
}
};
}
connectedCallback() {
super.connectedCallback();
const grid = this.shadowRoot.querySelector('vaadin-grid');
grid.addEventListener('active-item-changed', function(event) {
const item = event.detail.value;
grid.selectedItems = [item];
if (item) {
this.set('currentProject', item.name);
} else {
this.set('currentProject', '');
}
console.log(this.currentProject);
});
}
}
My expected result would be that every time the currentProject property is updated, the div displaying the property updates as well.
The active-item-changed callback does not have its context bound to the Polymer instance (i.e., this is the grid and not the Polymer component). Instead of the function expression, use an arrow function to automatically bind this to the correct context.
// grid.addEventListener('active-item-changed', function(event) { // DON'T DO THIS
grid.addEventListener('active-item-changed', (event) => {
/* this is the Polymer instance here */
this.set('currentProject', ...);
})
Your scope is wrong. You're using an anonymous function so when you try to set currentProject, you do that when your this is your anonymous function. Use .bind(this) to fix your problem.
grid.addEventListener('active-item-changed', function(event) {
const item = event.detail.value;
grid.selectedItems = [item];
if (item) {
this.set('currentProject', item.name);
} else {
this.set('currentProject', '');
}
console.log(this.currentProject);
}.bind(this));

Vue js custom select - binding to the

I have a custom select box.
<select-box :options="['Male', 'Female', ]"
title="Gender"
v-bind:value="selected"
v-model="person.gender"
>
</select-box>
The .vue code
<script>
export default {
props:['title', 'options'],
data () {
return {
selected: this.title,
dropdownVisible: false,
}
},
methods: {
toggleOptions() {
this.dropdownVisible = !this.dropdownVisible
},
selectValue(option) {
this.selected = option;
this.toggleOptions();
}
}
}
How can I bind the selected value directly to the model (person.gender)?
I assume that above .vue code belongs to select-box component.
Because I saw you use v-model, to bind value directly to v-model, you need to $emit inside children component.
You can change your selectValue function
selectValue(option) {
this.selected = option;
this.$emit('input', option);
this.toggleOptions();
}

How to use aurelia-validate with a object properties to validate?

I'm using aurelia-validate and my validation works fine if I use variables, but I need it to validate properties of an object rather than a variable:
Here's what works:
import {Validation} from 'aurelia-validation';
import {ensure} from 'aurelia-validation';
import {ItemService} from './service';
export class EditItem {
static inject() {
return [Validation, ItemService];
}
#ensure(function(it){
it.isNotEmpty()
.hasLengthBetween(3,10);
})
name = '';
#ensure(function(it){
it.isNotEmpty()
.hasMinLength(10)
.matches(/^https?:\/\/.{3,}$/) //looks like a url
.matches(/^\S*$/); //no spaces
})
url = '';
constructor(validation, service) {
this.validation = validation.on(this);
this.service = service;
}
activate(params){
return this.service.getItem(params.id).then(res => {
console.log(res);
this.name = res.content.name; //populate
this.url = res.content.url;
});
}
update() {
this.validation.validate().then(
() => {
var data = {
name: this.name,
url: this.url
};
this.service.updateItem(data).then(res => {
this.message = "Thank you!";
})
}
);
}
}
Here's what I'm trying to do (but doesn't work)...also I'm not sure if it's better to keep the properties on the class or have a property called this.item which contains the properties (this is the typical angular way):
import {Validation} from 'aurelia-validation';
import {ensure} from 'aurelia-validation';
import {ItemService} from './service';
export class EditItem {
static inject() {
return [Validation, ItemService];
}
#ensure(function(it){
it.isNotEmpty()
.hasLengthBetween(3,10);
})
this.item.name; //no assignment here should happen
#ensure(function(it){
it.isNotEmpty()
.hasMinLength(10)
.matches(/^https?:\/\/.{3,}$/) //looks like a url
.matches(/^\S*$/); //no spaces
})
this.item.url; //no assignment?
constructor(validation, service) {
this.validation = validation.on(this);
this.service = service;
this.item = null;
}
activate(params){
return this.service.getItem(params.id).then(res => {
console.log(res);
this.item = res.content; //populate with object from api call
});
}
update() {
this.validation.validate().then(
() => {
var data = {
name: this.item.name,
url: this.item.url
};
this.service.updateItem(data).then(res => {
this.message = "Thank you!";
})
}
);
}
}
Can someone give me some guidance here on how to use a validator against an existing object (for an edit page)?
The validation works in all kinds of situations, but using the #ensure decorator can only be used to declare your rules on simple properties (like you found out).
Hence...
Option a: replace the ensure decorator with the fluent API 'ensure' method, this supports 'nested' or 'complex' binding paths such as:
import {Validation} from 'aurelia-validation';
import {ItemService} from './service';
export class EditItem {
static inject() {
return [Validation, ItemService];
}
constructor(validation, service) {
this.validation = validation.on(this)
.ensure('item.url')
.isNotEmpty()
.hasMinLength(10)
.matches(/^https?:\/\/.{3,}$/) //looks like a url
.matches(/^\S*$/)
.ensure('item.name')
.isNotEmpty()
.hasLengthBetween(3,10);
this.service = service;
this.item = null;
}
activate(params){
return this.service.getItem(params.id).then(res => {
console.log(res);
this.item = res.content; //populate with object from api call
});
}
update() {
this.validation.validate().then(
() => {
var data = {
name: this.item.name,
url: this.item.url
};
this.service.updateItem(data).then(res => {
this.message = "Thank you!";
})
}
);
}
}
Note: you can set up your validation even before item is set. Cool, no?
Option b: Since the validation rules are specific to the item, you could move your validation rules inside your item class using the #ensure decorator inside that class instead.
You can then set up validation in your VM after you've retrieved the item: this.validation = validation.on(this.item); or, your service can set up the validation when it returns your item to your VM and make it an intrinsic part of the model: item.validation = validation.on(item);
Option a is easiest and seems to match your experience. Option b is more maintainable, as the validation rules for your model will live on the model, not on the view-model. However if you go with option b, you might have to adjust your HTML a bit to make sure validation hints appear.
Use the .on method of the validator to apply your rules to object properties.
The example below is called after I retrieve an object named stock, it validates that the quantity is not empty and is numeric only. Hope this helps...
let stock = {
name: 'some name'
minimumQuantity: '1'
};
applyRules() {
ValidationRules
.ensure((m: EditStock) => m.minimumQuantity)
.displayName("Minimum Quantity")
.required()
.withMessage(`\${$displayName} cannot be blank.`)
.matches( /^[0-9]*$/)
.withMessage(`\${$displayName} must be numeric only.`)
.on(this.stock);
}

Resources