diff --git a/doc/tutorial/Javascript.lhs b/doc/tutorial/Javascript.lhs index 4054e4b3..2d9ab6bd 100644 --- a/doc/tutorial/Javascript.lhs +++ b/doc/tutorial/Javascript.lhs @@ -151,21 +151,120 @@ so we need a `Proxy` for our API type `API'` without its `Raw` endpoint. Very similarly to how one can derive haskell functions, we can derive the javascript with just a simple function call to `jsForAPI` from -`Servant.JQuery`. +`Servant.JS`. ``` haskell -apiJS :: Text -apiJS = jsForAPI api vanillaJS +apiJS1 :: Text +apiJS1 = jsForAPI api jquery ``` This `Text` contains 2 Javascript functions, 'getPoint' and 'getBooks': ``` javascript + +var getPoint = function(onSuccess, onError) +{ + $.ajax( + { url: '/point' + , success: onSuccess + , error: onError + , type: 'GET' + }); +} + +var getBooks = function(q, onSuccess, onError) +{ + $.ajax( + { url: '/books' + '?q=' + encodeURIComponent(q) + , success: onSuccess + , error: onError + , type: 'GET' + }); +} +``` + +We created a directory `static` that contains two static files: `index.html`, +which is the entrypoint to our little web application; and `ui.js`, which +contains some hand-written javascript. This javascript code assumes the two +generated functions `getPoint` and `getBooks` in scope. Therefore we need to +write the generated javascript into a file: + +``` haskell +writeJSFiles :: IO () +writeJSFiles = do + T.writeFile "static/api.js" apiJS1 + jq <- T.readFile =<< Language.Javascript.JQuery.file + T.writeFile "static/jq.js" jq +``` + +(We're also writing the jquery library into a file, as it's also used by +`ui.js`.) `static/api.js` will be included in `index.html` and the two +generated functions will therefore be available in `ui.js`. + +And we're good to go. You can start the `main` function of this file and go to +`http://localhost:8000/`. Start typing in the name of one of the authors in our +database or part of a book title, and check out how long it takes to +approximate pi using the method mentioned above. + +## Customizations + +Instead of calling `jquery`, you can call its variant `jqueryWith`. +Here are the type definitions + +```haskell ignore +jquery :: JavaScriptGenerator +jqueryWith :: CommonGeneratorOptions -> JavaScriptGenerator +``` + +The `CommonGeneratorOptions` will let you define different behaviors to +change how functions are generated. Here is the definition of currently +available options: + +```haskell ignore +data CommonGeneratorOptions = CommonGeneratorOptions + { + -- | function generating function names + functionNameBuilder :: FunctionName -> Text + -- | name used when a user want to send the request body (to let you redefine it) + , requestBody :: Text + -- | name of the callback parameter when the request was successful + , successCallback :: Text + -- | name of the callback parameter when the request reported an error + , errorCallback :: Text + -- | namespace on which we define the js function (empty mean local var) + , moduleName :: Text + -- | a prefix that should be prepended to the URL in the generated JS + , urlPrefix :: Text + } +``` + +This pattern is available with all supported backends, and default values are provided. + +## Vanilla support + +If you don't use JQuery for your application, you can reduce your +dependencies to simply use the `XMLHttpRequest` object from the standard API. + +Use the same code as before but simply replace the previous `apiJS` with +the following one: + +``` haskell +apiJS2 :: Text +apiJS2 = jsForAPI api vanillaJS +``` + +The rest is *completely* unchanged. + +The output file is a bit different, but it has the same parameters, + +``` javascript + + var getPoint = function(onSuccess, onError) { var xhr = new XMLHttpRequest(); xhr.open('GET', '/point', true); - xhr.setRequestHeader("Accept","application/json"); + xhr.setRequestHeader(\"Accept\",\"application/json\"); xhr.onreadystatechange = function (e) { if (xhr.readyState == 4) { if (xhr.status == 204 || xhr.status == 205) { @@ -186,7 +285,7 @@ var getBooks = function(q, onSuccess, onError) { var xhr = new XMLHttpRequest(); xhr.open('GET', '/books' + '?q=' + encodeURIComponent(q), true); - xhr.setRequestHeader("Accept","application/json"); + xhr.setRequestHeader(\"Accept\",\"application/json\"); xhr.onreadystatechange = function (e) { if (xhr.readyState == 4) { if (xhr.status == 204 || xhr.status == 205) { @@ -202,27 +301,218 @@ var getBooks = function(q, onSuccess, onError) } xhr.send(null); } + + ``` -We created a directory `static` that contains two static files: `index.html`, -which is the entrypoint to our little web application; and `ui.js`, which -contains some hand-written javascript. This javascript code assumes the two -generated functions `getPoint` and `getBooks` in scope. Therefore we need to -write the generated javascript into a file: +And that's all, your web service can of course be accessible from those +two clients at the same time! + +## Axios support + +### Simple usage + +If you use Axios library for your application, we support that too! + +Use the same code as before but simply replace the previous `apiJS` with +the following one: ``` haskell -writeJSFiles :: IO () -writeJSFiles = do - T.writeFile "static/api.js" apiJS - jq <- T.readFile =<< Language.Javascript.JQuery.file - T.writeFile "static/jq.js" jq +apiJS3 :: Text +apiJS3 = jsForAPI api $ axios defAxiosOptions ``` -(We're also writing the jquery library into a file, as it's also used by -`ui.js`.) `static/api.js` will be included in `index.html` and the two -generated functions will therefore be available in `ui.js`. +The rest is *completely* unchanged. + +The output file is a bit different, + +``` javascript + + +var getPoint = function() +{ + return axios({ url: '/point' + , method: 'get' + }); +} + + + +var getBooks = function(q) +{ + return axios({ url: '/books' + '?q=' + encodeURIComponent(q) + , method: 'get' + }); +} + +``` + +**Caution:** In order to support the promise style of the API, there are no onSuccess +nor onError callback functions. + +### Defining Axios configuration + +Axios lets you define a 'configuration' to determine the behavior of the +program when the AJAX request is sent. + +We mapped this into a configuration + +``` haskell +data AxiosOptions = AxiosOptions + { -- | indicates whether or not cross-site Access-Control requests + -- should be made using credentials + withCredentials :: !Bool + -- | the name of the cookie to use as a value for xsrf token + , xsrfCookieName :: !(Maybe Text) + -- | the name of the header to use as a value for xsrf token + , xsrfHeaderName :: !(Maybe Text) + } +``` + +## Angular support + +### Simple usage + +You can apply the same procedure as with `vanillaJS` and `jquery`, and +generate top level functions. + +The difference is that `angular` Generator always takes an argument. + +``` haskell +apiJS4 :: Text +apiJS4 = jsForAPI api $ angular defAngularOptions +``` + +The generated code will be a bit different than previous generators. An extra +argument `$http` will be added to let Angular magical Dependency Injector +operate. + +**Caution:** In order to support the promise style of the API, there are no onSuccess +nor onError callback functions. + +``` javascript + + +var getPoint = function($http) +{ + return $http( + { url: '/point' + , method: 'GET' + }); +} + + + +var getBooks = function($http, q) +{ + return $http( + { url: '/books' + '?q=' + encodeURIComponent(q) + , method: 'GET' + }); +} + +``` + +You can then build your controllers easily + +``` javascript + +app.controller("MyController", function($http) { + this.getPoint = getPoint($http) + .success(/* Do something */) + .error(/* Report error */); + + this.getPoint = getBooks($http, q) + .success(/* Do something */) + .error(/* Report error */); +}); +``` + +### Service generator + +You can also generate automatically a service to wrap the whole API as +a single Angular service: + +``` javascript +app.service('MyService', function($http) { + return ({ + postCounter: function() + { + return $http( + { url: '/counter' + , method: 'POST' + }); + }, + getCounter: function() + { + return $http( + { url: '/books' + '?q=' + encodeURIComponent(q), true); + , method: 'GET' + }); + } + }); +}); +``` + +To do so, you just have to use an alternate generator. + +``` haskell +apiJS5 :: Text +apiJS5 = jsForAPI api $ angularService defAngularOptions +``` + +Again, it is possible to customize some portions with the options. + +``` haskell +data AngularOptions = AngularOptions + { -- | When generating code with wrapInService, name of the service to generate, default is 'app' + serviceName :: Text + , -- | beginning of the service definition + prologue :: Text -> Text -> Text + , -- | end of the service definition + epilogue :: Text + } +``` + +# Custom function name builder + +Servant comes with three name builders included: + +- camelCase (the default) +- concatCase +- snakeCase + +Keeping the JQuery as an example, let's see the impact: + +``` haskell +apiJS6 :: Text +apiJS6 = jsForAPI api $ jqueryWith defCommonGeneratorOptions { functionNameBuilder= snakeCase } +``` + +This `Text` contains 2 Javascript functions: + +``` javascript + + +var get_point = function(onSuccess, onError) +{ + $.ajax( + { url: '/point' + , success: onSuccess + , error: onError + , type: 'GET' + }); +} + +var get_books = function(q, onSuccess, onError) +{ + $.ajax( + { url: '/books' + '?q=' + encodeURIComponent(q) + , success: onSuccess + , error: onError + , type: 'GET' + }); +} + +``` -And we're good to go. You can start the `main` function of this file and go to -`http://localhost:8000/`. Start typing in the name of one of the authors in our -database or part of a book title, and check out how long it takes to -approximate pi using the method mentioned above. diff --git a/doc/tutorial/test/JavascriptSpec.hs b/doc/tutorial/test/JavascriptSpec.hs index 2d6007a5..7dfd4cec 100644 --- a/doc/tutorial/test/JavascriptSpec.hs +++ b/doc/tutorial/test/JavascriptSpec.hs @@ -15,7 +15,10 @@ spec = do describe "apiJS" $ do it "is contained verbatim in Javascript.lhs" $ do code <- readFile "Javascript.lhs" - cs apiJS `shouldSatisfy` (`isInfixOf` code) + cs apiJS1 `shouldSatisfy` (`isInfixOf` code) + cs apiJS3 `shouldSatisfy` (`isInfixOf` code) + cs apiJS4 `shouldSatisfy` (`isInfixOf` code) + cs apiJS6 `shouldSatisfy` (`isInfixOf` code) describe "writeJSFiles" $ do it "[not a test] write apiJS to static/api.js" $ do @@ -24,7 +27,7 @@ spec = do describe "app" $ with (return app) $ do context "/api.js" $ do it "delivers apiJS" $ do - get "/api.js" `shouldRespondWith` (fromString (cs apiJS)) + get "/api.js" `shouldRespondWith` (fromString (cs apiJS1)) context "/" $ do it "delivers something" $ do diff --git a/servant-js/src/Servant/JS.hs b/servant-js/src/Servant/JS.hs index 498d45da..d11c6eb0 100644 --- a/servant-js/src/Servant/JS.hs +++ b/servant-js/src/Servant/JS.hs @@ -112,6 +112,7 @@ module Servant.JS , javascript , NoTypes , GenerateList(..) + , FunctionName(..) ) where import Prelude hiding (writeFile)