.. -*- coding: utf-8 -*- .. _TutosMuseumsEnhanceViews: Enhance views ------------- In :ref:`TutosMuseumsGettingStarted`, we saw how to develop our views by writing html code directly in CubicWeb views. In this part, we will see how to customize our web application using different methods : with pyramid views using jinja2 templates and with React. Pyramid and Jinja2 ~~~~~~~~~~~~~~~~~~ React in a CubicWeb view ~~~~~~~~~~~~~~~~~~~~~~~~ In this section, we want to add a map in museum pages to display where is the museum associated with the page. To do this, we will use `React simple maps`_, a React_ library. Our goal is to add a react component inside our museum primary view. First, we will setup our environment. At logilab, we use Typescript_ when it is possible, so we will use it also in this tutorial. As module builder, we will use Webpack_. .. _`React simple maps`: https://www.react-simple-maps.io/ .. _React: https://reactjs.org/ .. _Typescript: https://www.typescriptlang.org/ .. _Webpack: https://webpack.js.org/ Thus, we need to create three files at the root of our cube: `package.json`, `tsconfig.json` and `webpack.config.js`. A lot of documentation can be find on the Web about how to configure a React/Typescript environment, so we are not going to dwell on it in this tutorial; and we will simply copy and paste the following files. `package.json`: .. sourcecode:: json { "name": "cubicweb_tuto", "version": "1.0.0", "description": "Summary ------- A cube for new CW tutorial", "directories": { "test": "test" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack", "watch": "webpack --watch --mode=development" }, "author": "Logilab", "license": "GPL-2.0-or-later", "dependencies": { "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", "@types/react-simple-maps": "^1.0.3", "prop-types": "^15.7.2", "react": "^17.0.1", "react-dom": "^17.0.1", "react-simple-maps": "^2.3.0", "ts-loader": "^8.0.14", "typescript": "^4.1.3", "webpack": "^5.18.0", "webpack-cli": "^4.4.0" } } `tsconfig.json`: .. sourcecode:: json { "compilerOptions": { "target": "es5", "module": "commonjs", "jsx": "react", "strict": true, "esModuleInterop": true } } `webpack.config.js`: .. sourcecode:: javascript const path = require("path"); module.exports = { entry: { "map.js": "./appjs/geomap.tsx", }, output: { filename: "[name]", path: path.resolve(__dirname, "./cubicweb_tuto/data/") }, resolve: { extensions: [".tsx", ".ts", ".jsx", ".js"] }, module: { rules: [ { test: [/\.tsx?$/], exclude: /node_modules/, use: ["ts-loader"] } ] }, plugins: [] }; Now we have our configuration files, we have to install NodeJS_ and then install our project using `npm`. .. _NodeJS: https://nodejs.org/ .. code-block:: console sudo apt-get install nodejs npm install They are two last things to do: * create a component to display a museum on the map; * integrate our component in a CubicWeb view. By convention, we put our js files in a `appjs` directory, and bundle are built in `cubicweb_tuto/data` (as you can see in our `webpack.config.js`). Then, we will create a file `geomap.tsx` in `appjs/`. For our component, we will need three parameters: our museum name, its latitude and its longitude. These parameters will be defined in our CubicWeb view when we will call our script. Our file `geomap.tsx` can be written like this: .. sourcecode:: javascript import React from 'react'; import ReactDOM from 'react-dom'; import { ComposableMap, Geographies, Geography, Marker, Point } from "react-simple-maps"; const geoUrl = "https://raw.githubusercontent.com/zcreativelabs/react-simple-maps/master/topojson-maps/world-110m.json"; declare const data: { name: string, latitude: number, longitude: number, } const MapChart = () => { return ( {({ geographies }) => geographies .map(geo => ( )) } {data.name} ); }; function App() { return } const root = document.getElementById("awesome-map"); ReactDOM.render(, root); Now we will override the `render_entity(self, entity)` function of the Museum PrimaryView, in :file:`cubicweb-tuto/views.py` to add: * the bundle javascript including our component; * a div with the id `awesome-map` which will be used by our component. .. sourcecode:: python class MuseumPrimaryView(PrimaryView): __select__ = is_instance("Museum") def render_entity(self, entity): self.render_entity_toolbox(entity) self.render_entity_title(entity) # entity's attributes and relations, excluding meta data # if the entity isn't meta itself if self.is_primary(): boxes = self._prepare_side_boxes(entity) else: boxes = None if boxes or hasattr(self, "render_side_related"): self.w('
') self.w('
') self.content_navigation_components("navcontenttop") self.render_entity_attributes(entity) if self.main_related_section: self.render_entity_relations(entity) self.render_map(entity) self.content_navigation_components("navcontentbottom") self.w("
") # side boxes if boxes or hasattr(self, "render_side_related"): self.w("
") self.w('
') self.render_side_boxes(boxes) self.w("
") self.w("
") def render_entity_title(self, entity): """Renders the entity title, by default using entity's :meth:`dc_title()` method. """ self.w(f"

{entity.title_with_city}

") def render_map(self, entity): """Renders a map displaying where the museum is.""" if not (entity.latitude and entity.longitude): return js_file = f"{self._cw.vreg.config.datadir_url}map.js" data = json_dumps(entity) self.w('
') self.w( f""" """ ) Most part of `render_entity(self, entity)` are the same as its definition in `PrimaryView`, except that we add a call to `render_map(self, entity)`; which will add a `div` tag with a specific id and a `script` tag adding our javascript bundle, and define variables containing information to display a museum on the map. The specific id must be the same as the one we defined in our javascript file, *awesome-map*. Now, it's time to build the javascript bundle using: .. code-block:: console npm run build And then, run our application: .. code-block:: console cubicweb-ctl pyramid -D tuto_instance We now have a world map displaying the location of our museum on museum pages. A lot of things could be done to have a better result, like center the map on the museum, but it's out of the scope of this tutorial. .. image:: ../../images/tutos-museum_react_map.png :alt: Our application with a World Map. React in a Pyramid view ~~~~~~~~~~~~~~~~~~~~~~~ .. TODO complete documentation