I have defined an error banner in my angular app which is supposed to be visible only when an error event is triggered and there it is supposed to be hidden when the page loads.
Here is my jade code:
div.container.content(ng-init='root.error.show = false')
div.col-md-12.error-container.ng-cloak(ng-show='root.error.show')
div.alert.alert-danger.alert-dismissible(role='alert')
button.close(type='button')
span(aria-hidden='true', ng-click='root.error.show = !root.error.show') ×
span.sr-only Close
p {{ root.error.message }}
My controller
exports.RootCtrl = function RootCtrl($scope, $log) {
var self = this;
this.error = {
show: false,
message: 'Oups, something wrong just happend'
}
$scope.$on('error', function(event, data) {
self.error.show = true;
self.error.message = data;
})
}
My problem is while angular is loading, the banner is visible with {{ root.error.message }} as an error message.
I have tried using ng-cloak (body(ng-cloak)) and ng-init to hide it but it is not working.
I believe I can tweak the css to play with the display properties but this would be quite messy.
What are the common best practices to solve this?
As per the AngularJS website (https://docs.angularjs.org/api/ng/directive/ngCloak) the solution is the following CSS:
[ng\:cloak], [ng-cloak], .ng-cloak {
display: none !important;
}
Related
I have dynamic list of 'posts' where I wanted to truncate the text (if it goes beyound a certain # of lines) and show a Read More button that users can click to show the entire text.
In VueJS, I decided to attach a ref to the div I want to append the button to (if the text is truncated).
The component is just a button really but it has some stylings and behaviors I want to copy over. The reason why this got more complicated then it needs to (bad thing?) is because I'm doing the truncating with CSS. I understand that using Javascript might have been easier.
So anyways, how can I dynamically add a component to this div (or its parent) using Javascript only? My own reference to the location would be the ref item.
// code after the promise of getting the posts has resolved in the created() hook
.then(() => {
const posts = this.$refs.posts
posts.forEach(p => {
if (this.Overflown(f)) {
// I want to attach a component (AwesomeButtonComponent) to this p div.
}
}
})
And for clarity:
HTML:
<div v-for="post in posts">
<div class="postBody ref="posts">{{ post.body }}</div>
</div>
isOverflown(el) {
return el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth;
}
The CSS that is truncating the text
.postsBody {
white-space: pre-line;
display: -webkit-box;
-webkit-line-clamp: 5;
-webkit-box-orient: vertical;
overflow: hidden;
}
ALTERNATIVE POSSIBILITIES:
This button will only have ONE functionality, so it being a component is not important and adding styling isn't so difficult.
.then(() => {
const announcementBodies = this.$refs.announcementBody;
announcementBodies.forEach(a => {
if (this.isOverflown(a)) {
const button = document.createElement('button');
button.innerText = 'Click Me';
button.onClick = 'doThis';
a.parentElement.appendChild(button);
}
});
In which case the difficult part would be to add a v-on:click directive to that button and then target that specific tag to remove the clamp css attribute.
Following our discussion into comments, I'll show you 2 way you can do this and try to explain the difference between them and let you decide how you will achieve this.
EXAMPLE ONE
The first example is the shortest I could do. This will need every post to have an isOverflow attribute. There is many way to do it client or server side. The other example will not need it.
<div v-for="post in posts">
{{(post.isOverflow == true) ? post.body.substring(0,3)+'...' : post.body}} <button v-on:click="post.isOverflow = !post.isOverflow">{{(post.isOverflow == true) ? 'SHOW MORE' : 'SHOW LESS'}}</button>
</div>
This is not beautiful, but it work and it let you understand that you can manipulate the post inside the v-for. Each button will be automatically associate with the right post, so when you will click it, only the post associated will be affected.
EXAMPLE TWO
The other example i'll give you is by creating a new component for each post. Let's start with the v-for:
<post-component v-for="post in posts" v-bind:key="post.id" v-bind:post="post"></post-component>
And the new component:
<template>
<div v-bind:class="{'postsBody': isOverflow}">
{{post.body}}
<button v-on:click="changeState()">{{(post.isOverflow) ? 'SHOW LESS' : 'SHOW MORE'}}</button>
</div>
</template>
<script>
export default {
props: {
post:{}
},
data() {
return {
isOverflow: true
}
},
methods: {
changeState: function() {
this.isOverflow = !this.isOverflow;
}
},
}
</script>
<style> //Please, put this in a CSS file, it's only for the example purpose.
.postsBody {
white-space: pre-line;
display: -webkit-box;
-webkit-line-clamp: 5;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
CONCLUSION
In the end, both of them will have the same result. The difference is what you prefer. I tried to show you two different way to let you understand how things work with Vue. Let me know if you need more explanations.
I am trying to create a directive which will take the html content and place it n a jsPanel element.
So far I am able to compile the scope and able get elements with the scope. Then i tried to get html tag with ng-repeat, where i got stuck.
Following is the code used to create the directive
mainApp.directive('jspanelcontent', function($compile) {
console.log("loading");
var directive = {};
directive.restrict = 'AE';
directive.compile = function(element, attributes) {
var linkFunction = function($scope, element, attributes) {
console.log(element.html());
var contenthtml = element.html();
contenthtml = angular.element($compile(contenthtml)($scope));
element.html("");
$scope.jspanleinstance = $.jsPanel({
position: {
my: "left-bottom",
at: "left-bottom",
offsetY: 15
},
theme: "rebeccapurple",
contentSize: {
width: 600,
height: 350
},
headerTitle: attributes.panelcaption,
content: contenthtml,
});
// });
}
return linkFunction;
}
return directive;
});
And using like following in html
<div jspanelcontent panelcaption="list">
<div ng-controller="ListController">
{{test}}
<div ng-repeat="user in users">
<span>{{user.id}}</span>
<span>{{user.name}}</span>
<span>{{user.email}}</span>
<br />
</div>
</div>
The console log returns as
Output i am getting (jsPanel)
As you see the "test" variable is properly bind and the ng-repeat element is display as commented, I am not aware why it's returning like that. If anyone can figure out the issue, then i might get it working.
I know i can access the users data as $scope.users inside the directive. The problem here is that user able to use the directive commonly, so i have to assume that i don't the variables in the $scope.
I stuck in this place, and couldn't find any solutions to try. Any suggestions or solutions will be more helpful. :)
NOTE: No syntax errors, outside the directive the data is displaying (Tested)
I'm now trying to hide clicked row after changing status in angularjs. Here is my coding and please let me know how to do it?
table.table
tr(data-ng-repeat="application in job.applications", ng-hide="application.hideApplication")
td.status
div.bold #{getMessage('Change Status:')}
div.normal
a(ng-class="app_status === 'shortlist' ? 'admin_edit_bold' : 'admin_edit_normal'", ng-click="changeApplicationStatus(application.id, 'shortlist', application)") #{getMessage('Shortlist')}
td.rating
div(ng-init='rating = application.app_rating')
.star-rating(star-rating='', rating-value='rating', data-max='5', on-rating-selected='rateFunction(application.id, rating)')
Here is controllerjs.
$scope.changeApplicationStatus = function (appId, app_status, application) {
return jobsService.changeApplicationStatus(appId, app_status).then(
function () {
application.hideApplication = false;
}
);
};
Put this attribute on whichever element you're wanting to show/hide
ng-hide="application.hideApplication"
Edit subsequent to comment:
That attribute wouldn't work on the same element as the ng-repeat, I don't think the application variable would be in scope...
Instead, you could change your repeat to:
application in job.applications | filter: { hideApplication : false }
I've been messing around with aurelia-dialog trying to get a modal dynamically populated with some information. I have some stuff working but the modal is the incorrect size for the data its displaying.
welcome.js
import {DialogService} from 'aurelia-dialog';
import {CmdModal} from './cmd-modal';
export class Welcome {
static inject = [DialogService];
constructor(dialogService) {
this.dialogService = dialogService;
}
OpenCmd(intName, opName, opDescription, parameters){
var cmd = { "CmdName" : opName, "Description" : opDescription, "Params" : parameters};
this.dialogService.open({ viewModel: CmdModal, model: cmd}).then(response => {
if (!response.wasCancelled) {
console.log('good - ', response.output);
} else {
console.log('bad');
}
console.log(response.output);
});
}
cmd-modal.html
<template>
<ai-dialog>
<ai-dialog-header>
<h2>${cmd.CmdName}</h2>
</ai-dialog-header>
<ai-dialog-body>
<p>${cmd.Description}</p>
<b>Parameters</b>
<div repeat.for="param of cmd.Params">
<p class="col-md-6">${param.Key}</p>
<p class="col-md-6">${param.Value}</p>
</div>
</ai-dialog-body>
<ai-dialog-footer>
<button click.trigger="controller.cancel()">Cancel</button>
<button click.trigger="controller.ok(person)">Ok</button>
</ai-dialog-footer>
</ai-dialog>
</template>
cmd-modal.js
import {DialogController} from 'aurelia-dialog';
export class CmdModal {
static inject = [DialogController];
constructor(controller){
this.controller = controller;
}
activate(cmd){
this.cmd = cmd;
}
}
When a link is clicked, a modal like the following is displayed:
As the image shows, the modal is the wrong size for the body and some of the text spills over the side. I think this is because cmd-modal.html is being rendered before the data for the repeater has been inserted.
Does anybody know how I could resize the modal to be the correct size for the body or delay the modal display until cmd-modal.htmlhas been correctly evaluated?
You can add style for width and height to the ai-dialog tag like this:
<ai-dialog style="width:600px; height: 350px;">
I think I found something similar to this when trying to add items of varying width to the dialog. The widths weren't know until after the dialog had been rendered. Well I think that is why!
In the end I added a CSS class on the ai-dialog element which included a general width setting and a media query.
...
width: 90vw;
#media (min-width: 46em) {
width: 44em;
}
....
I know I mixed vw and em measurements and there's probably better ways - but it works well in this app. I'm sure there's probably a "correct" Aurelia way to get the dialog to re-render but this is ample for our situation.
FWIW I also added a "margin-top: 4em !important" so that the dialog would appear just below the fixed header bar that Bootstrap was providing us.
I am quite new with Meteor but have really been enjoying it and this is my first reactive app that I am building.
I would like to know a way that I can remove the .main element when the user clicks or maybe a better way would be to remove the existing template (with main content) and then replace with another meteor template? Something like this would be simple and straightforward in html/js app (user clicks-> remove el from dom) but here it is not all that clear.
I am just looking to learn and for some insight on best practice.
//gallery.html
<template name="gallery">
<div class="main">First run info.... Only on first visit should user see this info.</div>
<div id="gallery">
<img src="{{selectedPhoto.url}}">
</div>
</template>
//gallery.js
firstRun = true;
Template.gallery.events({
'click .main' : function(){
$(".main").fadeOut();
firstRun = false;
}
})
if (Meteor.isClient) {
function showSelectedPhoto(photo){
var container = $('#gallery');
container.fadeOut(1000, function(){
Session.set('selectedPhoto', photo);
Template.gallery.rendered = function(){
var $gallery = $(this.lastNode);
if(!firstRun){
$(".main").css({display:"none"});
console.log("not");
}
setTimeout(function(){
$gallery.fadeIn(1000);
}, 1000)
}
});
}
Deps.autorun(function(){
selectedPhoto = Photos.findOne({active : true});
showSelectedPhoto(selectedPhoto);
});
Meteor.setInterval(function(){
selectedPhoto = Session.get('selectedPhoto');
//some selections happen here for getting photos.
Photos.update({_id: selectedPhoto._id}, { $set: { active: false } });
Photos.update({_id: newPhoto._id}, { $set: { active: true } });
}, 10000 );
}
If you want to hide or show an element conditionaly you should use the reactive behavior of Meteor: Add a condition to your template:
<template name="gallery">
{{#if isFirstRun}}
<div class="main">First run info.... Only on first visit should user see this info.</div>
{{/if}}
<div id="gallery">
<img src="{{selectedPhoto.url}}">
</div>
</template>
then add a helper to your template:
Template.gallery.isFirstRun = function(){
// because the Session variable will most probably be undefined the first time
return !Session.get("hasRun");
}
and change the action on click:
Template.gallery.events({
'click .main' : function(){
$(".main").fadeOut();
Session.set("hasRun", true);
}
})
you still get to fade out the element but then instead of hiding it or removing it and having it come back on the next render you ensure that it will never come back.
the render is triggered by changing the Sessionvariable, which is reactive.
I think using conditional templates is a better approach,
{{#if firstRun }}
<div class="main">First run info.... Only on first visit should user see this info.</div>
{{else}}
gallery ...
{{/if}}
You'll have to make firstRun a session variable, so that it'll trigger DOM updates.
Meteor is reactive. You don't need to write the logic for redrawing the DOM when the data changes. Just write the code that when X button is clicked, Y is removed from the database. That's it; you don't need to trouble yourself with any interface/DOM changes or template removal/redrawing or any of that. Whenever the data that underpins a template changes, Meteor automatically rerenders the template with the updated data. This is Meteor’s core feature.