mirror of
https://github.com/flutter/samples.git
synced 2025-11-09 22:38:42 +00:00
Publish web_embedding (#1777)
## Pre-launch Checklist - [x] I read the [Flutter Style Guide] _recently_, and have followed its advice. - [x] I signed the [CLA]. - [x] I read the [Contributors Guide]. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-devrel channel on [Discord]. <!-- Links --> [Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo [CLA]: https://cla.developers.google.com/ [Discord]: https://github.com/flutter/flutter/wiki/Chat [Contributors Guide]: https://github.com/flutter/samples/blob/main/CONTRIBUTING.md Co-authored-by: Mark Thompson <2554588+MarkTechson@users.noreply.github.com> Co-authored-by: David Iglesias <ditman@gmail.com> Co-authored-by: Mark Thompson <2554588+MarkTechson@users.noreply.github.com> Co-authored-by: David Iglesias <ditman@gmail.com>
This commit is contained in:
35
web_embedding/ng-flutter/src/app/app.component.spec.ts
Normal file
35
web_embedding/ng-flutter/src/app/app.component.spec.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterTestingModule
|
||||
],
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it(`should have as title 'ng-flutter'`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app.title).toEqual('ng-flutter');
|
||||
});
|
||||
|
||||
it('should render title', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('.content span')?.textContent).toContain('ng-flutter app is running!');
|
||||
});
|
||||
});
|
||||
171
web_embedding/ng-flutter/src/app/app.component.ts
Normal file
171
web_embedding/ng-flutter/src/app/app.component.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { ChangeDetectorRef, Component } from '@angular/core';
|
||||
import { NgFlutterComponent } from './ng-flutter/ng-flutter.component';
|
||||
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatSliderModule } from '@angular/material/slider';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app-root',
|
||||
template: `
|
||||
<mat-toolbar color="primary">
|
||||
<button
|
||||
aria-label="Toggle sidenav"
|
||||
mat-icon-button
|
||||
(click)="drawer.toggle()">
|
||||
<mat-icon aria-label="Side nav toggle icon">menu</mat-icon>
|
||||
</button>
|
||||
<span>Angular 🤝 Flutter</span>
|
||||
<span class="toolbar-spacer"></span>
|
||||
<mat-icon aria-hidden="true">flutter_dash</mat-icon>
|
||||
</mat-toolbar>
|
||||
<mat-sidenav-container [hasBackdrop]=false class="sidenav-container">
|
||||
<mat-sidenav #drawer mode="side" [opened]=false class="sidenav">
|
||||
<mat-nav-list autosize>
|
||||
<section>
|
||||
<h2>Effects</h2>
|
||||
<div class="button-list">
|
||||
<button class="mb-control" mat-stroked-button color="primary"
|
||||
(click)="container.classList.toggle('fx-shadow')">Shadow</button>
|
||||
<button class="mb-control" mat-stroked-button color="primary"
|
||||
(click)="container.classList.toggle('fx-mirror')">Mirror</button>
|
||||
<button class="mb-control" mat-stroked-button color="primary"
|
||||
(click)="container.classList.toggle('fx-resize')">Resize</button>
|
||||
<button class="mb-control" mat-stroked-button color="primary"
|
||||
(click)="container.classList.toggle('fx-spin')">Spin</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>JS Interop</h2>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Screen</mat-label>
|
||||
<mat-select
|
||||
(valueChange)="this.flutterState?.setScreen($event)"
|
||||
[value]="this.flutterState?.getScreen()">
|
||||
<mat-option value="counter">Counter</mat-option>
|
||||
<mat-option value="text">TextField</mat-option>
|
||||
<mat-option value="dash">Custom App</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" *ngIf="this.flutterState?.getScreen() == 'counter'">
|
||||
<mat-label>Clicks</mat-label>
|
||||
<input type="number" matInput (input)="onCounterSet($event)" [value]="this.flutterState?.getClicks()" />
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" *ngIf="this.flutterState?.getScreen() != 'counter'">
|
||||
<mat-label>Text</mat-label>
|
||||
<input type="text" matInput (input)="onTextSet($event)" [value]="this.flutterState?.getText()" />
|
||||
<button *ngIf="this.flutterState?.getText()" matSuffix mat-icon-button aria-label="Clear" (click)="this.flutterState?.setText('')">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
</mat-nav-list>
|
||||
</mat-sidenav>
|
||||
|
||||
<mat-sidenav-content class="sidenav-content">
|
||||
<div class="flutter-app" #container>
|
||||
<ng-flutter
|
||||
src="flutter/main.dart.js"
|
||||
assetBase="/flutter/"
|
||||
(appLoaded)="onFlutterAppLoaded($event)"></ng-flutter>
|
||||
</div>
|
||||
</mat-sidenav-content>
|
||||
</mat-sidenav-container>
|
||||
`,
|
||||
styles: [`
|
||||
:host{
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
.toolbar-spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.sidenav-container {
|
||||
flex: 1;
|
||||
}
|
||||
.sidenav {
|
||||
width: 300px;
|
||||
padding: 10px;
|
||||
}
|
||||
.button-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.button-list button {
|
||||
min-width: 130px;
|
||||
}
|
||||
.sidenav-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.flutter-app {
|
||||
border: 1px solid #eee;
|
||||
border-radius: 5px;
|
||||
height: 480px;
|
||||
width: 320px;
|
||||
transition: all 150ms ease-in-out;
|
||||
overflow: hidden;
|
||||
}
|
||||
`],
|
||||
imports: [
|
||||
NgFlutterComponent,
|
||||
MatToolbarModule,
|
||||
MatSidenavModule,
|
||||
MatSidenavModule,
|
||||
MatIconModule,
|
||||
CommonModule,
|
||||
MatListModule,
|
||||
MatCardModule,
|
||||
MatSliderModule,
|
||||
MatButtonModule,
|
||||
MatFormFieldModule,
|
||||
MatSelectModule,
|
||||
MatInputModule,
|
||||
],
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'ng-flutter';
|
||||
flutterState?: any;
|
||||
|
||||
constructor(private changeDetectorRef: ChangeDetectorRef, private breakpointObserver: BreakpointObserver) { }
|
||||
|
||||
onFlutterAppLoaded(state: any) {
|
||||
this.flutterState = state;
|
||||
this.flutterState.onClicksChanged(() => { this.onCounterChanged() });
|
||||
this.flutterState.onTextChanged(() => { this.onTextChanged() });
|
||||
}
|
||||
|
||||
onCounterSet(event: Event) {
|
||||
let clicks = parseInt((event.target as HTMLInputElement).value, 10) || 0;
|
||||
this.flutterState.setClicks(clicks);
|
||||
}
|
||||
|
||||
onTextSet(event: Event) {
|
||||
this.flutterState.setText((event.target as HTMLInputElement).value || '');
|
||||
}
|
||||
|
||||
// I need to force a change detection here. When clicking on the "Decrement"
|
||||
// button, everything works fine, but clicking on Flutter doesn't trigger a
|
||||
// repaint (even though this method is called)
|
||||
onCounterChanged() {
|
||||
this.changeDetectorRef.detectChanges();
|
||||
}
|
||||
|
||||
onTextChanged() {
|
||||
this.changeDetectorRef.detectChanges();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { NgFlutterComponent } from './ng-flutter.component';
|
||||
|
||||
describe('NgFlutterComponent', () => {
|
||||
let component: NgFlutterComponent;
|
||||
let fixture: ComponentFixture<NgFlutterComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ NgFlutterComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(NgFlutterComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Component, AfterViewInit, SimpleChanges, ViewChild, ElementRef, Input, EventEmitter, Output } from '@angular/core';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
|
||||
// The global _flutter namespace
|
||||
declare var _flutter: any;
|
||||
declare var window: {
|
||||
_debug: any
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'ng-flutter',
|
||||
standalone: true,
|
||||
template: `
|
||||
<div #flutterTarget>
|
||||
<div class="spinner">
|
||||
<mat-spinner></mat-spinner>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
:host div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.spinner {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}`,
|
||||
],
|
||||
imports: [
|
||||
MatProgressSpinnerModule,
|
||||
],
|
||||
})
|
||||
export class NgFlutterComponent implements AfterViewInit {
|
||||
// The target that will host the Flutter app.
|
||||
@ViewChild('flutterTarget') flutterTarget!: ElementRef;
|
||||
|
||||
@Input() src: String = 'main.dart.js';
|
||||
@Input() assetBase: String = '';
|
||||
@Output() appLoaded: EventEmitter<Object> = new EventEmitter<Object>();
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
const target: HTMLElement = this.flutterTarget.nativeElement;
|
||||
|
||||
_flutter.loader.loadEntrypoint({
|
||||
entrypointUrl: this.src,
|
||||
onEntrypointLoaded: async (engineInitializer: any) => {
|
||||
let appRunner = await engineInitializer.initializeEngine({
|
||||
hostElement: target,
|
||||
assetBase: this.assetBase,
|
||||
});
|
||||
await appRunner.runApp();
|
||||
}
|
||||
});
|
||||
|
||||
target.addEventListener("flutter-initialized", (event: Event) => {
|
||||
let state = (event as CustomEvent).detail;
|
||||
window._debug = state;
|
||||
this.appLoaded.emit(state);
|
||||
}, {
|
||||
once: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
0
web_embedding/ng-flutter/src/assets/.gitkeep
Normal file
0
web_embedding/ng-flutter/src/assets/.gitkeep
Normal file
BIN
web_embedding/ng-flutter/src/favicon.ico
Normal file
BIN
web_embedding/ng-flutter/src/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 948 B |
16
web_embedding/ng-flutter/src/index.html
Normal file
16
web_embedding/ng-flutter/src/index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>NgFlutter</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
</head>
|
||||
<body class="mat-typography">
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
14
web_embedding/ng-flutter/src/main.ts
Normal file
14
web_embedding/ng-flutter/src/main.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { provideRouter, Routes } from '@angular/router';
|
||||
import { AppComponent } from './app/app.component';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { importProvidersFrom } from '@angular/core';
|
||||
|
||||
const appRoutes: Routes = [];
|
||||
|
||||
bootstrapApplication(AppComponent, {
|
||||
providers: [
|
||||
provideRouter(appRoutes),
|
||||
importProvidersFrom(BrowserAnimationsModule)
|
||||
]
|
||||
})
|
||||
54
web_embedding/ng-flutter/src/styles.css
Normal file
54
web_embedding/ng-flutter/src/styles.css
Normal file
@@ -0,0 +1,54 @@
|
||||
html, body { height: 100%; }
|
||||
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
|
||||
|
||||
/* FX */
|
||||
.fx-resize {
|
||||
width: 480px !important;
|
||||
height: 320px !important;
|
||||
}
|
||||
.fx-spin { animation: spin 6400ms ease-in-out infinite; }
|
||||
.fx-shadow { position: relative; overflow: visible !important; }
|
||||
.fx-shadow::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 100%;
|
||||
top: calc(100% - 1px);
|
||||
left: 0;
|
||||
height: 1px;
|
||||
background-color: black;
|
||||
border-radius: 50%;
|
||||
z-index: -1;
|
||||
transform: rotateX(80deg);
|
||||
box-shadow: 0px 0px 60px 38px rgb(0 0 0 / 25%);
|
||||
}
|
||||
.fx-mirror {
|
||||
-webkit-box-reflect: below 0px linear-gradient(to bottom, rgba(0,0,0,0.0), rgba(0,0,0,0.4));
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: perspective(1000px) rotateY(0deg);
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
10% {
|
||||
transform: perspective(1000px) rotateY(0deg);
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
40% {
|
||||
transform: perspective(1000px) rotateY(180deg);
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
60% {
|
||||
transform: perspective(1000px) rotateY(180deg);
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
90% {
|
||||
transform: perspective(1000px) rotateY(359deg);
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
100% {
|
||||
transform: perspective(1000px) rotateY(360deg);
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user