Near Api JS with Rails 7 Introduction One mentioned before that React isn't happy to developed with. Rails is better. Hence, we want to port some functionalities to Rails. Particularly, the `near-api-js` is what we need to call functions from corresponding smart contract(s) deployed separately. You might ask, why not use Gem? Well, Gem requires separate development than keeping everything in one. If core team only updates the `near-api-js`, it would always have the latest functionality before the community updates the corresponding Gem. If you would like to look at gem, here it is. We will look at gem in another article, whenever one finishes experimentation on it; here, we focus on javascript. https://github.com/near-rails-guide/near_api Note before continuing: Beware that one have very limited experience with Ruby and Ruby on Rails (barely 30 hours maybe?), so set your expectations. Things that professionals find a habit doesn't apply here. Also, this is a ***technical blog!*** If you decided to use **bootstrap**, please jump to the bootstrap section immediately before coming back to here. Import near-api-js to Rails 7 Rails 7 introduces importmap. With importmap, you still can use `npm`, but not how you would do it like React does (`npm i near-api-js`). Instead, it integrates with importmap. After creating a new rails project, navigate into the project directory and call this function. ./bin/importmap pin near-api-js If that does not work for some reason, force it by copying these into `config/importmap.rb`: pin "near-api-js", to: "https://ga.jspm.io/npm:near-api-js@0.44.2/lib/browser-index.js" pin "base-x", to: "https://ga.jspm.io/npm:base-x@3.0.9/src/index.js" pin "bn.js", to: "https://ga.jspm.io/npm:bn.js@5.2.0/lib/bn.js" pin "borsh", to: "https://ga.jspm.io/npm:borsh@0.6.0/lib/index.js" pin "bs58", to: "https://ga.jspm.io/npm:bs58@4.0.1/index.js" pin "buffer", to: "https://ga.jspm.io/npm:@jspm/core@2.0.0-beta.19/nodelibs/browser/buffer.js" pin "capability", to: "https://ga.jspm.io/npm:capability@0.2.5/index.js" pin "capability/es5", to: "https://ga.jspm.io/npm:capability@0.2.5/es5.js" pin "crypto", to: "https://ga.jspm.io/npm:@jspm/core@2.0.0-beta.19/nodelibs/browser/crypto.js" pin "depd", to: "https://ga.jspm.io/npm:depd@2.0.0/lib/browser/index.js" pin "error-polyfill", to: "https://ga.jspm.io/npm:error-polyfill@0.1.3/index.js" pin "http-errors", to: "https://ga.jspm.io/npm:http-errors@1.8.1/index.js" pin "inherits", to: "https://ga.jspm.io/npm:inherits@2.0.4/inherits_browser.js" pin "js-sha256", to: "https://ga.jspm.io/npm:js-sha256@0.9.0/src/sha256.js" pin "mustache", to: "https://ga.jspm.io/npm:mustache@4.2.0/mustache.js" pin "o3", to: "https://ga.jspm.io/npm:o3@1.0.3/index.js" pin "process", to: "https://ga.jspm.io/npm:@jspm/core@2.0.0-beta.19/nodelibs/browser/process-production.js" pin "safe-buffer", to: "https://ga.jspm.io/npm:safe-buffer@5.2.1/index.js" pin "setprototypeof", to: "https://ga.jspm.io/npm:setprototypeof@1.2.0/index.js" pin "statuses", to: "https://ga.jspm.io/npm:statuses@1.5.0/index.js" pin "text-encoding-utf-8", to: "https://ga.jspm.io/npm:text-encoding-utf-8@1.0.2/lib/encoding.lib.js" pin "toidentifier", to: "https://ga.jspm.io/npm:toidentifier@1.0.1/index.js" pin "tweetnacl", to: "https://ga.jspm.io/npm:tweetnacl@1.0.3/nacl-fast.js" pin "u3", to: "https://ga.jspm.io/npm:u3@0.1.1/index.js" Add custom javascript to Rails Next, we want to add a custom javascript to Rails. Follow the second answer from this link. Particularly: https://stackoverflow.com/questions/70548841/how-to-add-custom-js-file-to-new-rails-7-project (This is a copy of the answer) Go to `config/importmap.rb` and add the following: `pin_all_from "app/javascript/custom", under: "custom"` Go to `app/javascript/application.js` file and add the following: `import "custom/main"` In `app/javascript` directory, add `custom` folder. In `app/javascript/custom` directory add your custom js file `main.js`. Run In your terminal: `rails assets:precompile` Start your rails server. Voilà 👍 Add config.js There are lots of React projects you can see online. Let's just take a look at the guest book example. In the config.js, we need to make some changes for it to work. In line 1, we need to manually define where our smart contract is deployed to: unlike react which saves the configuration in `process`, Rails cannot detect `process` keyword, as smart contract is deployed separately from it (React is deployed together). So for example: https://github.com/near-examples/guest-book/blob/master/src/config.js const contract_name = 'guest-book.testnet'; Second, change `function getConfig(env)` to `export default function getConfig(env)`. Lastly, since we already `export default`, we can remove the last line (**remove** `module.exports = getConfig`). Add contents from index.js to main.js We named it main.js, but you could name it index.js too (just because the answer uses main.js one is lazy to change it). So in main.js (or index.js): Copy this content and put it in: https://github.com/Wabinab/NCD_demo/blob/main/main_project/src/utils.js We surely could call an init function when starting Rails Server. What we do is make changes at `application.html.erb`. In the `<body>` tag, add a `javascript_tag` before `yield`. <!DOCTYPE html> <html> ... <body> <% javascript_tag "javascript:initContract(#{Rails.env})" %> <%= yield %> </body> </html> So we added an extra input to pass in `Rails.env` to `initContract`, let's also update our function: (This replaces the `process.env` that Rails cannot detect we mentioned before): export async function initContract(environment) { const nearConfig = getConfig(environment) const near = ... ... window.contract = ... } Remember to update the `viewMethods` and `changeMethods` with the function names deployed at your contract! Also, remember to update `config.js` accordingly to reflect the three environments from Rails (`production`, `test`, and `development`). (Actually, the original code one sees already have the three tags, so most probably you can ignore, just double check to make sure). There's a caveat though: if you're at the home page (i.e. `root_path`), you cannot discover those `window.something` while on it. Only if you move to other paths will it be discovered. The error isn't something solvable unless removing it outside function. Let's look at the `login` and `logout` function next. What every function needs For every function, exporting them to `window` is required, otherwise Rails cannot find them. function login() { window.walletConnection.requestSignIn(nearConfig.contractName) } window.login = login So for our original contract, we need to add extra two lines in `main.js`: At the end of the file, add: window.login = login window.logout = logout If you have more functions, do that for all of them. What works in ERb Not all ERb function works. As far as one experimented, only `link_to` works. One only experimented with `link_to` and `button_to`, but there are others that are known to work like `javascript_tag` that one didn't experiment with; but you can try yourself. Oh, `button_to` don't work. If you want to use a button, you can do two ways: Define the class on `link_to`. Example with bootstrap (we'll discuss bootstrap next and its issues) <%= link_to "Login", "javascript:login()", class: "btn btn-primary" %> (see the `class` tag, which is optional). Another would be to not use ERb, just use a normal HTML button will work: <button onClick="javascript:login()">Sign in</button> One didn't experiment with `javascript_tag`, so you shall experiment it yourself. Setup Bootstrap If you started off running `rails new project-name --css=bootstrap` for rails v7.0.2.3 (or before), you're doomed! As of v7.0.2.3, bootstrap does not initialize with importmap, but with turbolinks. What we've done so far exists on the basis of importmaps. Turbolinks might work, but you need to ask someone specialized with turbolinks to ask the equivalent; or if you're professional with turbolinks, go forward translating the instructions yourself. If you start off with `rails new project-name`, let's see how to migrate to bootstrap. Based on the `cssbundling-rails` github page, we run these commands: ./bin/bundle add cssbundling-rails ./bin/rails css:install:bootstrap If you jump immediately to here as one mentioned, then the next paragraph aren't applicable: "build:css" will be created for you automatically. If you already add other stuffs, it fails unfortunately, so you need manual adding. These aren't complete though. Try running `yarn build:css` and you'll encounter error, as `package.json` has missing `build:css`. This isn't the problem if you ran `rails new project-name --css=bootstrap`; so we can just copy the commands from that `package.json` and paste it here for it to work. For your convenience, one attached the command below for v7.0.2.3 (one do not promise other Rails versions uses the same command, do your own research (DYOR)): Add this under `scripts`: (which should just currently have a `test`) "build:css": "sass ./app/assets/stylesheets/application.bootstrap.scss ./app/assets/builds/application.css --no-source-map --load-path=node_modules" Compilation of CSS also have errors. Fortunately, solving them is easy; just run these commands **everytime you updated the** `application.bootstrap.scss` **file, including any custom scss and perhaps css files you've defined in the** `app/assets/stylesheets` **folder**. #!/bin/bash yarn build:css rails assets:precompile bin/dev `yarn build:css` might be optional. For those developing in AWS Cloud 9 (like one does), follow Michael Hartl tutorial to setup `rails server` properly (otherwise cannot preview). In fact, `bin/dev` **does not work** with the setup, so after the server run, press CTRL+C to exit, and run ... bin/dev # wait for it to boot up finish, the command line will stop updating if you don't # do anything to it. CTRL+C rails server As long as you don't update any stylesheets, you can retain `rails server`. Otherwise, run through this command from `yarn build:css` again. What you can do and can't do We can get value like this: (theoretically) <% @varname = "javascript:window.walletConnection.isSignedIn" %> <% link_to "Create Post", @varname %> But practically, one couldn't test the above code, because the returned value is a boolean, not a link to some page.However, if you have a function that returns a hyperlink, it certainly works, so we would assume it works. This works: <% @param = "javascript:getParameters()" %> where in Javascript: function getParameters() { return "message = hello_world"; } window.getParameters = getParameters But this doesn't work: "javascript:window.getParameters()" fails with Uncaught TypeError: window.getParameters is not a function When one tries to call the contract like this: <% @param = "javascript:window.contract.get_article_by_id({'article_id': 'some_id'})" %> **It works!** The result are in F12 browser console though, so open that up first before calling the function to see the results. And you certainly could pass in arguments just like how you usually append something to a string in Ruby: "Some variable in Ruby: #{variable_name}" This is useful if you have to take some input from users, or inputs from users' actions that lead to some choices to insert to the string. Turn it into a function `window.contract.get_article_by_id` is not fancy to use within ERb, so let's move it to Javascript. We want to reduce our action to something like this: "javascript:get_article_by_id('#{variable_name}')" (Note the single quote around `#{...}` if you want to make it a string; if you forget these, it will fail if it should be a string input; if float or integer, it might pass, one isn't sure). Inside `main.js`, add this: function get_article_by_id(article_id) { return window.contract.get_article_by_id({ "article_id": article_id }).then( value => alert(value), err => alert(err), ); } window.get_article_by_id = get_article_by_id So this function returns a promise, and the `.then()` will give a popup with the return values once the `Promise` is fulfilled or error. If fulfilled, it'll do whatever you ask in the `value` strand, while if it failed, it'll do whatever you tell in the `err` strand. Here, both are `alert`, so that makes no difference. Try to use `console.log` for one and `alert` for another and you could see the differences. We could repeat these for other contract functions also, depending on how you want to handle it. Conclusion Adapter to near-api-js works! It isn't as convenient as gem functions, though. Following Rails convention over configuration rule, you might prefer near api with Gems (which one can tell you, isn't mature yet). Until it becomes mature, resorting to adapter with Javascript is okay; we can migrate to gems later on when things got more mature. Additionally, developers also require programming in Javascript, which we try to reduce to as little as possible when using Rails. Though Rails support javascript, having to run functions wrapped in string quotes aren't convenient; and there may be lots of integration problem with fetching data from javascript too! Imagine writing getter functions just to get functions that doesn't work by calling `window.something` directly. It's gonna flood our javascript. One might continue on with building an application with Rails and NEAR Protocol so stay updated with future blogs. **Like and Subscribe!** References https://github.com/rails/importmap-rails https://blog.appsignal.com/2021/12/15/whats-new-in-rails7.html https://docs.near.org/docs/api/naj-quick-reference https://stackoverflow.com/questions/70548841/how-to-add-custom-js-file-to-new-rails-7-project https://github.com/rails/cssbundling-rails How to use near-api-js: https://github.com/near/near-api-js/blob/master/examples/quick-reference.md
6¢
Log in to join in Reading is open to everyone. Replying needs an account.
3 comments
Yeah I see that it's a technical blog. That's why it went over me. I am hardly capable understanding basic java😂( and basic isn't what you think basic is, it's much more basic than that).
Hahah yeah it's a difficult decision: technical audiences and non-technical audiences... perhaps one would write non-technical brief summary next time, and a link to a more technical blog somewhere else. What do you think?
Yeah, I agree. If you ask me I love the fact that we have someone here that have advanced knowledge(whatever the field), in fact, for instance I even searched read.cash for java tutorials 😂 as i like reading articles, but must keep another factor in mind that, technical audience is much less in number than the non-technical( no matter what the field). So the conclusion is that you are Right. Sure do work on technical articles as well but prefer non-technical(at least for read.cash). Stay happy, Stay blessed.