Using Polymer 1.0, I set up an iron-form to submit a simple contact form. The idea is to submit the form to a database table using PHP and then display a response from the PHP side into the browser without refreshing - typical AJAX. I'm getting hung up on the Polymer environment though - it seems like there should be a correct way to do this, but hours of searching and tinkering hasn't been fruitful.
I did start this project using the Polymer Starter Kit (lite), which uses a script (app.js) to add event listeners and such. So far I haven't broken that functionality, though all of the examples in documentation do NOT do it this way, so it makes things a little more complicated since I'm still getting used to Polymer in general.
Here's what I've got so far. Thanks so much for any advice you can offer.
index.html
<!-- this is where the output should be displayed -->
<div id="output"></div>
<!-- this is the web form -->
<form is="iron-form" id="contactus-form" method="post" action="/">
<input type="hidden" name="action" value="contactus-form">
<paper-input id="contactus-field-Name" name="Name" label="Name" value="test"></paper-input>
<paper-button onclick="submitHandler(event)">Send</paper-button>
</form>
...
<script src="script/app.js"></script>
<script>
function submitHandler(event) {
Polymer.dom(event).localTarget.parentElement.submit();
}
</script>
app.js
(function(document) {
'use strict';
addEventListener('iron-form-submit', function(e) {
// this works and displays the POSTed values in the browser
document.querySelector('#output').innerHTML = JSON.stringify(e.detail);
// I'm looking for a way to first submit this data through PHP and
// display the output of that process in the #output div rather than the
// raw form input itself.
}
})(document);
FAILED METHOD 1
I tried adding an iron-ajax element into index.html and referencing it from app.js as shown below. Unfortunately, when it tries to add the event listener, the entire app crashes. It seems strange because there are many other pieces in app.js which add event listeners in the same way.
index.html
<iron-ajax id="contactus-output" url="/form/contact.php" params="" handle-as="json"></iron-ajax>
<!-- same form as before goes here -->
app.js
var coutput = document.querySelector('#contactus-output');
coutput.addEventListener('response', function() {
// nothing fancy here yet, just trying to see if I can do this
document.querySelector('#output').innerHTML = 'hello world';
}
FAILED METHOD 2
I found This Answer on SO and decided to try the iron-form-response event. The output I receive now is [object HTMLElement], which is at least something, although I'm not sure if it is actually working or not.
Everything else staying the same, I changed the target of my form to point to my php script and then replaced what I had in app.js with the following:
app.js
addEventListener('iron-form-response', function(e) {
document.querySelector('#output').innerHTML = e.detail;
});
Am I getting any closer?
NOT GIVING UP
Using my second failed method above, the iron-form appears to be making a request, because when I listen for the 'iron-form-response' event, it does fire.
However, the only thing that gets returned is [object HTMLElement] - no idea what to do with that. I tried spitting out some of its properties (as documented on developer.mozilla.org - .title, .properties, .style, etc) but they appear to be empty. Does iron-form really return an HTMLElement object or is this a mistake? I had figured it would return the results from the PHP script I'm submitting the form to just like a normal XMLHttpRequest. If iron-form somehow compresses this into an object, is there a way to pull it out again?
TL;DR
I think what this entire thing boils down to is this: HOW can I properly add an Event Listener (for iron-form-request) when my iron-form is in index.html and index.html is bootstrapped by app.js as happens by default in the Polymer 1.0 Starter Kit?
Further Simplified: How do I add event listeners properly to Polymer's shadow DOM when I'm NOT creating an element (just using it)?
BUG?
With the help of user2422321's wonderful answer below, the iron-request is being performed and a successful iron-request response is received. However, its "response" property returns NULL even though "succeeded" returns true, there were no errors, and the XHR resolved completely. Both "get" and "post" methods were tested with the same NULL result.
I see that there was a bug which matches these symptoms precisely logged in GitHub 10 days ago, though it hasn't seen much attention: Issue 83. This is unfortunate, but it appears to be a bug. I'm not convinced there will be any way to get this working until the element itself is repaired.
WHAT DOES IRON-REQUEST WANT TO SEE?!
As I explore this further, I see that even the XHR directly is returning "null" even though it has the responseURL correct and a statusText of "OK". I'm beginning to wonder if the actual PHP script I'm trying to run - which currently just outputs "Hello World" - is at fault.
Does iron-form-request expect a certain format or data type in the results? I tried adding header('Content-Type: text/plain'); to my PHP file, then I tried formatting it as a verified JSON string, but response is still null. Seems that nothing works.
Old school direct XMLHttpRequests work normally... is iron-form malforming something before the request even gets submitted?
I did set up a handler to catch iron-form-error but none are received. According to every single piece of information in the response, everything is perfect in the world. Just... null response. Over and over and over again... this is so incredibly frustrating.
SOLUTION! (sort of)
Okay, I got desperate enough that I started thumbing through the iron source code itself. It appears that iron-form is still somewhat glitchy at the moment and only returns content if the response is properly formatted json. In iron-request.html, it appears to allow the following types, but don't be fooled. I could only get json to work - I assume the rest will eventually fall into line.
json (application/json)
text (text/plain)
html (text/html)
xml (application/xml)
arraybuffer (application/octet-stream)
Therefore, for the time being, we need to format our response as JSON and include a DOCTYPE declaration to match.
In my case, this looks like so (thanks goes out to user2422321 for helping me so much):
index.php
<div id="output">{{myOutput}}</div>
<form is="iron-form" id="contactUsForm" method="get" action="/contactus.php" on-iron-form-response="_onResponseRetrieved">
<paper-input id="Name" name="Name" value="text" label="Name"></paper-input>
<paper-button id="contactSubmitButton" on-tap="_submitHandler">Submit</paper-button>
</form>
app.js
(function(document) {
...
app._onResponseRetrieved = function(e) {
this.myOutput = e.detail;
console.log(e);
};
app._submitHandler = function(e) {
this.$.contactUsForm.submit();
});
...
})(document);
Then finally, and this was the important last piece of the puzzle. I hadn't previously considered that the contents this file outputs would be very important since straight up XMLHttpRequests return whatever the file spits out.
contactus.php
<?php
// This is the line I added
header('Content-Type: application/json');
// Actual Code goes here
// Then make sure to wrap your final output in JSON
echo '{"test":"this is some test json to try"}';
With all those pieces in place, it works and e.detail.response contains the JSON response we echoed from contactus.php.
Not quite sure I understand what is it that doesn't work, but this is how I think it should be done "pure" Polymer way (by that I mean as little Javascript as possible).
<div id="output">{{myOutput}}</div>
<!-- this is the web form -->
<form is="iron-form" id="contactUsForm" method="post" action="/" on-iron-form-response="_onResponseRetrieved">
<input type="hidden" name="action" value="contactUsForm">
<paper-input id="contactus-field-Name" name="Name" label="Name" value="test"></paper-input>
<paper-button on-tap="_submitHandler">Send</paper-button>
</form>
_onResponseRetrieved: function(e)
{
//I'm not 100% sure what e.detail actually contain, but the value your looking for should be inside there somewhere
this.myOutput = e.detail;
}
_submitHandler: function(e)
{
//Note that I renamed the id of your form :)
this.$.contactUsForm.submit();
}
I've also seen recommendation that onclick should be on-tap so that it works properly on mobile devices, therefore I changed it.
The UI should now update as soon as you receive a response from the server!
-
EDIT:
Because you are using Polymer Starter Kit which is doing some of main logics inside the index.html some things are a bit different. In the Polymer documentation one is usually given examples where they show some piece of code inside an element, while the index.html does not look or function exactly as an element, which indeed can be confusing. In my own project I actually skipped all logics inside the index.html because I thought it seemed messy and complicated, but when looking back it's not all that weird (still not pretty in my opinion). I'm not sure about this, but the way index.html is setup might be the way to setup custom-elements if you want to separate the code (javascript) and the look (html/css).
So to get your code working:
In app.js you see this line:
var app = document.querySelector('#app');
You can think of the app variable as a custom-element.
In index.html you can see this line, which kinda says: "if you click on me I will call the method onDataRouteClick in the element app":
<a data-route="home" href="/" on-click="onDataRouteClick">
So, why will it call the method on the element app? That's because the line above is a child of: <template is="dom-bind" id="app"> (note the id has nothing to do with this, other than that we located it in Javascript by that id, so when I talk about the app object I'm talking about the one in Javascript).
Inside app.js we can then define what will happen when onDataRouteClick is called by doing the following:
app.onDataRouteClick = function() {
var drawerPanel = document.querySelector('#paperDrawerPanel');
if (drawerPanel.narrow) {
drawerPanel.closeDrawer();
}
};
I don't know why, but they keep using this line to find objects:
var drawerPanel = document.querySelector('#paperDrawerPanel');
But when you are in the scope of app you can actually use this instead, which I think is more Polymerish:
var drawerPanel = this.$.paperDrawerPanel;
-
Sorry if you knew all this already, now how do we get your code working?
Inside app.js you add this:
app._onResponseRetrieved = function(e)
{
//I'm not 100% sure what e.detail actually contain, but the value your looking for should be inside there somewhere
this.myOutput = e.detail;
console.log(e); //Check the console to see exactly where the data is
};
app._submitHandler = function(e)
{
//Note that I renamed the id of your form :)
//We are in the scope of 'app' therefore we can write this
this.$.contactUsForm.submit();
};
And in index.html you would have something similar to this (obviously it should be within the template element):
<div id="output">{{myOutput}}</div>
<!-- this is the web form -->
<form is="iron-form" id="contactUsForm" method="post" action="/" on-iron-form-response="_onResponseRetrieved">
<input type="hidden" name="action" value="contactUsForm">
<paper-input id="contactus-field-Name" name="Name" label="Name" value="test"></paper-input>
<paper-button on-tap="_submitHandler">Send</paper-button>
</form>
I am using Polymer 2 and I had a similar problem like this.
Here is my element file :
<template is="dom-bind">
<iron-ajax
auto
id="ajax"
url="test.php"
handle-as="json"
method="POST"
body='{"email":"ankita#gmail.com", "lastlogin":"Feb 21st 2016", "notifications":6}'
content-type = "application/json"
last-response="{{responseObject}}" >
</iron-ajax>
<login-element details="[[responseObject]]"></login-element>
</template>
And the login-element.html looks like this:
<dom-module id="login-element" >
<template>
<!--<form action="test1.php" method="post" enctype='application/json'>-->
<!--Name: <input type="text" name="name"><br>-->
<!--E-mail: <input type="text" name="email"><br>-->
<!--<input type="submit" onclick="submitForm()">-->
<!--</form>-->
<h2>{{details.email}}</h2>
</template>
<script>
Polymer({
is:"login-element",
properties:{
details:Object
}
});
</script>
And test.php
<?php
$data = json_decode(file_get_contents('php://input'), true);
echo json_encode($data);
exit;
Related
I am new to Angular and have run into a problem that seems to have a javascript work around but they aren't very elegant.
I have a model with an array property. I ngfor the list property to build some html selection options. This is all working nicely. The problem comes when I am trying to set default value...the html elements don't have a load event.
I tried numerous html elements and they don't appear to have a load event either but I certainly could be doing it wrong.
I have seen a solution to put javascript tag right after the html and I could do that but I was really looking for a more elegant way in Angular.
I saw this SO post and thought that was my answer but there is a warning given that I agree with and thus it doesn't appear to be a good solution.
Regardless I tried it just to see if it would work and I got:
Failed to execute 'setAttribute' on 'Element': '{{loadDefaults()}}' is not a valid attribute name
<span {{loadDefaults()}} ></span>
So how can I fire an AS2 function in the component to load the default values?
HTML (btw this is NOT a full page load so there is no body tag):
<tr>
<td *ngFor="let loc of locOptions;">
<span>{{loc.text}}</span>
<input type="radio" name="radiogroup" [value]="loc.value" (change)="onSelectionChange(loc.value)">
</td>
</tr>
Edit
I thought perhaps mistakenly that ngoninit would fire too soon...before the html elements are rendered.
So perhaps what is being suggested is that I add a boolean is default to the model and bind THAT as the element is rendered.
In your ngonit function set this.locOptions to your default values. The value can be changed later on in any function and the change will be reflected in the view. Hope this helps you.
You should use ngOnInit to init you data, and call retrieve your data from your component :
defaults : any;
ngOnInit {
this.defaults = loadDefaults();
}
loadDefaults() {
//get data
}
HTML :
<span>{{defaults}}</span>
I'm trying to understand the behavior of innerHTML in the code below. I want to permanently add a new div block every time I hit the button, but it seems that the new block only pops up for a split second then disappears.
Does anyone know why this is the case, and how to fix it?
Also, when I change the code to use appendChild instead of innerHTML, I get an error saying Argument 1 of Node.appendChild is not an object.. I'm not sure what this means.
Any help is much appreciated!
Below is the code:
<DOCTYPE html>
<html>
<body>
<form onSubmit="loadData()">
<input type="submit" id="button">
</form>
<div id="block">List of items:</div>
<script>
function loadData(){
document.getElementById("block").innerHTML += "<div>item</div>";
// document.getElementById("block").appendChild("<div>item</div>");
};
</script>
</body>
</html>
Because you are submitting then the page reloads and your HTML is obliterated.
If you need items to persist then you will need to use cookies, localStorage or a server-side solution.
function addItem()
{
document.getElementById("block").innerHTML += "<div>item</div>";
}
<DOCTYPE html>
<html>
<body>
<form>
<input type="button" id="button" onclick="addItem()" />
</form>
<div id="block">List of items:</div>
</body>
</html>
you are submitting the page. appendChild or innerHtml happen directly after submit, before the new page is loaded. once the new page is loaded, the current page (with the applied modifications) is dismissed and replaced with the new page.
if you wanted something to happen on the new page, you would need to execute the code on that page. (or don't use a form submit, but rather some ajax for sending the form).
The reason why appendChild is not working for you, is that appendChild expects a dom node as parameter, not a string. it would be like document.getElementById("foo).appendChild(document.createElement("div")). (the tricky part is that with createElement you get an empty element, you would also need to put the content you want into it.
Your first question is already answered by #lee.
Your problem with your second answer is, that you can not use appendChild like you did. If u want to use append child, according to the mozilla developer docs you will have to to something like this:
var mydiv = document.createElement("div");
mydiv.appendChild(document.createTextNode("item"));
document.getElementById("block").appendChild(mydiv);
to get the result you asked for.
I first initialize my app with ng-app="myApp" in the body tag and this works fine for all angularized-html that is loaded on first page load.
Later on I have some code that loads angularized-html in to the DOM.
In angular 1.08 I could just run angular.bootstrap($newLoadHTML, ["myApp"]) after the load and it would work; where $newLoadHTML is the newly added HTML grabbed with jQuery.
In angular 1.2 this does no longer work:(
Error: [ng:btstrpd] App Already Bootstrapped with this Element '' http://errors.angularjs.org/1.2.0-rc.2/ng/btstrpd?p0=%3Cdiv%20ng-controller%3D%22AfterCtrl%22%3E
I am getting this error which I understand, but I don't know how to solve it.
What I need to be able to do is load angularized-html and then make angular aware of it.
Here is a plunker to illustrate it: http://plnkr.co/edit/AHMkqEO4T6LxJvjuiMeT?p=preview
I will echo what others have mentioned: this kind of thing is generally a bad idea, but I also understand that you sometimes have to work with legacy code in ways you'd prefer not to. All that said, you can turn HTML loaded from outside Angular into Angular-bound views with the $compile service. Here's how you might rewrite your current example to make it work with $compile:
// We have to set up controllers ahead of time.
myApp.controller('AfterCtrl', function($scope) {
$scope.loaded = 'Is now loaded';
});
//loads html and afterwards creates a controller
$('button').on('click', function() {
$.get('ajax.html', function(data) {
// Get the $compile service from the app's injector
var injector = $('[ng-app]').injector();
var $compile = injector.get('$compile');
// Compile the HTML into a linking function...
var linkFn = $compile(data);
// ...and link it to the scope we're interested in.
// Here we'll use the $rootScope.
var $rootScope = injector.get('$rootScope');
var elem = linkFn($rootScope);
$('.content').append(elem);
// Now that the content has been compiled, linked,
// and added to the DOM, we must trigger a digest cycle
// on the scope we used in order to update bindings.
$rootScope.$digest();
}, 'html');
});
Here is an example: http://plnkr.co/edit/mfuyRJFfA2CjIQBW4ikB?p=preview
It simplifies things a bit if you can build your functionality as a directive instead of using raw jQuery--you can inject the $compile and $rootScope services into it, or even use the local scope inside the directive. Even better if you can use dynamic binding into an <ng-include> element instead.
Your approach doesn't seem right. You are usinging jQuery and Angular together in an inappropriate way that is likely to have conflicts.
Angular's built in template support is the best way to do this either using ng-include or you can use Angular's routing and along with ng-view. The documentation is here:
http://docs.angularjs.org/api/ng.directive:ngInclude
http://docs.angularjs.org/api/ngRoute.directive:ngView
The simplest possible thing would be to just set the ng-include to the url string:
<div ng-include="'ajax.html'"></div>
If you actually need it to load dynamically when you do something then this is a more complete solution for you:
http://plnkr.co/edit/a9DVEQArS4yzirEQAK8c?p=preview
HTML:
<div ng-controller="InitCtrl">
<p>{{ started }}</p>
<button ng-click="loadTemplate()">Load</button>
<div class="content" ng-include="template"></div>
</div>
Javascript:
var myApp = angular.module('myApp', [])
.controller('InitCtrl', function($scope)
{
$scope.started = 'App is started';
$scope.loadTemplate = function() {
console.log('loading');
$scope.template = "ajax.html";
}
}).controller('AfterCtrl', function($scope)
{
$scope.loaded = 'Is now loaded';
});
Loading an AngularJS controller dynamically
The answer to this question fixed my problem. Since I need to create the controllers after the content was added to the DOM. This fix requires me too register controllers after I have declared it. If someone has an easier solution pleace chip in.
One other gotcha that leads to this Bootstrapping error is the nginclude or ngview scenarios where your dynamic html includes script references to angular js.
My html below was causing this issue when it got injected into an existing Angular page. The reference to the angular.min.js caused Angular to rebootstrap:
<div id="fuelux-wizard" class="row-fluid" data-target="#step-container">
<ul class="wizard-steps">
<li data-target="#step1">
<span class="step">1</span>
<span class="title">Submit</span>
</li>
<li data-target="#step2">
<span class="step">2</span>
<span class="title">Approve</span>
</li>
<li data-target="#step3">
<span class="step">3</span>
<span class="title">Complete</span>
</li>
</ul>
</div>
<script src="/Scripts/Angular/angular.min.js"></script>
<script type="text/javascript">
angular.element('#requestMaster').scope().styleDisplayURL();
</script>
I'm searching for a way to access an attribute on a Polymer custom element from the DOM
or to send data from Polymer.register to the DOM.
This really simple element below takes two values and multiplies them, placing the result in its result attribute.
How can I access this result from the outside?
<element attributes='value times result' name='value-box'>
<template>
<p>{{result}}</p>
</template>
<script>
Polymer.register(this, {
ready: function() {
if (this.value != null && this.times != null) {
this.result = this.value * this.times;
}
}
});
</script>
</element>
result is a property on your element just like times and value. You can access it from outside JS, as you would any property on a normal HTML element. For example:
<value-box value="2" times="10"></value-box>
<script>
document.querySelector('value-box').result;
</script>
Internal to your element, what you want is to keep the result computed property up to date as times/value change. There are a couple of ways to do that. One is to use <property>Changed watchers [1]:
<element name="value-box" attributes="value times result">
<template>
<p>result: {{result}}</p>
</template>
<script>
Polymer.register(this, {
valueChanged: function() {
this.result = this.value * this.times;
},
timesChanged: function() {
this.result = this.value * this.times;
}
});
</script>
</element>
Demo: http://jsbin.com/idecun/2/edit
Alternatively, you can use a getter for result:
Polymer.register(this, {
get result() {
return this.value * this.times;
}
});
Demo: http://jsbin.com/oquvap/2/edit
Note For this second case, if the browser doesn't support Object.observe, Polymer will setup a timer to dirty check result. This is why you see "here" printed in the console for this second example. Run the same thing in Chrome Canary with "Experimental WebKit features" enabled in about:flags, and you won't see the timer. Yet another reason why I can't wait for Object.observe to be everywhere! :)
Hope this helps.
Just wanted to add a useful follow up to this (Even though the question has been answered).
My follow up is in response to the following comment on the actual answer:
I'm curious as to why selection with jQuery didn't work. Does it not recognize Custom Elements? – CletusW Jul 8 '13 at 19:57
The most likely reason jQuery didn't see your element is because it was not fully formed by the browsers run time at that point.
I ran into this problem while developing my ASP.NET MVC + polymer js sample app on my github page, and essentially what I was trying to do was call methods and access properties on my polymer object before polymer had made everything usable.
Once I moved the code I was using into a button click (So I could trigger it manually after I visually could see my component was ready) everything worked fine.
For now, if you try to access anything too soon, EG: in your jQ doc.ready handler, there's a good chance you'll run into all sorts of daft problems like this.
If you can find a way of delaying your action, or even better using polymer signals to signal from the components ready handler to an outside agent, that sets a flag telling you the component is ready, then you can sort this easily.
Example:
<form>
<input type='submit'>
</form>
When submitted results in:
http://example.com/?
How to make it:
http://example.com/
?
[This is a very simple example of the problem, the actual form has many fields, but some are disabled at times. When all are disabled, the trailing ? appears]
In my case I'm using window.location, not sure it's the best alternative, but it's the only one I could make it work:
$('#myform').submit(function()
{
... if all parameters are empty
window.location = this.action;
return false;
});
My real use was to convert GET parameter to real url paths, so here is the full code:
$('#myform').submit(function()
{
var form = $(this),
paths = [];
// get paths
form.find('select').each(function()
{
var self = $(this),
value = self.val();
if (value)
paths[paths.length] = value;
// always disable to prevent edge cases
self.prop('disabled', true);
});
if (paths.length)
this.action += paths.join('/')+'/';
window.location = this.action;
return false;
});
Without using Javascript, I'm not sure there is one. One way to alleviate the problem may be to create a hidden input that just holds some junk value that you can ignore on the other side like this:
<input type="hidden" name="foo" value="bar" />
That way you will never have an empty GET request.
This is an old post, but hey.. here ya go
if you are using something like PHP you could submit the form to a "proxy" page that redirects the header to a specific location + the query.
For example:
HTML:
<form action="proxy.php" method="get">
<input type="text" name="txtquery" />
<input type="button" id="btnSubmit" />
</form>
PHP (proxy.php)
<?php
if(isset($_GET['txtquery']))
$query = $_GET['txtquery'];
header("Location /yourpage/{$query}");
?>
I am assuming this it what you are trying to do
I was looking for similar answer. What I ended up doing was creating a button that redirects to a certain page when clicked.
Example:
<button type="button" value="Play as guest!" title="Play as guest!" onclick="location.href='/play'">Play as guest!</button>
This is not an "answer" to your question but might be a good work around. I hope this helps.
Another option would be to check the FormData with javascript before submitting.
var myNeatForm = document.getElementById("id_of_form");
var formData = new FormData(myNeatForm); // Very nice browser support: https://developer.mozilla.org/en-US/docs/Web/API/FormData
console.log(Array.from(formData.entries())); // Should show you an array of the data the form would be submitting.
// Put the following inside an event listener for your form's submit button.
if (Array.from(formData.entries()).length > 0) {
dealTypesForm.submit(); // We've got parameters - submit them!
} else {
window.location = myNeatForm.action; // No parameters here - just go to the page normally.
}
I know this is a super old question, but I came across the same issue today. I would approach this from a different angle and my thinking is that in this day and age you should probably be using POST rather than GET in your forms, because passing around values in a querystring isn't great for security and GDPR. We have ended with a lot of issues where various tracking scripts have been picking up the querystring (with PII in the parameters), breaking whatever terms of services they have.
By posting, you will always get the "clean url", and you won't need to make any modifications to the form submit script. You might however need to change whatever is receiving the form input if it is expecting a GET.
You will get a trailing question mark when submitting an empty form, if your server adding trailing slash to URL and your action URL of form - is directory (and not file) and:
Trailing slash in the action attribute URL (action="/path/").
With dot (with or without trailing slash after it) instead specific URL (action="." or action="./").
With empty action (action="").
Form without action attribute.
Try to specify an action-URL without trailing slash:
action="path"
or
action="./path/sub"
and
action="/path"
or
action="/path/sub"