Drawer in a component gives warning "Avoid mutating a prop directly..." in Quasar framework - quasar-framework

I use Quasar framework in Version 1.6.2. and want to use a component (Drawer.vue) for my drawer. The component is used in my layout file (MainLayout.vue).
I get the following error message in my console:
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "rightDrawerOpen"
The drawer works, but not on the first click – only on subsequent clicks.
What is the correct way to pass the parents model to my drawer?
Component: Drawer.vue
<template>
<q-drawer show-if-above v-model="rightDrawerOpen" side="right" bordered>
<q-list>
<q-item-label header>Menü</q-item-label>
</q-list>
</q-drawer>
</template>
<script>
export default {
name: "Drawer",
props: {
rightDrawerOpen: Boolean
}
};
</script>
Parent: MainLayout.vue
<Drawer :right-drawer-open="rightDrawerOpen" />

I would move the q-drawer component back into the MainLayout.vue component, then put the q-list into the child component. Then toggling "rightDrawerOpen" will occur in the "data" of MainLayout.vue instead of on the props of the child component which is where the error is coming from. Example:
MainLayout.vue:
<template>
<q-layout>
<q-drawer show-if-above v-model="rightDrawerOpen" side="right" bordered>
<DrawerContents />
</q-drawer>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<script>
import DrawerContents from './DrawerContents.vue';
export default {
components: {
DrawerContents,
},
data() {
return {
rightDrawerOpen: true,
};
},
};
</script>
DrawerContents.vue:
<template>
<q-list>
<q-item-label header>Menü</q-item-label>
</q-list>
</template>

Related

Vue3 with ASP.NET MVC: Props not being passed in

I have an ASP.NET MVC application which I've inherited. The most recent new bits of functionality use custom Vue2 components. Each MVC view is effectively a Single Page Application as I understand it. Each view has its own index.js file e.g.
import Vue from "vue"
window.Vue = Vue
import Test from '#/components/test'
const vm = new Vue({
el: "#test",
components: {
Test
},
data() {
return {}
}
})
The top level component is a single file component e.g. Test.vue:
<template>
<div>
<h1>{{ message }}</h1>
</div>
</template>
<script>
export default {
name: 'Test',
props: {
message: {
type: String,
default: 'This has not worked'
}
}
}
</script>
The MVC view itself looks like:
#{
ViewBag.Title = "Test";
Layout = "~/Views/Shared/_MentorNet.cshtml";
}
<div id="test" v-cloak>
<test message="This has worked"></test>
</div>
#section scripts
{
<script src="#Url.Content("~/dist/js/test.js")" defer></script>
}
The /dist/js/test.js file is created as a result of running Webpack with vue-loader.
With Vue2 this works fine. In this example the text "This has worked" appears when the controller returns the view.
However I'm trying to upgrade to Vue3 and this isn't working. The index.js file now looks like:
import { createApp } from "vue"
import Test from '#/views/admin/test'
const app = createApp(Test);
app.mount("#test");
The component and the view are the same. The component is displayed but the props are not passed through - the view returns the text "This has not worked".
I've looked at the Vue2 migration guide and can't see any breaking changes that would affect this. Can anyone help please?
I've got the answer thanks to a response on the Vue Land Discord channel.
The problem was with the code in the index.js file:
const app = createApp(Test);
app.mount("#test");
When passing a component with a template/render function to createApp it completely overwrites the content of the mount target, in this case
<div id="test">
<test message="This has worked"></test>
</div>
so the
<test message="This has worked"></test>
isn't used. The correct way to do it is either to pass the props as the second argument of create App:
const app = createApp(Test, {
message: "This has worked"
});
or, and the way I'm going to be doing it:
const app = createApp({
components: { Test }
});

Why are shadow DOM events in a setTimeout re-targeted?

A custom element with a shadow DOM that listens for events on itself will (may?) have its events retargeted if those events are read from within a setTimeout:
<script>
class Widget extends HTMLElement {
constructor() {
super();
this.attachShadowDom();
this.registerListener();
}
attachShadowDom() {
let shadow = this.attachShadow({mode: 'open'});
let templateClone = document.querySelector("#widget-template").content.cloneNode(true);
shadow.appendChild(templateClone);
}
registerListener() {
this.shadowRoot.querySelector("#my-input").addEventListener("input", (event) => {
console.log(event.target);
setTimeout(() => console.log(event.target), 0);
});
}
}
document.addEventListener("DOMContentLoaded", () => {
customElements.define("my-widget", Widget);
});
</script>
<html>
<my-widget></my-widget>
</html>
<template id="widget-template">
<input type="text" id="my-input">
</template>
Each character entered into the input element above logs 2 separate event targets to the console: the input element, and the my-widget element. I am wondering if this is by design?
Yes, it is by design.
As explained in javascript.info:
The idea behind shadow tree is to encapsulate internal implementation details of a component [...]
So, to keep the details encapsulated, the browser retargets the event.
And from Google presentation of Shadow DOM:
When an event bubbles up from shadow DOM its target is adjusted to maintain the encapsulation that shadow DOM provides. That is, events are re-targeted to look like they've come from the component rather than internal elements within your shadow DOM.

Changes don't appear to fire for computed bindings

If I create an observer for all changes on a structure object, the observer will get called unless the the binding is a change to a value in a computed binding.
Is this the expected behavior? If so, how can I capture changes to the property in the computed binding?
Example:
<link rel="import" href="../../bower_components/paper-input/paper-input.html">
<dom-module id="binding-test">
<template>
<paper-input label="Not computed" value="{{myObject.prop1}}"></paper-input>
<paper-input label="Computed" value="{{computeIt(myObject.prop2)}}"></paper-input>
</template>
<script>
Polymer({
is:"binding-test",
properties: {
myObject: {
type: Object,
notify: true,
value: {
prop1: 1,
prop2: 2
}
}
},
observers: [
'somethingChanged(myObject.*)'
],
somethingChanged: function(changeRecord) {
// This code is never executed when the Computed input field is changed
console.log(changeRecord);
},
computeIt: function(value) {
return value;
}
});
</script>
</dom-module>
I could be wrong with this one but I think computed binding is one-way, same as computed property.
If you really want to notify the change on a paper-input like that, you can listen to the value-changed event and then do a notifyPath/set on "myObject.prop2".
<paper-input label="Computed" on-value-changed="valueChanged" value="{{computeIt(myObject.prop2)}}"></paper-input>
valueChanged: function(e) {
this.set("myObject.prop2", e.detail.value);
}
Check out this plunker.
Update
I think there's a better solution for your problem. Instead of converting values back and forth using expressions/filters, paper-input now allows you to define prefix and suffix like the following -
<paper-input label="revenue" type="number">
<div prefix>$</div>
</paper-input>
<paper-input label="email">
<div suffix>#email.com</div>
</paper-input>
You can even define complex inputs like this (you will need to create your own date-input element though) -
<paper-input-container auto-validate>
<label>Social Security Number</label>
<ssn-input class="paper-input-input"></ssn-input>
<paper-input-error>SSN invalid!</paper-input-error>
</paper-input-container>
Code samples above are taken from here. You can read more about it on Polymer's official website over here.

Data binding in a dynamically inserted polymer element

There're some times when we could need adding a custom element dynamically into a context. Then:
The inserted polymer could receive some properties bound to another
property inside the context, so it can change accordingly.
At polymer 0.5 we could use PathObserver to binding a property to a
context property for a recently added component. However, I did not
find a workaround or equivalent at polymer 1.0.
I have created an example for 0.5 and just the same for 1.0. See below the code of the polymer that it makes the injection. Also you can see the full plunker examples for clarity.
Ej 0.5:
<polymer-element name="main-context">
<template>
<one-element foo="{{foo}}"></one-element>
<div id="dynamic">
</div>
</template>
<script>
Polymer({
domReady: function() {
// injecting component into polymer and binding foo via PathObserver
var el = document.createElement("another-element");
el.bind("foo", new PathObserver(this,"foo"));
this.$.dynamic.appendChild(el);
}
});
</script>
</polymer-element>
Please, see the full plunkr example: http://plnkr.co/edit/2Aj3LcGP1t42xo1eq5V6?p=preview
Ej 1.0:
<dom-module id="main-context">
<template>
<one-element foo="{{foo}}"></one-element>
<div id="dynamic">
</div>
</template>
</dom-module>
<script>
Polymer({
is: "main-context",
ready: function() {
// injecting component into polymer and binding foo via PathObserver
var el = document.createElement("another-element");
// FIXME, there's no a path observer: el.bind("foo", new PathObserver(this,"foo"));
this.$.dynamic.appendChild(el);
}
});
</script>
Please, see the full plunkr example: http://plnkr.co/edit/K463dqEqduNH10AqSzhp?p=preview
Do you know some workaround or equivalent with polymer 1.0?
Right now, there is no direct way to do it. You should manually do the double binding by listening to changes in the foo property of the parent element and listening to the foo-changed event of the programmatically created element.
<script>
Polymer({
is: "main-context",
properties: {
foo: {
type: String,
observer: 'fooChanged'
}
},
ready: function() {
var self = this;
var el = this.$.el = document.createElement("another-element");
//set the initial value of child's foo property
el.foo = this.foo;
//listen to foo-changed event to sync with parent's foo property
el.addEventListener("foo-changed", function(ev){
self.foo = this.foo;
});
this.$.dynamic.appendChild(el);
},
//sync changes in parent's foo property with child's foo property
fooChanged: function(newValue) {
if (this.$.el) {
this.$.el.foo = newValue;
}
}
});
</script>
You can see a working example here: http://plnkr.co/edit/mZd7BNTvXlqJdJ5xSq0o?p=preview
Unfortunately I think it's not possible to do this by a "clean" way. To replace the Path Observer, we have to add link on the "foo" value changes to the dynamic elements. The first step is observe the "foo" property value changes. The second step is replicate the changes to each dynamic elements created.
<dom-module id="main-context">
<template>
<one-element foo="{{foo}}"></one-element>
<div id="dynamic">
</div>
</template>
</dom-module>
<script>
Polymer({
is: "main-context",
// Properties used to make the link between the foo property and the dynamic elements.
properties: {
foo: {
type: String,
observer: 'fooChanged'
},
dynamicElements: {
type: Array,
value: []
}
},
ready: function() {
// injecting component into polymer and binding foo via PathObserver
var el = document.createElement("another-element");
// Keeps a reference to the elements dynamically created
this.dynamicElements.push(el);
this.$.dynamic.appendChild(el);
},
// Propagates the "foo" property value changes
fooChanged: function(newValue) {
this.dynamicElements.forEach(function(el) {
el.foo = newValue;
});
}
});
</script>
See the full Plunkr example: http://plnkr.co/edit/TSqcikNH5bpPk9AQufez

How to remove a child component with a delete button in the child itself

I have an email component (email-tag.html) that consist of a label, a select and a delete button element.
The email-tag.html component is hosted in its parent email-view-tag.html. email-view-tag contains an add-email-button that adds the email-tag element to the DOM each time it is clicked.
I need help in removing an added email-tag component when its delete-button is clicked. It is the compnoent that contains the delete-button that should be removed.
The two components are shown below:
email-tag.html
<!DOCTYPE html>
<polymer-element name='email-tag'>
<template>
<style>
.main-flex-container
{
display:flex;
flex-flow:row wrap;
align-content:flex-start;
}
.col
{
display:flex;
flex-flow:column;
align-content:flex-start;
flex-grow:1;
}
</style>
<div id='email' class='main-flex-container'>
<section id='col1' class='col'>
<input id=emailTxt
type='text'
list='_emails'
value='{{webContact.homeEmail}}'>
<datalist id='_emails'>
<template repeat='{{email in emails}}'>
<option value='{{email}}'>{{email}}</option>
</template>
</datalist>
</section>
<section id='col2' class='col'>
<button id='delete-email-btn' type='button' on-click='{{deletePhone}}'>Delete</button>
</section>
</div>
</template>
<script type="application/dart">
import 'package:polymer/polymer.dart' show CustomTag, PolymerElement;
import 'dart:html' show Event, Node;
#CustomTag( 'email-tag' )
class EmailElement extends PolymerElement
{
//#observable
EmailElement.created() : super.created();
List<String> emails = [ '', 'Home', 'Personal', 'Private', 'Work', ];
void deletePhone( Event e, var detail, Node target)
{
//shadowRoot.querySelector('#new-phone' ).remove();
//print( 'Current row deleted' );
}
}
</script>
</polymer-element>
email-view-tag.html
<!DOCTYPE html>
<link rel="import" href="email-tag.html">
<polymer-element name='email-view-tag'>
<template>
<style>
.main-flex-container
{
display:flex;
flex-flow:row wrap;
align-content:flex-start;
}
.col
{
display:flex;
flex-flow:column;
align-content:flex-start;
flex-grow:1;
}
</style>
<div id='email-view' class='main-flex-container'>
<section id='row0' >
<button id='add-email-btn' type='button' on-click='{{addPhone}}'>Add Phone</button>
</section >
<section id='rows' class='col'>
<!-- <epimss-phone-header-tag id='col1' class='col'></epimss-phone-header-tag> -->
</section>
</div>
</template>
<script type="application/dart">
import 'package:polymer/polymer.dart' show CustomTag, PolymerElement;
import 'dart:html' show Event, Node, Element;
#CustomTag( 'email-view-tag' )
class EmailViewElement extends PolymerElement
{
//#observable
EmailViewElement.created() : super.created();
void addPhone( Event e, var detail, Node target )
{
$[ 'rows' ].children.add( new Element.tag( 'email-tag' ) );
}
#override
void attached() {
super.attached();
$[ 'add-email-btn' ].click();
}
}
</script>
</polymer-element>
The application does execute normally and clicking the add button does add the email component. The delete button does not work - it is here I am asking for help.
Thanks
The child component, <email-tag> should not be in the business of deleting itself. Instead, it should delegate that responsibility to the the parent component, email-view-tag, by dispatching a custom event.
Here is the code for dispatching a custom event from deletePhone:
void deletePhone( Event e, var detail, Node target){
dispatchEvent(new CustomEvent('notneeded'));
}
Then, in the parent, <custom-view>, change your code for adding <email-tag>s like so:
void addPhone( Event e, var detail, Node target ) {
$['rows'].children.add( new Element.tag('email-tag'));
$['rows'].on["notneeded"].listen((Event e) {
(e.target as Element).remove();
});
}
Also, I would change the name of deletePhone, since the method no longer deletes the record but merely informs the parent that it is not needed. Call it 'notNeeded' or something similar.
EDIT
#ShailenTuli is right about encapsulation should not be broken.
But also JS Polymer elements access the parent in their layout elements because it's still convenient in some scenarios.
This works now in PolymerDart too.
(this.parentNode as ShadowRoot).host
ORIGINAL
You can fire an event and make the email-view-tag listen to this tag and the event handler can remove the event target from it's childs.
I had a similar question a while ago:
How to access parent model from polymer component
This was actually the question I wanted refer to
How can I access the host of a custom element
but the first one may be of some use too.
PolymerJS FAQ - When is the best time to access an element’s parent node?
attached() currently still named enteredView() in Dart, but will be renamed probably soon.

Resources