1
0
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:
Brett Morgan
2023-05-06 10:53:17 +10:00
committed by GitHub
parent b703f1f3f9
commit 91cb685d1f
61 changed files with 14950 additions and 0 deletions

View 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!');
});
});

View 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();
}
}

View File

@@ -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();
});
});

View File

@@ -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,
});
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 B

View 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>

View 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)
]
})

View 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;
}
}