Skip to content

Commit

Permalink
CCDM: connect client and server UIs from javascript (#6210)
Browse files Browse the repository at this point in the history
Fixes  #6133 by:
- It Makes possible to append flow UI to browser without 
   requiring an exported web component.
- After flow-client configures the bootstrap, is it possible to
   attach UI to any slot in the browser.
- This PR does not resolve client side navigation because server
   side is not implemented yet (#6221), instead it attaches a static 
   view to the outlet from server
- Fixes a couple of errors in the generated client javascript when
  importing the file as an ES6 module:
  - There was a JSNI block with non declared variable in a for
     statement
  - There was a weird function where a variable was not declared
     when the method was inlined by gwt compiler
- Adding `eslint` rules to early detect whether the produced
   javascript has declaration errors
  • Loading branch information
manolo committed Aug 12, 2019
1 parent a521d55 commit 25e39da
Show file tree
Hide file tree
Showing 11 changed files with 245 additions and 75 deletions.
39 changes: 37 additions & 2 deletions flow-client/.eslintrc.js
Expand Up @@ -13,5 +13,40 @@ module.exports = {
"sourceType": "module"
},
"rules": {
}
};
},
"overrides": [{
"files": ["FlowClient.js"],

"rules": {
"brace-style": "off",
"camelcase": "off",
"comma-spacing": "off",
"curly": "off",
"indent": "off",
"key-spacing": "off",
"keyword-spacing": "off",
"max-len": "off",
"no-caller": "off",
"no-constant-condition": "off",
"no-control-regex": "off",
"no-debugger": "off",
"no-empty": "off",
"no-ex-assign": "off",
"no-extra-semi": "off",
"no-func-assign": "off",
"no-invalid-this": "off",
"no-redeclare": "off",
"no-self-assign": "off",
"no-throw-literal": "off",
"no-unused-vars": "off",
"quotes": "off",
"semi": "off",
"semi-spacing": "off",
"space-before-blocks": "off",
"space-before-function-paren": "off",
"space-infix-ops": "off",
"no-trailing-spaces": "off",
"func-call-spacing": "off"
}
}]
};
4 changes: 2 additions & 2 deletions flow-client/package.json
Expand Up @@ -9,14 +9,14 @@
"url": "https://github.com/vaadin/flow/issues"
},
"scripts": {
"lint": "eslint 'src/main/resources/META-INF/resources/frontend/FlowBootstrap.js' && tslint 'src/main/resources/META-INF/resources/frontend/**/*.ts'",
"lint": "eslint 'src/main/resources/META-INF/resources/frontend/*.js' && tslint 'src/main/resources/META-INF/resources/frontend/*.ts'",
"_cp_to_src": "ncp target/classes/META-INF/resources/VAADIN/static/client/client-*.cache.js src/main/resources/META-INF/resources/frontend/FlowClient.js",
"_wrap_fnc": "replace-in-files --regex '(.+[\\s\\S]*)' --replacement 'const init = function(){\\n$1\\n};\\nexport {init};\\n' src/main/resources/META-INF/resources/frontend/FlowClient.js",
"_cp_to_tgt": "ncp src/main/resources/META-INF/resources/frontend/FlowClient.js target/classes/META-INF/resources/frontend/FlowClient.js",
"client": "npm run _cp_to_src && npm run _wrap_fnc && npm run _cp_to_tgt",
"webpack": "webpack --config=webpack.tests.config.js",
"build": "npm run client && tsc",
"compile": "npm run lint && npm run build",
"compile": "npm run build && npm run lint",
"test": "npm run build && npm run webpack && intern",
"debug": "npm run build && npm run webpack && intern serveOnly"
},
Expand Down
Expand Up @@ -184,17 +184,18 @@ public void run() {
* @return The URI to use for server messages.
*/
protected String getUri() {
String uri = registry.getApplicationConfiguration().getServiceUrl();
uri = SharedUtil.addGetParameter(uri,
ApplicationConstants.REQUEST_TYPE_PARAMETER,
ApplicationConstants.REQUEST_TYPE_UIDL);

uri = SharedUtil.addGetParameter(uri,
ApplicationConstants.UI_ID_PARAMETER,
registry.getApplicationConfiguration().getUIId());

return uri;

// This code is in one line because an odd bug in GWT
// compiler inlining this piece of code and not declaring
// the variable in JS scope, breaking strict mode which is
// needed for ES6 imports.
// See https://github.com/vaadin/flow/pull/6227
return SharedUtil.addGetParameter(
SharedUtil.addGetParameter(
registry.getApplicationConfiguration().getServiceUrl(),
ApplicationConstants.REQUEST_TYPE_PARAMETER,
ApplicationConstants.REQUEST_TYPE_UIDL),
ApplicationConstants.UI_ID_PARAMETER,
registry.getApplicationConfiguration().getUIId());
}

private static native boolean resendRequest(XMLHttpRequest xhr)
Expand Down
Expand Up @@ -335,6 +335,7 @@ private native void hookUpPolymerElement(StateNode node, Element element)
var props = Object.getOwnPropertyNames(changedProps);
var items = "items.";
var i;
for(i=0; i<props.length; i++){
// There should be a property which starts with "items."
// and the next token is the index of changed item
Expand Down
32 changes: 30 additions & 2 deletions flow-client/src/main/resources/META-INF/resources/frontend/Flow.ts
Expand Up @@ -12,6 +12,10 @@ interface AppInitResponse {
appConfig: AppConfig;
}

export interface NavigationParameters {
path: string;
}

/**
* Client API for flow UI operations.
*/
Expand Down Expand Up @@ -53,8 +57,32 @@ export class Flow {
/**
* Go to a route defined in server.
*/
navigate(): Promise<void> {
return Promise.resolve();
async navigate(params : NavigationParameters): Promise<HTMLElement> {
await this.start();
return this.getFlowElement(params.path);
}

private async getFlowElement(routePath : string): Promise<HTMLElement> {
return new Promise(resolve => {
// we create any tag using the route as reference
const tag = 'flow-' + routePath.replace(/[^\w]+/g, "-").toLowerCase();

// flow use body for keep references
const flowRoot = document.body as any;
flowRoot.$ = flowRoot.$ || {counter: 0};

// Create the wrapper element with an unique id
const id = `${tag}-${flowRoot.$.counter ++}`;
const element = flowRoot.$[id] = document.createElement(tag);
element.id = id;
window.console.log("Created new element for a flow view: " + id);

// The callback run from server side once the view is ready
(element as any).serverConnected = () => resolve(element);

// Call server side to navigate to the given route
flowRoot.$server.connectClient(tag, id, routePath);
});
}

private async initFlowClient(clientMod: any): Promise<void> {
Expand Down
86 changes: 54 additions & 32 deletions flow-client/src/test/frontend/FlowTests.ts
Expand Up @@ -27,39 +27,10 @@ suite("Flow", () => {
});

test("should initialize Flow client when calling start()", () => {
// Configure a valid server response
mock.get('VAADIN/?v-r=init', (req, res) => {
assert.equal('GET', req.method());
return res
.status(200)
.header("content-type","application/json")
.body(`
{
"appConfig": {
"heartbeatInterval" : 300,
"contextRootUrl" : "../",
"debug" : true,
"v-uiId" : 0,
"serviceUrl" : "//localhost:8080/flow/",
"webComponentMode" : false,
"productionMode": false,
"appId": "foobar-1111111",
"uidl": {
"syncId": 0,
"clientId": 0,
"changes": [],
"timings": [],
"Vaadin-Security-Key": "119a6005-e663-4a4c-a882-bbfa8bd0c304",
"Vaadin-Push-ID": "4b915ffb-4e0a-484c-9995-09500fe9fa3a"
}
}
}
`);
});

const $wnd = window as any;
assert.isUndefined($wnd.Vaadin);

mockInitResponse();
return new Flow()
.start()
.then(response => {
Expand Down Expand Up @@ -93,7 +64,58 @@ suite("Flow", () => {
});
});

test("should have the navigate() method in the API", () => {
return new Flow().navigate();
test("should connect client and server on navigation", () => {
const flowRoot = (window.document.body as any);

flowRoot.$server = {
connectClient: () => {
// Resolve the promise
flowRoot.$['flow-foo-bar-baz-0'].serverConnected();
}
};

mockInitResponse();
return new Flow()
.navigate({path: "Foo/Bar.baz"})
.then(() => {
// Check that start() was called
assert.isDefined((window as any).Vaadin.Flow.resolveUri);

// Assert that element was created amd put in flowRoot so as server can find it
assert.equal(1, flowRoot.$.counter);
assert.isDefined(flowRoot.$['flow-foo-bar-baz-0']);
});
});
});

function mockInitResponse() {
// Configure a valid server initialization response
mock.get('VAADIN/?v-r=init', (req, res) => {
assert.equal('GET', req.method());
return res
.status(200)
.header("content-type","application/json")
.body(`
{
"appConfig": {
"heartbeatInterval" : 300,
"contextRootUrl" : "../",
"debug" : true,
"v-uiId" : 0,
"serviceUrl" : "//localhost:8080/flow/",
"webComponentMode" : false,
"productionMode": false,
"appId": "foobar-1111111",
"uidl": {
"syncId": 0,
"clientId": 0,
"changes": [],
"timings": [],
"Vaadin-Security-Key": "119a6005-e663-4a4c-a882-bbfa8bd0c304",
"Vaadin-Push-ID": "4b915ffb-4e0a-484c-9995-09500fe9fa3a"
}
}
}
`);
});
}
Expand Up @@ -25,10 +25,13 @@

import com.vaadin.flow.component.ClientCallable;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.HtmlContainer;
import com.vaadin.flow.component.PushConfiguration;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.dom.Element;
import com.vaadin.flow.function.DeploymentConfiguration;
import com.vaadin.flow.internal.UsageStatistics;
import com.vaadin.flow.internal.nodefeature.NodeProperties;
import com.vaadin.flow.router.HasUrlParameter;
import com.vaadin.flow.router.QueryParameters;
import com.vaadin.flow.router.Router;
Expand Down Expand Up @@ -82,17 +85,44 @@ protected Optional<ThemeDefinition> getTheme() {
* Custom UI for {@link JavaScriptBootstrapHandler}.
*/
public static class JavaScriptBootstrapUI extends UI {
public static final String NO_NAVIGATION = "Navigation is not implemented yet";
public static final String NO_NAVIGATION =
"Classic flow navigation is not supported for clien-side projects";

/**
* Connect a client side with server side UI.
* Connect a client with the server side UI.
*
* @param containerId
* client side id of the element
* @param clientElementTag
* client side element tag
* @param clientElementId
* client side element id
* @param flowRoute
* flow route that should be attached to the client element
*/
@ClientCallable
public void connectClient(String containerId) {
// NOT implemented yet
public void connectClient(String clientElementTag, String clientElementId, String flowRoute) {

// Get the flow view that the user wants to navigate to.
final Element viewElement = getViewForRoute(flowRoute).getElement();

// Create flow reference for the client outlet element
final Element wrapperElement = new Element(clientElementTag);
wrapperElement.appendChild(viewElement);

// Connect server with client
getElement().getStateProvider().appendVirtualChild(
getElement().getNode(), wrapperElement,
NodeProperties.INJECT_BY_ID, clientElementId);

// Inform the client, that everything went fine.
wrapperElement.executeJs("$0.serverConnected()");
}

private Component getViewForRoute(String route) {
// Create a temporary view until navigation is implemented
HtmlContainer view = new HtmlContainer("div");
view.setText(String.format(
"Navigation not implemented yet. cannot show '%s'", route));
return view;
}

@Override
Expand Down
Expand Up @@ -81,26 +81,24 @@ public void createStateNode_stateNodeHasRequiredElementDataFeature() {

@Test
public void visitOnlyNode_hasDescendants_nodeVisitedAndNoDescendantsVisited() {
TestNodeVisitor visitor = new TestNodeVisitor();
visitor.visitDescendants = false;
TestNodeVisitor visitor = new TestNodeVisitor(false);

Map<Node<?>, ElementType> map = new HashMap<>();

Element subject = createHierarchy(map);

BasicElementStateProvider.get().visit(subject.getNode(), visitor);

Assert.assertEquals(1, visitor.visited.size());
Assert.assertEquals(1, visitor.getVisited().size());
Assert.assertEquals(subject,
visitor.visited.keySet().iterator().next());
visitor.getVisited().keySet().iterator().next());
Assert.assertEquals(ElementType.REGULAR,
visitor.visited.values().iterator().next());
visitor.getVisited().values().iterator().next());
}

@Test
public void visitOnlyNode_hasDescendants_nodeAndDescendatnsAreVisited() {
TestNodeVisitor visitor = new TestNodeVisitor();
visitor.visitDescendants = true;
TestNodeVisitor visitor = new TestNodeVisitor(true);

Map<Node<?>, ElementType> map = new HashMap<>();

Expand All @@ -112,7 +110,7 @@ public void visitOnlyNode_hasDescendants_nodeAndDescendatnsAreVisited() {

Assert.assertEquals(
"The collected descendants doesn't match expected descendatns",
map, visitor.visited);
map, visitor.getVisited());
}

@Test
Expand All @@ -121,7 +119,7 @@ public void visitNode_noChildren_featuresNotInitialized() {

assertNoChildFeatures(element);

element.accept(new TestNodeVisitor());
element.accept(new TestNodeVisitor(true));

assertNoChildFeatures(element);
}
Expand Down

0 comments on commit 25e39da

Please sign in to comment.