I have a div that needs to be fixed to the bottom of the screen, but must be the same width as the content it scrolls past. Here's a picture of what I'm talking about:
The problem with just setting the div's width as a percent of the screen size is that there's a sidenav (not shown, but the gray area to the left indicates where it is horizontally; it's above the top of the image) which hides when the screen size gets too small to display it and the mdCards. So, when it's hidden each mdCard takes up a much larger portion of the screen than when it isn't hidden, and all of this is handled by angular because these elements are angular built-ins. However, my fixed div (it's actually also an mdCard, but that's irrelevant... maybe) is not resized in this way, obviously. So I need a way to make its width always the same as its siblings' width. My template looks something like this:
<!-- content container -->
<div>
<!-- bunch of mdCards -->
<md-card class="searchResult">
<!-- This one is guaranteed to exist -->
</md-card>
<md-card class="searchResult" ng-repeat="result in searchResults track by $index">
<!-- These are not -->
</md-card>
<!-- my fixed div -->
<md-card id="totals" ix-totalbar>
</md-card>
</div>
and their styles look something like this:
.searchResult{
box-sizing: border-box;
display: flex;
flex-direction: column;
}
#totalsbar{
position: fixed;
bottom: 55px;
}
So far, I've tried doing this with a directive called ixTotalbar, but it isn't working any way I try it, I tried adding all of the things in comments but none of them properly adjusted the size.
namespace incode.directives.label {
interface IScope extends ng.IScope {
}
export class IncodeTotalsBarDirective implements ng.IDirective {
restrict = 'AE';
public require: 'ngModel';
public scope: Object;
replace = true;
public link: ng.IDirectiveLinkFn | ng.IDirectivePrePost;
constructor() {
this.link = (scope: IScope, element: ng.IAugmentedJQuery, attrs: ng.IAttributes, ctlr: any) => {
element.bind('load',
() => {
element.css({
width: element.siblings()[0].offsetWidth
});
window.addEventListener('resize',
() => {
console.log("window resized");
element.css({
width: element.siblings()[0].offsetWidth
});
});
element.siblings()[0].on('resize',
() => {
console.log("element resized");
element.css({
width: element.siblings()[0].offsetWidth
});
});
});
console.log("Element width: "+element.width().toString() + " Sibling Style: "+element.siblings()[0].style.toString());
}
}
public static factory(): ng.IDirectiveFactory {
var directive = () => new IncodeTotalsBarDirective();
//directive.$inject = ['$window'];
return directive;
}
}
angular.module('incode.module')
.directive('ixTotalbar', incode.directives.label.IncodeTotalsBarDirective.factory());
}
What's interesting is you can see some console.log()s, one of which output's the sibling's style, which I've verified is correct. However, the width isn't being set properly, so I don't understand what I need to do.
Might be similar to what you're looking for, but this is AngularJS 1. Since your comment said anything, here it is:
JS:
app.directive('bindToHeight', function ($window, $parse) {
return {
restrict: 'A',
link: function (scope, elem, attrs) {
var attributes = scope.$eval(attrs['bindToHeight']);
var targetElem = angular.element(document.querySelector(attributes[1]));
elem.css(attributes[0], targetElem.outerHeight());
angular.element($window).on('scroll', function() {
elem.css(attributes[0], targetElem.outerHeight());
});
angular.element($window).on('resize', function() {
elem.css(attributes[0], targetElem.outerHeight());
});
scope.$watch(function () {
return targetElem.outerHeight();
},
function (newValue, oldValue) {
if (newValue != oldValue) {
elem.css(attributes[0], newValue);
}
});
}
};
})
And HTML:
<div id="regularHeightItem"></div>
<div bind-to-height="['height', '#regularHeightItem']" id="height2"></div>
I utilized this for a placeholder which needed to maintain the same height as an element that switched to fixed when you scroll down, but it had dynamic content so it needed to be dynamic itself. You can change the $window.on() stuff based on what you need it to update on.
Related
I need to add a dynamically imported component, just add a virtual tag to specific place in DOM structure. Unfortunately, every method that I found, didn't solve my problem.
How I try it first:
parent component (Editor.vue):
<template>
<div>
<div class="toolbar">
<button #click="addContainer">ADD CONTAINER</button>
</div>
<div id="editor" ref="editor" contenteditable="true">
//here, when in conteneditable div is coursor I need to add dynamically, programically virtual tag <container />
</div>
</div>
</template>
and javascript
<script>
import container from '../container/Container.vue';
export default {
name: "editor",
components: {
container
},
data() {
return {};
},
methods: {
addContainer(){
document.execCommand('insertHTML', false, <container />); // execCommand let me add html in specyfic place, but I have error Unexpected token
}
},
};
And child component that has to be adding how many times user need in exactly place then user need (Container.vue)
<template>
<div
class="container editor--space"
#mouseover="highlightIn"
#mouseout="highlightOut"
contenteditable="true"
>
<div
class="editor--labelspace"
v-if="showLabel"
contenteditable="false"
>
container
</div>
{{ container }}
</div>
</template>
and javascript
<script>
export default {
name: "container",
data() {
return {
showLabel: false,
container: "Container here ..."
};
},
methods: {
highlightIn(){
this.showLabel = true;
},
highlightOut(){
this.showLabel = false;
}
}
};
</script>
Maybe someone can give me some idea, how to do this?
By the help of this: https://stackoverflow.com/a/2925633/7741865 and by dynamically creating the child components, you should achieve what you want. Sample:
addContainer() {
// dynamically create component, replace 'Child' with your component
var ComponentClass = Vue.extend(Child);
var instance = new ComponentClass();
instance.$mount();
// get the caret position and insert component at that place
var sel, range;
if (window.getSelection) {
sel = window.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
range = sel.getRangeAt(0);
range.deleteContents();
range.insertNode(instance.$el);
// remove the highlight (if you want)
window.getSelection().removeAllRanges();
}
}
}
SANDBOX
I am converting an app from polymer 1 to polymer 3. I used juicy-html, but they have not updated to Polymer 3 and I see that there is lit-html. I am wondering how I can change this snippet to using lit-html. It is used for expanding a string like: 'Hello <span class="highlight">world</span>!'
Here is the snippet of code from my polymer 1 component.
<template is="dom-repeat" items="[[item.snippets]]">
<template is="dom-repeat" items="[[item.matches]]">
<div><template is="juicy-html" content$="[[item.text]]"></template></div>
</template>
</template>
Do I need to implement a new component for the inner div? Is there an example that I can look at?
Here is the resulting Polymer 3 element to display a highlighted text within a string:
import {html, LitElement} from '#polymer/lit-element/lit-element.js';
/**
* `search-snippet-highlight`
*
*
* #customElement
* #polymer
* #demo demo/index.html
*/
class SearchSnippetHighlight extends LitElement {
static get properties() {
return {
snippet: { type: String }
};
}
render() {
return html`
<style>.highlight { background-color: yellow; }</style>
<div .innerHTML="${this.sanitizeHtml(this.snippet)}"></div>`;
}
sanitizeHtml(input) {
return input; // TODO: actually sanitize input with sanitize-html library
}
}
window.customElements.define('search-snippet-highlight', SearchSnippetHighlight);
The equivalent of that <template> with juicy-html in Polymer's LitElement (the recommended base element that uses lit-html) is:
render() {
let content = '';
for (const s of this.item.snippets) {
for (const m of s.matches) {
content += `<div>${m.text}</div>`;
}
}
return html`<div .innerHTML="${this.sanitizeHtml(content)}"></div>`;
}
The render function above does the following:
builds an HTML string from inputs
sanitizes the HTML
puts the result into the container div's innerHTML, using LitElement syntax (.PROPERTY="VALUE")
<html>
<head>
<!-- Polyfills only needed for Firefox and Edge. -->
<script src="https://unpkg.com/#webcomponents/webcomponentsjs#latest/webcomponents-loader.js"></script>
</head>
<body>
<!-- Works only on browsers that support Javascript modules like
Chrome, Safari, Firefox 60, Edge 17 -->
<script type="module">
import {LitElement, html} from 'https://unpkg.com/#polymer/lit-element/lit-element.js?module';
class MyElement extends LitElement {
static get properties() {
return {
item: { type: Object }
}
}
constructor() {
super();
this.item = {
snippets: [
{
matches: [
{text: 'hello <span class="highlight">world</span>'},
{text: 'we are the <span class="highlight">world</span>'},
{text: 'war of the <span class="highlight">world</span>s'},
]
},
{
matches: [
{text: 'the <span class="highlight">cat</span> in the hat'},
{text: '<span class="highlight">cat</span>fish are in the water'},
{text: '<span class="highlight">cat</span>erpillars become butterflies'},
]
},
]
};
}
render() {
let content = '';
for (const s of this.item.snippets) {
for (const m of s.matches) {
content += `<div>${m.text}</div>`;
}
}
return html`
<style>.highlight { background: rgb(255, 251, 222); }</style>
<div .innerHTML="${this.sanitizeHtml(content)}"></div>`;
}
sanitizeHtml(input) {
return input; // TODO: actually sanitize input with sanitize-html library
}
}
customElements.define('my-element', MyElement);
</script>
<my-element mood="great"></my-element>
</body>
</html>
I put this in an answer as #tony19 was suggesting it can be formatted properly. I wasn't really asking a separate question, really just asking a rhetorical one rather than offering a separate solution.
You can simplify the approach quite a bit as lit allows you to build up your html in layers through the map function.
render() {
return html`
<div>${this.item.snippets.map(s => s.matches.map(m =>
html`<div>${this.sanitizeHtml(m.text)}</div>`
))}
</div>
`;
}
Now one issue with this (and the accepted answer) is that every render you recalculate the whole thing, including sanitizing the Html. lit-html has a guard directive that can help with this. Adding this will provide for it only re-rendering on a change.
render() {
return html`
<div>${guard(this.item.snippets, () => this.item.snippets.map(s =>
guard(s.matches, () => s.matches.map(m =>
html`<div>${this.sanitizeHtml(m.text)}</div>`
))))}
</div>
`;
}
This does require you impose some discipline on how both this.item.snippets and the underlying matches get updated. You have to ensure that the "Array" references being used change when there is an update. Something like this (assuming the matches are being updated with a new match) and sindex is the index of the snippet you want to update and mindex is the index of the match within that snippet you are updating with newMatch;
this.items.snippets = this.items.snippets.map((snippet, index) => index === sindex ?
{...snippet, matches: snippet.matches.map((match, index) => index === mindex ?
newMatch : match)} : snippet);
I'm trying to reference a component's element in my template and the height is always 0.
export class LoginComponent {
#ViewChild("loginForm", {read: ElementRef})
loginForm;
constructor() {}
ngAfterViewInit() {
console.log("form height: ", this.loginForm.nativeElement.offsetHeight);
}
click() {
console.log("form height: ", this.loginForm.nativeElement.offsetHeight);
}
}
Template
<div class="modal-content"
[style.height.px]="contentHeight">
<login-form #loginForm
(click)="click()"
[class.active]="currentForm === 'login'">
</login-form>
<register-form
[class.active]="currentForm === 'register'">
</register-form>
<div #registerSuccess class="register-success"
[class.active]="currentForm === 'registerSuccess'">
Thank you for registering
</div>
</div>
It's odd because the element is rendering fine and takes up space but even clicking after a few seconds still returns a height of 0.
https://gyazo.com/6504d4f41e6e0072df517082f63fa6ae
I just added setTimeout() in my ngAfterViewInit() function like this:
Simple way:
setTimeout(() => {
// Do what you want here
console.log(this.myElement.nativeElement.offsetHeight);
}, _timeout); // Mine worked even with _timeout = 1
And the output was not zero any-more.
Better way
And 100 percent way that works is:
let offsetHeight = 0;
const refreshInterval = setInterval(() => {
if (offsetHeight === 0) {
offsetHeight = this.giftImage.nativeElement.offsetHeight;
// Do what you want here
console.log(this.giftImage.nativeElement.offsetHeight);
} else {
clearInterval(refreshInterval);
}
}, 10);
You can set :host { display: block } for the component so it will have height. Default is display: inline. If you leave it default, width and height will be 0
I am trying to trigger an event for my reactjs component when it is outside it. Currently I have a collapsible div (blue background) that I want to close once the user clicks outside of it. I have an method pageClick in it to log the event but I can't find a property to use:
componentDidMount() {
window.addEventListener('mousedown', this.pageClick, false)
}
pageClick(e) {
console.log('testing=pageClick', e)
}
How can I detect whether I am on the component with the collapseclass or not so I can change the state of it?
codepen here
You can check the class of the clicked element to know if it belongs to your collapsible element
componentDidMount() {
window.addEventListener('mousedown', this.pageClick.bind(this), false)
// ^^^^^^^^^^
// bind your function pageClick to this so you can call setState inside
}
pageClick(e) {
const el = e.target;
if (e.target.classList.contains('blue')) {
this.setState({ open: false });
}
}
But this is a poor solution because if you have many different DOM nodes in your collapsible element e.target will be the element below the mouse, not the parent .collapse element.
So I suggest you to use a library to detect the click outside your element : react-onclickoutside do the job perfectly.
You can see an implementation of your use case using react-click-outside in this fiddle.
You can listen for click event on the document like this -
document.addEventListener("click", this.closeComponent);
As an example you can define your collapsible component like this -
import React, { Component } from 'react';
export default class CollapsibleComponent extends Component {
constructor(props) {
super(props);
this.state = {
style : {
width : 350
}
};
this.showComponent = this.showComponent.bind(this);
this.closeComponent = this.closeComponent.bind(this);
}
componentDidMount() {
document.addEventListener("click", this.closeComponent);
}
componentWillUnmount() {
document.removeEventListener("click", this.closeComponent);
}
showComponent() {
const style = { width : 350 };
this.setState({ style });
document.body.style.backgroundColor = "rgba(0,0,0,0.4)";
document.addEventListener("click", this.closeComponent);
}
closeComponent() {
document.removeEventListener("click", this.closeComponent);
const style = { width : 0 };
this.setState({ style });
}
render() {
return (
<div
id = "myCollapsibleComp"
ref = "ccomp"
style = {this.state.style}
>
<div className = "comp-container">
<a
href = "javascript:void(0)"
className = "closebtn"
onClick = {this.closeComponent}
>
×
</a>
</div>
</div>
);
}
}
I am creating a webapp with vueJs and bootstrap. I want to change CSS class of an element after a particular amount of scroll, Is there some vue way of doing it.
I want something like following:
<div :class="{classA: scrollPosition < 100, classB: scrollPosition > 100}">
</div>
One option I found is by using vue-scroll, which seems promising, but not working.
Is there some other native way as well to achive the same?
You could try to make it like this
const app = new Vue({
el: '#app',
data: {
scrollPosition: null
},
methods: {
updateScroll() {
this.scrollPosition = window.scrollY
}
},
mounted() {
window.addEventListener('scroll', this.updateScroll);
}
})
You should also consider removing event listener when component is being destroyed, in order to prevent leaks:
destroy() {
window.removeEventListener('scroll', this.updateScroll)
}