Merge pull request #73 from praeclarum/prerender

Prerender HTML to help SEO and load time
This commit is contained in:
Frank A. Krueger 2018-02-01 21:53:05 -08:00 committed by GitHub
commit 01c452acfd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 454 additions and 113 deletions

View File

@ -1,33 +1,56 @@
using System; using System;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Ooui.AspNetCore namespace Ooui.AspNetCore
{ {
public class ElementResult : ActionResult public class ElementResult : ActionResult
{ {
readonly Element element; readonly Element element;
readonly string title; readonly string title;
public ElementResult (Element element, string title = "") public ElementResult (Element element, string title = "")
{ {
this.element = element; this.element = element;
this.title = title; this.title = title;
} }
public override async Task ExecuteResultAsync (ActionContext context) public override async Task ExecuteResultAsync (ActionContext context)
{ {
var response = context.HttpContext.Response; var response = context.HttpContext.Response;
response.StatusCode = 200; response.StatusCode = 200;
response.ContentType = "text/html; charset=utf-8"; response.ContentType = "text/html; charset=utf-8";
if (element.WantsFullScreen) {
element.Style.Width = GetCookieDouble (context.HttpContext.Request.Cookies, "oouiWindowWidth", 32, 640, 10000);
element.Style.Height = GetCookieDouble (context.HttpContext.Request.Cookies, "oouiWindowHeight", 24, 480, 10000);
}
var sessionId = WebSocketHandler.BeginSession (context.HttpContext, element); var sessionId = WebSocketHandler.BeginSession (context.HttpContext, element);
var html = UI.RenderTemplate (WebSocketHandler.WebSocketPath + "?id=" + sessionId, title: title); var initialHtml = element.OuterHtml;
var html = UI.RenderTemplate (WebSocketHandler.WebSocketPath + "?id=" + sessionId, title: title, initialHtml: initialHtml);
var htmlBytes = Encoding.UTF8.GetBytes (html); var htmlBytes = Encoding.UTF8.GetBytes (html);
response.ContentLength = htmlBytes.Length; response.ContentLength = htmlBytes.Length;
using (var s = response.Body) { using (var s = response.Body) {
await s.WriteAsync (htmlBytes, 0, htmlBytes.Length).ConfigureAwait (false); await s.WriteAsync (htmlBytes, 0, htmlBytes.Length).ConfigureAwait (false);
} }
} }
static double GetCookieDouble (IRequestCookieCollection cookies, string key, double min, double def, double max)
{
if (cookies.TryGetValue (key, out var s)) {
if (double.TryParse (s, out var d)) {
if (d < min) return min;
if (d > max) return max;
return d;
}
return def;
}
else {
return def;
}
}
} }
} }

View File

@ -21,6 +21,8 @@ namespace Ooui.Forms.Renderers
/// </summary> /// </summary>
protected virtual bool ManageNativeControlLifetime => true; protected virtual bool ManageNativeControlLifetime => true;
protected override bool HtmlNeedsFullEndElement => TagName == "div";
public ViewRenderer (string tagName = "div") public ViewRenderer (string tagName = "div")
: base (tagName) : base (tagName)
{ {

View File

@ -24,7 +24,7 @@ namespace Ooui.Forms.Renderers
if (_iframe != null) if (_iframe != null)
{ {
_iframe.Src = html; _iframe.Source = html;
} }
} }
catch (Exception ex) catch (Exception ex)
@ -47,7 +47,7 @@ namespace Ooui.Forms.Renderers
if (_iframe != null) if (_iframe != null)
{ {
_iframe.Src = url; _iframe.Source = url;
} }
} }
catch (Exception ex) catch (Exception ex)

View File

@ -62,6 +62,8 @@ namespace Ooui.Forms
} }
} }
protected override bool HtmlNeedsFullEndElement => TagName == "div";
public VisualElementRenderer (string tagName = "div") : base (tagName) public VisualElementRenderer (string tagName = "div") : base (tagName)
{ {
_propertyChangedHandler = OnElementPropertyChanged; _propertyChangedHandler = OnElementPropertyChanged;

View File

@ -4,10 +4,9 @@ namespace Ooui
{ {
public class Anchor : Element public class Anchor : Element
{ {
string href = "";
public string HRef { public string HRef {
get => href; get => GetStringAttribute ("href", "");
set => SetProperty (ref href, value ?? "", "href"); set => SetAttributeProperty ("href", value);
} }
public Anchor () public Anchor ()

View File

@ -9,8 +9,8 @@ namespace Ooui
{ {
ButtonType typ = ButtonType.Submit; ButtonType typ = ButtonType.Submit;
public ButtonType Type { public ButtonType Type {
get => typ; get => GetAttribute ("type", ButtonType.Submit);
set => SetProperty (ref typ, value, "type"); set => SetAttributeProperty ("type", value);
} }
public Button () public Button ()

View File

@ -7,16 +7,14 @@ namespace Ooui
CanvasRenderingContext2D context2d = new CanvasRenderingContext2D (); CanvasRenderingContext2D context2d = new CanvasRenderingContext2D ();
int gotContext2d = 0; int gotContext2d = 0;
int width = 300;
public int Width { public int Width {
get => width; get => GetAttribute ("width", 300);
set => SetProperty (ref width, value <= 0 ? 150 : value, "width"); set => SetAttributeProperty ("width", value < 0 ? 0 : value);
} }
int height = 150;
public int Height { public int Height {
get => height; get => GetAttribute ("height", 150);
set => SetProperty (ref height, value <= 0 ? 150 : value, "height"); set => SetAttributeProperty ("height", value < 0 ? 0 : value);
} }
public Canvas () public Canvas ()

View File

@ -2,6 +2,7 @@
var debug = false; var debug = false;
const nodes = {}; const nodes = {};
const hasText = {};
let socket = null; let socket = null;
@ -35,10 +36,27 @@ function getSize () {
}; };
} }
function setCookie (name, value, days) {
var expires = "";
if (days) {
var date = new Date ();
date.setTime(date.getTime () + (days*24*60*60*1000));
expires = "; expires=" + date.toUTCString();
}
document.cookie = name + "=" + (value || "") + expires + "; path=/";
}
function saveSize (s) {
setCookie ("oouiWindowWidth", s.width, 7);
setCookie ("oouiWindowHeight", s.height, 7);
}
// Main entrypoint // Main entrypoint
function ooui (rootElementPath) { function ooui (rootElementPath) {
var initialSize = getSize (); var initialSize = getSize ();
saveSize (initialSize);
var wsArgs = (rootElementPath.indexOf("?") >= 0 ? "&" : "?") + var wsArgs = (rootElementPath.indexOf("?") >= 0 ? "&" : "?") +
"w=" + initialSize.width + "&h=" + initialSize.height; "w=" + initialSize.width + "&h=" + initialSize.height;
@ -123,12 +141,22 @@ function getNode (id) {
} }
} }
function getOrCreateElement (id, tagName) {
var e = document.getElementById (id);
if (e) {
if (e.firstChild && e.firstChild.nodeType == Node.TEXT_NODE)
hasText[e.id] = true;
return e;
}
return document.createElement (tagName);
}
function msgCreate (m) { function msgCreate (m) {
const id = m.id; const id = m.id;
const tagName = m.k; const tagName = m.k;
const node = tagName === "#text" ? const node = tagName === "#text" ?
document.createTextNode ("") : document.createTextNode ("") :
document.createElement (tagName); getOrCreateElement (id, tagName);
if (tagName !== "#text") if (tagName !== "#text")
node.id = id; node.id = id;
nodes[id] = node; nodes[id] = node;
@ -164,6 +192,17 @@ function msgSetAttr (m) {
if (debug) console.log ("SetAttr", node, m.k, m.v); if (debug) console.log ("SetAttr", node, m.k, m.v);
} }
function msgRemAttr (m) {
const id = m.id;
const node = getNode (id);
if (!node) {
console.error ("Unknown node id", m);
return;
}
node.removeAttribute(m.k);
if (debug) console.log ("RemAttr", node, m.k);
}
function msgCall (m) { function msgCall (m) {
const id = m.id; const id = m.id;
const node = getNode (id); const node = getNode (id);
@ -173,6 +212,12 @@ function msgCall (m) {
} }
const isJQuery = m.k.startsWith ("$."); const isJQuery = m.k.startsWith ("$.");
const target = isJQuery ? $(node) : node; const target = isJQuery ? $(node) : node;
if (m.k === "insertBefore" && m.v[0].nodeType == Node.TEXT_NODE && m.v[1] == null && hasText[id]) {
// Text is already set so it clear it first
if (target.firstChild)
target.removeChild (target.firstChild);
delete hasText[id];
}
const f = isJQuery ? target[m.k.slice(2)] : target[m.k]; const f = isJQuery ? target[m.k.slice(2)] : target[m.k];
if (debug) console.log ("Call", node, f, m.v); if (debug) console.log ("Call", node, f, m.v);
const r = f.apply (target, m.v); const r = f.apply (target, m.v);
@ -227,6 +272,9 @@ function processMessage (m) {
case "setAttr": case "setAttr":
msgSetAttr (m); msgSetAttr (m);
break; break;
case "remAttr":
msgRemAttr (m);
break;
case "call": case "call":
msgCall (m); msgCall (m);
break; break;

View File

@ -5,6 +5,8 @@ namespace Ooui
{ {
public class Div : Element public class Div : Element
{ {
protected override bool HtmlNeedsFullEndElement => true;
public Div () public Div ()
: base ("div") : base ("div")
{ {

View File

@ -1,28 +1,29 @@
using System; using System;
using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
namespace Ooui namespace Ooui
{ {
public abstract class Element : Node public abstract class Element : Node
{ {
string className = ""; readonly Dictionary<string, object> attributes = new Dictionary<string, object> ();
public string ClassName { public string ClassName {
get => className; get => GetStringAttribute ("class", "");
set => SetProperty (ref className, value, "className"); set => SetAttributeProperty ("class", value);
} }
public Style Style { get; private set; } = new Style (); public Style Style { get; private set; } = new Style ();
string title = "";
public string Title { public string Title {
get => title; get => GetStringAttribute ("title", "");
set => SetProperty (ref title, value, "title"); set => SetAttributeProperty ("title", value);
} }
bool hidden = false; bool hidden = false;
public bool IsHidden { public bool IsHidden {
get => hidden; get => GetBooleanAttribute ("hidden");
set => SetProperty (ref hidden, value, "hidden"); set => SetBooleanAttributeProperty ("hidden", value);
} }
public event TargetEventHandler Click { public event TargetEventHandler Click {
@ -102,8 +103,64 @@ namespace Ooui
Style.PropertyChanged += HandleStylePropertyChanged; Style.PropertyChanged += HandleStylePropertyChanged;
} }
public void SetAttribute (string attributeName, string value) protected bool SetAttributeProperty (string attributeName, object newValue, [System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
{ {
var old = GetAttribute (attributeName);
if (old != null && old.Equals (newValue))
return false;
SetAttribute (attributeName, newValue);
OnPropertyChanged (propertyName);
return true;
}
protected bool SetBooleanAttributeProperty (string attributeName, bool newValue, [System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
{
var old = GetAttribute (attributeName) != null;
if (old != newValue)
return false;
if (newValue)
SetAttribute (attributeName, string.Empty);
else
RemoveAttribute (attributeName);
OnPropertyChanged (propertyName);
return true;
}
protected bool UpdateAttributeProperty (string attributeName, object newValue, string propertyName)
{
lock (attributes) {
if (attributes.TryGetValue (attributeName, out var oldValue)) {
if (newValue != null && newValue.Equals (oldValue))
return false;
}
attributes[attributeName] = newValue;
}
OnPropertyChanged (propertyName);
return true;
}
protected bool UpdateBooleanAttributeProperty (string attributeName, bool newValue, string propertyName)
{
lock (attributes) {
var oldValue = attributes.ContainsKey (attributeName);
if (newValue == oldValue)
return false;
if (newValue) {
attributes[attributeName] = "";
}
else {
attributes.Remove (attributeName);
}
}
OnPropertyChanged (propertyName);
return true;
}
public void SetAttribute (string attributeName, object value)
{
lock (attributes) {
attributes[attributeName] = value;
}
Send (new Message { Send (new Message {
MessageType = MessageType.SetAttribute, MessageType = MessageType.SetAttribute,
TargetId = Id, TargetId = Id,
@ -112,6 +169,62 @@ namespace Ooui
}); });
} }
public object GetAttribute (string attributeName)
{
lock (attributes) {
attributes.TryGetValue (attributeName, out var v);
return v;
}
}
public T GetAttribute<T> (string attributeName, T defaultValue)
{
lock (attributes) {
attributes.TryGetValue (attributeName, out var v);
if (v is T) {
return (T)v;
}
else {
return defaultValue;
}
}
}
public bool GetBooleanAttribute (string attributeName)
{
lock (attributes) {
return attributes.TryGetValue (attributeName, out var _);
}
}
public string GetStringAttribute (string attributeName, string defaultValue)
{
lock (attributes) {
if (attributes.TryGetValue (attributeName, out var v)) {
if (v == null) return "null";
else return v.ToString ();
}
else {
return defaultValue;
}
}
}
public void RemoveAttribute (string attributeName)
{
bool removed;
lock (attributes) {
removed = attributes.Remove (attributeName);
}
if (removed) {
Send (new Message {
MessageType = MessageType.RemoveAttribute,
TargetId = Id,
Key = attributeName,
});
}
}
void HandleStylePropertyChanged (object sender, PropertyChangedEventArgs e) void HandleStylePropertyChanged (object sender, PropertyChangedEventArgs e)
{ {
SendSet ("style." + Style.GetJsName (e.PropertyName), Style[e.PropertyName]); SendSet ("style." + Style.GetJsName (e.PropertyName), Style[e.PropertyName]);
@ -130,5 +243,38 @@ namespace Ooui
return base.SaveStateMessageIfNeeded (message); return base.SaveStateMessageIfNeeded (message);
} }
} }
protected virtual bool HtmlNeedsFullEndElement => false;
public override void WriteOuterHtml (System.Xml.XmlWriter w)
{
w.WriteStartElement (TagName);
w.WriteAttributeString ("id", Id);
var style = Style.ToString ();
if (style.Length > 0) {
w.WriteAttributeString ("style", style);
}
lock (attributes) {
foreach (var a in attributes) {
var value = (a.Value == null) ? "null" : Convert.ToString (a.Value, System.Globalization.CultureInfo.InvariantCulture);
w.WriteAttributeString (a.Key, value);
}
}
WriteInnerHtml (w);
if (HtmlNeedsFullEndElement) {
w.WriteFullEndElement ();
}
else {
w.WriteEndElement ();
}
}
public virtual void WriteInnerHtml (System.Xml.XmlWriter w)
{
var children = Children;
foreach (var c in children) {
c.WriteOuterHtml (w);
}
}
} }
} }

View File

@ -83,12 +83,12 @@ namespace Ooui
} }
} }
protected bool SetProperty<T> (ref T backingStore, T newValue, string attributeName, [System.Runtime.CompilerServices.CallerMemberName] string propertyName = "") protected bool SetProperty<T> (ref T backingStore, T newValue, string jsPropertyName, [System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
{ {
if (EqualityComparer<T>.Default.Equals (backingStore, newValue)) if (EqualityComparer<T>.Default.Equals (backingStore, newValue))
return false; return false;
backingStore = newValue; backingStore = newValue;
SendSet (attributeName, newValue); SendSet (jsPropertyName, newValue);
OnPropertyChanged (propertyName); OnPropertyChanged (propertyName);
return true; return true;
} }
@ -120,12 +120,12 @@ namespace Ooui
Send (Message.Call (Id, methodName, args)); Send (Message.Call (Id, methodName, args));
} }
protected void SendSet (string attributeName, object value) protected void SendSet (string jsPropertyName, object value)
{ {
Send (new Message { Send (new Message {
MessageType = MessageType.Set, MessageType = MessageType.Set,
TargetId = Id, TargetId = Id,
Key = attributeName, Key = jsPropertyName,
Value = value, Value = value,
}); });
} }
@ -169,6 +169,11 @@ namespace Ooui
state.Add (message); state.Add (message);
}); });
break; break;
case MessageType.RemoveAttribute:
this.UpdateStateMessages (state => {
state.RemoveAll (x => x.MessageType == MessageType.SetAttribute && x.Key == message.Key);
});
return true;
case MessageType.Listen: case MessageType.Listen:
AddStateMessage (message); AddStateMessage (message);
break; break;

View File

@ -6,20 +6,18 @@ namespace Ooui
{ {
string action = ""; string action = "";
public string Action { public string Action {
get => action; get => GetStringAttribute ("action", "");
set => SetProperty (ref action, value ?? "", "action"); set => SetAttributeProperty ("action", value ?? "");
} }
string method = "GET";
public string Method { public string Method {
get => method; get => GetStringAttribute ("method", "GET");
set => SetProperty (ref method, value ?? "", "method"); set => SetAttributeProperty ("method", value ?? "");
} }
string enctype = "application/x-www-form-urlencoded";
public string EncodingType { public string EncodingType {
get => enctype; get => GetStringAttribute ("enctype", "application/x-www-form-urlencoded");
set => SetProperty (ref enctype, value ?? "", "enctype"); set => SetAttributeProperty ("enctype", value ?? "");
} }
public event TargetEventHandler Submit { public event TargetEventHandler Submit {

View File

@ -4,16 +4,15 @@ namespace Ooui
{ {
public abstract class FormControl : Element public abstract class FormControl : Element
{ {
string name = "";
public string Name { public string Name {
get => name; get => GetStringAttribute ("name", "");
set => SetProperty (ref name, value, "name"); set => SetAttributeProperty ("name", value);
} }
bool isDisabled = false; bool isDisabled = false;
public bool IsDisabled { public bool IsDisabled {
get => isDisabled; get => GetBooleanAttribute ("disabled");
set => SetProperty (ref isDisabled, value, "disabled"); set => SetBooleanAttributeProperty ("disabled", value);
} }
public FormControl (string tagName) public FormControl (string tagName)

View File

@ -2,17 +2,15 @@
{ {
public class Iframe : Element public class Iframe : Element
{ {
public Iframe() public string Source
: base("iframe")
{ {
get => GetStringAttribute ("src", null);
set => SetAttributeProperty ("src", value);
} }
string src = null; public Iframe ()
public string Src : base ("iframe")
{ {
get => src;
set => SetProperty(ref src, value, "src");
} }
} }
} }

View File

@ -4,10 +4,10 @@ namespace Ooui
{ {
public class Image : Element public class Image : Element
{ {
string src = ""; public string Source
public string Source { {
get => src; get => GetStringAttribute ("src", null);
set => SetProperty (ref src, value ?? "", "src"); set => SetAttributeProperty ("src", value);
} }
public Image () public Image ()

View File

@ -7,16 +7,14 @@ namespace Ooui
{ {
public class Input : FormControl public class Input : FormControl
{ {
InputType typ = InputType.Text;
public InputType Type { public InputType Type {
get => typ; get => GetAttribute ("type", InputType.Text);
set => SetProperty (ref typ, value, "type"); set => SetAttributeProperty ("type", value);
} }
string val = "";
public string Value { public string Value {
get => val; get => GetStringAttribute ("value", "");
set => SetProperty (ref val, value ?? "", "value"); set => SetAttributeProperty ("value", value ?? "");
} }
public double NumberValue { public double NumberValue {
@ -35,37 +33,33 @@ namespace Ooui
remove => RemoveEventListener ("change", value); remove => RemoveEventListener ("change", value);
} }
string placeholder = "";
public string Placeholder { public string Placeholder {
get => placeholder; get => GetStringAttribute ("placeholder", "");
set => SetProperty (ref placeholder, value, "placeholder"); set => SetAttributeProperty ("placeholder", value ?? "");
} }
bool isChecked = false;
public bool IsChecked { public bool IsChecked {
get => isChecked; get => GetBooleanAttribute ("checked");
set { set {
SetProperty (ref isChecked, value, "checked"); if (SetBooleanAttributeProperty ("checked", value)) {
TriggerEventFromMessage (Message.Event (Id, "change", isChecked)); TriggerEventFromMessage (Message.Event (Id, "change", IsChecked));
}
} }
} }
double minimum = 0;
public double Minimum { public double Minimum {
get => minimum; get => GetAttribute ("min", 0.0);
set => SetProperty (ref minimum, value, "min"); set => SetAttributeProperty ("min", value);
} }
double maximum = 100;
public double Maximum { public double Maximum {
get => maximum; get => GetAttribute ("max", 100.0);
set => SetProperty (ref maximum, value, "max"); set => SetAttributeProperty ("max", value);
} }
double step = 1;
public double Step { public double Step {
get => step; get => GetAttribute ("step", 1.0);
set => SetProperty (ref step, value, "step"); set => SetAttributeProperty ("step", value);
} }
public Input () public Input ()
@ -86,10 +80,10 @@ namespace Ooui
if (message.TargetId == Id && message.MessageType == MessageType.Event && (message.Key == "change" || message.Key == "input")) { if (message.TargetId == Id && message.MessageType == MessageType.Event && (message.Key == "change" || message.Key == "input")) {
// Don't need to notify here because the base implementation will fire the event // Don't need to notify here because the base implementation will fire the event
if (Type == InputType.Checkbox) { if (Type == InputType.Checkbox) {
isChecked = message.Value != null ? Convert.ToBoolean (message.Value) : false; UpdateBooleanAttributeProperty ("checked", message.Value != null ? Convert.ToBoolean (message.Value) : false, "IsChecked");
} }
else { else {
val = message.Value != null ? Convert.ToString (message.Value) : ""; UpdateAttributeProperty ("value", message.Value != null ? Convert.ToString (message.Value) : "", "Value");
} }
} }
return base.TriggerEventFromMessage (message); return base.TriggerEventFromMessage (message);

View File

@ -4,10 +4,9 @@ namespace Ooui
{ {
public class Label : Element public class Label : Element
{ {
Element htmlFor = null;
public Element For { public Element For {
get => htmlFor; get => GetAttribute<Element> ("for", null);
set => SetProperty (ref htmlFor, value, "htmlFor"); set => SetAttributeProperty ("for", value);
} }
public Label () public Label ()

View File

@ -48,6 +48,8 @@ namespace Ooui
Set, Set,
[EnumMember (Value = "setAttr")] [EnumMember (Value = "setAttr")]
SetAttribute, SetAttribute,
[EnumMember(Value = "remAttr")]
RemoveAttribute,
[EnumMember(Value = "call")] [EnumMember(Value = "call")]
Call, Call,
[EnumMember(Value = "listen")] [EnumMember(Value = "listen")]

View File

@ -180,5 +180,24 @@ namespace Ooui
} }
return false; return false;
} }
public virtual string OuterHtml {
get {
using (var stream = new System.IO.MemoryStream ()) {
var settings = new System.Xml.XmlWriterSettings {
OmitXmlDeclaration = true,
ConformanceLevel = System.Xml.ConformanceLevel.Fragment,
CloseOutput = false,
};
using (var w = System.Xml.XmlWriter.Create (stream, settings)) {
WriteOuterHtml (w);
}
stream.Position = 0;
return new System.IO.StreamReader (stream).ReadToEnd ();
}
}
}
public abstract void WriteOuterHtml (System.Xml.XmlWriter w);
} }
} }

View File

@ -4,22 +4,19 @@ namespace Ooui
{ {
public class Option : Element public class Option : Element
{ {
string val = "";
public string Value { public string Value {
get => val; get => GetStringAttribute ("value", "");
set => SetProperty (ref val, value ?? "", "value"); set => SetAttributeProperty ("value", value ?? "");
} }
string label = "";
public string Label { public string Label {
get => label; get => GetStringAttribute ("label", "");
set => SetProperty (ref label, value ?? "", "label"); set => SetAttributeProperty ("label", value ?? "");
} }
bool defaultSelected = false;
public bool DefaultSelected { public bool DefaultSelected {
get => defaultSelected; get => GetBooleanAttribute ("selected");
set => SetProperty (ref defaultSelected, value, "defaultSelected"); set => SetBooleanAttributeProperty ("selected", value);
} }
public Option () public Option ()

View File

@ -4,10 +4,9 @@ namespace Ooui
{ {
public class Select : FormControl public class Select : FormControl
{ {
string val = "";
public string Value { public string Value {
get => val; get => GetStringAttribute ("value", "");
set => SetProperty (ref val, value ?? "", "value"); set => SetAttributeProperty ("value", value ?? "");
} }
public event TargetEventHandler Change { public event TargetEventHandler Change {
@ -35,6 +34,7 @@ namespace Ooui
protected override void OnChildInsertedBefore (Node newChild, Node referenceChild) protected override void OnChildInsertedBefore (Node newChild, Node referenceChild)
{ {
base.OnChildInsertedBefore (newChild, referenceChild); base.OnChildInsertedBefore (newChild, referenceChild);
var val = Value;
if (string.IsNullOrEmpty (val) && newChild is Option o && !string.IsNullOrEmpty (o.Value)) { if (string.IsNullOrEmpty (val) && newChild is Option o && !string.IsNullOrEmpty (o.Value)) {
val = o.Value; val = o.Value;
} }
@ -43,7 +43,8 @@ namespace Ooui
protected override bool TriggerEventFromMessage (Message message) protected override bool TriggerEventFromMessage (Message message)
{ {
if (message.TargetId == Id && message.MessageType == MessageType.Event && (message.Key == "change" || message.Key == "input")) { if (message.TargetId == Id && message.MessageType == MessageType.Event && (message.Key == "change" || message.Key == "input")) {
val = message.Value != null ? Convert.ToString (message.Value) : ""; SetAttribute ("value", message.Value != null ? Convert.ToString (message.Value) : "");
OnPropertyChanged ("Value");
} }
return base.TriggerEventFromMessage (message); return base.TriggerEventFromMessage (message);
} }

View File

@ -399,7 +399,7 @@ namespace Ooui
o.Append (head); o.Append (head);
o.Append (p.Key); o.Append (p.Key);
o.Append (":"); o.Append (":");
o.Append (String.Format (System.Globalization.CultureInfo.InvariantCulture, "{0}", p.Value)); o.Append (Convert.ToString (p.Value, System.Globalization.CultureInfo.InvariantCulture));
head = ";"; head = ";";
} }
} }

View File

@ -20,18 +20,19 @@ namespace Ooui
set => SetProperty (ref val, value ?? "", "value"); set => SetProperty (ref val, value ?? "", "value");
} }
int rows = 2;
public int Rows { public int Rows {
get => rows; get => GetAttribute ("rows", 2);
set => SetProperty (ref rows, value, "rows"); set => SetAttributeProperty ("rows", value);
} }
int cols = 20; int cols = 20;
public int Columns { public int Columns {
get => cols; get => GetAttribute ("cols", 20);
set => SetProperty (ref cols, value, "cols"); set => SetAttributeProperty ("cols", value);
} }
protected override bool HtmlNeedsFullEndElement => true;
public TextArea () public TextArea ()
: base ("textarea") : base ("textarea")
{ {
@ -53,5 +54,10 @@ namespace Ooui
} }
return base.TriggerEventFromMessage (message); return base.TriggerEventFromMessage (message);
} }
public override void WriteInnerHtml (System.Xml.XmlWriter w)
{
w.WriteString (val ?? "");
}
} }
} }

View File

@ -20,5 +20,10 @@ namespace Ooui
{ {
Text = text; Text = text;
} }
public override void WriteOuterHtml (System.Xml.XmlWriter w)
{
w.WriteString (text);
}
} }
} }

View File

@ -44,7 +44,10 @@ namespace Ooui
<style>@Styles</style> <style>@Styles</style>
</head> </head>
<body> <body>
<div id=""ooui-body"" class=""container-fluid""></div>
<div id=""ooui-body"" class=""container-fluid"">
@InitialHtml
</div>
<script type=""text/javascript"" src=""https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.min.js""></script> <script type=""text/javascript"" src=""https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.min.js""></script>
<script type=""text/javascript"" src=""https://gitcdn.github.io/bootstrap-toggle/2.2.2/js/bootstrap-toggle.min.js""></script> <script type=""text/javascript"" src=""https://gitcdn.github.io/bootstrap-toggle/2.2.2/js/bootstrap-toggle.min.js""></script>
@ -390,9 +393,9 @@ namespace Ooui
} }
} }
public static string RenderTemplate (string webSocketPath, string title = "") public static string RenderTemplate (string webSocketPath, string title = "", string initialHtml = "")
{ {
return Template.Replace ("@WebSocketPath", webSocketPath).Replace ("@Styles", rules.ToString ()).Replace ("@Title", title); return Template.Replace ("@WebSocketPath", webSocketPath).Replace ("@Styles", rules.ToString ()).Replace ("@Title", title).Replace ("@InitialHtml", initialHtml);
} }
class DataHandler : RequestHandler class DataHandler : RequestHandler

View File

@ -58,8 +58,8 @@ namespace Tests
Assert.AreEqual (480, c.Height); Assert.AreEqual (480, c.Height);
c.Width = 0; c.Width = 0;
c.Height = -100; c.Height = -100;
Assert.AreEqual (150, c.Width); Assert.AreEqual (0, c.Width);
Assert.AreEqual (150, c.Height); Assert.AreEqual (0, c.Height);
} }
} }
} }

95
Tests/WriteHtmlTests.cs Normal file
View File

@ -0,0 +1,95 @@
using System;
#if NUNIT
using NUnit.Framework;
using TestClassAttribute = NUnit.Framework.TestFixtureAttribute;
using TestMethodAttribute = NUnit.Framework.TestCaseAttribute;
#else
using Microsoft.VisualStudio.TestTools.UnitTesting;
#endif
using Ooui;
namespace Tests
{
[TestClass]
public class WriteHtmlTests
{
System.Text.RegularExpressions.Regex idre = new System.Text.RegularExpressions.Regex ("\\sid=\"[^\"]*\"");
string OuterHtmlWithoutIds (Element e)
{
return idre.Replace (e.OuterHtml, "");
}
[TestMethod]
public void TextAreaWithTextStyled ()
{
var e = new TextArea {
Value = "Hello World!",
};
e.Style.BackgroundColor = "#18f";
Assert.AreEqual ("<textarea style=\"background-color:#18f\">Hello World!</textarea>", OuterHtmlWithoutIds (e));
}
[TestMethod]
public void TextAreaEmptyStyled ()
{
var e = new TextArea ();
e.Style.BackgroundColor = "#18f";
Assert.AreEqual ("<textarea style=\"background-color:#18f\"></textarea>", OuterHtmlWithoutIds (e));
}
[TestMethod]
public void Style ()
{
var e = new Div ();
e.Style.BackgroundColor = "#18f";
Assert.AreEqual ("<div style=\"background-color:#18f\"></div>", OuterHtmlWithoutIds (e));
}
[TestMethod]
public void TwoGrandChildren ()
{
var e = new Div (new Div (new Anchor (), new Anchor ()), new Paragraph ());
Assert.AreEqual ("<div><div><a /><a /></div><p /></div>", OuterHtmlWithoutIds (e));
}
[TestMethod]
public void Child ()
{
var e = new Div (new Anchor ());
Assert.AreEqual ("<div><a /></div>", OuterHtmlWithoutIds (e));
}
[TestMethod]
public void TextChild ()
{
var e = new Paragraph ("Hello world!");
Assert.AreEqual ("<p>Hello world!</p>", OuterHtmlWithoutIds (e));
}
[TestMethod]
public void IdIsFirst ()
{
var e = new Anchor ();
Assert.IsTrue (e.OuterHtml.StartsWith ("<a id=\""));
}
[TestMethod]
public void EmptyElement ()
{
var e = new Anchor ();
Assert.AreEqual ("<a />", OuterHtmlWithoutIds (e));
}
[TestMethod]
public void AnchorHRef ()
{
var e = new Anchor {
HRef = "http://google.com"
};
Assert.AreEqual ("<a href=\"http://google.com\" />", OuterHtmlWithoutIds (e));
}
}
}