SSL configuration issue with RabbitMQ Web-Stomp Plugin - ssl

Firstly, I followed this to generate keys, certificates and CA certificates to directories which are client, server and testca. Then I verified, SSL works.
Then I followed this to configure RabbitMQ Web-Stomp Plugin, and my ssl_config is as following:
[
{rabbitmq_web_stomp,
[{ssl_config, [{port, 15671},
{backlog, 1024},
{certfile, "path/to/certs/client/cert.pem"},
{keyfile, "path/to/certs/client/key.pem"},
{cacertfile, "path/to/certs/testca/cacert.pem"},
{password, "changeme"}]}]}
].
However, when I tried to connect it via websockets by following code, which is copied from here, and I made some modifications.
<!DOCTYPE html>
<html><head>
<script src="jquery.min.js"></script>
<script src="stomp.js"></script>
<style>
.box {
width: 440px;
float: left;
margin: 0 20px 0 20px;
}
.box div, .box input {
border: 1px solid;
-moz-border-radius: 4px;
border-radius: 4px;
width: 100%;
padding: 5px;
margin: 3px 0 10px 0;
}
.box div {
border-color: grey;
height: 300px;
overflow: auto;
}
div code {
display: block;
}
#first div code {
-moz-border-radius: 2px;
border-radius: 2px;
border: 1px solid #eee;
margin-bottom: 5px;
}
#second div {
font-size: 0.8em;
}
</style>
<title>RabbitMQ Web STOMP Examples : Echo Server</title>
<link href="main.css" rel="stylesheet" type="text/css"/>
</head><body lang="en">
<h1>RabbitMQ Web STOMP Examples > Echo Server</h1>
<div id="first" class="box">
<h2>Received</h2>
<div></div>
<form><input autocomplete="off" value="Type here..."></input></form>
</div>
<div id="second" class="box">
<h2>Logs</h2>
<div></div>
</div>
<script>
var has_had_focus = false;
var pipe = function(el_name, send) {
var div = $(el_name + ' div');
var inp = $(el_name + ' input');
var form = $(el_name + ' form');
var print = function(m, p) {
p = (p === undefined) ? '' : JSON.stringify(p);
div.append($("<code>").text(m + ' ' + p));
div.scrollTop(div.scrollTop() + 10000);
};
if (send) {
form.submit(function() {
send(inp.val());
inp.val('');
return false;
});
}
return print;
};
// Stomp.js boilerplate
var client = Stomp.client('wss://192.168.111.131:15671/ws');
client.debug = pipe('#second');
var print_first = pipe('#first', function(data) {
client.send('/queue/webstomp', {"content-type":"text/plain"}, data);
});
var on_connect = function(x) {
id = client.subscribe("/queue/webstomp", function(d) {
print_first(d.body);
});
};
var on_error = function() {
console.log('error');
};
client.connect('test', 'test', on_connect, on_error, '/');
$('#first input').focus(function() {
if (!has_had_focus) {
has_had_focus = true;
$(this).val("");
}
});
</script>
</body></html>
it replied me that I lost connection as following screenshot.
I'd be really appreciate any helpful suggestion on this issue.
BTW: this code example works when I didn't use SSL.

Finally I figured this out by referring this post, so the key point is to explicitly authorized my certificate by visiting the address in https first, in my case is wss://192.168.111.131:15671/ws. So I need to visit https://192.168.111.131:15671/ws in browser and authorize the exception and then I can make my wss connection normally.

Related

I don't understand what is wrong in my code - Password generator with reCAPTCHA

So I have this code for a password generator with reCAPTCHA as a term of use.
this is the code:
<!DOCTYPE html>
<html>
<head>
<style>
#password-container {
width: 500px;
margin: 0 auto;
text-align: center;
font-size: 20px;
font-weight: bold;
padding: 20px;
border: 2px solid black;
border-radius: 10px;
}
button {
margin-top: 20px;
padding: 10px 20px;
border-radius: 5px;
background-color: blue;
color: white;
cursor: pointer;
}
</style>
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
</head>
<body>
<div id="password-container">
<div id="password">Press the button to generate new password</div>
<button id="generate-button">new password</button>
<button id="copy-button" style="display: none;">copy to clipboard</button>
<br><br>
<div class="g-recaptcha" data-sitekey="your-site-key"></div>
</div>
<script>
function generatePassword() {
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!##$%^&*()_+-=[]{};:'\"\\|,.<>/?`~";
const numbers = "0123456789";
const symbols = "!##$%^&*()_+-=[]{};:'\"\\|,.<>/?`~";
let password = "";
for (let i = 0; i < 8; i++) {
password += chars.charAt(Math.floor(Math.random() * chars.length));
}
for (let i = 0; i < 2; i++) {
password += numbers.charAt(Math.floor(Math.random() * numbers.length));
}
for (let i = 0; i < 2; i++) {
password += symbols.charAt(Math.floor(Math.random() * symbols.length));
}
return password;
}
document.querySelector("#generate-button").addEventListener("click", function() {
const password = generatePassword();
document.querySelector("#password").innerHTML = password;
document.querySelector("#copy-button").style.display = "inline-block";
});
document.querySelector("#copy-button").addEventListener("click", function() {
const password = document.querySelector("#password").innerHTML;
navigator.clipboard.writeText(password).then(function() {
alert("copied to clipboard");
});
});
</script>
</body>
</html>
When pressing on the "New password" button, the password is generated even when the reCAPTCHA isn't solved.
how can I make code follow the right order?
I think the code is pretty self explanatory.

How to anchor position popup to left screen in openlayers 6

I have a popup when click to layer on map and popup displayed at selected position. I want to show popup to left screen, how can i do that. Please help me, my english not good. Thanks
My popup show like this
Popup here
I want when click popup will show left screen, not center
If you want the popup on center left, you could do it like this:
<html>
<head>
<meta charset="utf-8" />
<style>
.map {
height: 100%;
width: 100%;
}
html,
body {
height: 100%
}
.ol-popup {
background-color: white;
box-shadow: 0 1px 4px rgba(0,0,0,0.2);
padding: 15px;
border-radius: 10px;
border: 1px solid #cccccc;
bottom: 12px;
min-width: 280px;
}
.ol-popup:after {
border-top-color: white;
border-width: 10px;
left: 48px;
margin-left: -10px;
}
.ol-popup:before {
border-top-color: #cccccc;
border-width: 11px;
left: 48px;
margin-left: -11px;
}
</style>
<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io#master/en/v6.13.0/build/ol.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io#master/en/v6.13.0/css/ol.css">
</head>
<body>
<div id="popup" class="ol-popup">
<div id="popup-content"></div>
</div>
<div id="map" class="map"></div>
<script type="text/javascript">
const container = document.getElementById('popup');
const content = document.getElementById('popup-content');
document.addEventListener("DOMContentLoaded", function () {
drawMap();
});
function drawMap() {
const osmLayer = new ol.layer.Tile({
source: new ol.source.OSM({
attributions: '© OpenStreetMap',
})
});
map = new ol.Map({
target: 'map',
layers: [
osmLayer,
],
view: new ol.View(),
});
const popup = new ol.Overlay({
element: document.getElementById('popup'),
});
map.addOverlay(popup);
map.getView().fit([0,0,0,0]);
map.on('click', function (evt) {
const element = popup.getElement();
const popupMap = popup.getMap();
const coordinate = evt.coordinate;
const viewExtent = popupMap.getView().calculateExtent();
const centerPoint = ol.extent.getCenter(viewExtent);
content.innerHTML = '<p>You clicked here:</p><code>' + coordinate + '</code>';
popup.setPosition( [viewExtent[0], centerPoint[1]] );
});
}
</script>
</body>
</html>
Remove position absolute and determine the position:
https://jsfiddle.net/ve5mn0by/5/
EDIT:
without Openlayers Overlay, just HTML:
map.on('click', function (evt) {
const container = document.createElement('div');
const content = document.createElement('div');
container.style.width = '10rem';
container.style.height = '100%'
//container.style.backgroundColor = '#FF0000';
container.style.position = 'absolute';
container.style.display = 'flex';
content.style.width = '100%';
content.style.height = '10rem'
content.style.backgroundColor = '#FFFF00';
content.style.alignSelf = 'center';
container.appendChild(content);
document.body.appendChild(container);
});

getUserMedia recording do not stop after wait on mdn example

This example using promises on mdn to grab/record video stream on fly, work fine until you click on Stop recording button, then all is correctly stopped, also audio/video hardware that will result off on browser: but if you let elapse the counter time, without clicking on the Stop button, you'll see that the example will not close the audio/video that will remain opened, like still recording (and the hardware result still engaged):
https://developer.mozilla.org/en-US/docs/Web/API/MediaStream_Recording_API/Recording_a_media_element
(the working example is on bottom of the same page or on codepen or jsfiddle as linked)
How it should be closed when wait function elapsed the time, without require a click into the Stop button? Anybody know if it is possible?
It is very possible. This is the code i've come up, now all is stopped, even if no click on button stop: the click is simulated in case.
Thank to this referral
https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events
the resulting (and working fine) code is just like this (complete to copy/paste as html file to test out):
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
body {
font: 14px "Open Sans", "Arial", sans-serif;
}
video {
margin-top: 2px;
border: 1px solid black;
}
.button {
cursor: pointer;
display: block;
width: 160px;
border: 1px solid black;
font-size: 16px;
text-align: center;
padding-top: 2px;
padding-bottom: 4px;
color: white;
background-color: darkgreen;
text-decoration: none;
}
h2 {
margin-bottom: 4px;
}
.left {
margin-right: 10px;
float: left;
width: 160px;
padding: 0px;
}
.right {
margin-left: 10px;
float: left;
width: 160px;
padding: 0px;
}
.bottom {
clear: both;
padding-top: 10px;
}
</style>
</head>
<body>
<p>Click the "Start" button to begin video recording for a few seconds. You can stop
the video by clicking the creatively-named "Stop" button. The "Download"
button will download the received data (although it's in a raw, unwrapped form
that isn't very useful).
</p>
<br>
<div class="left">
<div id="startButton" class="button">
Start
</div>
<h2>Preview</h2>
<video id="preview" width="160" height="120" autoplay muted></video>
</div>
<div class="right">
<div id="stopButton" class="button">
Stop
</div>
<h2>Recording</h2>
<video id="recording" width="160" height="120" controls></video>
<a id="downloadButton" class="button">
Download
</a>
</div>
<div class="bottom">
<pre id="log"></pre>
</div>
<script>
let preview = document.getElementById("preview");
let recording = document.getElementById("recording");
let startButton = document.getElementById("startButton");
let stopButton = document.getElementById("stopButton");
let downloadButton = document.getElementById("downloadButton");
let logElement = document.getElementById("log");
let recordingTimeMS = 3000;
function log(msg) {
logElement.innerHTML += msg + "\n";
}
function wait(delayInMS) {
return new Promise(resolve => setTimeout(resolve, delayInMS));
}
function startRecording(stream, lengthInMS) {
let recorder = new MediaRecorder(stream);
let data = [];
recorder.ondataavailable = event => data.push(event.data);
recorder.start();
log(recorder.state + " for " + (lengthInMS/1000) + " seconds...");
recorder.addEventListener('dataavailable', function(event) {
console.log(recorder.state);
w3simulateClick(stopButton, 'click');
});
let stopped = new Promise((resolve, reject) => {
recorder.onstop = resolve;
recorder.onerror = event => reject(event.name);
});
let recorded = wait(lengthInMS).then(
() => recorder.state == "recording" && recorder.stop()
)
return Promise.all([
stopped,
recorded
])
.then(() => data);
}
function stop(stream) {
stream.getTracks().forEach(track => track.stop());
}
startButton.addEventListener("click", function() {
navigator.mediaDevices.getUserMedia({
video: true,
audio: true
}).then(stream => {
preview.srcObject = stream;
downloadButton.href = stream;
preview.captureStream = preview.captureStream || preview.mozCaptureStream;
return new Promise(resolve => preview.onplaying = resolve);
}).then(() => startRecording(preview.captureStream(), recordingTimeMS))
.then (recordedChunks => {
let recordedBlob = new Blob(recordedChunks, { type: "video/webm" });
recording.src = URL.createObjectURL(recordedBlob);
downloadButton.href = recording.src;
downloadButton.download = "RecordedVideo.webm";
log("Successfully recorded " + recordedBlob.size + " bytes of " +
recordedBlob.type + " media.");
})
.catch(log);
}, false); stopButton.addEventListener("click", function() {
stop(preview.srcObject);
}, false);
function w3simulateClick(el) {
var event = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true
});
//var cb = document.getElementById('checkbox');
var cancelled = el.dispatchEvent(event);
if (cancelled) {
// A handler called preventDefault.
alert("cancelled");
} else {
// None of the handlers called preventDefault.
alert("not cancelled");
}
}
</script>
</body>
</html>
cheers to all cool people!

Declarative ember component which passes in a list of objects to be rendered / bound, while passing state that nested components can access?

I'm having a hard time figuring out how to build an ember component which has nested components which are rendered as a list based on an input list, while also allowing state to be passed in which is accessible from the nested components.
This is easy in angular:
<!doctype html>
<html ng-app="angular-accordion">
<head>
<style>
.angular-accordion-header {
background-color: #999;
color: #ffffff;
padding: 10px;
margin: 0;
line-height: 14px;
-webkit-border-top-left-radius: 5px;
-webkit-border-top-right-radius: 5px;
-moz-border-radius-topleft: 5px;
-moz-border-radius-topright: 5px;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
cursor: pointer;
text-decoration: none;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
}
.angular-accordion-container {
height: 100%;
width: 100%;
}
.angular-accordion-pane {
padding: 2px;
}
.angularaccordionheaderselected {
background-color: #bbb;
color: #333;
font-weight: bold;
}
.angular-accordion-header:hover {
text-decoration: underline !important;
}
.angularaccordionheaderselected:hover {
text-decoration: underline !important;
}
.angular-accordion-pane-content {
padding: 5px;
overflow-y: auto;
border-left: 1px solid #bbb;
border-right: 1px solid #bbb;
border-bottom: 1px solid #bbb;
-webkit-border-bottom-left-radius: 5px;
-webkit-border-bottom-right-radius: 5px;
-moz-border-radius-bottomleft: 5px;
-moz-border-radius-bottomright: 5px;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
}
.loading {
opacity: .2;
}
</style>
</head>
<body style="margin: 0;">
<div style="height: 90%; width: 100%; margin: 0;" ng-controller="outerController">
<div class="angular-accordion-header" ng-click="fakeXhrService.load()">
Click here to simulate loading new data.
</div>
<angular-accordion list-of-accordion-pane-objects="outerControllerData" loading="fakeXhrService.isLoading()">
<pane>
<pane-header ng-class="{loading:loading}">{{accordionPaneObject.firstName}}</pane-header>
<pane-content>{{accordionPaneObject.lastName}}</pane-content>
</pane>
</angular-accordion>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular.js"></script>
<script>
angular.module('angular-accordion', [])
.factory('fakeXhrService', ['$timeout', function($timeout) {
var loading = false,
fakeXhrService;
fakeXhrService = {
load: function() {
loading = true;
$timeout(function() {
loading = false;
}, 2000);
},
isLoading: function() {
return loading;
}
};
return fakeXhrService;
}])
.directive('angularAccordion', function() {
var template = '';
return {
restrict: 'E',
transclude: true,
replace: true,
template: '<div>' +
'<div ng-transclude class="angular-accordion-container" ng-repeat="accordionPaneObject in listOfAccordionPaneObjects" ng-cloak></div>' +
'</div>',
controller: ['$scope', function($scope) {
var panes = [];
this.addPane = function(pane) {
panes.push(pane);
};
}],
scope: {
listOfAccordionPaneObjects: '=',
loading: '='
}
};
})
.directive('pane', function() {
return {
restrict: 'E',
transclude: true,
require: '^angularAccordion',
replace: true,
template: '<div ng-transclude class="angular-accordion-pane"></div>'
};
})
.directive('paneHeader', function() {
return {
restrict: 'E',
require: '^angularAccordion',
transclude: true,
replace: true,
link: function(scope, iElement, iAttrs, controller) {
controller.addPane(scope);
scope.toggle = function() {
scope.expanded = !scope.expanded;
};
},
template: '<div ng-transclude class="angular-accordion-header" ng-click="toggle()"></div>'
};
})
.directive('paneContent', function() {
return {
restrict: 'EA',
require: '^paneHeader',
transclude: true,
replace: true,
template: '<div ng-transclude class="angular-accordion-pane-content" ng-show="expanded"></div>'
};
})
.controller('outerController', ['$scope', 'fakeXhrService', function($scope, fakeXhrService) {
var people = [],
i = 0;
for(i; i < 10; i++) {
people.push({
firstName: 'first ' + i.toString(),
lastName: 'last ' + i.toString()
});
}
$scope.outerControllerData = people;
$scope.fakeXhrService = fakeXhrService;
}]);
</script>
</body>
</html>
plunkr: http://plnkr.co/edit/NxXAgP8Ba7MK1cz2IGXg?p=preview
Here's my attempt so far doing it in ember:
<!doctype html>
<html>
<head>
<style>
.ember-accordion-header {
background-color: #999;
color: #fff;
padding: 10px;
margin: 0;
line-height: 14px;
-webkit-border-top-left-radius: 5px;
-webkit-border-top-right-radius: 5px;
-moz-border-radius-topleft: 5px;
-moz-border-radius-topright: 5px;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
cursor: pointer;
text-decoration: none;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
}
.ember-accordion-pane {
padding: 2px;
}
.ember-accordion-pane-content {
padding: 5px;
overflow-y: auto;
border-left: 1px solid #bbb;
border-right: 1px solid #bbb;
border-bottom: 1px solid #bbb;
-webkit-border-bottom-left-radius: 5px;
-webkit-border-bottom-right-radius: 5px;
-moz-border-radius-bottomleft: 5px;
-moz-border-radius-bottomright: 5px;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
}
.ember-accordion-container {}
.loading {
opacity: .2;
}
</style>
</head>
<body style="margin: 0;">
<script type="text/x-handlebars" data-template-name="components/ember-accordion">
{{#each listOfAccordionPaneObjects itemViewClass="view.emberAccordionItemView"}}
<div class="ember-accordion-container">
<div class="ember-accordion-pane">
{{yield}}
</div>
</div>
{{/each}}
</script>
<script type="text/x-handlebars" data-template-name="components/ember-accordion-header">
{{yield}}
</script>
<script type="text/x-handlebars" data-template-name="components/ember-accordion-body">
{{#if parentView.expanded}}
<div class="ember-accordion-pane-content">
{{yield}}
</div>
{{/if}}
</script>
<script type="text/x-handlebars" data-template-name="index">
from outside the component: {{test}}
{{#ember-accordion listOfAccordionPaneObjects=model test=test}}
{{#ember-accordion-header class="header"}}
{{firstName}}<br />
child inner scope test defined directly on the view inside the component: {{view.parentView.specifiedInComponent}}<br />
child inner scope test passed into the component: {{view.parentView.test}}
{{/ember-accordion-header}}
{{#ember-accordion-body class="body"}}
{{lastName}}<br />
{{/ember-accordion-body}}
{{/ember-accordion}}
</script>
<script type="text/x-handlebars" data-template-name="application">
{{outlet}}
</script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/handlebars.js/1.2.1/handlebars.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/ember.js/1.2.0/ember.debug.js"></script>
<script>
var Person = Ember.Object.extend({
firstName: '',
lastName: '',
fullName: function() {
return this.get('firstName') + ' ' + this.get('lastName');
}.property()
}),
EmberAccordionComponent = Ember.Component.extend({
// each accordion header/body item, will have a instance of that view.
// so we can isolate the expanded state for each accordion header/body
emberAccordionItemView: Ember.View.extend({
expanded: false,
specifiedInComponent: 'this works, but how to get it from the property passed to the component?'
}),
_yield: function(context, options) {
var get = Ember.get,
view = options.data.view,
parentView = this._parentView,
template = get(this, 'template');
if (template) {
Ember.assert("A Component must have a parent view in order to yield.", parentView);
view.appendChild(Ember.View, {
isVirtual: true,
tagName: '',
_contextView: parentView,
template: template,
context: get(view, 'context'), // the default is get(parentView, 'context'),
controller: get(view, 'controller'), // the default is get(parentView, 'context'),
templateData: { keywords: parentView.cloneKeywords() }
});
}
}
}),
EmberAccordionHeaderComponent = Ember.Component.extend({
classNames: ['ember-accordion-header'],
classNameBindings: ['expanded'],
expanded: false,
click: function() {
// here we toggle the emberAccordionItemView.expanded property
this.toggleProperty('parentView.expanded');
this.toggleProperty('expanded');
}
}),
App = Ember.Application.create(),
people = [],
i = 0;
App.EmberAccordionComponent = EmberAccordionComponent;
App.EmberAccordionHeaderComponent = EmberAccordionHeaderComponent;
for(i; i < 10; i++) {
people.push(Person.create({
firstName: 'first ' + i.toString(),
lastName: 'last ' + i.toString()
}));
}
App.IndexRoute = Ember.Route.extend({
model: function() {
return people;
}
});
App.IndexController = Ember.Controller.extend({
init: function() {
this._super();
this.set('test', 'TEST WORKED!');
}
});
</script>
</body>
</html>
jsbin: http://jsbin.com/apIYurEN/1/edit
Where I'm stuck is:
Why isn't the parameter 'test' accessible inside the ember-accordion-header component? If I define it directly on the view inside the outer component I can access it.
How can I avoid putting view.parentView in front of the parameter I want to access, ie: view.parentView.specifiedInComponent? This is not simple for api consumers.
Why do I have to override a private method of ember to get this far ( _yield ). Overriding private members is a bad idea since they can change between versions of ember.
Thanks!
You had me at: "This is easy in angular" :P
It's not necessary to override _yield. The default behaviour allows you to access view properties defined in the parentView by using {{view.property}}. When nesting components this means that the properties are passed down recursively. Also you forgot to set test=test on the ember-accordion-header component.
Here is a working version: http://jsbin.com/apIYurEN/6/

how to access parent component scope from a child components scope in ember?

I'm curious if this is even possible in ember. This is an easy thing to do in angular ( plunkr: http://plnkr.co/edit/O2e0ukyXdKMs4FcgKGmX?p=preview ):
The goal is to make an easy to use, generic, reusable accordion api for api consumers.
The api I want the caller to be able to use is this (just like the angular api):
{{#ember-accordion listOfAccordionPaneObjects=model}}
{{#ember-accordion-heading}}
heading template html {{accordionPaneObject.firstName}}
{{/ember-accordion-heading}}
{{#ember-accordion-body}}
this is the accordion body {{accordionPaneObject.lastName}}
{{/ember-accordion-body}}
{{/ember-accordion}}
Here is a working example I wrote using angular:
<!doctype html>
<html ng-app="angular-accordion">
<head>
<style>
.angular-accordion-header {
background-color: #999;
color: #ffffff;
padding: 10px;
margin: 0;
line-height: 14px;
-webkit-border-top-left-radius: 5px;
-webkit-border-top-right-radius: 5px;
-moz-border-radius-topleft: 5px;
-moz-border-radius-topright: 5px;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
cursor: pointer;
text-decoration: none;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
}
.angular-accordion-container {
height: 100%;
width: 100%;
}
.angular-accordion-pane {
padding: 2px;
}
.angularaccordionheaderselected {
background-color: #bbb;
color: #333;
font-weight: bold;
}
.angular-accordion-header:hover {
text-decoration: underline !important;
}
.angularaccordionheaderselected:hover {
text-decoration: underline !important;
}
.angular-accordion-pane-content {
padding: 5px;
overflow-y: auto;
border-left: 1px solid #bbb;
border-right: 1px solid #bbb;
border-bottom: 1px solid #bbb;
-webkit-border-bottom-left-radius: 5px;
-webkit-border-bottom-right-radius: 5px;
-moz-border-radius-bottomleft: 5px;
-moz-border-radius-bottomright: 5px;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
}
.angulardisabledpane {
opacity: .2;
}
</style>
</head>
<body style="margin: 0;">
<div style="height: 90%; width: 100%; margin: 0;" ng-controller="outerController">
<angular-accordion list-of-accordion-pane-objects="outerControllerData">
<pane>
<pane-header>Header {{accordionPaneObject}}</pane-header>
<pane-content>Content {{accordionPaneObject}}</pane-content>
</pane>
</angular-accordion>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular.js"></script>
<script>
angular.module('angular-accordion', [])
.directive('angularAccordion', function() {
var template = '';
return {
restrict: 'E',
transclude: true,
replace: true,
template: '<div>' +
'<div ng-transclude class="angular-accordion-container" ng-repeat="accordionPaneObject in listOfAccordionPaneObjects"></div>' +
'</div>',
controller: ['$scope', function($scope) {
var panes = [];
this.addPane = function(pane) {
panes.push(pane);
};
}],
scope: {
listOfAccordionPaneObjects: '='
}
};
})
.directive('pane', function() {
return {
restrict: 'E',
transclude: true,
replace: true,
template: '<div ng-transclude class="angular-accordion-pane"></div>'
};
})
.directive('paneHeader', function() {
return {
restrict: 'E',
require: '^angularAccordion',
transclude: true,
replace: true,
link: function(scope, iElement, iAttrs, controller) {
controller.addPane(scope);
scope.toggle = function() {
scope.expanded = !scope.expanded;
};
},
template: '<div ng-transclude class="angular-accordion-header" ng-click="toggle()"></div>'
};
})
.directive('paneContent', function() {
return {
restrict: 'EA',
require: '^paneHeader',
transclude: true,
replace: true,
template: '<div ng-transclude class="angular-accordion-pane-content" ng-show="expanded"></div>'
};
})
.controller('outerController', ['$scope', function($scope) {
$scope.outerControllerData = [1, 2, 3];
}]);
</script>
</body>
</html>
here's where I'm stuck doing the same with ember:
index.html
<!DOCTYPE html>
<html>
<body>
<script src="//cdnjs.cloudflare.com/ajax/libs/require.js/2.1.9/require.js" data-main="main.js"></script>
</body>
</html>
main.js
require.config({
paths: {
'ember': 'bower_components/ember/ember',
'handlebars': 'bower_components/handlebars/handlebars',
'jquery': 'bower_components/jquery/jquery',
'text': 'bower_components/requirejs-text/text'
},
shim: {
ember: {
deps: ['jquery', 'handlebars'],
exports: 'Ember'
}
}
});
define(function(require) {
var Ember = require('ember'),
EmberAccordionComponent = require('src/EmberAccordionComponent'),
EmberAccordionTemplate = require('text!templates/ember-accordion.hbs'),
EmberAccordionHeaderTemplate = require('text!templates/ember-accordion-header.hbs'),
EmberAccordionBodyTemplate = require('text!templates/ember-accordion-body.hbs'),
ApplicationTemplate = require('text!templates/application.hbs'),
IndexTemplate = require('text!templates/index.hbs');
var App = Ember.Application.create({
LOG_STACKTRACE_ON_DEPRECATION : true,
LOG_BINDINGS : true,
LOG_TRANSITIONS : true,
LOG_TRANSITIONS_INTERNAL : true,
LOG_VIEW_LOOKUPS : true,
LOG_ACTIVE_GENERATION : true
});
Ember.TEMPLATES = {};
Ember.TEMPLATES['application'] = Ember.Handlebars.compile(ApplicationTemplate);
Ember.TEMPLATES['index'] = Ember.Handlebars.compile(IndexTemplate);
Ember.TEMPLATES['components/ember-accordion'] = Ember.Handlebars.compile(EmberAccordionTemplate);
Ember.TEMPLATES['components/ember-accordion-header'] = Ember.Handlebars.compile(EmberAccordionHeaderTemplate);
Ember.TEMPLATES['components/ember-accordion-body'] = Ember.Handlebars.compile(EmberAccordionBodyTemplate);
App.EmberAccordionComponent = EmberAccordionComponent;
App.IndexRoute = Ember.Route.extend({
model: function() {
return [
{
name: 'Bob'
},
{
name: 'Jill'
}]
}
})
});
EmberAccordionComponent.js
define(function(require) {
require('ember');
var EmberAccordionComponent = Ember.Component.extend({});
return EmberAccordionComponent;
});
application.hbs
{{outlet}}
ember-accordion-header.hbs
<div style="color: blue;">
{{yield}}
</div>
ember-accordion-body.hbs
<div style="color: green;">
{{yield}}
</div>
index.hbs
{{#ember-accordion listOfAccordionPaneObjects=model}}
{{#ember-accordion-header}}
{{log this.constructor}}
{{log this}}
Header {{accordionPaneObject.name}}
{{/ember-accordion-header}}
{{#ember-accordion-body}}
Body {{accordionPaneObject.name}}
{{/ember-accordion-body}}
{{/ember-accordion}}
ember-accordion.hbs
{{#each accordionPaneObject in listOfAccordionPaneObjects}}
{{yield}}
{{/each}}
--
This is tricky to debug. So putting in the:
{{log this.constructor}}
and the:
{{log this}}
into the:
{{#ember-accordion-header}}
outputs the following:
Class.model = undefined (why?)
Ember.ArrayController
I've tried overriding the private _yield method of Ember.Component as suggested by this article ( http://www.thesoftwaresimpleton.com/blog/2013/11/21/component-block/ ):
var EmberAccordionHeaderComponent = Ember.Component.extend({
_yield: function(context, options) {
var get = Ember.get,
view = options.data.view,
parentView = this._parentView,
template = get(this, 'template');
if (template) {
Ember.assert("A Component must have a parent view in order to yield.", parentView);
view.appendChild(Ember.View, {
isVirtual: true,
tagName: '',
_contextView: parentView,
template: template,
context: get(view, 'context'), // the default is get(parentView, 'context'),
controller: get(view, 'controller'), // the default is get(parentView, 'context'),
templateData: { keywords: parentView.cloneKeywords() }
});
}
}
});
but when I do this I still don't have access to accordionPaneObject in my child component scope, and my {{log this.constructor}} now points to: .EmberAccordionHeaderComponent
So it looks like I'm getting somewhere, I just need to go one more level up.
When I try that using this code in EmberAccordionHeaderComponent.js:
var EmberAccordionHeaderComponent = Ember.Component.extend({
_yield: function(context, options) {
var get = Ember.get,
view = options.data.view,
parentView = this._parentView,
grandParentView = this._parentView._parentView,
template = get(this, 'template');
if (template) {
Ember.assert("A Component must have a parent view in order to yield.", parentView);
view.appendChild(Ember.View, {
isVirtual: true,
tagName: '',
_contextView: parentView,
template: template,
context: get(grandParentView, 'context'), // the default is get(parentView, 'context'),
controller: get(grandParentView, 'controller'), // the default is get(parentView, 'context'),
templateData: { keywords: parentView.cloneKeywords() }
});
}
}
});
I still don't access to accordionPaneObject in, but now I see {{log this.constructor}} outputting .EmberAccordionComponent. So it appears I'm in the right scope, but the data still doesn't bind.
Interestingly enough, if I use any of these variations of reassigning context and controller in my overridden _yield, I can access the data I am after in the console using:
this._parentView._context.content
I updated your code with some comments please give a look http://emberjs.jsbin.com/ivOyiZa/1/edit.
Javascript
App = Ember.Application.create();
App.IndexRoute = Ember.Route.extend({
model: function() {
return [
{ head: "foo head", body: "foo body " },
{ head: "bar head", body: "bar body " },
{ head: "ya head", body: "yo body " }
];
}
});
App.EmberAccordionComponent = Ember.Component.extend({
// each accordion header/body item, will have a instance of that view.
// so we can isolate the expanded state for each accordion header/body
emberAccordionItemView: Ember.View.extend({
expanded: false
}),
_yield: function(context, options) {
var get = Ember.get,
view = options.data.view,
parentView = this._parentView,
template = get(this, 'template');
if (template) {
Ember.assert("A Component must have a parent view in order to yield.", parentView);
view.appendChild(Ember.View, {
isVirtual: true,
tagName: '',
_contextView: parentView,
template: template,
context: get(view, 'context'), // the default is get(parentView, 'context'),
controller: get(view, 'controller'), // the default is get(parentView, 'context'),
templateData: { keywords: parentView.cloneKeywords() }
});
}
}
});
App.EmberAccordionHeaderComponent = Ember.Component.extend({
classNames: ['ember-accordion-header'],
click: function() {
// here we toggle the emberAccordionItemView.expanded property
this.toggleProperty('parentView.expanded');
}
});
Templates
<script type="text/x-handlebars" data-template-name="index">
{{#ember-accordion listOfAccordionPaneObjects=model}}
{{#ember-accordion-header}}
{{head}} <!-- each object passed in listOfAccordionPaneObjects=model can be accessed here -->
{{/ember-accordion-header}}
{{#ember-accordion-body}}
{{body}} <!-- each object passed in listOfAccordionPaneObjects=model can be accessed here -->
{{/ember-accordion-body}}
{{/ember-accordion}}
</script>
<script type="text/x-handlebars" data-template-name="components/ember-accordion">
{{#each listOfAccordionPaneObjects itemViewClass="view.emberAccordionItemView"}}
<div class="ember-accordion-container">
<div class="ember-accordion-pane">
{{yield}}
</div>
</div>
{{/each}}
</script>
<script type="text/x-handlebars" data-template-name="components/ember-accordion-header">
{{yield}}
</script>
<script type="text/x-handlebars" data-template-name="components/ember-accordion-body">
<!-- when EmberAccordionHeaderComponent.click is called, the expanded property change and the content can be visible or not, based on expanded truth -->
{{#if parentView.expanded}}
<div class="ember-accordion-pane-content">
{{yield}}
</div>
{{/if}}
</script>
Css
.ember-accordion-header {
background-color: #999;
color: #ffffff;
padding: 10px;
margin: 0;
line-height: 14px;
-webkit-border-top-left-radius: 5px;
-webkit-border-top-right-radius: 5px;
-moz-border-radius-topleft: 5px;
-moz-border-radius-topright: 5px;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
cursor: pointer;
text-decoration: none;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
}
.ember-accordion-container {
height: 100%;
width: 100%;
}
.ember-accordion-pane {
padding: 2px;
}
.emberaccordionheaderselected {
background-color: #bbb;
color: #333;
font-weight: bold;
}
.ember-accordion-header:hover {
text-decoration: underline !important;
}
.emberaccordionheaderselected:hover {
text-decoration: underline !important;
}
.ember-accordion-pane-content {
padding: 5px;
overflow-y: auto;
border-left: 1px solid #bbb;
border-right: 1px solid #bbb;
border-bottom: 1px solid #bbb;
-webkit-border-bottom-left-radius: 5px;
-webkit-border-bottom-right-radius: 5px;
-moz-border-radius-bottomleft: 5px;
-moz-border-radius-bottomright: 5px;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
}
.emberdisabledpane {
opacity: .2;
}
Yes, it's easy to do.
Here's a really simplistic, un-styled example, where it's on hover instead of click, but click is in the jsbin if you uncomment it, and comment out the mouseenter/mouseleave functions.
http://emberjs.jsbin.com/ijEwItO/3/edit
<script type="text/x-handlebars" data-template-name="components/unicorn-accordian">
<ul>
{{#each item in content itemController='unicornItem' itemView='unicornItem'}}
<li>{{item.title}}
{{#if bodyVisible}}
<br/>
{{item.body}}
{{/if}}
</li>
{{/each}}
</ul>
</script>
App.UnicornAccordianComponent = Em.Component.extend();
App.UnicornItemController = Em.ObjectController.extend({
bodyVisible: false
});
App.UnicornItemView = Em.View.extend({
mouseEnter: function(){
this.set('controller.bodyVisible', true);
},
mouseLeave: function(){
this.set('controller.bodyVisible', false);
}
});
Surely a much easier-to-implement solution is to pass the view (or other parent) as an argument to the component. This will give you access to all the properties of the view whilst still retaining the advantages of using a contained component. For example:
{{#ember-accordion listOfAccordionPaneObjects=model info=view}}{{!-- Pass view in here--}}
{{log view.info}}{{!-- This will log what view.parentView would have done--}}
{{ember-accordion-heading firstName=accordionPaneObject.firstName}}
{{ember-accordion-body lastName=accordionPaneObject.lastName}}
{{/ember-accordion}}
Your header template would look something like this:
Header template html here {{firstName}}
And your body template would look something like this:
Body html here {{lastName}}