Ember Search App

Hi

I’m starting this the same as 99% of all the other posts on here. I’m new to Ember.Js . I’m building a Ember app that queries an API that I built in Node.js. The node.js api/app is just a simple api that pulls data from a sql database using a stored procedure. The node.js app seems to be working perfectly and returns all expected data.

I’m having lots off issues building the ember app. The EmberApp/user starts with a home page that contains a form. The form has input boxes deepening on what they need to search for. Searching and returning the data seems to be working okay. Not sure if I am doing it the best way possible though.

Where I am having the most issues is returning the data to a new page. I am able to return the data to the same page were the form is without issues. So I guess my 1st question is.

What is the best way to search for data from an api, using a form. Then returning that data to a different page?

Thanks for your help!

What have you tried?

If your API is using custom formats, maybe fetch is what you want?

It can be invoked from the route’s model hook

Example: https://guides.emberjs.com/release/routing/specifying-a-routes-model/#toc_fetch-example

Hi

Thank you for your response. I’m already using Fetch to post to the API and it is returning data without issue. The issue I am having is using that data after its returned. I am able to return the data to the template, but only in the main template associated with the component that dies the main fetch.

Are you you saying I should use a fetch again to return the data to a new page?

Here is part of the code I have so far.

home.js component…

import Component from '@ember/component';

export default Component.extend({
		
	actions: {		
		search12: function () {			
			/////clear textarea at each submit
			//document.getElementById('disrecords').value = "";
			/////clear form at each submit

			var andor = document.getElementById("andor").checked;
			var nebid = document.getElementById("nebid").value;
			var catalognumber = document.getElementById("catalognumber").value;
			var organism = document.getElementById("organism").value;
			var depositor = document.getElementById("depositor").value;
			var productionstatus = document.getElementById("productionstatus").value;
			var productname = document.getElementById("productname").value;
			var depositdate = document.getElementById("depositdate").value;
			var erhost = document.getElementById("erhost").value;
			
			if (andor === true) {
				andor = 1;
			} else {
				andor = 0;
			}
			
			fetch("http://localhost:3000/routes", {
				method: 'POST',				
				headers: {
					'Accept': 'application/json',
					'Content-Type': 'application/json'
				},
				body: JSON.stringify({
					record: {
						andor: andor,
						nebid: nebid,
						catalognumber: catalognumber,
						organism: organism,
						depositor: depositor,
						productionstatus: productionstatus,
						productname: productname,
						depositdate: depositdate,
						erhost: erhost
					}				
				})				
			}).then(function (res) {
				return res.json();				
			}).then(function (data) {
				var x = 0;
				while (!data.EOF) {					

				var result = Object.keys(data.recordset[x]).map(						
				//var result = Object.keys(data.recordset[x]).forEach(
					function (k, i) {					
													
					var recordx = data.recordset[x][k];
					var record =  `${k} : ${recordx}` ;
							
	
				document.getElementById("disrecords").value += ("\n") + record;
				
						});
					x++;		
				}
			return record;
			//return result;
			
			
			}).catch(function(err){	

        });

		},
			
	}		
});

home.hbs template for component home.js …

<h2>	{{this.title}}</h2>

<header class="logo">
</header>
	
<div>
	<form id="userSubmit" method="POST" {{action "search12" on="submit"}}>
		<br>	

  <label for="and">UNCHECKED "AND" CHECKED FOR "OR": 
	{{input id="andor" type="checkbox" checked=andor}}
    <!-- {{input id="andor" type="checkbox" checked=andor}}     -->
  </label>			
	<hr><br>		
				<label for="nebid">NEBID:</label>
    {{input id="nebid" class = "inputField" type="text" value=nebid}}<br>	
	
				<label for="productname">Product Name:</label>
    {{input id="productname" class = "inputField" type="text" value=productname}}<br>

				<label for="catalognumber">Catalog Number:</label>
    {{input id="catalognumber" class = "inputField" type="text" value=catalognumber}}<br>
								
				<label for="erhost">ER Host:</label>
    {{input id="erhost" class = "inputField" size="155" type="text" value=Hi}}<br>

				<label for="organism">Organism:</label>
    {{input id="organism" class = "inputField" type="text" value=organism}}<br>
	
				<label for="productionstatus">Status:</label>
    {{input id="productionstatus" class = "inputField" type="text" value=productionstatus}}<br>

				<label for="depositor">Depositor:</label>
    {{input id="depositor" class = "inputField" type="text" value=depositor}}<br>

				<label for="depositdate">Deposit Date:</label>
    {{input id="depositdate" class = "inputField" type="date" value=depositdate}}

<br><br><br><br>

<div>
	<button type="submit" name="search">Search</button> 
	<textarea id='disrecords' rows=10 cols=70></textarea>
	<br><br>	
</div>

<br><br>
</form>
</div>


{{yield}}

How do I paste code in this discussion better?

you can surround your code with triple backticks (`) for better formatting

I think it helps to define explicitly what you mean by page. I’m not trying to be pedantic, it just makes a big difference. Ember is a SPA (single page application) framework meaning in terms of actual HTML pages you only ever interact with one. Ember swaps out DOM content as the user interacts with it. Typically when we refer to pages in Ember we are referring to routes. All applications have an application route which is always rendered (usually with stuff like navbar, etc) and then the user can navigate between sub-routes which looks more or less like “pages”. Each route template typically renders zero to a great many components.

So assuming by page you mean route you could use a link-to and pass a model to the destination route. Alternatively you could use a service to set the data and the reference it from the other route. Last, instead of using routes you could just use components. Typically it’s easier to use routes because they handle al of the async fetching states better (that’s what they’re designed for) but you’re already fetching in the component. Of course if you want the url to change you’ll also need to use a route.

Another thing I’ll note is that you should not have to use DOM operations like these very often when using Ember:

		var catalognumber = document.getElementById("catalognumber").value;
		var organism = document.getElementById("organism").value;
		var depositor = document.getElementById("depositor").value;

You’re already binding the inputs to the component:

			<label for="depositor">Depositor:</label>
{{input id="depositor" class = "inputField" type="text" value=depositor}}<br>

The above is binding the value of the input to the “depositor” prop on the component.

So instead of using document.getElementById("depositor").value; you could just get this.depositor in the component js.

So… for example, you could replace this whole block:

		var andor = document.getElementById("andor").checked;
		var nebid = document.getElementById("nebid").value;
		var catalognumber = document.getElementById("catalognumber").value;
		var organism = document.getElementById("organism").value;
		var depositor = document.getElementById("depositor").value;
		var productionstatus = document.getElementById("productionstatus").value;
		var productname = document.getElementById("productname").value;
		var depositdate = document.getElementById("depositdate").value;
		var erhost = document.getElementById("erhost").value;

with lines like this:

var erhost = this.erhost;

or more modern and concise syntax (for that entire block) might look like this:

let { andor, nebid, catalognumber, organism, depositor, productionstatus, productname, depositdate, erhost } = this;

Hi dknutsen

Thank you for your response.

I did try replacing the getElementById block with

let { andor, nebid, catalognumber, organism, depositor, productionstatus, productname, depositdate, erhost } = this;

but the node,js api doesnt like the date field now.

I also tried this

var depositdate = this.depositdate;
			
			let { andor, nebid, catalognumber, organism, depositor, productionstatus, productname, erhost } = this;

same error from node.js api

(\node_modules\readable-stream\lib\_stream_readable.js:24
0:10)
      at Parser.Transform.push (\node_modules\readable-stream\lib\_stream_transform.js:
139:32) {
    info: {
      number: 8114,
      state: 1,
      class: 16,
      message: 'Error converting data type varchar to datetime.',
      serverName: '',
      procName: 'neb.usp_StrainSearchGetStrain',
      lineNumber: 0,
      name: 'ERROR',
      event: 'errorMessage'
    }
  },
  name: 'RequestError',
  precedingErrors: []
}
undefined

the node.js api is getting passed undefined for most of the parms now?

EXEC [neb].[usp_StrainSearchGetStrain] '2940' , 0 , 'undefined' , 'undefined' , 'undefined' , 'undefined' , 'undefined'
, 'undefined' , 'undefined'

Putting the above (this. (undefined) aside for now.

In response to the “page” question. Yes I’m referring to another route or component or window for the results of the search. basically anything is fine.

Basically user gets a form enters search strings… clicks search and the results are displayed in a popup window(new route) or a new window(new route)

you mentioned: “link-to and pass a model to the destination route.”

Ive been attempting this for quite awhile and Ive had no luck.

One big part I keep getting stuck on is the home.js component. How would I prepare the returned results so they are available to pass to a model to the results route?

just to note: Ive spend hours going through the tutorials and trying parts in my app, with no luck.

The closest Ive got was creating a results route and I was only able to get the returned data to appear on the results route after the search was performed and displayed on the home.hbs route.

So it sounds like most of the hangups you’re having are the template binding concepts. If you can get that part figured out you will 1. be able to fix the input => server request issue, and 2. be able to set the results on the component and then either pass them somewhere else or use them directly in the component template.

I guess first off I should have asked what versions of ember-source and ember-cli you’re using.

If I were writing this, I’d keep it all in one component and do something like this:

{{!-- templates/components/home.js --}}
<h2>	{{this.title}}</h2>
<header class="logo"></header>

{{#if results}}
<div>
  <form id="userSubmit" {{action "search12" on="submit"}}>	

    <label for="and">UNCHECKED "AND" CHECKED FOR "OR": 
      {{!-- {{input id="andor" type="checkbox" checked=this.andor}} --}}
      <input type="checkbox" checked={{this.andor}} onclick={{action (mut this.andor) value="target.checked"}}>
    </label>			
    <hr>	
    <label for="nebid">NEBID:</label>
    {{!--  {{input id="nebid" class = "inputField" type="text" value=this.nebid}} --}}
    <input id="nebid" value={{this.nebid}} oninput={{action (mut this.nebid) value="target.value"}}>

	
    <label for="productname">Product Name:</label>
    {{input id="productname" class = "inputField" type="text" value=this.productname}}


    <label for="catalognumber">Catalog Number:</label>
    {{input id="catalognumber" class = "inputField" type="text" value=this.catalognumber}}
								
    <label for="erhost">ER Host:</label>
    {{input id="erhost" class = "inputField" size="155" type="text" value=this.erhost}}

    <label for="organism">Organism:</label>
    {{input id="organism" class = "inputField" type="text" value=this.organism}}
	
    <label for="productionstatus">Status:</label>
    {{input id="productionstatus" class = "inputField" type="text" value=this.productionstatus}}

    <label for="depositor">Depositor:</label>
    {{input id="depositor" class = "inputField" type="text" value=this.depositor}}

    <label for="depositdate">Deposit Date:</label>
    {{input id="depositdate" class = "inputField" type="date" value=this.depositdate}}

    <div>
      <button type="submit" name="search">Search</button> 
    </div>
  </form>
</div>
{{else}}
  {{#each data as |item|}}
    {{#each-in item as |recordName recordValue|}}
      {{recordName}}: {{recordValue}}
    {{/each-in}}
  {{/each}}
{{/if}}

notice i for the first couple inputs i used one-way bound HTML <input> instead of the input helper. it provides a little more clarity about how the bindings are being done and can lead to fewer edge cases if you have more complicated inputs.

and the component js:

// components/home.js
import Component from '@ember/component';

export default Component.extend({
		
  actions: {		
    search12: function () {			
      var andor = this.get('andor');
      var nebid = this.get('nebid');
      ...
      // and so on for each input value

      andor = andor ? 1 : 0;
			
      fetch("http://localhost:3000/routes", {
        method: 'POST',				
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
        record: {
          andor,
          nebid,
          catalognumber,
          organism,
          depositor,
          productionstatus,
          productname,
          depositdate,
          erhost
        }
      }).then((data) => {
        this.set('data', data);
      }).catch((err) => {	
        this.set('error', err);
      });
    },		
  }
});

The biggest things to note are that I’m using this.get(<input value>) to get the values from the inputs and then this.set('data', data)to set the data to a component prop that can be accessed in the template. Then in the bottom of the template I’m using the {{each}} and {{each-in}} loops to do (I think) the same thing you were doing in the original js you posted (except in my example it’s not going into a textarea).

Anyway, I would highly recommend focusing more on the concepts of data bindings and how get/set work in Ember. in Octane (due very soon) there’s less ceremony around it but I’m not sure what version you’re on so the code I wrote above should be “safer”. That said it’s off the top of my head so it’s possible I have some typos or bugs. It was really more to illustrate:

  1. binding inputs to component properties
  2. getting the bound values in the action
  3. setting the results data on the component class once it has been fetched
  4. using that results data in the template

Hi

Thank you for your help. These are the versions I’m using:

ember-cli: 3.11.0
node: 12.9.1
os: win32 x64

In your code specifically the home.js, I dont need to parse the results array ? or is that done in the template?

I added the extra variables to your home.js and tried it out.

// components/home.js
import Component from '@ember/component';

export default Component.extend({
		
  actions: {		
    search12: function () {			
      var andor = this.get('andor');
      var nebid = this.get('nebid');
	  var catalognumber = this.get('catalognumber');
      var organism = this.get('organism');
      var depositor = this.get('depositor');
      var productionstatus = this.get('productionstatus');
      var productname = this.get('productname');
      var depositdate = this.get('depositdate');
      var erhost = this.get('erhost');

      andor = andor ? 1 : 0;
			
      fetch("http://localhost:3000/routes", {
        method: 'POST',				
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
        record: {
          andor,
          nebid,
          catalognumber,
          organism,
          depositor,
          productionstatus,
          productname,
          depositdate,
          erhost
        }
      }).then((data) => {
        this.set('data', data);
      }).catch((err) => {	
        this.set('error', err);
      });
    },		
  }
});

I think there is a missing } somewhere, getting errors:

\components\home.js: Unexpected token, expected "," (42:8)

  40 |       }).catch((err) => {	
  41 |         this.set('error', err);
> 42 |       });
     |         ^
  43 |     },		
  44 |   }
  45 | });

I will look through it and see if I can find where the error is coming from.

Thank you so much for your help so far!!

Hi I have this showing no errors now… home.js … it was missing the closing curly-brace and parenthesis }) on line 38…

// components/home.js
import Component from '@ember/component';

export default Component.extend({
		
  actions: {		
    search12: function () {			
			var andor = this.get('andor');
			var nebid = this.get('nebid');
			var catalognumber = this.get('catalognumber');
			var organism = this.get('organism');
			var depositor = this.get('depositor');
			var productionstatus = this.get('productionstatus');
			var productname = this.get('productname');
			var depositdate = this.get('depositdate');
			var erhost = this.get('erhost');

      andor = andor ? 1 : 0;
			
      fetch("http://localhost:3000/routes", {
        method: 'POST',				
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
        record: {
          andor,
          nebid,
          catalognumber,
          organism,
          depositor,
          productionstatus,
          productname,
          depositdate,
          erhost
        }
		})
      }).then((data) => {
        this.set('data', data);
      }).catch((err) => {	
        this.set('error', err);
      });
    },		
  }
});

On the home.hbs template its not getting past this?

{{#if results}} on line 5 ?

The title and the logo at the top ar all thats displayed.

{{!-- templates/components/home.js --}}
<h2>	{{this.title}}</h2>
<header class="logo">logo</header>

{{#if results}}
<div>
  <form id="userSubmit" {{action "search12" on="submit"}}>	

    <label for="and">UNCHECKED "AND" CHECKED FOR "OR": 
      {{!-- {{input id="andor" type="checkbox" checked=this.andor}} --}}
      <input type="checkbox" checked={{this.andor}} onclick={{action (mut this.andor) value="target.checked"}}>
    </label>			
    <hr>	
    <label for="nebid">NEBID:</label>
    {{!--  {{input id="nebid" class = "inputField" type="text" value=this.nebid}} --}}
    <input id="nebid" value={{this.nebid}} oninput={{action (mut this.nebid) value="target.value"}}>

	
    <label for="productname">Product Name:</label>
    {{input id="productname" class = "inputField" type="text" value=this.productname}}


    <label for="catalognumber">Catalog Number:</label>
    {{input id="catalognumber" class = "inputField" type="text" value=this.catalognumber}}
								
    <label for="erhost">ER Host:</label>
    {{input id="erhost" class = "inputField" size="155" type="text" value=this.erhost}}

    <label for="organism">Organism:</label>
    {{input id="organism" class = "inputField" type="text" value=this.organism}}
	
    <label for="productionstatus">Status:</label>
    {{input id="productionstatus" class = "inputField" type="text" value=this.productionstatus}}

    <label for="depositor">Depositor:</label>
    {{input id="depositor" class = "inputField" type="text" value=this.depositor}}

    <label for="depositdate">Deposit Date:</label>
    {{input id="depositdate" class = "inputField" type="date" value=this.depositdate}}

    <div>
      <button type="submit" name="search">Search</button> 
    </div>
  </form>
</div>
{{else}}
  {{#each data as |item|}}
    {{#each-in item as |recordName recordValue|}}
      {{recordName}}: {{recordValue}}
    {{/each-in}}
  {{/each}}
{{/if}}

Oh man that was my bad that was a really dumb mistake I just kept getting pulled away in the middle of typing. It should look like this:

{{#if data}}
  {{#each data as |item|}}
    {{#each-in item as |recordName recordValue|}}
      {{recordName}}: {{recordValue}}
    {{/each-in}}
  {{/each}}
{{else}}
  <div>
    <form id="userSubmit" {{action "search12" on="submit"}}>
      {{!-- form contents omitted for clarity --}}
    </form>
  </div>
{{/if}}

Does that make sense? Basically you’re establishing a control flow in the template. It’s basically saying “if there is data (meaning the query was successful) then render the data, otherwise render the form”

Note that this is a very simplistic approach to this type of scenario, it doesn’t do anything with error results, and it doesn’t have any code for resetting the results to get back to the form, etc, but it’s a good starting point to understand the data flow from template => js => server => js => template. After this point you could build on it in a bunch of different ways, abstracting it out into routes, keeping it in a component and using more logic for error/refetch/etc, using ember-data instead of raw fetch, etc.

Hi

The form template is working again. But everything but the nebid and andor is getting undefined?

EXEC [neb].[usp_StrainSearchGetStrain] '2940' , 0 , 'undefined' , 'undefined' , 'undefined' , 'undefined' , 'undefined'
, 'undefined' , 'undefined'

template home.hbs

{{!-- templates/components/home.js --}}
<h2>{{this.title}}</h2>
<header class="logo"></header>

{{#if data}}
  {{#each data as |item|}}
    {{#each-in item as |recordName recordValue|}}
      {{recordName}}: {{recordValue}}
    {{/each-in}}
  {{/each}}
{{else}}
  <div>
  
    <form id="userSubmit" {{action "search12" on="submit"}}>
	
        <label for="and">UNCHECKED "AND" CHECKED FOR "OR": 
      {{!-- {{input id="andor" type="checkbox" checked=this.andor}} --}}
      <input type="checkbox" checked={{this.andor}} onclick={{action (mut this.andor) value="target.checked"}}><br>
    </label>			
    <hr>	
    <label for="nebid">NEBID:</label>
    {{!--  {{input id="nebid" class = "inputField" type="text" value=this.nebid}} --}}
    <input id="nebid" value={{this.nebid}} oninput={{action (mut this.nebid) value="target.value"}}><br>
	
    <label for="productname">Product Name:</label>
    {{input id="productname" class = "inputField" type="text" value=this.productname}}<br>

    <label for="catalognumber">Catalog Number:</label>
    {{input id="catalognumber" class = "inputField" type="text" value=this.catalognumber}}<br>
								
    <label for="erhost">ER Host:</label>
    {{input id="erhost" class = "inputField" type="text" value=this.erhost}}<br>

    <label for="organism">Organism:</label>
    {{input id="organism" class = "inputField" type="text" value=this.organism}}<br>
	
    <label for="productionstatus">Status:</label>
    {{input id="productionstatus" class = "inputField" type="text" value=this.productionstatus}}<br>

    <label for="depositor">Depositor:</label>
    {{input id="depositor" class = "inputField" type="text" value=this.depositor}}<br>

    <label for="depositdate">Deposit Date:</label>
    {{input id="depositdate" class = "inputField" type="date" value=this.depositdate}}<br>

    <div>
      <button type="submit" name="search">Search</button> 
    </div> 
	  
	  
    </form>
  </div>
{{/if}}

oh wait I think I know you only provided examples for the 1st two parameters, correct?

I need to complete the rest: e.g. :

change this:

<label for="productname">Product Name:</label>
    {{input id="productname" class = "inputField" type="text" value=this.productname}} 

to this

    <label for="productname">Product Name:</label>
    {{!--  {{input id="productname" class = "inputField" type="text" value=this.productname}} --}}
	<input id="productname" value={{this.productname}} oninput={{action (mut this.productname) value="target.value"}}><br>

actually same results… undefined for the all but nebid… I think andor is undefined also but is getting handled by the

andor = andor ? 1 : 0;

and not the form…

In theory either input method should work, I just mentioned the one-way version because they tend to be a little more conventional and clear. Not sure what’s going on with the bindings. I made a twiddle and instead of the fetch just wrote the inputs to the screen (as well as console.log) and everything appears to work as expected.

https://ember-twiddle.com/e8c1e7687ba145378061869b72a9a163?openFiles=components.my-component.js%2Ctemplates.components.my-component.hbs

Thanks dknutsen, I am learning a lot from you!!!

using the code from your twiddle and un-commenting the fetch part its working better.

Is there a way to pause the results when they are displayed? They flash by and then disappear?

It also seems to miss multiple records and only flash by with one.

This is the home.js right now.

import Ember from 'ember';

export default Ember.Component.extend({
		andor: null,
		nebid: null,
		catalognumber: null,
		organism: null,
		depositor: null,
		productionstatus: null,
		productname: null,
		depositedate: null,
		erhost: null,
	
	actions: {		
    search12: function () {			
			let { andor, nebid, catalognumber, organism, depositor, productionstatus, productname, depositdate, erhost } = this;

      andor = andor ? 1 : 0;

      let data = { andor, nebid, catalognumber, organism, depositor, productionstatus, productname, depositdate, erhost };
      console.log(data);
      data = Object.keys(data).map((key) => {
        let obj = { };
        obj[key] = data[key];
        return obj;
      });
      this.set('data', data);

      fetch("http://localhost:3000/routes", {
        method: 'POST',				
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
        record: {
          andor,
          nebid,
          catalognumber,
          organism,
          depositor,
          productionstatus,
          productname,
          depositdate,
          erhost
        }
		})
      }).then((data) => {
        this.set('data', data);
      }).catch((err) => {	
        this.set('error', err);
      });
    },		
  }
});

This is whats in the console. So it returning data…

if the format of the response is { recordsets: [[ ... data ]] } I’d mod it to look something like:

      }).then((data) => {
        this.set('data', data.recordsets[0]);
      }).catch((err) => {

the template code (as written) loops through an array and then for each object in that array loops through the keys and prints them as key/val pairs

Thanks

Its stopping on the results page now. But it doesn’t seem to be returning the data from the api. It seems to be just returning what was keyed into the input fields?

So if I search for nebid: 2941

it returns just the nebid and not the full resultsreturned from the api? .

image

How do I isolate each field returned and display them? Do I need to parse the json in an array like I had in the original? Then how do I display the returned results not just what I type in the input boxes?

I can see the data is being returned in the browser debugger.