How to disable parent page of modal in angular - css

I have a modal window in Angular 4 that works fine but if the user clicks on the background / parent page the modal is closed.
I have found some solutions that suggest using backdrop='static' and keyboard=false when opening the modal but our modal uses a local Dialog class with a BehaviorSubject object so is opened using the .next method. I've also tried setting these attributes using div config but to no avail.
Therefore I'm looking for another solution, maybe using CSS or another setting / attribute that can be directly applied to the parent page or modal HTML.
See below for some of the relevant code.
dialog.component.ts:
constructor(private location: PlatformLocation,
private _dialog: DialogService,
private router: Router) { }
open() {
this.showDialog = true;
const body = document.body;
body.classList.add('cell-modal-open');
}
close() {
this.dialog = undefined;
}
private handleDialog(d: Dialog) {
if (!d) {
this.close();
} else if (d.template) {
if (this.showDialog) {
this.close();
}
this.dialog = d;
this.open();
}
}
ngOnInit() {
this.subscription = this
._dialog
.getDialog()
.subscribe({
next: (d) => { this.handleDialog(d); console.log('subscribed dialog') },
error: (err) => this.handleDialogError(err)
});
this.initialiseRoutingEventListeners();
}
dialog.service.ts
private d: Dialog = { template: null, size: DialogSizeEnum.XLarge };
private dialogSubject = new BehaviorSubject<Dialog>({ template: null, size: DialogSizeEnum.XLarge });
constructor() { }
showDialog(template: TemplateRef<any>, size = DialogSizeEnum.XLarge, requiresAction = false) {
Object.assign(this.d, { template: template, size: size, requiresAction: requiresAction });
if (this.d !== null) {
this.dialogSubject.next(this.d);
}
}
getDialog(): BehaviorSubject<Dialog> {
return this.dialogSubject;
}
clear() {
this.dialogSubject.next(null);
}
Any suggested approaches are welcome!

Added flag to the close() method and adding condition to only set to undefined if true (i.e. from a valid location).

Related

Hide web component until browser knows what to do with it

Similar to this question:
How to prevent flickering with web components?
But different in that I can't just set the inner HTML to nothing until loaded because there is slotted content, and I don't wish to block rendering the page while it executes the web component JS.
I thought I could add CSS to hide the element, and then the init of the webcomponent unhides itself, but then that CSS snippet needs to included where ever the web component is used, which is not very modular, and prone to be forgotten
I am working on modal component, here's the code (although I don't think its particularly relevant:
<div id="BLUR" part="blur" class="display-none">
<div id="DIALOGUE" part="dialogue">
<div id="CLOSE" part="close">
X
</div>
<slot></slot>
</div>
</div>
const name = "wc-modal";
const template = document.getElementById("TEMPLATE_" + name);
class Component extends HTMLElement {
static get observedAttributes() { return ["open"]; } // prettier-ignore
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
connectedCallback() {
if (this.initialised) return; // Prevent initialising twice is item is moved
this.setupEventListners();
this.init();
this._upgradeProperty("open");
this.initialised = true;
}
init() {}
get(id) {
return this.shadowRoot.getElementById(id);
}
_upgradeProperty(prop) {
/*
Setting a property before the component has loaded will result in the setter being overriden by the value. Delete the property and reinstate the setter.
https://developers.google.com/web/fundamentals/web-components/best-practices#lazy-properties
*/
if (this.hasOwnProperty(prop)) {
let value = this[prop];
delete this[prop];
this[prop] = value;
}
}
// Setup Event Listeners ___________________________________________________
setupEventListners() {
this.get("CLOSE").addEventListener("click", () => this.removeAttribute("open"));
this.get("BLUR").addEventListener("click", () => this.removeAttribute("open"));
// If the dialogue does not handle click, it propagates up to the blur, and closes the modal
this.get("DIALOGUE").addEventListener("click", (event) => event.stopPropagation());
}
// Attributes _____________________________________________________________
attributeChangedCallback(name, oldValue, newValue) {
switch (name) {
case "open":
// Disabled is blank string for true, null for false
if (newValue === null) this.hideModal();
else this.showModal();
}
}
// Property Getters/Setters _______________________________________________
get open() { return this.hasAttribute("open"); } // prettier-ignore
set open(value) { value ? this.setAttribute("open", "") : this.removeAttribute("open"); } // prettier-ignore
// Utils & Handlers _______________________________________________________
showModal() {
this.get("BLUR").classList.remove("display-none");
// Disable scrolling of the background
document.body.style.overflow = "hidden";
}
hideModal() {
this.get("BLUR").classList.add("display-none");
// Renable scrolling of the background
document.body.style.overflow = "unset";
}
}
window.customElements.define(name, Component);
Q: How do I hide a web component until the browser knows what to do with it?
A: Here's a solution with outside CSS. Make use of the :defined pseudo class:
class X extends HTMLElement {
constructor() {
super().attachShadow({mode: 'open'}).append(document.createElement('slot'));
}
}
foo.onclick = () => {
customElements.define('ab-cd', X);
foo.disabled = true;
foo.textContent = 'registered!';
}
ab-cd:not(:defined) { display: none; }
<ab-cd>text</ab-cd>
<button id="foo">click to register component</button>
I have tried to see where :defined can cause a FOUC
Only when you apply the display:none too late
<my-element>:not(:defined) { display:none }</my-element>
<style>
my-element:not(:defined) {
border: 2px solid red;
}
my-element:defined {
background: pink;
}
</style>
<style id="STYLE"></style>
<button id="BTN_STYLE">click to style component</button>
<button id="BTN_DEFINE">click to register component</button>
<script>
BTN_STYLE.onclick = () => {
STYLE.innerHTML = `my-element:not(:defined) {display:none}`;
BTN_STYLE.remove();
}
BTN_DEFINE.onclick = () => {
customElements.define('my-element', class extends HTMLElement {
constructor() {
super().attachShadow({mode: 'open'}).innerHTML = `constructed`;
}
connectedCallback(){
setTimeout(() => this.shadowRoot.innerHTML = `connected after 3s`,3e3);
}
});
BTN_DEFINE.remove();
}
</script>

SwiftUI: Button in Form

I am creating a Form in SwiftUi with a section that is including a flexible number of instruction.
Next to the last instruction TextField, I am showing a "+"-Button that is extending the instructions array with a new member:
var body: some View {
NavigationView {
Form {
...
Section(header: Text("Instructions")) {
InstructionsSectionView(instructions: $recipeViewModel.recipe.instructions)
}
...
struct InstructionsSectionView: View {
#Binding var instructions: [String]
var body: some View {
ForEach(instructions.indices, id: \.self) { index in
HStack {
TextField("Instruction", text: $instructions[index])
if(index == instructions.count-1) {
addInstructionButton
}
}
}
}
var addInstructionButton: some View {
Button(action: {
instructions.append("")
}) {
Image(systemName: "plus.circle.fill")
}
}
}
Now the problem is, that the button click-area is not limited to the picture but to the whole last row. Precisely the part just around the textField, meaning if I click in it, I can edit the text, but if I click on the border somewhere, a new entry is added.
I assume that this is specific to Form {} (or also List{}), since it does not happen if I use a Button next to a text field in a "normal" set-up.
Is there something wrong with my code? Is this an expected behaviour?
I am not sure why border is getting tappable, but as a workaround I used plainButtonStyle and that seems to fix this issue, and keeps functionality intact .
struct TestView: View {
#State private var endAmount: CGFloat = 0
#State private var recipeViewModel = ["abc","Deef"]
var body: some View {
NavigationView {
Form {
Section(header: Text("Instructions")) {
InstructionsSectionView(instructions: $recipeViewModel)
}
}
}
}
}
struct InstructionsSectionView: View {
#Binding var instructions: [String]
var body: some View {
ForEach(instructions.indices, id: \.self) { index in
HStack {
TextField("Instruction", text: $instructions[index])
Spacer()
if(index == instructions.count-1) {
addInstructionButton
.buttonStyle(PlainButtonStyle())
.foregroundColor(.blue)
}
}
}
}
var addInstructionButton: some View {
Button(action: {
instructions.append("")
}) {
Image(systemName: "plus.circle.fill")
}
}
}

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));

Aurelia: Stylesheet loaded but not removed

In an aurelia project I have several components that import additional stylesheets, e.g. from semantic-ui. After leaving the components page, the stylesheet is still active and not removed. Is it possible to 'unload' the stylesheets?
Update (2018-03-27):
I submitted a PR to enable this as an opt-in, you can keep track of it here: https://github.com/aurelia/templating-resources/pull/344
Original answer:
A word of warning, this is untested and aurelia-internals-hacky.
What you could do is override the default CSSViewEngineHooks and CSSResource classes to keep track of the style elements it injects, and then add an beforeUnbind hook to remove the styles again.. before unbind (right after detached)
Unfortunately the CSSResource class is not exported from aurelia-templating-resources so we need to go one layer deeper and overwrite the existing style loader plugins that returns instances of CSSResource.
Here's how:
First we grab the code from aurelia-templating-resources/src/css-resource.js, put it in our own src/css-resource.js/ts and make a few tweaks to it (don't think too much of the size, it's just a copy-paste with a few small tweaks, annotated with comments):
import {ViewResources, resource, ViewCompileInstruction} from 'aurelia-templating';
import {Loader} from 'aurelia-loader';
import {Container} from 'aurelia-dependency-injection';
import {relativeToFile} from 'aurelia-path';
import {DOM, FEATURE} from 'aurelia-pal';
let cssUrlMatcher = /url\((?!['"]data)([^)]+)\)/gi;
function fixupCSSUrls(address, css) {
if (typeof css !== 'string') {
throw new Error(`Failed loading required CSS file: ${address}`);
}
return css.replace(cssUrlMatcher, (match, p1) => {
let quote = p1.charAt(0);
if (quote === '\'' || quote === '"') {
p1 = p1.substr(1, p1.length - 2);
}
return 'url(\'' + relativeToFile(p1, address) + '\')';
});
}
class CSSResource {
constructor(address: string) {
this.address = address;
this._scoped = null;
this._global = false;
this._alreadyGloballyInjected = false;
}
initialize(container: Container, target: Function): void {
this._scoped = new target(this);
}
register(registry: ViewResources, name?: string): void {
if (name === 'scoped') {
registry.registerViewEngineHooks(this._scoped);
} else {
this._global = true;
}
}
load(container: Container): Promise<CSSResource> {
return container.get(Loader)
.loadText(this.address)
.catch(err => null)
.then(text => {
text = fixupCSSUrls(this.address, text);
this._scoped.css = text;
if (this._global) {
this._alreadyGloballyInjected = true;
// DOM.injectStyles(text); <- replace this
// this is one of the two possible moments where the style is injected
// _scoped is the CSSViewEngineHooks instance, and we handle the removal there
this._scoped.styleNode = DOM.injectStyles(text);
}
});
}
}
class CSSViewEngineHooks {
constructor(owner: CSSResource) {
this.owner = owner;
this.css = null;
}
beforeCompile(content: DocumentFragment, resources: ViewResources, instruction: ViewCompileInstruction): void {
if (instruction.targetShadowDOM) {
DOM.injectStyles(this.css, content, true);
} else if (FEATURE.scopedCSS) {
let styleNode = DOM.injectStyles(this.css, content, true);
styleNode.setAttribute('scoped', 'scoped');
} else if (this._global && !this.owner._alreadyGloballyInjected) {
// DOM.injectStyles(this.css); <- replace this
// save a reference to the node so we can easily remove it later
this.styleNode = DOM.injectStyles(this.css);
this.owner._alreadyGloballyInjected = true;
}
}
// this is the hook we add, here we remove the node again
beforeUnbind(): void {
if (this._global && this.owner._alreadyGloballyInjected) {
DOM.removeNode(this.styleNode);
this.owner._alreadyGloballyInjected = false;
}
}
}
export function _createCSSResource(address: string): Function {
#resource(new CSSResource(address))
class ViewCSS extends CSSViewEngineHooks {}
return ViewCSS;
}
Then, in our main.ts/js we do the same thing aurelia-templating-resources.js does, but with our own version.
So we do this after the call to aurelia.use.standardConfiguration() etc, to override the existing one
let viewEngine = config.container.get(ViewEngine);
let styleResourcePlugin = {
fetch(address) {
return { [address]: _createCSSResource(address) };
}
};
['.css', '.less', '.sass', '.scss', '.styl'].forEach(ext => viewEngine.addResourcePlugin(ext, styleResourcePlugin));
And that should pretty much do the trick.. :)
I have found a plugin to resolve the issue:
https://github.com/jbockle/aurelia-useable-style-loader
But for the latest Webpack webpack.config.js should be a little bit different than in a plugin readme.
You should load .css files this way:
use: [
{ loader: 'style-loader', options: { injectType: 'lazyStyleTag' } },
'css-loader'
]
Instead of this:
use: ['style-loader/useable', 'css-loader']

Resources